@cloudcli-ai/cloudcli 1.28.0 → 1.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import{r as em,a as ve}from"./vendor-react-CdSTmIF1.js";var Cs={exports:{}},Bi={};/**
1
+ import{r as em,a as ve}from"./vendor-react-D7WwDXvu.js";var Cs={exports:{}},Bi={};/**
2
2
  * @license React
3
3
  * react-jsx-runtime.production.min.js
4
4
  *
@@ -56,4 +56,4 @@ Error generating stack: `+u.message+`
56
56
  * LICENSE.md file in the root directory of this source tree.
57
57
  *
58
58
  * @license MIT
59
- */const up="6";try{window.__reactRouterVersion=up}catch{}const ip="startTransition",Ha=od[ip];function dp(o){let{basename:f,children:a,future:y,window:g}=o,C=j.useRef();C.current==null&&(C.current=pd({window:g,v5Compat:!0}));let _=C.current,[T,P]=j.useState({action:_.action,location:_.location}),{v7_startTransition:U}=y||{},$=j.useCallback(N=>{U&&Ha?Ha(()=>P(N)):P(N)},[P,U]);return j.useLayoutEffect(()=>_.listen($),[_,$]),j.useEffect(()=>np(y),[y]),j.createElement(lp,{basename:f,children:a,location:T.location,navigationType:T.action,navigator:_,future:y})}var Qa;(function(o){o.UseScrollRestoration="useScrollRestoration",o.UseSubmit="useSubmit",o.UseSubmitFetcher="useSubmitFetcher",o.UseFetcher="useFetcher",o.useViewTransitionState="useViewTransitionState"})(Qa||(Qa={}));var Ka;(function(o){o.UseFetcher="useFetcher",o.UseFetchers="useFetchers",o.UseScrollRestoration="useScrollRestoration"})(Ka||(Ka={}));export{dp as B,sp as R,j as a,fd as b,id as c,op as d,cp as e,fp as f,Ya as g,rp as h,Xa as r,ap as u};
59
+ */const up="6";try{window.__reactRouterVersion=up}catch{}const ip="startTransition",Ha=od[ip];function dp(o){let{basename:f,children:a,future:y,window:g}=o,C=j.useRef();C.current==null&&(C.current=pd({window:g,v5Compat:!0}));let _=C.current,[T,P]=j.useState({action:_.action,location:_.location}),{v7_startTransition:U}=y||{},$=j.useCallback(N=>{U&&Ha?Ha(()=>P(N)):P(N)},[P,U]);return j.useLayoutEffect(()=>_.listen($),[_,$]),j.useEffect(()=>np(y),[y]),j.createElement(lp,{basename:f,children:a,location:T.location,navigationType:T.action,navigator:_,future:y})}var Qa;(function(o){o.UseScrollRestoration="useScrollRestoration",o.UseSubmit="useSubmit",o.UseSubmitFetcher="useSubmitFetcher",o.UseFetcher="useFetcher",o.useViewTransitionState="useViewTransitionState"})(Qa||(Qa={}));var Ka;(function(o){o.UseFetcher="useFetcher",o.UseFetchers="useFetchers",o.UseScrollRestoration="useScrollRestoration"})(Ka||(Ka={}));export{dp as B,sp as R,j as a,fd as b,dd as c,id as d,op as e,cp as f,Ya as g,fp as h,rp as i,Xa as r,ap as u};
package/dist/index.html CHANGED
@@ -25,11 +25,11 @@
25
25
 
26
26
  <!-- Prevent zoom on iOS -->
27
27
  <meta name="format-detection" content="telephone=no" />
28
- <script type="module" crossorigin src="/assets/index-BWQPmiP5.js"></script>
29
- <link rel="modulepreload" crossorigin href="/assets/vendor-react-CdSTmIF1.js">
30
- <link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-C8f1vU1x.js">
28
+ <script type="module" crossorigin src="/assets/index-Cdn2Scm1.js"></script>
29
+ <link rel="modulepreload" crossorigin href="/assets/vendor-react-D7WwDXvu.js">
30
+ <link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-NA4v81it.js">
31
31
  <link rel="modulepreload" crossorigin href="/assets/vendor-xterm-CJZjLICi.js">
32
- <link rel="stylesheet" crossorigin href="/assets/index-B1XAhG_4.css">
32
+ <link rel="stylesheet" crossorigin href="/assets/index-ByHZ8-LL.css">
33
33
  </head>
