Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

### Added

- `base44 branches list --app-id <id> --json` lists main and active branch names for agents working outside Builder.

- Global `--branch <name>` targets sandbox commands at a specific app branch. Names resolve within the selected app; missing or ambiguous names fail. Other commands reject the flag explicitly; omitting it or using `--branch main` targets main.
- App visibility: `base44 visibility <public|private|workspace>` sets it on the server directly (accepts `--app-id` to target any app). Also configurable via `"visibility"` in `config.jsonc`, which `base44 deploy` applies. New projects scaffold `"visibility": "public"`.
- `base44 build` runs the site's `buildCommand` with `VITE_BASE44_APP_ID` injected, so built bundles always carry the linked app's id.
- `base44 deploy` (and `base44 site deploy`) can now build first: interactive runs ask, and `--build` / `--no-build` pre-answer the prompt.
Expand Down
20 changes: 20 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,26 @@

Commands live in `src/cli/commands/<domain>/`. They use a **factory pattern** — each file exports a function that returns a `Base44Command`.

## Branch targeting

Discover names with `base44 branches list --app-id <app-id> --json`.
The result contains `branches` with `name` and `status`, including main and active
feature branches. Choose the branch matching the user's request before editing.

`--branch <name>` is global, but currently supported only by sandbox commands.
`--branch feature/checkout` resolves an exact name within the selected app.
Missing or ambiguous names fail. `--branch main` explicitly targets main.
No checkout state is saved. IDs are internal; the CLI accepts branch names only.
For example: `base44 --branch feature/checkout sandbox read <path> --app-id <app-id>`.
Other commands (including `functions pull`, `functions list`, and `entities push`)
reject it before authentication or command execution, rather than silently targeting main.
There is no `entities pull` command; branch source files can be read through `sandbox read`.

To add support, set `supportsBranch: true` on the command and consume
`ctx.branchId`. First verify that every backend operation honors that scope;
accepting the query parameter alone is not proof of branch isolation.
Omitting the flag preserves existing main-app behavior.

## Command File Template

