Conversation
…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>
🚀 Package Preview Available!Install this PR's preview build with npm: npm i @base44-preview/cli@0.0.45-pr.357.9565bc5Prefer not to change any import paths? Install using npm alias so your code still imports npm i "base44@npm:@base44-preview/cli@0.0.45-pr.357.9565bc5"Or add it to your {
"dependencies": {
"base44": "npm:@base44-preview/cli@0.0.45-pr.357.9565bc5"
}
}
Preview published to npm registry — try new features instantly! |
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>
netanelgilad
left a comment
There was a problem hiding this comment.
@claude address the PR review
|
Claude finished @netanelgilad's task in 6m 19s —— View job Addressed PR Review CommentsAll 6 review comments have been implemented:
Summary of changes (
|
- 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>
src/cli/commands/exec.ts
Outdated
| // 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); |
There was a problem hiding this comment.
Does it mean we not only require Deno, but also Deno 2 and above?
Should we then check for that?
kfirstri
left a comment
There was a problem hiding this comment.
Looks good, some comments
src/cli/commands/exec.ts
Outdated
| 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(); |
There was a problem hiding this comment.
Please move all the validation to a commander hook (you can see other commands do it)
src/cli/commands/exec.ts
Outdated
| }, | ||
| stdio: "inherit", | ||
| }, | ||
| ); |
There was a problem hiding this comment.
We need to test this one windows
There was a problem hiding this comment.
how do we test? can we use github's matrix tests?
There was a problem hiding this comment.
No windows yet.. i just meant to check it manually on windows to see that everything works fine
- 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
|
@claude merge main and resolve conflicts |
|
Claude finished @netanelgilad's task in 6m 33s —— View job Merged main into feat/exec-command
Conflicts resolved
Also added |
- 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
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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' }, |
There was a problem hiding this comment.
maybe we can give a better example? like console.log(base44.entities.list())?
| throw noInputError; | ||
| } | ||
|
|
||
| const { exitCode } = await runScript({ code }); |
There was a problem hiding this comment.
do we need any nicer try/catch?
There was a problem hiding this comment.
oh the try/catch is inside the wrapper.. so maybe it's fine
kfirstri
left a comment
There was a problem hiding this comment.
Looks good, added some comments
Note
Description
Adds a new
base44 execcommand that allows users to run TypeScript/JavaScript scripts with the Base44 SDK pre-authenticated and available as a globalbase44variable. 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
Changes Made
base44 execCLI command (packages/cli/src/cli/commands/exec.ts) that reads a script from stdin and runs it via DenorunScriptcore function (packages/cli/src/core/exec/run-script.ts) that spawns Deno with the exec wrapper, passing auth tokens via environment variablespackages/cli/deno-runtime/exec.ts) that creates a pre-authenticated@base44/sdkclient and exposes it as a globalbase44before importing the user's scriptgetAppUserToken()andgetSiteUrl()topackages/cli/src/core/project/api.tsfor token exchange and site URL resolutionverifyDenoInstalled()utility (packages/cli/src/core/utils/dependencies.ts) that gives a user-friendly error with install instructions if Deno is missingprogram.tsand include the Deno runtime asset in the build pipelinetest.yml) so integration tests can actually spawn Denotests/cli/exec.spec.ts) covering stdin validation, token exchange errors, SDK availability, auth headers, and exit code forwardingTesting
npm test)Checklist
docs/(AGENTS.md) if I made architectural changesAdditional Notes
The exec wrapper is copied to a temporary location outside
node_modulesbefore being passed to Deno. This is required for Deno 2.x compatibility — files insidenode_modulesare treated as Node modules and blocknpm:specifiers. The wrapper uses--node-modules-dir=autoso Deno can resolvenpm:@base44/sdkwithout a lock file.🤖 Generated by Claude | 2026-03-18 00:00 UTC