34
34
  <body>
35
35
  <div id="root"></div>
package/dist/sw.js CHANGED
@@ -1,4 +1,4 @@
1
- // Service Worker for Claude Code UI PWA
1
+ // Service Worker for CloudCLI PWA
2
2
  // Cache only manifest (needed for PWA install). HTML and JS are never pre-cached
3
3
  // so a rebuild + refresh always picks up the latest assets.
4
4
  const CACHE_NAME = 'claude-ui-v2';
@@ -79,7 +79,7 @@ self.addEventListener('push', event => {
79
79
  try {
80
80
  payload = event.data.json();
81
81
  } catch {
82
- payload = { title: 'Claude Code UI', body: event.data.text() };
82
+ payload = { title: 'CloudCLI', body: event.data.text() };
83
83
  }
84
84
 
85
85
  const options = {
@@ -92,7 +92,7 @@ self.addEventListener('push', event => {
92
92
  };
93
93
 
94
94
  event.waitUntil(
95
- self.registration.showNotification(payload.title || 'Claude Code UI', options)
95
+ self.registration.showNotification(payload.title || 'CloudCLI', options)
96
96
  );
97
97
  });
98
98
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudcli-ai/cloudcli",
3
- "version": "1.28.0",
3
+ "version": "1.29.0",
4
4
  "description": "A web-based UI for Claude Code CLI",
5
5
  "type": "module",
6
6
  "main": "server/index.js",
@@ -35,7 +35,8 @@
35
35
  "release": "./release.sh",
36
36
  "prepublishOnly": "npm run build",
37
37
  "postinstall": "node scripts/fix-node-pty.js",
38
- "prepare": "husky"
38
+ "prepare": "husky",
39
+ "update:platform": "./update-platform.sh"
39
40
  },
40
41
  "keywords": [
41
42
  "claude code",
package/server/cli.js CHANGED
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Claude Code UI CLI
3
+ * CloudCLI CLI
4
4
  *
5
- * Provides command-line utilities for managing Claude Code UI
5
+ * Provides command-line utilities for managing CloudCLI
6
6
  *
7
7
  * Commands:
8
8
  * (no args) - Start the server (default)
9
9
  * start - Start the server
10
+ * sandbox - Manage Docker sandbox environments
10
11
  * status - Show configuration and data locations
11
12
  * help - Show help information
12
13
  * version - Show version information
@@ -84,7 +85,7 @@ function getInstallDir() {
84
85
 
85
86
  // Show status command
86
87
  function showStatus() {
87
- console.log(`\n${c.bright('Claude Code UI - Status')}\n`);
88
+ console.log(`\n${c.bright('CloudCLI UI - Status')}\n`);
88
89
  console.log(c.dim('═'.repeat(60)));
89
90
 
90
91
  // Version info
@@ -141,7 +142,7 @@ function showStatus() {
141
142
  function showHelp() {
142
143
  console.log(`
143
144
  ╔═══════════════════════════════════════════════════════════════╗
144
- Claude Code UI - Command Line Tool ║
145
+ CloudCLI - Command Line Tool ║
145
146
  ╚═══════════════════════════════════════════════════════════════╝
146
147
 
147
148
  Usage:
@@ -149,7 +150,8 @@ Usage:
149
150
  cloudcli [command] [options]
150
151
 
151
152
  Commands:
152
- start Start the Claude Code UI server (default)
153
+ start Start the CloudCLI server (default)
154
+ sandbox Manage Docker sandbox environments
153
155
  status Show configuration and data locations
154
156
  update Update to the latest version
155
157
  help Show this help information
@@ -164,8 +166,7 @@ Options:
164
166
  Examples:
165
167
  $ cloudcli # Start with defaults
166
168
  $ cloudcli --port 8080 # Start on port 8080
167
- $ cloudcli -p 3000 # Short form for port
168
- $ cloudcli start --port 4000 # Explicit start command
169
+ $ cloudcli sandbox ~/my-project # Run in a Docker sandbox
169
170
  $ cloudcli status # Show configuration
170
171
 
171
172
  Environment Variables:
@@ -244,6 +245,357 @@ async function updatePackage() {
244
245
  }
245
246
  }
246
247
 
248
+ // ── Sandbox command ─────────────────────────────────────────
249
+
250
+ const SANDBOX_TEMPLATES = {
251
+ claude: 'docker.io/cloudcliai/sandbox:claude-code',
252
+ codex: 'docker.io/cloudcliai/sandbox:codex',
253
+ gemini: 'docker.io/cloudcliai/sandbox:gemini',
254
+ };
255
+
256
+ const SANDBOX_SECRETS = {
257
+ claude: 'anthropic',
258
+ codex: 'openai',
259
+ gemini: 'google',
260
+ };
261
+
262
+ function parseSandboxArgs(args) {
263
+ const result = {
264
+ subcommand: null,
265
+ workspace: null,
266
+ agent: 'claude',
267
+ name: null,
268
+ port: 3001,
269
+ template: null,
270
+ env: [],
271
+ };
272
+
273
+ const subcommands = ['ls', 'stop', 'start', 'rm', 'logs', 'help'];
274
+
275
+ for (let i = 0; i < args.length; i++) {
276
+ const arg = args[i];
277
+
278
+ if (i === 0 && subcommands.includes(arg)) {
279
+ result.subcommand = arg;
280
+ } else if (arg === '--agent' || arg === '-a') {
281
+ result.agent = args[++i];
282
+ } else if (arg === '--name' || arg === '-n') {
283
+ result.name = args[++i];
284
+ } else if (arg === '--port') {
285
+ result.port = parseInt(args[++i], 10);
286
+ } else if (arg === '--template' || arg === '-t') {
287
+ result.template = args[++i];
288
+ } else if (arg === '--env' || arg === '-e') {
289
+ result.env.push(args[++i]);
290
+ } else if (!arg.startsWith('-')) {
291
+ if (!result.subcommand) {
292
+ result.workspace = arg;
293
+ } else {
294
+ result.name = arg; // for stop/start/rm/logs <name>
295
+ }
296
+ }
297
+ }
298
+
299
+ // Default subcommand based on what we got
300
+ if (!result.subcommand) {
301
+ result.subcommand = 'create';
302
+ }
303
+
304
+ // Derive name from workspace path if not set
305
+ if (!result.name && result.workspace) {
306
+ result.name = path.basename(path.resolve(result.workspace.replace(/^~/, os.homedir())));
307
+ }
308
+
309
+ // Default template from agent
310
+ if (!result.template) {
311
+ result.template = SANDBOX_TEMPLATES[result.agent] || SANDBOX_TEMPLATES.claude;
312
+ }
313
+
314
+ return result;
315
+ }
316
+
317
+ function showSandboxHelp() {
318
+ console.log(`
319
+ ${c.bright('CloudCLI Sandbox')} — Run CloudCLI inside Docker Sandboxes
320
+
321
+ Usage:
322
+ cloudcli sandbox <workspace> Create and start a sandbox
323
+ cloudcli sandbox <subcommand> [name] Manage sandboxes
324
+
325
+ Subcommands:
326
+ ${c.bright('(default)')} Create a sandbox and start the web UI
327
+ ${c.bright('ls')} List all sandboxes
328
+ ${c.bright('start')} Restart a stopped sandbox and re-launch the web UI
329
+ ${c.bright('stop')} Stop a sandbox (preserves state)
330
+ ${c.bright('rm')} Remove a sandbox
331
+ ${c.bright('logs')} Show CloudCLI server logs
332
+ ${c.bright('help')} Show this help
333
+
334
+ Options:
335
+ -a, --agent <agent> Agent to use: claude, codex, gemini (default: claude)
336
+ -n, --name <name> Sandbox name (default: derived from workspace folder)
337
+ -t, --template <image> Custom template image
338
+ -e, --env <KEY=VALUE> Set environment variable (repeatable)
339
+ --port <port> Host port for the web UI (default: 3001)
340
+
341
+ Examples:
342
+ $ cloudcli sandbox ~/my-project
343
+ $ cloudcli sandbox ~/my-project --agent codex --port 8080
344
+ $ cloudcli sandbox ~/my-project --env SERVER_PORT=8080 --env HOST=0.0.0.0
345
+ $ cloudcli sandbox ls
346
+ $ cloudcli sandbox stop my-project
347
+ $ cloudcli sandbox start my-project
348
+ $ cloudcli sandbox rm my-project
349
+
350
+ Prerequisites:
351
+ 1. Install sbx CLI: https://docs.docker.com/ai/sandboxes/get-started/
352
+ 2. Authenticate and store your API key:
353
+ sbx login
354
+ sbx secret set -g anthropic # for Claude
355
+ sbx secret set -g openai # for Codex
356
+ sbx secret set -g google # for Gemini
357
+
358
+ Advanced usage:
359
+ For branch mode, multiple workspaces, memory limits, network policies,
360
+ or passing prompts to the agent, use sbx directly with the template:
361
+
362
+ sbx run --template docker.io/cloudcliai/sandbox:claude-code claude ~/my-project --branch my-feature
363
+ sbx run --template docker.io/cloudcliai/sandbox:claude-code claude ~/project ~/libs:ro --memory 8g
364
+
365
+ Full Docker Sandboxes docs: https://docs.docker.com/ai/sandboxes/usage/
366
+ `);
367
+ }
368
+
369
+ async function sandboxCommand(args) {
370
+ const { execFileSync } = await import('child_process');
371
+
372
+ // Safe execution — uses execFileSync (no shell) to prevent injection
373
+ const sbx = (subcmd, opts = {}) => {
374
+ const result = execFileSync('sbx', subcmd, {
375
+ encoding: 'utf8',
376
+ stdio: opts.inherit ? 'inherit' : 'pipe',
377
+ });
378
+ return result || '';
379
+ };
380
+
381
+ const opts = parseSandboxArgs(args);
382
+
383
+ if (opts.subcommand === 'help') {
384
+ showSandboxHelp();
385
+ return;
386
+ }
387
+
388
+ // Validate name (alphanumeric, hyphens, underscores only)
389
+ if (opts.name && !/^[\w-]+$/.test(opts.name)) {
390
+ console.error(`\n${c.error('❌')} Invalid sandbox name: ${opts.name}`);
391
+ console.log(` Names may only contain letters, numbers, hyphens, and underscores.\n`);
392
+ process.exit(1);
393
+ }
394
+
395
+ // Check sbx is installed
396
+ try {
397
+ sbx(['version']);
398
+ } catch {
399
+ console.error(`\n${c.error('❌')} ${c.bright('sbx')} CLI not found.\n`);
400
+ console.log(` Install it from: ${c.info('https://docs.docker.com/ai/sandboxes/get-started/')}`);
401
+ console.log(` Then run: ${c.bright('sbx login')}`);
402
+ console.log(` And store your API key: ${c.bright('sbx secret set -g anthropic')}\n`);
403
+ process.exit(1);
404
+ }
405
+
406
+ switch (opts.subcommand) {
407
+
408
+ case 'ls':
409
+ sbx(['ls'], { inherit: true });
410
+ break;
411
+
412
+ case 'stop':
413
+ if (!opts.name) {
414
+ console.error(`\n${c.error('❌')} Sandbox name required: cloudcli sandbox stop <name>\n`);
415
+ process.exit(1);
416
+ }
417
+ sbx(['stop', opts.name], { inherit: true });
418
+ break;
419
+
420
+ case 'rm':
421
+ if (!opts.name) {
422
+ console.error(`\n${c.error('❌')} Sandbox name required: cloudcli sandbox rm <name>\n`);
423
+ process.exit(1);
424
+ }
425
+ sbx(['rm', opts.name], { inherit: true });
426
+ break;
427
+
428
+ case 'logs':
429
+ if (!opts.name) {
430
+ console.error(`\n${c.error('❌')} Sandbox name required: cloudcli sandbox logs <name>\n`);
431
+ process.exit(1);
432
+ }
433
+ try {
434
+ sbx(['exec', opts.name, 'bash', '-c', 'cat /tmp/cloudcli-ui.log'], { inherit: true });
435
+ } catch (e) {
436
+ console.error(`\n${c.error('❌')} Could not read logs: ${e.message || 'Is the sandbox running?'}\n`);
437
+ }
438
+ break;
439
+
440
+ case 'start': {
441
+ if (!opts.name) {
442
+ console.error(`\n${c.error('❌')} Sandbox name required: cloudcli sandbox start <name>\n`);
443
+ process.exit(1);
444
+ }
445
+ console.log(`\n${c.info('▶')} Starting sandbox ${c.bright(opts.name)}...`);
446
+ try {
447
+ sbx(['start', opts.name], { inherit: true });
448
+ } catch { /* might already be running */ }
449
+
450
+ console.log(`${c.info('▶')} Launching CloudCLI web server...`);
451
+ sbx(['exec', '-d', opts.name, 'bash', '-c', '. ~/.cloudcli-start.sh']);
452
+
453
+ console.log(`${c.info('▶')} Forwarding port ${opts.port} → 3001...`);
454
+ try {
455
+ sbx(['ports', opts.name, '--publish', `${opts.port}:3001`]);
456
+ } catch (e) {
457
+ const msg = e.stdout || e.stderr || e.message || '';
458
+ if (msg.includes('address already in use')) {
459
+ const altPort = opts.port + 1;
460
+ console.log(`${c.warn('⚠')} Port ${opts.port} in use, trying ${altPort}...`);
461
+ try {
462
+ sbx(['ports', opts.name, '--publish', `${altPort}:3001`]);
463
+ opts.port = altPort;
464
+ } catch {
465
+ console.error(`${c.error('❌')} Ports ${opts.port} and ${altPort} both in use. Use --port to specify a free port.`);
466
+ process.exit(1);
467
+ }
468
+ } else {
469
+ throw e;
470
+ }
471
+ }
472
+
473
+ console.log(`\n${c.ok('✔')} ${c.bright('CloudCLI is ready!')}`);
474
+ console.log(` ${c.info('→')} ${c.bright(`http://localhost:${opts.port}`)}\n`);
475
+ break;
476
+ }
477
+
478
+ case 'create': {
479
+ if (!opts.workspace) {
480
+ console.error(`\n${c.error('❌')} Workspace path required: cloudcli sandbox <path>\n`);
481
+ console.log(` Example: ${c.bright('cloudcli sandbox ~/my-project')}\n`);
482
+ process.exit(1);
483
+ }
484
+
485
+ const workspace = opts.workspace.startsWith('~')
486
+ ? opts.workspace.replace(/^~/, os.homedir())
487
+ : path.resolve(opts.workspace);
488
+
489
+ if (!fs.existsSync(workspace)) {
490
+ console.error(`\n${c.error('❌')} Workspace path not found: ${c.dim(workspace)}\n`);
491
+ process.exit(1);
492
+ }
493
+
494
+ const secret = SANDBOX_SECRETS[opts.agent] || 'anthropic';
495
+
496
+ // Check if the required secret is stored
497
+ try {
498
+ const secretList = sbx(['secret', 'ls']);
499
+ if (!secretList.includes(secret)) {
500
+ console.error(`\n${c.error('❌')} No ${c.bright(secret)} API key found.\n`);
501
+ console.log(` Run: ${c.bright(`sbx secret set -g ${secret}`)}\n`);
502
+ process.exit(1);
503
+ }
504
+ } catch { /* sbx secret ls not available, skip check */ }
505
+
506
+ console.log(`\n${c.bright('CloudCLI Sandbox')}`);
507
+ console.log(c.dim('─'.repeat(50)));
508
+ console.log(` Agent: ${c.info(opts.agent)} ${c.dim(`(${secret} credentials)`)}`);
509
+ console.log(` Workspace: ${c.dim(workspace)}`);
510
+ console.log(` Name: ${c.dim(opts.name)}`);
511
+ console.log(` Template: ${c.dim(opts.template)}`);
512
+ console.log(` Port: ${c.dim(String(opts.port))}`);
513
+ if (opts.env.length > 0) {
514
+ console.log(` Env: ${c.dim(opts.env.join(', '))}`);
515
+ }
516
+ console.log(c.dim('─'.repeat(50)));
517
+
518
+ // Step 1: Create sandbox
519
+ console.log(`\n${c.info('▶')} Creating sandbox ${c.bright(opts.name)}...`);
520
+ try {
521
+ sbx(
522
+ ['create', '--template', opts.template, '--name', opts.name, opts.agent, workspace],
523
+ { inherit: true }
524
+ );
525
+ } catch (e) {
526
+ const msg = e.stdout || e.stderr || e.message || '';
527
+ if (msg.includes('already exists')) {
528
+ console.log(`${c.warn('⚠')} Sandbox ${c.bright(opts.name)} already exists. Starting it instead...\n`);
529
+ try { sbx(['start', opts.name]); } catch { /* may already be running */ }
530
+ } else {
531
+ throw e;
532
+ }
533
+ }
534
+
535
+ // Step 2: Inject environment variables
536
+ if (opts.env.length > 0) {
537
+ console.log(`${c.info('▶')} Setting environment variables...`);
538
+ const exports = opts.env
539
+ .filter(e => /^\w+=.+$/.test(e))
540
+ .map(e => `export ${e}`)
541
+ .join('\n');
542
+ if (exports) {
543
+ sbx(['exec', opts.name, 'bash', '-c', `echo '${exports}' >> /etc/sandbox-persistent.sh`]);
544
+ }
545
+ const invalid = opts.env.filter(e => !/^\w+=.+$/.test(e));
546
+ if (invalid.length > 0) {
547
+ console.log(`${c.warn('⚠')} Skipped invalid env vars: ${invalid.join(', ')} (expected KEY=VALUE)`);
548
+ }
549
+ }
550
+
551
+ // Step 3: Start CloudCLI
552
+ console.log(`${c.info('▶')} Launching CloudCLI web server...`);
553
+ try {
554
+ sbx(['exec', '-d', opts.name, 'bash', '-c', '. ~/.cloudcli-start.sh']);
555
+ } catch (e) {
556
+ console.error(`${c.error('❌')} Failed to start CloudCLI: ${e.message}`);
557
+ process.exit(1);
558
+ }
559
+
560
+ // Step 4: Forward port
561
+ console.log(`${c.info('▶')} Forwarding port ${opts.port} → 3001...`);
562
+ try {
563
+ sbx(['ports', opts.name, '--publish', `${opts.port}:3001`]);
564
+ } catch (e) {
565
+ const msg = e.stdout || e.stderr || e.message || '';
566
+ if (msg.includes('address already in use')) {
567
+ const altPort = opts.port + 1;
568
+ console.log(`${c.warn('⚠')} Port ${opts.port} in use, trying ${altPort}...`);
569
+ try {
570
+ sbx(['ports', opts.name, '--publish', `${altPort}:3001`]);
571
+ opts.port = altPort;
572
+ } catch {
573
+ console.error(`${c.error('❌')} Ports ${opts.port} and ${altPort} both in use. Use --port to specify a free port.`);
574
+ process.exit(1);
575
+ }
576
+ } else {
577
+ throw e;
578
+ }
579
+ }
580
+
581
+ // Done
582
+ console.log(`\n${c.ok('✔')} ${c.bright('CloudCLI is ready!')}`);
583
+ console.log(` ${c.info('→')} Open ${c.bright(`http://localhost:${opts.port}`)}`);
584
+ console.log(`\n${c.dim(' Manage with:')}`);
585
+ console.log(` ${c.dim('$')} cloudcli sandbox ls`);
586
+ console.log(` ${c.dim('$')} cloudcli sandbox stop ${opts.name}`);
587
+ console.log(` ${c.dim('$')} cloudcli sandbox start ${opts.name}`);
588
+ console.log(` ${c.dim('$')} cloudcli sandbox rm ${opts.name}\n`);
589
+ break;
590
+ }
591
+
592
+ default:
593
+ showSandboxHelp();
594
+ }
595
+ }
596
+
597
+ // ── Server ──────────────────────────────────────────────────
598
+
247
599
  // Start the server
248
600
  async function startServer() {
249
601
  // Check for updates silently on startup
@@ -274,6 +626,10 @@ function parseArgs(args) {
274
626
  parsed.command = 'version';
275
627
  } else if (!arg.startsWith('-')) {
276
628
  parsed.command = arg;
629
+ if (arg === 'sandbox') {
630
+ parsed.remainingArgs = args.slice(i + 1);
631
+ break;
632
+ }
277
633
  }
278
634
  }
279
635
 
@@ -283,7 +639,7 @@ function parseArgs(args) {
283
639
  // Main CLI handler
284
640
  async function main() {
285
641
  const args = process.argv.slice(2);
286
- const { command, options } = parseArgs(args);
642
+ const { command, options, remainingArgs } = parseArgs(args);
287
643
 
288
644
  // Apply CLI options to environment variables
289
645
  if (options.serverPort) {
@@ -299,6 +655,9 @@ async function main() {
299
655
  case 'start':
300
656
  await startServer();
301
657
  break;
658
+ case 'sandbox':
659
+ await sandboxCommand(remainingArgs || []);
660
+ break;
302
661
  case 'status':
303
662
  case 'info':
304
663
  showStatus();