```typescript
Expand Down
33 changes: 33 additions & 0 deletions packages/cli/src/cli/commands/branches/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Command } from "commander";
import type { CLIContext, RunCommandResult } from "@/cli/types.js";
import { Base44Command } from "@/cli/utils/index.js";
import { listBranches } from "@/core/resources/branch/api.js";

async function listBranchesAction({
log,
runTask,
jsonMode,
}: CLIContext): Promise<RunCommandResult> {
const remote = await runTask("Fetching branches", () => listBranches());
const branches = [
{ name: "main", status: "active" },
...remote.map((branch) => ({
name: branch.branch_name,
status: branch.status,
})),
];
if (jsonMode) return { stdout: `${JSON.stringify({ branches })}\n` };
for (const branch of branches)
log.message(`${branch.name} (${branch.status})`);
return { outroMessage: `${branches.length} branches` };
}

export function getBranchesCommand(): Command {
return new Command("branches")
.description("Discover an app's branches")
.addCommand(
new Base44Command("list")
.description("List main and active branch names for use with --branch")
.action(listBranchesAction),
);
}
9 changes: 6 additions & 3 deletions packages/cli/src/cli/commands/sandbox/checkpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,23 @@ interface CheckpointOptions {
}

async function checkpointAction(
{ runTask }: CLIContext,
{ runTask, branchId }: CLIContext,
options: CheckpointOptions,
): Promise<RunCommandResult> {
const { id: appId } = getAppContext();

const result = await runTask("Creating checkpoint", () =>
createCheckpoint(appId, { name: options.name }),
createCheckpoint(appId, {
name: options.name,
branch_id: branchId,
}),
);

return { outroMessage: "Created checkpoint", stdout: toJsonStdout(result) };
}

export function getSandboxCheckpointCommand(): Command {
return new Base44Command("checkpoint")
return new Base44Command("checkpoint", { supportsBranch: true })
.description("Create a restore-point checkpoint of an app's remote sandbox")
.option(
"--name <name>",
Expand Down
12 changes: 9 additions & 3 deletions packages/cli/src/cli/commands/sandbox/edit-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ function parseEdits(raw: string): EditSpec[] {
}

async function editFileAction(
{ runTask }: CLIContext,
{ runTask, branchId }: CLIContext,
path: string,
options: EditFileOptions,
): Promise<RunCommandResult> {
Expand All @@ -52,7 +52,13 @@ async function editFileAction(

const result = await runTask(
options.dryRun ? "Previewing edit" : "Editing file",
() => editFile(appId, { path, edits, dry_run: options.dryRun }),
() =>
editFile(appId, {
path,
edits,
dry_run: options.dryRun,
branch_id: branchId,
}),
);

return {
Expand All @@ -62,7 +68,7 @@ async function editFileAction(
}

export function getSandboxEditFileCommand(): Command {
return new Base44Command("edit")
return new Base44Command("edit", { supportsBranch: true })
.description("Apply exact old→new string edits to a file in the sandbox")
.argument("<path>", "File path relative to the app root")
.option(
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/cli/commands/sandbox/grep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ interface GrepOptions {
}

async function grepAction(
{ runTask }: CLIContext,
{ runTask, branchId }: CLIContext,
pattern: string,
options: GrepOptions,
): Promise<RunCommandResult> {
Expand All @@ -29,14 +29,15 @@ async function grepAction(
case_sensitive: options.caseSensitive,
glob: options.glob,
max_results: maxResults,
branch_id: branchId,
}),
);

return { outroMessage: "Searched files", stdout: toJsonStdout(result) };
}

export function getSandboxGrepCommand(): Command {
return new Base44Command("grep")
return new Base44Command("grep", { supportsBranch: true })
.description("Search files for a pattern in an app's remote sandbox")
.argument("<pattern>", "Search pattern")
.option("--path <path>", "Subtree to search, relative to the app root")
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/cli/commands/sandbox/list-directory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ interface ListDirectoryOptions {
}

async function listDirectoryAction(
{ runTask }: CLIContext,
{ runTask, branchId }: CLIContext,
path: string | undefined,
options: ListDirectoryOptions,
): Promise<RunCommandResult> {
Expand All @@ -22,6 +22,7 @@ async function listDirectoryAction(
const result = await runTask("Listing directory", () =>
listDirectory(appId, {
path,
branch_id: branchId,
recursive: options.recursive,
max_depth: maxDepth,
include_hidden: options.includeHidden,
Expand All @@ -32,7 +33,7 @@ async function listDirectoryAction(
}

export function getSandboxListDirectoryCommand(): Command {
return new Base44Command("ls")
return new Base44Command("ls", { supportsBranch: true })
.description("List directory entries in an app's remote sandbox")
.argument(
"[path]",
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/cli/commands/sandbox/read-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ interface ReadFileOptions {
}

async function readFileAction(
{ runTask }: CLIContext,
{ runTask, branchId }: CLIContext,
paths: string[],
options: ReadFileOptions,
): Promise<RunCommandResult> {
Expand All @@ -20,14 +20,14 @@ async function readFileAction(
const limit = parsePositiveInt(options.limit, "--limit");

const result = await runTask("Reading file", () =>
readFile(appId, { paths, offset, limit }),
readFile(appId, { paths, offset, limit, branch_id: branchId }),
);

return { outroMessage: "Read file", stdout: toJsonStdout(result) };
}

export function getSandboxReadFileCommand(): Command {
return new Base44Command("read")
return new Base44Command("read", { supportsBranch: true })
.description("Read file contents from an app's remote sandbox")
.argument("<paths...>", "One or more file paths relative to the app root")
.option("--offset <n>", "1-based start line")
Expand Down
11 changes: 8 additions & 3 deletions packages/cli/src/cli/commands/sandbox/run-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ interface RunCommandOptions {
}

async function runCommandAction(
{ runTask }: CLIContext,
{ runTask, branchId }: CLIContext,
commandParts: string[],
options: RunCommandOptions,
): Promise<RunCommandResult> {
Expand All @@ -20,7 +20,12 @@ async function runCommandAction(
const command = commandParts.join(" ");

const result = await runTask("Running command", () =>
runCommand(appId, { command, cwd: options.cwd, timeout_ms: timeoutMs }),
runCommand(appId, {
command,
cwd: options.cwd,
timeout_ms: timeoutMs,
branch_id: branchId,
}),
);

// The HTTP call succeeded, so the CLI exits 0 regardless of the remote
Expand All @@ -29,7 +34,7 @@ async function runCommandAction(
}

export function getSandboxRunCommandCommand(): Command {
return new Base44Command("run")
return new Base44Command("run", { supportsBranch: true })
.description("Run a shell command in an app's remote sandbox")
.argument("<command...>", "Shell command to execute (quote to keep as one)")
.option("--cwd <path>", "Working directory relative to the app root")
Expand Down
11 changes: 8 additions & 3 deletions packages/cli/src/cli/commands/sandbox/write-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,27 @@ interface WriteFileOptions {
}

async function writeFileAction(
{ runTask }: CLIContext,
{ runTask, branchId }: CLIContext,
path: string,
options: WriteFileOptions,
): Promise<RunCommandResult> {
const { id: appId } = getAppContext();
const content = await resolveFlagOrStdin(options.content, "--content");

const result = await runTask("Writing file", () =>
writeFile(appId, { path, content, overwrite: options.overwrite }),
writeFile(appId, {
path,
content,
overwrite: options.overwrite,
branch_id: branchId,
}),
);

return { outroMessage: "Wrote file", stdout: toJsonStdout(result) };
}

export function getSandboxWriteFileCommand(): Command {
return new Base44Command("write")
return new Base44Command("write", { supportsBranch: true })
.description("Create or overwrite a file in an app's remote sandbox")
.argument("<path>", "File path relative to the app root")
.option("--content <content>", "File content (if omitted, read from stdin)")
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/src/cli/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getAuthCommand } from "@/cli/commands/auth/index.js";
import { getLoginCommand } from "@/cli/commands/auth/login.js";
import { getLogoutCommand } from "@/cli/commands/auth/logout.js";
import { getWhoamiCommand } from "@/cli/commands/auth/whoami.js";
import { getBranchesCommand } from "@/cli/commands/branches/index.js";
import { getConnectorsCommand } from "@/cli/commands/connectors/index.js";
import { getDashboardCommand } from "@/cli/commands/dashboard/index.js";
import { getEntitiesPushCommand } from "@/cli/commands/entities/push.js";
Expand Down Expand Up @@ -39,6 +40,10 @@ export function createProgram(context: CLIContext): Command {
"Base44 CLI - Unified interface for managing Base44 applications",
)
.version(packageJson.version)
.option(
"--branch <name>",
"Target an app branch by exact name (sandbox commands only)",
)
.addOption(
new Option("--app-id <id>", "Base44 app ID to use").env(
BASE44_APP_ID_ENV_VAR,
Expand Down Expand Up @@ -103,6 +108,7 @@ export function createProgram(context: CLIContext): Command {

// Register sandbox (remote development) commands
program.addCommand(getSandboxCommand());
program.addCommand(getBranchesCommand());

// Register auth config commands
program.addCommand(getAuthCommand());
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/cli/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { RunTaskFn } from "./utils/runTask.js";
export type Distribution = "npm" | "binary";

export interface CLIContext {
branchId?: string;
errorReporter: ErrorReporter;
isNonInteractive: boolean;
/**
Expand Down
24 changes: 21 additions & 3 deletions packages/cli/src/cli/utils/command/Base44Command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
formatPlainUpgradeMessage,
startUpgradeCheck,
} from "@/cli/utils/upgradeNotification.js";
import { ApiError, isCLIError } from "@/core/errors.js";
import { ApiError, InvalidInputError, isCLIError } from "@/core/errors.js";
import { resolveBranchName } from "@/core/resources/branch/api.js";

/**
* Write a command result to stdout as a single JSON document (the `--json`
Expand Down Expand Up @@ -67,6 +68,7 @@ function writeJsonError(error: unknown): void {
}

interface Base44CommandOptions {
supportsBranch?: boolean;
/**
* Require user authentication before running this command.
* If the user is not logged in, they will be prompted to login.
Expand Down Expand Up @@ -130,6 +132,7 @@ export class Base44Command extends Command {
requireAuth: options?.requireAuth ?? true,
requireAppContext: options?.requireAppContext ?? true,
fullBanner: options?.fullBanner ?? false,
supportsBranch: options?.supportsBranch ?? false,
};
}

Expand Down Expand Up @@ -172,6 +175,17 @@ export class Base44Command extends Command {
const upgradeCheckPromise = startUpgradeCheck();

try {
const { branch } = this.optsWithGlobals<{
branch?: string;
}>();
if (branch !== undefined && !this._commandOptions.supportsBranch) {
throw new InvalidInputError(
"--branch is not supported by this command. Use sandbox commands to read or edit branch files; no app changes were made.",
);
}
if (branch !== undefined && !branch.trim()) {
throw new InvalidInputError("--branch must not be empty.");
}
if (this._commandOptions.requireAuth) {
await ensureAuth(this.context);
}
Expand All @@ -180,8 +194,12 @@ export class Base44Command extends Command {
await ensureAppContext(this.context, { appId });
}

const result = ((await fn(this.context, ...args)) ??
{}) as RunCommandResult;
const resolvedBranchId =
branch !== undefined ? await resolveBranchName(branch) : undefined;
const result = ((await fn(
{ ...this.context, branchId: resolvedBranchId },
...args,
)) ?? {}) as RunCommandResult;

if (!quiet) {
await showCommandEnd(
Expand Down
Loading
Loading