Skip to content

feat: add base44 exec command#357

Merged
netanelgilad merged 31 commits intomainfrom
feat/exec-command
Mar 18, 2026
Merged

feat: add base44 exec command#357
netanelgilad merged 31 commits intomainfrom
feat/exec-command

Conversation

@netanelgilad
Copy link
Contributor

@netanelgilad netanelgilad commented Mar 2, 2026

Note

Description

Adds a new base44 exec command that allows users to run TypeScript/JavaScript scripts with the Base44 SDK pre-authenticated and available as a global base44 variable. Scripts are piped via stdin and executed using Deno as the runtime, with the CLI handling token exchange and SDK setup automatically.

Related Issue

None

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactoring (no functional changes)
  • Other (please describe):

Changes Made

  • Add base44 exec CLI command (packages/cli/src/cli/commands/exec.ts) that reads a script from stdin and runs it via Deno
  • Add runScript core function (packages/cli/src/core/exec/run-script.ts) that spawns Deno with the exec wrapper, passing auth tokens via environment variables
  • Add Deno exec wrapper (packages/cli/deno-runtime/exec.ts) that creates a pre-authenticated @base44/sdk client and exposes it as a global base44 before importing the user's script
  • Add getAppUserToken() and getSiteUrl() to packages/cli/src/core/project/api.ts for token exchange and site URL resolution
  • Add verifyDenoInstalled() utility (packages/cli/src/core/utils/dependencies.ts) that gives a user-friendly error with install instructions if Deno is missing
  • Register exec command in program.ts and include the Deno runtime asset in the build pipeline
  • Add Deno to the CI workflow (test.yml) so integration tests can actually spawn Deno
  • Add comprehensive integration tests (tests/cli/exec.spec.ts) covering stdin validation, token exchange errors, SDK availability, auth headers, and exit code forwarding

Testing

  • I have tested these changes locally
  • I have added/updated tests as needed
  • All tests pass (npm test)

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation (if applicable)
  • My changes generate no new warnings
  • I have updated docs/ (AGENTS.md) if I made architectural changes

Additional Notes

The exec wrapper is copied to a temporary location outside node_modules before being passed to Deno. This is required for Deno 2.x compatibility — files inside node_modules are treated as Node modules and block npm: specifiers. The wrapper uses --node-modules-dir=auto so Deno can resolve npm:@base44/sdk without a lock file.


🤖 Generated by Claude | 2026-03-18 00:00 UTC

…cated SDK

Allows users to run arbitrary Deno scripts with a pre-configured `base44` global
that has the SDK authenticated as the current user. Supports three input modes:
file path, inline eval (-e), and stdin pipe.

The CLI exchanges the platform token for an app user token via the builder auth
endpoint before spawning the Deno subprocess, so scripts run with the user's
app-level permissions (no service role access).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Contributor

github-actions bot commented Mar 2, 2026

🚀 Package Preview Available!


Install this PR's preview build with npm:

npm i @base44-preview/cli@0.0.45-pr.357.9565bc5

Prefer not to change any import paths? Install using npm alias so your code still imports base44:

npm i "base44@npm:@base44-preview/cli@0.0.45-pr.357.9565bc5"

Or add it to your package.json dependencies:

{
  "dependencies": {
    "base44": "npm:@base44-preview/cli@0.0.45-pr.357.9565bc5"
  }
}

Preview published to npm registry — try new features instantly!

netanelgilad and others added 8 commits March 2, 2026 21:14
When running in non-TTY environments (CI, editor integrations, agent
tools), process.stdin.isTTY is false even when a file path or -e flag
is given. This caused a false "Multiple input modes detected" error.

Now stdin is only considered as an input mode when neither file nor
eval is provided, so explicit input modes always take priority.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Deno 2.x requires explicit opt-in for npm: specifier resolution when
no deno.json is present. Without this flag, `npm:@base44/sdk` fails
with ERR_UNSUPPORTED_ESM_URL_SCHEME.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Deno 2.x treats files inside node_modules/ as Node modules, which
don't support npm: specifiers. Copy the exec wrapper to a temp
location outside node_modules so Deno processes it as a regular
Deno script where npm: imports work natively.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The SDK's analytics module starts a heartbeat setInterval on
createClient() which keeps the Deno event loop alive indefinitely.
Call base44.cleanup() after the user script finishes to clear the
interval and allow the process to exit naturally.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move verifyDenoIsInstalled() after input validation so users get
the right error message (e.g. conflicting flags) even when Deno
is not installed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fetch the app's published URL (subdomain) via the existing getSiteUrl()
API and pass it to the Deno exec wrapper as BASE44_APP_BASE_URL. The
wrapper uses it as serverUrl when creating the SDK client so that
functions.invoke() goes through the app domain instead of the platform
domain, which rejects function calls.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor Author

@netanelgilad netanelgilad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@claude address the PR review

