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
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,14 @@ The CLI will guide you through project setup. For step-by-step tutorials, see th
| [`whoami`](https://docs.base44.com/developers/references/cli/commands/whoami) | Display the current authenticated user |
| [`agents pull`](https://docs.base44.com/developers/references/cli/commands/agents-pull) | Pull agents from Base44 to local files |
| [`agents push`](https://docs.base44.com/developers/references/cli/commands/agents-push) | Push local agents to Base44 |
| [`connectors pull`](https://docs.base44.com/developers/references/cli/commands/connectors-pull) | Pull connectors from Base44 to local files |
| [`connectors push`](https://docs.base44.com/developers/references/cli/commands/connectors-push) | Push local connectors to Base44 |
| [`eject`](https://docs.base44.com/developers/references/cli/commands/eject) | Download the code for an existing Base44 project |
| [`entities push`](https://docs.base44.com/developers/references/cli/commands/entities-push) | Push local entity schemas to Base44 |
| [`functions deploy`](https://docs.base44.com/developers/references/cli/commands/functions-deploy) | Deploy local functions to Base44 |
| [`site deploy`](https://docs.base44.com/developers/references/cli/commands/site-deploy) | Deploy built site files to Base44 hosting |
| [`site open`](https://docs.base44.com/developers/references/cli/commands/site-open) | Open the published site in your browser |


<!--| [`eject`](https://docs.base44.com/developers/references/cli/commands/eject) | Create a Base44 backend project from an existing Base44 app | -->
| [`types generate`](https://docs.base44.com/developers/references/cli/commands/types-generate) | Generate TypeScript types from project resources |

## AI agent skills

Expand Down
173 changes: 0 additions & 173 deletions bun.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions bunfig.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[test]
timeout = 30000
4 changes: 2 additions & 2 deletions docs/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ The Base44 CLI (`base44` npm package) is a TypeScript command-line tool for crea
- **JSON5** - Parsing JSONC/JSON5 config files (supports comments and trailing commas)
- **TypeScript** - Primary language, strict types
- **Biome** - Linting and formatting (replaces ESLint)
- **Vitest** - Test runner
- **Bun Test** - Test runner

## Architecture

Expand Down Expand Up @@ -45,7 +45,7 @@ bun run build # Bundle to dist/index.js + copy templates
bun run typecheck # tsc --noEmit
bun run dev # Run bin/dev.ts (no build needed, Bun runs TS directly)
bun run start # Run bin/run.js (requires build first)
bun run test # Run tests with vitest (use `bun run test`, not `bun test`)
bun test # Run tests
bun run lint # Biome - lint and format check
bun run lint:fix # Biome - auto-fix
```
Expand Down
2 changes: 2 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,12 @@ await runCommand(myAction, { fullBanner: true, requireAuth: true }, context);
export interface CLIContext {
errorReporter: ErrorReporter;
isNonInteractive: boolean;
appConfig?: AppConfig; // Set by runCommand when requireAppConfig is true
}
```

- Created once in `runCLI()` at startup
- `appConfig` is set automatically by `runCommand()` when `requireAppConfig` is `true` (the default). Use `context.appConfig` to access the resolved app config instead of calling `getAppConfig()`.
- `isNonInteractive` is `true` when stdin/stdout are not a TTY (e.g., CI, piped output, AI agents). Use it to skip interactive prompts, browser opens, and animations.
- Passed to `createProgram(context)`, which passes it to each command factory
- Commands pass it to `runCommand()` for error reporting integration
Expand Down
10 changes: 5 additions & 5 deletions docs/testing.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Writing Tests

**Keywords:** test, vitest, testkit, setupCLITests, fixture, mock, Given/When/Then, BASE44_CLI_TEST_OVERRIDES, build before test, MSW
**Keywords:** test, bun test, testkit, setupCLITests, fixture, mock, Given/When/Then, BASE44_CLI_TEST_OVERRIDES, build before test, MSW

## Table of Contents

Expand All @@ -17,7 +17,7 @@
**Build before testing**: Tests import the bundled `dist/index.js`, so always run:

```bash
bun run build && bun run test
bun run build && bun test
```

## How Testing Works
Expand All @@ -27,7 +27,7 @@ Tests use **MSW (Mock Service Worker)** to intercept HTTP requests. The testkit
This means:
- **`vi.mock()` won't work** with path aliases like `@/some/path.js` (they're resolved in the bundle)
- Use the **`BASE44_CLI_TEST_OVERRIDES` env var** for mocking behavior instead (see below)
- Always `bun run build` before `bun run test` to ensure the bundle is fresh
- Always `bun run build` before `bun test` to ensure the bundle is fresh
- Tests always run with `isNonInteractive: true` (no TTY), so browser opens and animations are skipped

## Test Structure
Expand Down Expand Up @@ -57,7 +57,7 @@ tests/
## Writing a Test

```typescript
import { describe, it } from "vitest";
import { describe, it } from "vitest"; // Bun's vitest compat layer provides these
import { setupCLITests, fixture } from "./testkit/index.js";

describe("<command> command", () => {
Expand Down Expand Up @@ -320,7 +320,7 @@ function getTestOverride(): MyType | undefined {

## Testing Rules

1. **Build first** -- Always `bun run build` before `bun run test`
1. **Build first** -- Always `bun run build` before `bun test`
2. **Use fixtures** -- Don't create project structures in tests; use `tests/fixtures/`
3. **Fixtures need `.app.jsonc`** -- Add `base44/.app.jsonc` with `{ "id": "test-app-id" }`
4. **Interactive prompts can't be tested** -- Only test via non-interactive flags
Expand Down
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
"clean": "rm -rf dist && mkdir -p dist",
"lint": "biome check src tests",
"lint:fix": "biome check --write src tests",
"test": "vitest run",
"test:watch": "vitest",
"test": "bun test",
"test:watch": "bun test --watch",
"knip": "knip",
"knip:fix": "knip --fix"
},
Expand Down Expand Up @@ -73,7 +73,6 @@
"tar": "^7.5.4",
"tmp-promise": "^3.0.3",
"typescript": "^5.7.2",
"vitest": "^4.0.16",
"zod": "^4.3.5"
},
"engines": {
Expand Down
5 changes: 3 additions & 2 deletions src/cli/commands/dashboard/open.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import type { RunCommandResult } from "@/cli/utils/runCommand.js";

async function openDashboard(
isNonInteractive: boolean,
appId: string,
): Promise<RunCommandResult> {
const dashboardUrl = getDashboardUrl();
const dashboardUrl = getDashboardUrl(appId);

if (!isNonInteractive) {
await open(dashboardUrl);
Expand All @@ -21,7 +22,7 @@ export function getDashboardOpenCommand(context: CLIContext): Command {
.description("Open the app dashboard in your browser")
.action(async () => {
await runCommand(
() => openDashboard(context.isNonInteractive),
() => openDashboard(context.isNonInteractive, context.appConfig!.id),
{ requireAuth: true },
context,
);
Expand Down
208 changes: 109 additions & 99 deletions src/cli/commands/project/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
createProjectFiles,
listTemplates,
readProjectConfig,
setAppConfig,
withAppConfig,
} from "@/core/project/index.js";

const DEFAULT_TEMPLATE_ID = "backend-only";
Expand Down Expand Up @@ -163,120 +163,130 @@ async function executeCreate({
},
);

// Set app config in cache for sync access to getDashboardUrl and getAppClient
setAppConfig({ id: projectId, projectRoot: resolvedPath });
return await withAppConfig(
{ id: projectId, projectRoot: resolvedPath },
async () => {
const { project, entities } = await readProjectConfig(resolvedPath);
let finalAppUrl: string | undefined;

const { project, entities } = await readProjectConfig(resolvedPath);
let finalAppUrl: string | undefined;
if (entities.length > 0) {
let shouldPushEntities: boolean;

if (entities.length > 0) {
let shouldPushEntities: boolean;
if (isInteractive) {
const result = await confirm({
message:
"Set up the backend data now? (This pushes the data models used by the template to Base44)",
});
shouldPushEntities = !isCancel(result) && result;
} else {
shouldPushEntities = !!deploy;
}

if (isInteractive) {
const result = await confirm({
message:
"Set up the backend data now? (This pushes the data models used by the template to Base44)",
});
shouldPushEntities = !isCancel(result) && result;
} else {
shouldPushEntities = !!deploy;
}
if (shouldPushEntities) {
await runTask(
`Pushing ${entities.length} data models to Base44...`,
async () => {
await pushEntities(entities);
},
{
successMessage: theme.colors.base44Orange(
"Data models pushed successfully",
),
errorMessage: "Failed to push data models",
},
);
}
}

if (shouldPushEntities) {
await runTask(
`Pushing ${entities.length} data models to Base44...`,
async () => {
await pushEntities(entities);
},
{
successMessage: theme.colors.base44Orange(
"Data models pushed successfully",
),
errorMessage: "Failed to push data models",
},
);
}
}
if (project.site) {
const { installCommand, buildCommand, outputDirectory } = project.site;

if (project.site) {
const { installCommand, buildCommand, outputDirectory } = project.site;
let shouldDeploy: boolean;

let shouldDeploy: boolean;
if (isInteractive) {
const result = await confirm({
message:
"Would you like to deploy the site now? (Hosted on Base44)",
});
shouldDeploy = !isCancel(result) && result;
} else {
shouldDeploy = !!deploy;
}

if (isInteractive) {
const result = await confirm({
message: "Would you like to deploy the site now? (Hosted on Base44)",
});
shouldDeploy = !isCancel(result) && result;
} else {
shouldDeploy = !!deploy;
}
if (shouldDeploy && installCommand && buildCommand && outputDirectory) {
const { appUrl } = await runTask(
"Installing dependencies...",
async (updateMessage) => {
await execa({
cwd: resolvedPath,
shell: true,
})`${installCommand}`;

if (shouldDeploy && installCommand && buildCommand && outputDirectory) {
const { appUrl } = await runTask(
"Installing dependencies...",
async (updateMessage) => {
await execa({ cwd: resolvedPath, shell: true })`${installCommand}`;
updateMessage("Building project...");
await execa({ cwd: resolvedPath, shell: true })`${buildCommand}`;

updateMessage("Building project...");
await execa({ cwd: resolvedPath, shell: true })`${buildCommand}`;
updateMessage("Deploying site...");
return await deploySite(join(resolvedPath, outputDirectory));
},
{
successMessage: theme.colors.base44Orange(
"Site deployed successfully",
),
errorMessage: "Failed to deploy site",
},
);

updateMessage("Deploying site...");
return await deploySite(join(resolvedPath, outputDirectory));
},
{
successMessage: theme.colors.base44Orange(
"Site deployed successfully",
),
errorMessage: "Failed to deploy site",
},
);
finalAppUrl = appUrl;
}
}

finalAppUrl = appUrl;
}
}
// Add AI agent skills (--no-skills flag sets skills to false, otherwise defaults to true)
const shouldAddSkills = skills;

// Add AI agent skills (--no-skills flag sets skills to false, otherwise defaults to true)
const shouldAddSkills = skills;
if (shouldAddSkills) {
try {
await runTask(
"Installing AI agent skills...",
async () => {
await execa(
"npx",
["-y", "skills", "add", "base44/skills", "-y"],
{
cwd: resolvedPath,
shell: true,
},
);
},
{
successMessage: theme.colors.base44Orange(
"AI agent skills added successfully",
),
errorMessage:
"Failed to add AI agent skills - you can add them later with: npx skills add base44/skills",
},
);
} catch {
// Skills installation is non-critical (e.g., user may not have git installed)
// The error message is already shown by runTask, so we just continue
}
}

if (shouldAddSkills) {
try {
await runTask(
"Installing AI agent skills...",
async () => {
await execa("npx", ["-y", "skills", "add", "base44/skills", "-y"], {
cwd: resolvedPath,
shell: true,
});
},
{
successMessage: theme.colors.base44Orange(
"AI agent skills added successfully",
),
errorMessage:
"Failed to add AI agent skills - you can add them later with: npx skills add base44/skills",
},
log.message(
`${theme.styles.header("Project")}: ${theme.colors.base44Orange(name)}`,
);
log.message(
`${theme.styles.header("Dashboard")}: ${theme.colors.links(getDashboardUrl(projectId))}`,
);
} catch {
// Skills installation is non-critical (e.g., user may not have git installed)
// The error message is already shown by runTask, so we just continue
}
}

log.message(
`${theme.styles.header("Project")}: ${theme.colors.base44Orange(name)}`,
);
log.message(
`${theme.styles.header("Dashboard")}: ${theme.colors.links(getDashboardUrl(projectId))}`,
);

if (finalAppUrl) {
log.message(
`${theme.styles.header("Site")}: ${theme.colors.links(finalAppUrl)}`,
);
}
if (finalAppUrl) {
log.message(
`${theme.styles.header("Site")}: ${theme.colors.links(finalAppUrl)}`,
);
}

return { outroMessage: "Your project is set up and ready to use" };
return { outroMessage: "Your project is set up and ready to use" };
},
);
}

export function getCreateCommand(context: CLIContext): Command {
Expand Down
4 changes: 3 additions & 1 deletion src/cli/commands/project/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ interface DeployOptions {
yes?: boolean;
projectRoot?: string;
isNonInteractive?: boolean;
appId: string;
}

export async function deployAction(
Expand Down Expand Up @@ -109,7 +110,7 @@ export async function deployAction(
}

log.message(
`${theme.styles.header("Dashboard")}: ${theme.colors.links(getDashboardUrl())}`,
`${theme.styles.header("Dashboard")}: ${theme.colors.links(getDashboardUrl(options.appId!))}`,
);
if (result.appUrl) {
log.message(
Expand All @@ -132,6 +133,7 @@ export function getDeployCommand(context: CLIContext): Command {
deployAction({
...options,
isNonInteractive: context.isNonInteractive,
appId: context.appConfig!.id,
}),
{ requireAuth: true },
context,
Expand Down
Loading
Loading