diff --git a/src/cli.ts b/src/cli.ts index 46c549b..84efda7 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,4 +1,5 @@ #!/usr/bin/env node +import shellquote from 'shell-quote' import { Command } from 'commander' import { SandboxManager } from './index.js' import type { SandboxRuntimeConfig } from './sandbox/sandbox-config.js' @@ -147,8 +148,11 @@ async function main(): Promise { command = options.c logForDebugging(`Command string mode (-c): ${command}`) } else if (commandArgs.length > 0) { - // Default mode: simple join - command = commandArgs.join(' ') + // Default mode: argv-style invocation. The result is later + // executed via `bash -c `, so each arg must be + // shell-quoted to survive that re-parse — a plain join(' ') + // splits arguments containing whitespace (#157). + command = shellquote.quote(commandArgs) logForDebugging(`Original command: ${command}`) } else { console.error( diff --git a/test/cli.test.ts b/test/cli.test.ts index 2e9f74a..2671add 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -98,6 +98,24 @@ describe('CLI', () => { expect(result.stdout).toBe('no newline') expect(result.status).toBe(0) }) + + test('preserves argument boundaries when args contain spaces', () => { + // Regression for #157: positional args are later run via `bash -c`, + // so they must be shell-quoted. printf '%s\n' emits one line per + // arg, which exposes whether "hello world" arrived as one token + // (correct) or was re-split into "hello" and "world". + const result = runCli(['printf', '%s\n', 'hello world', 'a b']) + expect(result.stdout).toBe('hello world\na b\n') + expect(result.status).toBe(0) + }) + + test('preserves shell metacharacters in positional args', () => { + // Positional mode is argv-style; metacharacters in an arg are data, + // not shell syntax (use -c for shell semantics). + const result = runCli(['printf', '%s', '$HOME;|&']) + expect(result.stdout).toBe('$HOME;|&') + expect(result.status).toBe(0) + }) }) describe('error handling', () => {