#!/usr/bin/env node /** * Goal: `agentos-shell` should feel like the VM equivalent of `docker run`. * * Keep the CLI surface intentionally close to Docker's process flags: * stdin and TTY mode default on; `--no-interactive` and `--no-tty` disable them. * `-i/--interactive` and `-t/--tty` remain accepted for Docker CLI familiarity, * `-e/--env` and `--env-file` inject environment variables, `-v/--volume` * and `--mount type=bind,...` mount host paths, and `-w/--workdir` chooses * the guest cwd. When TTY mode is requested, the guest command goes through * Agent OS's terminal API instead of a custom prompt or line editor; non-TTY * commands use process spawn with Docker-like stdin attachment rules. */ import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, statSync, writeFileSync,} from "node:fs";import { tmpdir } from "node:os";import { basename, dirname, join, resolve } from "node:path";import { fileURLToPath } from "node:url";import codex from "@agentos-software/codex-cli";import coreutils from "@agentos-software/coreutils";import curl from "@agentos-software/curl";import diffutils from "@agentos-software/diffutils";import fd from "@agentos-software/fd";import file from "@agentos-software/file";import findutils from "@agentos-software/findutils";import gawk from "@agentos-software/gawk";import git from "@agentos-software/git";import grep from "@agentos-software/grep";import gzip from "@agentos-software/gzip";import jq from "@agentos-software/jq";import ripgrep from "@agentos-software/ripgrep";import sed from "@agentos-software/sed";import sqlite3 from "@agentos-software/sqlite3";import tar from "@agentos-software/tar";import tree from "@agentos-software/tree";import unzip from "@agentos-software/unzip";import vim from "@agentos-software/vim";import wget from "@agentos-software/wget";import yq from "@agentos-software/yq";import zip from "@agentos-software/zip";import type { MountConfig, SoftwareInput } from "@rivet-dev/agentos-core";import { AgentOs } from "@rivet-dev/agentos-core";import { allowAll } from "@rivet-dev/agentos-core/internal/runtime-compat";import { Command, Option } from "commander";import { createActorShellVm, type ShellVmHandle } from "./actor-vm.js"; const __dirname = dirname(fileURLToPath(import.meta.url));const workspaceRoot = resolve(__dirname, "../../..");const fallbackCommandDirs = [ resolve( workspaceRoot, "toolchain/target/wasm32-wasip1/release/commands", ),];const BRUSH_SHELL_COMMANDS = new Set(["bash", "sh"]);const SHELL_OPTIONS_WITH_VALUES = new Set([ "--command", "--debuglog-enable", "--init-file", "--input-backend", "--log-disable", "--rcfile", "-c",]); interface CliOptions { interactive: boolean; tty: boolean; actor: boolean; workdir: string; env: string[]; envFile: string[]; volume: string[]; mount: string[]; rm: boolean; name?: string; command: string; args: string[];} // The `@agentos-software/*` packages default-export a package descriptor// pointing at their packed `.aospkg` via `packagePath`. Local transition// builds may still export a package directory as `packagePath`.interface RegistryPackage { name?: string; packagePath?: string;} // Transition package dirs must carry the toolchain-input manifest.function isUsablePackageDir(dir: string | undefined): dir is string { return dir !== undefined && existsSync(resolve(dir, "agentos-package.json"));} function isUsablePackageFile(file: string | undefined): file is string { if (file === undefined) return false; try { return statSync(file).isFile(); } catch { return false; }} // Published packages ship their packed `.aospkg` materialized. Workspace// packages may not have run the native build yet, so the pack can be missing —// fall back to the native command-build output dir when it is. A package whose// dir exists but predates the agentos-package.json manifest requirement is// skipped (with a warning) rather than aborting VM creation for the shell.function withLocalCommandFallback(pkg: RegistryPackage): SoftwareInput | null { if (isUsablePackageFile(pkg.packagePath)) { return { packagePath: pkg.packagePath }; } if (isUsablePackageDir(pkg.packagePath)) { return { packagePath: pkg.packagePath }; } const fallbackCommandDir = fallbackCommandDirs.find(isUsablePackageDir); if (fallbackCommandDir !== undefined) { return { packagePath: fallbackCommandDir }; } console.warn( `agentos-shell: skipping software package without a usable packagePath: ${ pkg.name ?? pkg.packagePath ?? "<unknown>" }`, ); return null;} const software: SoftwareInput[] = [ coreutils, sed, grep, gawk, findutils, diffutils, tar, gzip, curl, zip, unzip, jq, ripgrep, fd, tree, file, yq, codex, git, sqlite3, wget,] .map(withLocalCommandFallback) .filter((input): input is SoftwareInput => input !== null); // vim ships as a software package (which carries its runtime tree + VIMRUNTIME// via the manifest `provides`). An unbuilt package is a valid empty placeholder// — skip it rather than projecting a command-less package (build it locally:// drop the wasm binary into toolchain/target/.../commands and run// `just software-build`).for (const editor of [vim] as RegistryPackage[]) { if (isUsablePackageFile(editor.packagePath)) { software.push({ packagePath: editor.packagePath }); continue; } const packageDir = editor.packagePath; if (packageDir === undefined || !isUsablePackageDir(packageDir)) continue; try { const bin = JSON.parse( readFileSync(join(packageDir, "package.json"), "utf8"), ).bin as Record<string, string> | undefined; if (bin && Object.keys(bin).length > 0) { software.push({ packagePath: packageDir }); } } catch { // placeholder/unreadable — leave the editor out }} function createShellDiagnosticStripper(): ( data: Uint8Array,) => Uint8Array | null { let suppressUntilNewline = false; return (data: Uint8Array) => { let text = Buffer.from(data).toString("utf8"); let output = ""; while (text.length > 0) { if (suppressUntilNewline) { const newlineIndex = text.indexOf("\n"); if (newlineIndex < 0) { return output.length > 0 ? Buffer.from(output, "utf8") : null; } text = text.slice(newlineIndex + 1); suppressUntilNewline = false; continue; } const warningIndex = text.indexOf("WARN could not retrieve pid"); if (warningIndex < 0) { output += text; break; } const lineStartIndex = text.lastIndexOf("\n", warningIndex); const lineStart = lineStartIndex < 0 ? 0 : lineStartIndex + 1; output += text.slice(0, lineStart); const lineEnd = text.indexOf("\n", warningIndex); if (lineEnd < 0) { suppressUntilNewline = true; break; } text = text.slice(lineEnd + 1); } return output.length > 0 ? Buffer.from(output, "utf8") : null; };} function collectOption(value: string, previous: string[]): string[] { previous.push(value); return previous;} function parseCli(argv: string[]): CliOptions { const program = new Command() .name("agentos-shell") .description("Run a command or terminal inside an Agent OS VM.") .exitOverride() .passThroughOptions() .allowExcessArguments() .argument("[command]", "guest command to run", "bash") .argument("[args...]", "guest command arguments") .addOption( new Option("-i, --interactive", "keep stdin attached").default(true), ) .addOption(new Option("--no-interactive", "detach stdin")) .addOption(new Option("-t, --tty", "connect a terminal").default(true)) .addOption(new Option("--no-tty", "disable terminal mode")) .addOption( new Option( "--actor", "run through the RivetKit AgentOS actor instead of the in-process core client", ).default(false), ) .option( "-e, --env <env>", "set environment variable (KEY=VALUE or KEY to copy from host)", collectOption, [], ) .option( "--env-file <path>", "read environment variables from a file", collectOption, [], ) .option( "-v, --volume <spec>", "bind mount a volume (host:guest[:ro|rw])", collectOption, [], ) .option( "--mount <spec>", "bind mount using Docker syntax (type=bind,src=...,target=...,readonly)", collectOption, [], ) .option("-w, --workdir <path>", "working directory inside the VM", "/") .option( "--name <name>", "container-style name label (accepted for Docker CLI parity)", ) .option("--rm", "remove VM after exit (always true for this CLI)", false); try { program.parse(["node", "agentos-shell", ...argv]); } catch (error) { if ( error && typeof error === "object" && "code" in error && error.code === "commander.helpDisplayed" ) { process.exit(0); } throw error; } const opts = program.opts<{ interactive: boolean; tty: boolean; actor: boolean; workdir: string; env: string[]; envFile: string[]; volume: string[]; mount: string[]; rm: boolean; name?: string; }>(); const [command = "bash", ...args] = program.args; return { interactive: opts.interactive, tty: opts.tty, actor: opts.actor, workdir: opts.workdir, env: opts.env, envFile: opts.envFile, volume: opts.volume, mount: opts.mount, rm: opts.rm, name: opts.name, command, args, };} function parseEnvLine(line: string): [string, string] | null { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) { return null; } const equalsIndex = trimmed.indexOf("="); if (equalsIndex < 0) { const hostValue = process.env[trimmed]; return hostValue === undefined ? null : [trimmed, hostValue]; } return [trimmed.slice(0, equalsIndex), trimmed.slice(equalsIndex + 1)];} function buildEnv(options: CliOptions): Record<string, string> { const env: Record<string, string> = {}; for (const envFilePath of options.envFile) { const content = readFileSync(resolve(envFilePath), "utf8"); for (const line of content.split(/\r?\n/)) { const entry = parseEnvLine(line); if (entry) { env[entry[0]] = entry[1]; } } } for (const value of options.env) { const entry = parseEnvLine(value); if (entry) { env[entry[0]] = entry[1]; } } return env;} function hostDirMount( hostPath: string, guestPath: string, readOnly: boolean,): MountConfig { return { path: guestPath, readOnly, plugin: { id: "host_dir", config: { hostPath: resolve(hostPath), readOnly, }, }, };} function parseVolumeSpec(spec: string): MountConfig { const [hostPath, guestPath, mode] = spec.split(":"); if (!hostPath || !guestPath) { throw new Error( `Invalid volume spec "${spec}"; expected host:guest[:ro|rw]`, ); } if (mode && mode !== "ro" && mode !== "rw") { throw new Error(`Invalid volume mode "${mode}" in "${spec}"`); } return hostDirMount(hostPath, guestPath, mode === "ro");} function parseMountSpec(spec: string): MountConfig { const fields = new Map<string, string | true>(); for (const rawPart of spec.split(",")) { const part = rawPart.trim(); if (!part) { continue; } const equalsIndex = part.indexOf("="); if (equalsIndex < 0) { fields.set(part, true); } else { fields.set(part.slice(0, equalsIndex), part.slice(equalsIndex + 1)); } } if (fields.get("type") !== "bind") { throw new Error(`Only bind mounts are supported: --mount ${spec}`); } const source = fields.get("source") ?? fields.get("src"); const target = fields.get("target") ?? fields.get("dst") ?? fields.get("destination"); if (typeof source !== "string" || typeof target !== "string") { throw new Error( `Invalid mount spec "${spec}"; expected type=bind,source=...,target=...`, ); } const readOnly = fields.has("readonly") || fields.get("ro") === "true"; return hostDirMount(source, target, readOnly);} function buildMounts(options: CliOptions): MountConfig[] { return [ ...options.volume.map(parseVolumeSpec), ...options.mount.map(parseMountSpec), ];} function isBrushShellCommand(command: string): boolean { return BRUSH_SHELL_COMMANDS.has(basename(command));} function hasBrushInputBackend(args: string[]): boolean { return args.some( (arg) => arg === "--input-backend" || arg.startsWith("--input-backend="), );} function hasInteractiveShellFlag(args: string[]): boolean { return args.some((arg) => arg === "-i" || arg === "--interactive");} function shellArgsRequestCommandOrScript(args: string[]): boolean { for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === "--") { return i + 1 < args.length; } if (arg.startsWith("--") && arg.includes("=")) { if (arg.startsWith("--command=")) { return true; } continue; } if (SHELL_OPTIONS_WITH_VALUES.has(arg)) { if (arg === "-c" || arg === "--command") { return true; } i++; continue; } if (arg.startsWith("-")) { continue; } return true; } return false;} // The error brush prints when the requested input backend is not compiled// into the wasm build (e.g. reedline missing from the shipped package). Used// to auto-fall back to the always-available `minimal` backend.const BRUSH_BACKEND_UNSUPPORTED_MARKER = "requested input backend type not supported"; /** * Terminal command candidates, tried in order. Prefer `reedline` (history, * arrows, reverse search — requires a brush wasm build with the reedline * feature) and fall back to `minimal` when the shipped build rejects it. An * explicit user-provided backend is used verbatim with no fallback. */function buildTerminalCommandAttempts(options: CliOptions): { command: string; args: string[];}[] { const baseArgs = [...options.args]; if (!isBrushShellCommand(options.command)) { return [{ command: options.command, args: baseArgs }]; } if ( !hasInteractiveShellFlag(baseArgs) && !shellArgsRequestCommandOrScript(baseArgs) ) { baseArgs.push("-i"); } if (hasBrushInputBackend(baseArgs)) { // Explicit backend: use it verbatim; the brush error surfaces as-is. return [{ command: options.command, args: baseArgs }]; } return ["reedline", "minimal"].map((backend) => ({Lines 1–500 of 704. Load more
#!/usr/bin/env node /** * Goal: `agentos-shell` should feel like the VM equivalent of `docker run`. * * Keep the CLI surface intentionally close to Docker's process flags: * stdin and TTY mode default on; `--no-interactive` and `--no-tty` disable them. * `-i/--interactive` and `-t/--tty` remain accepted for Docker CLI familiarity, * `-e/--env` and `--env-file` inject environment variables, `-v/--volume` * and `--mount type=bind,...` mount host paths, and `-w/--workdir` chooses * the guest cwd. When TTY mode is requested, the guest command goes through * Agent OS's terminal API instead of a custom prompt or line editor; non-TTY * commands use process spawn with Docker-like stdin attachment rules. */ import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, statSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import { basename, dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import codex from "@agentos-software/codex-cli"; import coreutils from "@agentos-software/coreutils"; import curl from "@agentos-software/curl"; import diffutils from "@agentos-software/diffutils"; import fd from "@agentos-software/fd"; import file from "@agentos-software/file"; import findutils from "@agentos-software/findutils"; import gawk from "@agentos-software/gawk"; import git from "@agentos-software/git"; import grep from "@agentos-software/grep"; import gzip from "@agentos-software/gzip"; import jq from "@agentos-software/jq"; import ripgrep from "@agentos-software/ripgrep"; import sed from "@agentos-software/sed"; import sqlite3 from "@agentos-software/sqlite3"; import tar from "@agentos-software/tar"; import tree from "@agentos-software/tree"; import unzip from "@agentos-software/unzip"; import vim from "@agentos-software/vim"; import wget from "@agentos-software/wget"; import yq from "@agentos-software/yq"; import zip from "@agentos-software/zip"; import type { MountConfig, SoftwareInput } from "@rivet-dev/agentos-core"; import { AgentOs } from "@rivet-dev/agentos-core"; import { allowAll } from "@rivet-dev/agentos-core/internal/runtime-compat"; import { Command, Option } from "commander"; import { createActorShellVm, type ShellVmHandle } from "./actor-vm.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); const workspaceRoot = resolve(__dirname, "../../.."); const fallbackCommandDirs = [ resolve( workspaceRoot, "toolchain/target/wasm32-wasip1/release/commands", ), ]; const BRUSH_SHELL_COMMANDS = new Set(["bash", "sh"]); const SHELL_OPTIONS_WITH_VALUES = new Set([ "--command", "--debuglog-enable", "--init-file", "--input-backend", "--log-disable", "--rcfile", "-c", ]); interface CliOptions { interactive: boolean; tty: boolean; actor: boolean; workdir: string; env: string[]; envFile: string[]; volume: string[]; mount: string[]; rm: boolean; name?: string; command: string; args: string[]; } // The `@agentos-software/*` packages default-export a package descriptor // pointing at their packed `.aospkg` via `packagePath`. Local transition // builds may still export a package directory as `packagePath`. interface RegistryPackage { name?: string; packagePath?: string; } // Transition package dirs must carry the toolchain-input manifest. function isUsablePackageDir(dir: string | undefined): dir is string { return dir !== undefined && existsSync(resolve(dir, "agentos-package.json")); } function isUsablePackageFile(file: string | undefined): file is string { if (file === undefined) return false; try { return statSync(file).isFile(); } catch { return false; } } // Published packages ship their packed `.aospkg` materialized. Workspace // packages may not have run the native build yet, so the pack can be missing — // fall back to the native command-build output dir when it is. A package whose // dir exists but predates the agentos-package.json manifest requirement is // skipped (with a warning) rather than aborting VM creation for the shell. function withLocalCommandFallback(pkg: RegistryPackage): SoftwareInput | null { if (isUsablePackageFile(pkg.packagePath)) { return { packagePath: pkg.packagePath }; } if (isUsablePackageDir(pkg.packagePath)) { return { packagePath: pkg.packagePath }; } const fallbackCommandDir = fallbackCommandDirs.find(isUsablePackageDir); if (fallbackCommandDir !== undefined) { return { packagePath: fallbackCommandDir }; } console.warn( `agentos-shell: skipping software package without a usable packagePath: ${ pkg.name ?? pkg.packagePath ?? "<unknown>" }`, ); return null; } const software: SoftwareInput[] = [ coreutils, sed, grep, gawk, findutils, diffutils, tar, gzip, curl, zip, unzip, jq, ripgrep, fd, tree, file, yq, codex, git, sqlite3, wget, ] .map(withLocalCommandFallback) .filter((input): input is SoftwareInput => input !== null); // vim ships as a software package (which carries its runtime tree + VIMRUNTIME // via the manifest `provides`). An unbuilt package is a valid empty placeholder // — skip it rather than projecting a command-less package (build it locally: // drop the wasm binary into toolchain/target/.../commands and run // `just software-build`). for (const editor of [vim] as RegistryPackage[]) { if (isUsablePackageFile(editor.packagePath)) { software.push({ packagePath: editor.packagePath }); continue; } const packageDir = editor.packagePath; if (packageDir === undefined || !isUsablePackageDir(packageDir)) continue; try { const bin = JSON.parse( readFileSync(join(packageDir, "package.json"), "utf8"), ).bin as Record<string, string> | undefined; if (bin && Object.keys(bin).length > 0) { software.push({ packagePath: packageDir }); } } catch { // placeholder/unreadable — leave the editor out } } function createShellDiagnosticStripper(): ( data: Uint8Array, ) => Uint8Array | null { let suppressUntilNewline = false; return (data: Uint8Array) => { let text = Buffer.from(data).toString("utf8"); let output = ""; while (text.length > 0) { if (suppressUntilNewline) { const newlineIndex = text.indexOf("\n"); if (newlineIndex < 0) { return output.length > 0 ? Buffer.from(output, "utf8") : null; } text = text.slice(newlineIndex + 1); suppressUntilNewline = false; continue; } const warningIndex = text.indexOf("WARN could not retrieve pid"); if (warningIndex < 0) { output += text; break; } const lineStartIndex = text.lastIndexOf("\n", warningIndex); const lineStart = lineStartIndex < 0 ? 0 : lineStartIndex + 1; output += text.slice(0, lineStart); const lineEnd = text.indexOf("\n", warningIndex); if (lineEnd < 0) { suppressUntilNewline = true; break; } text = text.slice(lineEnd + 1); } return output.length > 0 ? Buffer.from(output, "utf8") : null; }; } function collectOption(value: string, previous: string[]): string[] { previous.push(value); return previous; } function parseCli(argv: string[]): CliOptions { const program = new Command() .name("agentos-shell") .description("Run a command or terminal inside an Agent OS VM.") .exitOverride() .passThroughOptions() .allowExcessArguments() .argument("[command]", "guest command to run", "bash") .argument("[args...]", "guest command arguments") .addOption( new Option("-i, --interactive", "keep stdin attached").default(true), ) .addOption(new Option("--no-interactive", "detach stdin")) .addOption(new Option("-t, --tty", "connect a terminal").default(true)) .addOption(new Option("--no-tty", "disable terminal mode")) .addOption( new Option( "--actor", "run through the RivetKit AgentOS actor instead of the in-process core client", ).default(false), ) .option( "-e, --env <env>", "set environment variable (KEY=VALUE or KEY to copy from host)", collectOption, [], ) .option( "--env-file <path>", "read environment variables from a file", collectOption, [], ) .option( "-v, --volume <spec>", "bind mount a volume (host:guest[:ro|rw])", collectOption, [], ) .option( "--mount <spec>", "bind mount using Docker syntax (type=bind,src=...,target=...,readonly)", collectOption, [], ) .option("-w, --workdir <path>", "working directory inside the VM", "/") .option( "--name <name>", "container-style name label (accepted for Docker CLI parity)", ) .option("--rm", "remove VM after exit (always true for this CLI)", false); try { program.parse(["node", "agentos-shell", ...argv]); } catch (error) { if ( error && typeof error === "object" && "code" in error && error.code === "commander.helpDisplayed" ) { process.exit(0); } throw error; } const opts = program.opts<{ interactive: boolean; tty: boolean; actor: boolean; workdir: string; env: string[]; envFile: string[]; volume: string[]; mount: string[]; rm: boolean; name?: string; }>(); const [command = "bash", ...args] = program.args; return { interactive: opts.interactive, tty: opts.tty, actor: opts.actor, workdir: opts.workdir, env: opts.env, envFile: opts.envFile, volume: opts.volume, mount: opts.mount, rm: opts.rm, name: opts.name, command, args, }; } function parseEnvLine(line: string): [string, string] | null { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) { return null; } const equalsIndex = trimmed.indexOf("="); if (equalsIndex < 0) { const hostValue = process.env[trimmed]; return hostValue === undefined ? null : [trimmed, hostValue]; } return [trimmed.slice(0, equalsIndex), trimmed.slice(equalsIndex + 1)]; } function buildEnv(options: CliOptions): Record<string, string> { const env: Record<string, string> = {}; for (const envFilePath of options.envFile) { const content = readFileSync(resolve(envFilePath), "utf8"); for (const line of content.split(/\r?\n/)) { const entry = parseEnvLine(line); if (entry) { env[entry[0]] = entry[1]; } } } for (const value of options.env) { const entry = parseEnvLine(value); if (entry) { env[entry[0]] = entry[1]; } } return env; } function hostDirMount( hostPath: string, guestPath: string, readOnly: boolean, ): MountConfig { return { path: guestPath, readOnly, plugin: { id: "host_dir", config: { hostPath: resolve(hostPath), readOnly, }, }, }; } function parseVolumeSpec(spec: string): MountConfig { const [hostPath, guestPath, mode] = spec.split(":"); if (!hostPath || !guestPath) { throw new Error( `Invalid volume spec "${spec}"; expected host:guest[:ro|rw]`, ); } if (mode && mode !== "ro" && mode !== "rw") { throw new Error(`Invalid volume mode "${mode}" in "${spec}"`); } return hostDirMount(hostPath, guestPath, mode === "ro"); } function parseMountSpec(spec: string): MountConfig { const fields = new Map<string, string | true>(); for (const rawPart of spec.split(",")) { const part = rawPart.trim(); if (!part) { continue; } const equalsIndex = part.indexOf("="); if (equalsIndex < 0) { fields.set(part, true); } else { fields.set(part.slice(0, equalsIndex), part.slice(equalsIndex + 1)); } } if (fields.get("type") !== "bind") { throw new Error(`Only bind mounts are supported: --mount ${spec}`); } const source = fields.get("source") ?? fields.get("src"); const target = fields.get("target") ?? fields.get("dst") ?? fields.get("destination"); if (typeof source !== "string" || typeof target !== "string") { throw new Error( `Invalid mount spec "${spec}"; expected type=bind,source=...,target=...`, ); } const readOnly = fields.has("readonly") || fields.get("ro") === "true"; return hostDirMount(source, target, readOnly); } function buildMounts(options: CliOptions): MountConfig[] { return [ ...options.volume.map(parseVolumeSpec), ...options.mount.map(parseMountSpec), ]; } function isBrushShellCommand(command: string): boolean { return BRUSH_SHELL_COMMANDS.has(basename(command)); } function hasBrushInputBackend(args: string[]): boolean { return args.some( (arg) => arg === "--input-backend" || arg.startsWith("--input-backend="), ); } function hasInteractiveShellFlag(args: string[]): boolean { return args.some((arg) => arg === "-i" || arg === "--interactive"); } function shellArgsRequestCommandOrScript(args: string[]): boolean { for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === "--") { return i + 1 < args.length; } if (arg.startsWith("--") && arg.includes("=")) { if (arg.startsWith("--command=")) { return true; } continue; } if (SHELL_OPTIONS_WITH_VALUES.has(arg)) { if (arg === "-c" || arg === "--command") { return true; } i++; continue; } if (arg.startsWith("-")) { continue; } return true; } return false; } // The error brush prints when the requested input backend is not compiled // into the wasm build (e.g. reedline missing from the shipped package). Used // to auto-fall back to the always-available `minimal` backend. const BRUSH_BACKEND_UNSUPPORTED_MARKER = "requested input backend type not supported"; /** * Terminal command candidates, tried in order. Prefer `reedline` (history, * arrows, reverse search — requires a brush wasm build with the reedline * feature) and fall back to `minimal` when the shipped build rejects it. An * explicit user-provided backend is used verbatim with no fallback. */ function buildTerminalCommandAttempts(options: CliOptions): { command: string; args: string[]; }[] { const baseArgs = [...options.args]; if (!isBrushShellCommand(options.command)) { return [{ command: options.command, args: baseArgs }]; } if ( !hasInteractiveShellFlag(baseArgs) && !shellArgsRequestCommandOrScript(baseArgs) ) { baseArgs.push("-i"); } if (hasBrushInputBackend(baseArgs)) { // Explicit backend: use it verbatim; the brush error surfaces as-is. return [{ command: options.command, args: baseArgs }]; } return ["reedline", "minimal"].map((backend) => ({