@claude
Copy link

claude bot commented Mar 3, 2026

Claude finished @netanelgilad's task in 6m 19s —— View job


Addressed PR Review Comments

All 6 review comments have been implemented:

  • Read relevant files to understand the code
  • infra/build.ts - Removed Bun build step for exec.ts; Deno runs TypeScript natively so we now just copy the raw .ts file to dist/deno-runtime/ instead of bundling it
  • src/cli/commands/exec.ts:32 - Fixed Deno installation URL (getting-startedgetting_started)
  • src/cli/commands/exec.ts:183 - Added explicit --stdin flag to ExecOptions and the command definition; replaced TTY inference (!process.stdin.isTTY) with the explicit flag
  • src/cli/commands/exec.ts:119-121 - Token exchange and site URL fetch now run in parallel via Promise.all; site URL failure is now fatal (throws) instead of silently falling back
  • deno-runtime/exec.ts:40 - Always uses appBaseUrl, removed apiUrl fallbacks; validates appBaseUrl is set and exits with an error if not
  • Updated tests: added --stdin assertion to help test, updated conflict error message check

Summary of changes (10dc541):

  • deno-runtime/exec.ts — removed apiUrl env var, removed fallbacks, added appBaseUrl validation, always passes appBaseUrl to createClient
  • infra/build.ts — dropped denoExec Bun build; replaced with copyFileSync("./deno-runtime/exec.ts", "./dist/deno-runtime/exec.ts")
  • src/cli/commands/exec.ts — explicit --stdin flag, parallel Promise.all for token+siteUrl, fatal on siteUrl failure, .ts wrapper extension, removed getBase44ApiUrl import
  • tests/cli/exec.spec.ts — updated assertions to match new flag and error message
    |

- infra/build.ts: remove Bun build step for exec.ts; Deno runs TS natively so just copy the raw file
- src/cli/commands/exec.ts: add explicit --stdin flag instead of inferring from TTY, run token exchange and site URL fetch in parallel, make site URL fatal (required), fix Deno install URL, update EXEC_WRAPPER_PATH to .ts extension
- deno-runtime/exec.ts: always use appBaseUrl, remove apiUrl fallbacks, validate appBaseUrl is set
- tests/cli/exec.spec.ts: update test assertions for --stdin flag and new error message

Co-authored-by: Netanel Gilad <netanelgilad@users.noreply.github.com>
Comment on lines +121 to +125
// Copy the exec wrapper out of node_modules to a temp location.
// Deno 2.x treats files inside node_modules as Node modules and
// doesn't support npm: specifiers in them.
const tempWrapper = join(tmpdir(), `base44-exec-wrapper-${Date.now()}.ts`);
copyFileSync(EXEC_WRAPPER_PATH, tempWrapper);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it mean we not only require Deno, but also Deno 2 and above?
Should we then check for that?

Copy link
Collaborator

@kfirstri kfirstri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, some comments

Comment on lines +64 to +89
const hasFile = scriptArg !== undefined;
const hasEval = options.eval !== undefined;
const hasStdin = options.stdin === true;

const inputCount = [hasFile, hasEval, hasStdin].filter(Boolean).length;

if (inputCount > 1) {
throw new InvalidInputError(
"Cannot use more than one input mode. Provide only one of: file path, -e, or --stdin.",
);
}

if (inputCount === 0) {
throw new InvalidInputError(
"No script provided. Pass a file path, use -e for inline code, or use --stdin.",
{
hints: [
{ message: "File: base44 exec ./script.ts" },
{ message: 'Eval: base44 exec -e "console.log(1)"' },
{ message: "Stdin: echo 'code' | base44 exec --stdin" },
],
},
);
}

verifyDenoIsInstalled();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move all the validation to a commander hook (you can see other commands do it)

},
stdio: "inherit",
},
);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to test this one windows

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how do we test? can we use github's matrix tests?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No windows yet.. i just meant to check it manually on windows to see that everything works fine

@kfirstri kfirstri moved this from Backlog to In review in CLI Development Mar 4, 2026
@kfirstri kfirstri moved this from Backlog to In review in Backend Platform Mar 4, 2026
- Replace --stdin flag with `-` argument (Unix convention)
- Move exec business logic to core/exec/ module for separation of concerns
- Extract getUserAppToken() and verifyDenoInstalled() to core
- Use tmp-promise for temp file handling (consistency with codebase)
- Share verifyDenoInstalled() between exec and function-manager
- Move input validation to Commander preAction hook
- Add mockAuthToken() to test utils and real Deno success test
- Clarify Deno 1.x/2.x compatibility in comments

Made-with: Cursor
@netanelgilad
Copy link
Contributor Author

@claude merge main and resolve conflicts

@claude
Copy link

claude bot commented Mar 17, 2026

Claude finished @netanelgilad's task in 6m 33s —— View job


