@orchagent/cli 0.3.61 → 0.3.63

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.
@@ -92,6 +92,7 @@ function registerServiceCommand(program) {
92
92
  .option('--secret <NAME>', 'Workspace secret name (repeatable)', collectArray, [])
93
93
  .option('--command <cmd>', 'Override entrypoint command')
94
94
  .option('--arg <value>', 'Command argument (repeatable)', collectArray, [])
95
+ .option('--pin', 'Pin to deployed version (disable auto-update on publish)')
95
96
  .option('--json', 'Output as JSON')
96
97
  .action(async (agentArg, options) => {
97
98
  const config = await (0, config_1.getResolvedConfig)();
@@ -160,6 +161,7 @@ function registerServiceCommand(program) {
160
161
  args: options.arg.length > 0 ? options.arg : null,
161
162
  env: Object.keys(options.env).length > 0 ? options.env : null,
162
163
  secret_names: options.secret.length > 0 ? options.secret : null,
164
+ ...(options.pin ? { auto_update: false } : {}),
163
165
  }),
164
166
  headers: { 'Content-Type': 'application/json' },
165
167
  });
@@ -175,6 +177,9 @@ function registerServiceCommand(program) {
175
177
  process.stdout.write(` ${chalk_1.default.bold('Agent:')} ${svc.agent_name}@${svc.agent_version}\n`);
176
178
  process.stdout.write(` ${chalk_1.default.bold('State:')} ${stateColor(svc.current_state)}\n`);
177
179
  process.stdout.write(` ${chalk_1.default.bold('URL:')} ${svc.provider_url || svc.cloud_run_url || '-'}\n`);
180
+ if (options.pin) {
181
+ process.stdout.write(` ${chalk_1.default.bold('Pinned:')} ${chalk_1.default.yellow(`yes (won't auto-update on publish)`)}\n`);
182
+ }
178
183
  process.stdout.write(`\n`);
179
184
  process.stdout.write(chalk_1.default.gray(`View logs: orch service logs ${svc.id}\n`));
180
185
  }
@@ -344,6 +349,28 @@ function registerServiceCommand(program) {
344
349
  if (svc.alert_webhook_url) {
345
350
  process.stdout.write(` Alert URL: ${svc.alert_webhook_url.slice(0, 50)}...\n`);
346
351
  }
352
+ // Environment variables
353
+ const envKeys = Object.keys(svc.env_json || {});
354
+ if (envKeys.length > 0) {
355
+ process.stdout.write(`\n${chalk_1.default.bold('Environment Variables')} (${envKeys.length})\n`);
356
+ for (const key of envKeys) {
357
+ process.stdout.write(` ${key}=${svc.env_json[key]}\n`);
358
+ }
359
+ }
360
+ else {
361
+ process.stdout.write(`\n Env Vars: ${chalk_1.default.gray('none')}\n`);
362
+ }
363
+ // Attached secrets
364
+ const secrets = svc.secret_names || [];
365
+ if (secrets.length > 0) {
366
+ process.stdout.write(`\n${chalk_1.default.bold('Attached Secrets')} (${secrets.length})\n`);
367
+ for (const name of secrets) {
368
+ process.stdout.write(` ${name}\n`);
369
+ }
370
+ }
371
+ else {
372
+ process.stdout.write(` Secrets: ${chalk_1.default.gray('none')}\n`);
373
+ }
347
374
  // Events timeline
348
375
  const events = svc.events || [];
349
376
  if (events.length > 0) {
@@ -383,6 +410,230 @@ function registerServiceCommand(program) {
383
410
  throw e;
384
411
  }
385
412
  });
413
+ // ============================================
414
+ // orch service env — manage environment variables
415
+ // ============================================
416
+ const envCmd = service
417
+ .command('env')
418
+ .description('Manage service environment variables');
419
+ // orch service env set <service-id> <KEY=VALUE...>
420
+ envCmd
421
+ .command('set <service-id> <pairs...>')
422
+ .description('Set environment variables on a service (KEY=VALUE pairs)')
423
+ .option('--workspace <slug>', 'Workspace slug (default: current workspace)')
424
+ .option('--json', 'Output as JSON')
425
+ .action(async (serviceId, pairs, options) => {
426
+ const config = await (0, config_1.getResolvedConfig)();
427
+ if (!config.apiKey) {
428
+ throw new errors_1.CliError('Missing API key. Run `orch login` first.');
429
+ }
430
+ const workspaceId = await resolveWorkspaceId(config, options.workspace);
431
+ // Parse KEY=VALUE pairs
432
+ const newEnv = {};
433
+ for (const pair of pairs) {
434
+ const idx = pair.indexOf('=');
435
+ if (idx < 0) {
436
+ throw new errors_1.CliError(`Invalid format: '${pair}'. Use KEY=VALUE.`);
437
+ }
438
+ newEnv[pair.slice(0, idx)] = pair.slice(idx + 1);
439
+ }
440
+ // Fetch current service to merge env
441
+ const current = await (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/services/${serviceId}`);
442
+ const currentEnv = current.service.env_json || {};
443
+ const mergedEnv = { ...currentEnv, ...newEnv };
444
+ const spinner = (0, spinner_1.createSpinner)('Updating environment...');
445
+ spinner.start();
446
+ try {
447
+ const result = await (0, api_1.request)(config, 'PATCH', `/workspaces/${workspaceId}/services/${serviceId}`, {
448
+ body: JSON.stringify({ env: mergedEnv }),
449
+ headers: { 'Content-Type': 'application/json' },
450
+ });
451
+ spinner.succeed('Environment updated');
452
+ if (options.json) {
453
+ (0, output_1.printJson)(result.service);
454
+ return;
455
+ }
456
+ const addedKeys = Object.keys(newEnv);
457
+ process.stdout.write(`${chalk_1.default.green('\u2713')} Set ${addedKeys.length} variable${addedKeys.length !== 1 ? 's' : ''}: ${addedKeys.join(', ')}\n`);
458
+ if (current.service.desired_state !== 'stopped') {
459
+ process.stdout.write(chalk_1.default.gray('Service restarted to apply changes.\n'));
460
+ }
461
+ }
462
+ catch (e) {
463
+ spinner.fail('Failed to update environment');
464
+ throw e;
465
+ }
466
+ });
467
+ // orch service env unset <service-id> <KEY...>
468
+ envCmd
469
+ .command('unset <service-id> <keys...>')
470
+ .description('Remove environment variables from a service')
471
+ .option('--workspace <slug>', 'Workspace slug (default: current workspace)')
472
+ .option('--json', 'Output as JSON')
473
+ .action(async (serviceId, keys, options) => {
474
+ const config = await (0, config_1.getResolvedConfig)();
475
+ if (!config.apiKey) {
476
+ throw new errors_1.CliError('Missing API key. Run `orch login` first.');
477
+ }
478
+ const workspaceId = await resolveWorkspaceId(config, options.workspace);
479
+ // Fetch current service
480
+ const current = await (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/services/${serviceId}`);
481
+ const currentEnv = { ...(current.service.env_json || {}) };
482
+ // Remove specified keys
483
+ const removed = [];
484
+ for (const key of keys) {
485
+ if (key in currentEnv) {
486
+ delete currentEnv[key];
487
+ removed.push(key);
488
+ }
489
+ }
490
+ if (removed.length === 0) {
491
+ process.stdout.write(chalk_1.default.yellow('No matching environment variables found.\n'));
492
+ return;
493
+ }
494
+ const spinner = (0, spinner_1.createSpinner)('Updating environment...');
495
+ spinner.start();
496
+ try {
497
+ const result = await (0, api_1.request)(config, 'PATCH', `/workspaces/${workspaceId}/services/${serviceId}`, {
498
+ body: JSON.stringify({ env: currentEnv }),
499
+ headers: { 'Content-Type': 'application/json' },
500
+ });
501
+ spinner.succeed('Environment updated');
502
+ if (options.json) {
503
+ (0, output_1.printJson)(result.service);
504
+ return;
505
+ }
506
+ process.stdout.write(`${chalk_1.default.green('\u2713')} Removed ${removed.length} variable${removed.length !== 1 ? 's' : ''}: ${removed.join(', ')}\n`);
507
+ if (current.service.desired_state !== 'stopped') {
508
+ process.stdout.write(chalk_1.default.gray('Service restarted to apply changes.\n'));
509
+ }
510
+ }
511
+ catch (e) {
512
+ spinner.fail('Failed to update environment');
513
+ throw e;
514
+ }
515
+ });
516
+ // orch service env list <service-id>
517
+ envCmd
518
+ .command('list <service-id>')
519
+ .description('List environment variables for a service')
520
+ .option('--workspace <slug>', 'Workspace slug (default: current workspace)')
521
+ .option('--json', 'Output as JSON')
522
+ .action(async (serviceId, options) => {
523
+ const config = await (0, config_1.getResolvedConfig)();
524
+ if (!config.apiKey) {
525
+ throw new errors_1.CliError('Missing API key. Run `orch login` first.');
526
+ }
527
+ const workspaceId = await resolveWorkspaceId(config, options.workspace);
528
+ const result = await (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/services/${serviceId}`);
529
+ const envJson = result.service.env_json || {};
530
+ if (options.json) {
531
+ (0, output_1.printJson)(envJson);
532
+ return;
533
+ }
534
+ const keys = Object.keys(envJson);
535
+ if (keys.length === 0) {
536
+ process.stdout.write('No environment variables set.\n');
537
+ return;
538
+ }
539
+ for (const key of keys) {
540
+ process.stdout.write(`${key}=${envJson[key]}\n`);
541
+ }
542
+ });
543
+ // ============================================
544
+ // orch service secret — manage attached workspace secrets
545
+ // ============================================
546
+ const secretCmd = service
547
+ .command('secret')
548
+ .description('Manage workspace secrets attached to a service');
549
+ // orch service secret add <service-id> <NAME...>
550
+ secretCmd
551
+ .command('add <service-id> <names...>')
552
+ .description('Attach workspace secrets to a service')
553
+ .option('--workspace <slug>', 'Workspace slug (default: current workspace)')
554
+ .option('--json', 'Output as JSON')
555
+ .action(async (serviceId, names, options) => {
556
+ const config = await (0, config_1.getResolvedConfig)();
557
+ if (!config.apiKey) {
558
+ throw new errors_1.CliError('Missing API key. Run `orch login` first.');
559
+ }
560
+ const workspaceId = await resolveWorkspaceId(config, options.workspace);
561
+ // Fetch current service and merge
562
+ const current = await (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/services/${serviceId}`);
563
+ const currentSecrets = current.service.secret_names || [];
564
+ const merged = [...new Set([...currentSecrets, ...names])];
565
+ const spinner = (0, spinner_1.createSpinner)('Attaching secrets...');
566
+ spinner.start();
567
+ try {
568
+ const result = await (0, api_1.request)(config, 'PATCH', `/workspaces/${workspaceId}/services/${serviceId}`, {
569
+ body: JSON.stringify({ secret_names: merged }),
570
+ headers: { 'Content-Type': 'application/json' },
571
+ });
572
+ spinner.succeed('Secrets attached');
573
+ if (options.json) {
574
+ (0, output_1.printJson)(result.service);
575
+ return;
576
+ }
577
+ const added = names.filter(n => !currentSecrets.includes(n));
578
+ if (added.length > 0) {
579
+ process.stdout.write(`${chalk_1.default.green('\u2713')} Attached ${added.length} secret${added.length !== 1 ? 's' : ''}: ${added.join(', ')}\n`);
580
+ }
581
+ else {
582
+ process.stdout.write(chalk_1.default.yellow('All specified secrets were already attached.\n'));
583
+ }
584
+ if (current.service.desired_state !== 'stopped') {
585
+ process.stdout.write(chalk_1.default.gray('Service restarted to apply changes.\n'));
586
+ }
587
+ }
588
+ catch (e) {
589
+ spinner.fail('Failed to attach secrets');
590
+ throw e;
591
+ }
592
+ });
593
+ // orch service secret remove <service-id> <NAME...>
594
+ secretCmd
595
+ .command('remove <service-id> <names...>')
596
+ .description('Detach workspace secrets from a service')
597
+ .option('--workspace <slug>', 'Workspace slug (default: current workspace)')
598
+ .option('--json', 'Output as JSON')
599
+ .action(async (serviceId, names, options) => {
600
+ const config = await (0, config_1.getResolvedConfig)();
601
+ if (!config.apiKey) {
602
+ throw new errors_1.CliError('Missing API key. Run `orch login` first.');
603
+ }
604
+ const workspaceId = await resolveWorkspaceId(config, options.workspace);
605
+ // Fetch current service and filter
606
+ const current = await (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/services/${serviceId}`);
607
+ const currentSecrets = current.service.secret_names || [];
608
+ const namesToRemove = new Set(names);
609
+ const filtered = currentSecrets.filter(n => !namesToRemove.has(n));
610
+ const removed = currentSecrets.filter(n => namesToRemove.has(n));
611
+ if (removed.length === 0) {
612
+ process.stdout.write(chalk_1.default.yellow('No matching secrets found on this service.\n'));
613
+ return;
614
+ }
615
+ const spinner = (0, spinner_1.createSpinner)('Detaching secrets...');
616
+ spinner.start();
617
+ try {
618
+ const result = await (0, api_1.request)(config, 'PATCH', `/workspaces/${workspaceId}/services/${serviceId}`, {
619
+ body: JSON.stringify({ secret_names: filtered }),
620
+ headers: { 'Content-Type': 'application/json' },
621
+ });
622
+ spinner.succeed('Secrets detached');
623
+ if (options.json) {
624
+ (0, output_1.printJson)(result.service);
625
+ return;
626
+ }
627
+ process.stdout.write(`${chalk_1.default.green('\u2713')} Detached ${removed.length} secret${removed.length !== 1 ? 's' : ''}: ${removed.join(', ')}\n`);
628
+ if (current.service.desired_state !== 'stopped') {
629
+ process.stdout.write(chalk_1.default.gray('Service restarted to apply changes.\n'));
630
+ }
631
+ }
632
+ catch (e) {
633
+ spinner.fail('Failed to detach secrets');
634
+ throw e;
635
+ }
636
+ });
386
637
  }
387
638
  // ============================================
388
639
  // OPTION COLLECTORS