@cloudcli-ai/cloudcli 1.28.1 → 1.29.1

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.
package/dist/index.html CHANGED
@@ -25,7 +25,7 @@
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-_9VG3czA.js"></script>
28
+ <script type="module" crossorigin src="/assets/index-BOWaxSnV.js"></script>
29
29
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-D7WwDXvu.js">
30
30
  <link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-NA4v81it.js">
31
31
  <link rel="modulepreload" crossorigin href="/assets/vendor-xterm-CJZjLICi.js">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudcli-ai/cloudcli",
3
- "version": "1.28.1",
3
+ "version": "1.29.1",
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
@@ -7,6 +7,7 @@
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
@@ -150,6 +151,7 @@ Usage:
150
151
 
151
152
  Commands:
152
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,355 @@ 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, 'cloudcli', 'start', '--port', '3001']);
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 as a long-running detached exec session.
552
+ // Using -d with a long-running command (cloudcli start never exits)
553
+ // keeps the exec session alive, which keeps the sandbox running.
554
+ console.log(`${c.info('▶')} Launching CloudCLI web server...`);
555
+ sbx(['exec', '-d', opts.name, 'cloudcli', 'start', '--port', '3001']);
556
+
557
+ // Step 4: Forward port
558
+ console.log(`${c.info('▶')} Forwarding port ${opts.port} → 3001...`);
559
+ try {
560
+ sbx(['ports', opts.name, '--publish', `${opts.port}:3001`]);
561
+ } catch (e) {
562
+ const msg = e.stdout || e.stderr || e.message || '';
563
+ if (msg.includes('address already in use')) {
564
+ const altPort = opts.port + 1;
565
+ console.log(`${c.warn('⚠')} Port ${opts.port} in use, trying ${altPort}...`);
566
+ try {
567
+ sbx(['ports', opts.name, '--publish', `${altPort}:3001`]);
568
+ opts.port = altPort;
569
+ } catch {
570
+ console.error(`${c.error('❌')} Ports ${opts.port} and ${altPort} both in use. Use --port to specify a free port.`);
571
+ process.exit(1);
572
+ }
573
+ } else {
574
+ throw e;
575
+ }
576
+ }
577
+
578
+ // Done
579
+ console.log(`\n${c.ok('✔')} ${c.bright('CloudCLI is ready!')}`);
580
+ console.log(` ${c.info('→')} Open ${c.bright(`http://localhost:${opts.port}`)}`);
581
+ console.log(`\n${c.dim(' Manage with:')}`);
582
+ console.log(` ${c.dim('$')} sbx ls`);
583
+ console.log(` ${c.dim('$')} sbx stop ${opts.name}`);
584
+ console.log(` ${c.dim('$')} sbx start ${opts.name}`);
585
+ console.log(` ${c.dim('$')} sbx rm ${opts.name}`);
586
+ console.log(`\n${c.dim(' Or install globally:')} npm install -g @cloudcli-ai/cloudcli\n`);
587
+ break;
588
+ }
589
+
590
+ default:
591
+ showSandboxHelp();
592
+ }
593
+ }
594
+
595
+ // ── Server ──────────────────────────────────────────────────
596
+
247
597
  // Start the server
248
598
  async function startServer() {
249
599
  // Check for updates silently on startup
@@ -274,6 +624,10 @@ function parseArgs(args) {
274
624
  parsed.command = 'version';
275
625
  } else if (!arg.startsWith('-')) {
276
626
  parsed.command = arg;
627
+ if (arg === 'sandbox') {
628
+ parsed.remainingArgs = args.slice(i + 1);
629
+ break;
630
+ }
277
631
  }
278
632
  }
279
633
 
@@ -283,7 +637,7 @@ function parseArgs(args) {
283
637
  // Main CLI handler
284
638
  async function main() {
285
639
  const args = process.argv.slice(2);
286
- const { command, options } = parseArgs(args);
640
+ const { command, options, remainingArgs } = parseArgs(args);
287
641
 
288
642
  // Apply CLI options to environment variables
289
643
  if (options.serverPort) {
@@ -299,6 +653,9 @@ async function main() {
299
653
  case 'start':
300
654
  await startServer();
301
655
  break;
656
+ case 'sandbox':
657
+ await sandboxCommand(remainingArgs || []);
658
+ break;
302
659
  case 'status':
303
660
  case 'info':
304
661
  showStatus();
package/server/index.js CHANGED
@@ -435,13 +435,20 @@ app.post('/api/system/update', authenticateToken, async (req, res) => {
435
435
 
436
436
  console.log('Starting system update from directory:', projectRoot);
437
437
 
438
- // Run the update command based on install mode
439
- const updateCommand = installMode === 'git'
440
- ? 'git checkout main && git pull && npm install'
441
- : 'npm install -g @cloudcli-ai/cloudcli@latest';
438
+ // Platform deployments use their own update workflow from the project root.
439
+ const updateCommand = IS_PLATFORM
440
+ // In platform, husky and dev dependencies are not needed
441
+ ? 'npm run update:platform'
442
+ : installMode === 'git'
443
+ ? 'git checkout main && git pull && npm install'
444
+ : 'npm install -g @cloudcli-ai/cloudcli@latest';
445
+
446
+ const updateCwd = IS_PLATFORM || installMode === 'git'
447
+ ? projectRoot
448
+ : os.homedir();
442
449
 
443
450
  const child = spawn('sh', ['-c', updateCommand], {
444
- cwd: installMode === 'git' ? projectRoot : os.homedir(),
451
+ cwd: updateCwd,
445
452
  env: process.env
446
453
  });
447
454