Merged main into feat/exec-command

  • Fetch latest main
  • Merge main into feat/exec-command
  • Resolve conflicts — main restructured the repo into a packages/cli/ monorepo
  • Push updated branch

Conflicts resolved

File Resolution
packages/cli/infra/build.ts Kept copyDenoRuntime() from main; also copy exec.ts to dist/deno-runtime/
packages/cli/src/cli/dev/dev-server/function-manager.ts Used spawnSync import from main; removed stale dirname/join/fileURLToPath imports from HEAD
packages/cli/tests/cli/testkit/CLITestkit.ts Used subprocess (execa) approach from main; wired in stdin support via execa's input option
tests/cli/testkit/Base44APIMock.ts Deleted (removed in main); migrated mockAuthToken into TestAPIServer
File location conflicts Moved src/core/exec/packages/cli/src/core/exec/; other files auto-renamed by git

Also added mockAuthToken and mockError helpers to packages/cli/tests/cli/testkit/TestAPIServer.ts so exec.spec.ts tests continue to work.
|

github-actions bot and others added 4 commits March 17, 2026 10:24
- Resolve file location conflicts: moved exec.ts, run-script.ts, exec.spec.ts to packages/cli/ monorepo structure
- Resolve build.ts conflict: keep copyDenoRuntime() from main, also copy exec.ts to dist/deno-runtime/
- Resolve function-manager.ts conflict: keep spawnSync import from main
- Resolve CLITestkit.ts conflict: use subprocess (execa) approach from main with stdin support
- Add mockAuthToken and mockError helpers to TestAPIServer (replacing Base44APIMock)
- Remove Base44APIMock.ts (deleted in main, functionality merged into TestAPIServer)

Co-authored-by: Netanel Gilad <netanelgilad@users.noreply.github.com>
- Remove unused spawnSync import in function-manager.ts
- Fix Biome formatting in TestAPIServer.ts (mockAuthToken, mockError)
- Unexport internal-only symbols in run-script.ts (getUserAppToken,
  RunScriptOptions, RunScriptResult)
- Fix exec "no stdin" test timeout: add empty-stdin check after
  readStdin() since process.stdin.isTTY is never true in child processes
- Fix CLITestkit to close stdin immediately when no content is given,
  preventing hangs in all test commands

Made-with: Cursor
The exec wrapper (dist/deno-runtime/exec.ts) was missing from the
published package because the files array in package.json only
included dist/cli and dist/assets.

Made-with: Cursor
- Add setup-deno step to test workflow so exec tests can spawn Deno
- Move exec.ts into dist/assets/deno-runtime/ (alongside main.ts)
  so it's included in the assets tarball for binary distribution
- Add getExecWrapperPath() to assets.ts and use it in exec command
  instead of resolving via import.meta.url (which doesn't work in
  compiled binaries)
- Remove the separate dist/deno-runtime/ directory

Made-with: Cursor
Adapt exec.ts to use the new Base44Command class introduced in main
(#420) instead of the removed runCommand utility.

Made-with: Cursor
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the core/utils folder was more intended to small utility function for the other resources and files, i would rather have api calls like here to be in core/project folder?

}
}

export async function getSiteUrl(projectId?: string): Promise<string> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't this stay under core/site/api? or maybe core/project as well?

writeFileSync(tempScript.path, code, "utf-8");
const scriptPath = `file://${tempScript.path}`;

const appConfig = getAppConfig();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm working on some refactoring the getAppConfig(), can you call it in your command, and just pass the appId into the runScript function? (it will be provided later by the CLIContext in the command so im trying to minimize dependencies to getAppConfig)

},
);

if (process.stdin.isTTY) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets use the command.isNonInteractive, you can see other commands passing it into the action function and using it

{
hints: [
{ message: "File: cat ./script.ts | base44 exec" },
{ message: 'Eval: echo "console.log(1)" | base44 exec' },
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we can give a better example? like console.log(base44.entities.list())?

throw noInputError;
}

const { exitCode } = await runScript({ code });
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need any nicer try/catch?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh the try/catch is inside the wrapper.. so maybe it's fine

Copy link
Collaborator

@kfirstri kfirstri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, added some comments

@github-project-automation github-project-automation bot moved this from In review to Ready in CLI Development Mar 18, 2026
@github-project-automation github-project-automation bot moved this from Ready to In review in CLI Development Mar 18, 2026
@gonengar gonengar moved this from In review to Documentation in Backend Platform Mar 18, 2026
@netanelgilad netanelgilad merged commit 71849be into main Mar 18, 2026
10 checks passed
@netanelgilad netanelgilad deleted the feat/exec-command branch March 18, 2026 13:14
@github-project-automation github-project-automation bot moved this from Documentation to Done in Backend Platform Mar 18, 2026
@github-project-automation github-project-automation bot moved this from In review to Done in CLI Development Mar 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done
Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants