@orchagent/cli 0.3.63 → 0.3.65

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.
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.extractTemplateVariables = extractTemplateVariables;
7
7
  exports.deriveInputSchema = deriveInputSchema;
8
8
  exports.scanUndeclaredEnvVars = scanUndeclaredEnvVars;
9
+ exports.checkDependencies = checkDependencies;
9
10
  exports.registerPublishCommand = registerPublishCommand;
10
11
  const promises_1 = __importDefault(require("fs/promises"));
11
12
  const path_1 = __importDefault(require("path"));
@@ -15,7 +16,6 @@ const chalk_1 = __importDefault(require("chalk"));
15
16
  const config_1 = require("../lib/config");
16
17
  const api_1 = require("../lib/api");
17
18
  const errors_1 = require("../lib/errors");
18
- const api_2 = require("../lib/api");
19
19
  const analytics_1 = require("../lib/analytics");
20
20
  const bundle_1 = require("../lib/bundle");
21
21
  /**
@@ -270,6 +270,56 @@ function commandForEntrypoint(entrypoint) {
270
270
  }
271
271
  return `python ${entrypoint}`;
272
272
  }
273
+ /**
274
+ * Check if manifest dependencies are published and callable.
275
+ * Best-effort: network errors cause the check to be silently skipped
276
+ * (returns empty array) to avoid false alarms.
277
+ */
278
+ async function checkDependencies(config, dependencies, publishingOrgSlug, workspaceId) {
279
+ // Pre-fetch user's agents if any deps are in the same org (one API call)
280
+ let myAgents = null;
281
+ const hasSameOrgDeps = dependencies.some(d => {
282
+ const [org] = d.id.split('/');
283
+ return org === publishingOrgSlug;
284
+ });
285
+ if (hasSameOrgDeps) {
286
+ try {
287
+ const headers = {};
288
+ if (workspaceId)
289
+ headers['X-Workspace-Id'] = workspaceId;
290
+ myAgents = await (0, api_1.request)(config, 'GET', '/agents', { headers });
291
+ }
292
+ catch {
293
+ return []; // Can't reach API — skip check entirely
294
+ }
295
+ }
296
+ return Promise.all(dependencies.map(async (dep) => {
297
+ const parts = dep.id.split('/');
298
+ const ref = `${dep.id}@${dep.version}`;
299
+ if (parts.length !== 2)
300
+ return { ref, status: 'not_found' };
301
+ const [depOrg, depName] = parts;
302
+ // Same org: check against pre-fetched agent list
303
+ if (depOrg === publishingOrgSlug && myAgents) {
304
+ const match = myAgents.find(a => a.name === depName && a.version === dep.version);
305
+ if (!match)
306
+ return { ref, status: 'not_found' };
307
+ return { ref, status: match.callable ? 'found_callable' : 'found_not_callable' };
308
+ }
309
+ // Different org: try public endpoint
310
+ try {
311
+ const agent = await (0, api_1.getPublicAgent)(config, depOrg, depName, dep.version);
312
+ return { ref, status: agent.callable ? 'found_callable' : 'found_not_callable' };
313
+ }
314
+ catch (err) {
315
+ if (err?.status === 404) {
316
+ return { ref, status: 'not_found' };
317
+ }
318
+ // Network/unexpected error — don't false alarm
319
+ return { ref, status: 'found_callable' };
320
+ }
321
+ }));
322
+ }
273
323
  function registerPublishCommand(program) {
274
324
  program
275
325
  .command('publish')
@@ -287,18 +337,33 @@ function registerPublishCommand(program) {
287
337
  : undefined;
288
338
  const config = await (0, config_1.getResolvedConfig)({}, options.profile);
289
339
  const cwd = process.cwd();
340
+ // Resolve workspace context — if `orch workspace use` was called, publish
341
+ // to that workspace instead of the personal org (F-5)
342
+ const configFile = await (0, config_1.loadConfig)();
343
+ let workspaceId;
344
+ if (configFile.workspace) {
345
+ const { workspaces } = await (0, api_1.request)(config, 'GET', '/workspaces');
346
+ const ws = workspaces.find(w => w.slug === configFile.workspace);
347
+ if (!ws) {
348
+ throw new errors_1.CliError(`Workspace '${configFile.workspace}' not found. Run \`orch workspace list\` to see available workspaces.`);
349
+ }
350
+ workspaceId = ws.id;
351
+ }
290
352
  // Check for SKILL.md first (skills take precedence)
291
353
  const skillMdPath = path_1.default.join(cwd, 'SKILL.md');
292
354
  const skillData = await parseSkillMd(skillMdPath);
293
355
  if (skillData) {
294
356
  // Publish as a skill (server auto-assigns version)
295
- const org = await (0, api_1.getOrg)(config);
357
+ const org = await (0, api_1.getOrg)(config, workspaceId);
358
+ if (workspaceId && !options.dryRun) {
359
+ process.stdout.write(`Workspace: ${org.slug}\n`);
360
+ }
296
361
  // SC-05: Collect all files in the skill directory for multi-file support
297
362
  const skillFiles = await collectSkillFiles(cwd);
298
363
  const hasMultipleFiles = skillFiles.length > 1;
299
364
  // Handle dry-run for skills
300
365
  if (options.dryRun) {
301
- const preview = await (0, api_1.previewAgentVersion)(config, skillData.frontmatter.name);
366
+ const preview = await (0, api_1.previewAgentVersion)(config, skillData.frontmatter.name, workspaceId);
302
367
  const skillBodyBytes = Buffer.byteLength(skillData.body, 'utf-8');
303
368
  const totalFilesSize = skillFiles.reduce((sum, f) => sum + f.size, 0);
304
369
  const versionInfo = preview.existing_versions.length > 0
@@ -315,6 +380,9 @@ function registerPublishCommand(program) {
315
380
  process.stderr.write('\nSkill Preview:\n');
316
381
  process.stderr.write(` Name: ${skillData.frontmatter.name}\n`);
317
382
  process.stderr.write(` Type: skill\n`);
383
+ if (workspaceId) {
384
+ process.stderr.write(` Workspace: ${org.slug}\n`);
385
+ }
318
386
  process.stderr.write(` Version: ${versionInfo}\n`);
319
387
  process.stderr.write(` Visibility: private\n`);
320
388
  process.stderr.write(` Providers: any\n`);
@@ -345,7 +413,7 @@ function registerPublishCommand(program) {
345
413
  // SC-05: Include all skill files for UI preview
346
414
  skill_files: hasMultipleFiles ? skillFiles : undefined,
347
415
  allow_local_download: options.localDownload || false,
348
- });
416
+ }, workspaceId);
349
417
  const skillVersion = skillResult.agent?.version || 'v1';
350
418
  const skillAgentId = skillResult.agent?.id;
351
419
  await (0, analytics_1.track)('cli_publish', { agent_type: 'skill', multi_file: hasMultipleFiles });
@@ -574,8 +642,11 @@ function registerPublishCommand(program) {
574
642
  if (options.docker && executionEngine !== 'code_runtime') {
575
643
  throw new errors_1.CliError('--docker is only supported for code runtime agents');
576
644
  }
577
- // Get org info
578
- const org = await (0, api_1.getOrg)(config);
645
+ // Get org info (workspace-aware — returns workspace org if workspace is active)
646
+ const org = await (0, api_1.getOrg)(config, workspaceId);
647
+ if (workspaceId && !options.dryRun) {
648
+ process.stdout.write(`Workspace: ${org.slug}\n`);
649
+ }
579
650
  // Default to 'any' provider if not specified
580
651
  const supportedProviders = manifest.supported_providers || ['any'];
581
652
  // Detect SDK compatibility for code runtime agents
@@ -586,9 +657,33 @@ function registerPublishCommand(program) {
586
657
  process.stdout.write(`SDK detected - agent will be marked as Local Ready\n`);
587
658
  }
588
659
  }
660
+ // Check if manifest dependencies are published and callable (F-9b).
661
+ // Runs for both dry-run and normal publish so users catch issues early.
662
+ const manifestDeps = manifest.manifest?.dependencies;
663
+ if (manifestDeps?.length) {
664
+ const depResults = await checkDependencies(config, manifestDeps, org.slug, workspaceId);
665
+ const notFound = depResults.filter(r => r.status === 'not_found');
666
+ const notCallable = depResults.filter(r => r.status === 'found_not_callable');
667
+ if (notFound.length > 0) {
668
+ process.stderr.write(chalk_1.default.yellow(`\n⚠ Unpublished dependencies:\n`));
669
+ for (const dep of notFound) {
670
+ process.stderr.write(chalk_1.default.yellow(` - ${dep.ref}\n`));
671
+ }
672
+ process.stderr.write(`\n These agents must be published before this orchestrator can call them.\n` +
673
+ ` Publish each dependency first, then re-run this publish.\n\n`);
674
+ }
675
+ if (notCallable.length > 0) {
676
+ process.stderr.write(chalk_1.default.yellow(`\n⚠ Dependencies not marked as callable:\n`));
677
+ for (const dep of notCallable) {
678
+ process.stderr.write(chalk_1.default.yellow(` - ${dep.ref}\n`));
679
+ }
680
+ process.stderr.write(`\n Agents must have callable: true in orchagent.json to be invoked\n` +
681
+ ` by orchestrators. Update and republish each dependency.\n\n`);
682
+ }
683
+ }
589
684
  // Handle dry-run for agents
590
685
  if (options.dryRun) {
591
- const preview = await (0, api_1.previewAgentVersion)(config, manifest.name);
686
+ const preview = await (0, api_1.previewAgentVersion)(config, manifest.name, workspaceId);
592
687
  const versionInfo = preview.existing_versions.length > 0
593
688
  ? `${preview.next_version} (new version, ${preview.existing_versions[preview.existing_versions.length - 1]} exists)`
594
689
  : `${preview.next_version} (first version)`;
@@ -641,6 +736,9 @@ function registerPublishCommand(program) {
641
736
  process.stderr.write('\nAgent Preview:\n');
642
737
  process.stderr.write(` Name: ${manifest.name}\n`);
643
738
  process.stderr.write(` Type: ${canonicalType}\n`);
739
+ if (workspaceId) {
740
+ process.stderr.write(` Workspace: ${org.slug}\n`);
741
+ }
644
742
  process.stderr.write(` Run mode: ${runMode}\n`);
645
743
  process.stderr.write(` Engine: ${executionEngine}${shouldUploadBundle ? ' (hosted)' : ''}\n`);
646
744
  process.stderr.write(` Callable: ${callable ? 'enabled' : 'disabled'}\n`);
@@ -655,6 +753,9 @@ function registerPublishCommand(program) {
655
753
  else if (effectiveSkills?.length) {
656
754
  process.stderr.write(` Skills: ${effectiveSkills.join(', ')}\n`);
657
755
  }
756
+ if (manifest.required_secrets?.length) {
757
+ process.stderr.write(` Secrets: ${manifest.required_secrets.join(', ')}\n`);
758
+ }
658
759
  process.stderr.write(`\nWould publish: ${preview.org_slug}/${manifest.name}@${preview.next_version}\n`);
659
760
  if (shouldUploadBundle) {
660
761
  const bundlePreview = await (0, bundle_1.previewBundle)(cwd, {
@@ -719,11 +820,11 @@ function registerPublishCommand(program) {
719
820
  default_skills: skillsFromFlag || manifest.default_skills,
720
821
  skills_locked: manifest.skills_locked || options.skillsLocked || undefined,
721
822
  allow_local_download: options.localDownload || false,
722
- });
823
+ }, workspaceId);
723
824
  }
724
825
  catch (err) {
725
826
  // Improve SECURITY_BLOCKED error display
726
- if (err instanceof api_2.ApiError && err.status === 422) {
827
+ if (err instanceof api_1.ApiError && err.status === 422) {
727
828
  const payload = err.payload;
728
829
  const errorCode = payload?.error?.code;
729
830
  if (errorCode === 'SECURITY_BLOCKED') {
@@ -838,6 +939,18 @@ function registerPublishCommand(program) {
838
939
  process.stdout.write(`Callable: ${callable ? 'enabled' : 'disabled'}\n`);
839
940
  process.stdout.write(`Providers: ${supportedProviders.join(', ')}\n`);
840
941
  process.stdout.write(`Visibility: private\n`);
942
+ // Show required secrets with setup instructions (F-18)
943
+ if (manifest.required_secrets?.length) {
944
+ process.stdout.write(`\nRequired secrets:\n`);
945
+ for (const secret of manifest.required_secrets) {
946
+ process.stdout.write(` ${secret}\n`);
947
+ }
948
+ process.stdout.write(`\nSet secrets before running:\n`);
949
+ for (const secret of manifest.required_secrets) {
950
+ process.stdout.write(` orch secrets set ${secret} <value>\n`);
951
+ }
952
+ process.stdout.write(`\nView existing secrets: ${chalk_1.default.cyan('orch secrets list')}\n`);
953
+ }
841
954
  // Show security review result if available
842
955
  const secReview = result.security_review;
843
956
  if (secReview?.verdict) {
@@ -855,6 +968,22 @@ function registerPublishCommand(program) {
855
968
  process.stdout.write(`\nService key (save this - shown only once):\n`);
856
969
  process.stdout.write(` ${result.service_key}\n`);
857
970
  }
971
+ // Show next-step CLI command based on run mode
972
+ const runRef = `${org.slug}/${manifest.name}`;
973
+ if (runMode === 'always_on') {
974
+ process.stdout.write(`\nDeploy as service:\n`);
975
+ process.stdout.write(` orch service deploy ${runRef}\n`);
976
+ }
977
+ else {
978
+ const schemaProps = inputSchema && typeof inputSchema === 'object' && 'properties' in inputSchema
979
+ ? Object.keys(inputSchema.properties).slice(0, 3)
980
+ : null;
981
+ const exampleFields = schemaProps?.length
982
+ ? schemaProps.map(k => `"${k}": "..."`).join(', ')
983
+ : '"input": "..."';
984
+ process.stdout.write(`\nRun with CLI:\n`);
985
+ process.stdout.write(` orch run ${runRef} --data '{${exampleFields}}'\n`);
986
+ }
858
987
  process.stdout.write(`\nAPI endpoint:\n`);
859
988
  process.stdout.write(` POST ${config.apiUrl}/${org.slug}/${manifest.name}/${assignedVersion}/run\n`);
860
989
  if (shouldUploadBundle) {
@@ -1624,6 +1624,38 @@ async function executeCloud(agentRef, file, options) {
1624
1624
  runtime: agentMeta.runtime ?? null,
1625
1625
  loop: agentMeta.loop ?? null,
1626
1626
  });
1627
+ // Pre-flight: check required secrets before running (F-18)
1628
+ // Only for sandbox-backed engines where secrets are injected as env vars
1629
+ if (cloudEngine !== 'direct_llm') {
1630
+ const agentRequiredSecrets = agentMeta.required_secrets;
1631
+ if (agentRequiredSecrets?.length) {
1632
+ try {
1633
+ const wsSlug = configFile.workspace;
1634
+ if (wsSlug) {
1635
+ const { workspaces } = await (0, api_1.request)(resolved, 'GET', '/workspaces');
1636
+ const ws = workspaces.find((w) => w.slug === wsSlug);
1637
+ if (ws) {
1638
+ const secretsResult = await (0, api_1.request)(resolved, 'GET', `/workspaces/${ws.id}/secrets`);
1639
+ const existingNames = new Set(secretsResult.secrets.map((s) => s.name));
1640
+ const missing = agentRequiredSecrets.filter((s) => !existingNames.has(s));
1641
+ if (missing.length > 0) {
1642
+ throw new errors_1.CliError(`Agent requires secrets not found in workspace '${wsSlug}':\n` +
1643
+ missing.map((s) => ` - ${s}`).join('\n') + '\n\n' +
1644
+ `Set them before running:\n` +
1645
+ missing.map((s) => ` orch secrets set ${s} <value>`).join('\n') + '\n\n' +
1646
+ `Secrets are injected as environment variables into the agent sandbox.\n` +
1647
+ `View existing secrets: orch secrets list`);
1648
+ }
1649
+ }
1650
+ }
1651
+ }
1652
+ catch (err) {
1653
+ if (err instanceof errors_1.CliError)
1654
+ throw err;
1655
+ // Non-fatal: gateway will catch missing secrets at execution time
1656
+ }
1657
+ }
1658
+ }
1627
1659
  // Pre-call balance check for paid agents
1628
1660
  let pricingInfo;
1629
1661
  if ((0, pricing_1.isPaidAgent)(agentMeta)) {
@@ -1680,6 +1712,7 @@ async function executeCloud(agentRef, file, options) {
1680
1712
  const headers = {
1681
1713
  Authorization: `Bearer ${resolved.apiKey}`,
1682
1714
  'X-CLI-Version': package_json_1.default.version,
1715
+ 'X-OrchAgent-Client': 'cli',
1683
1716
  };
1684
1717
  if (options.tenant) {
1685
1718
  headers['X-OrchAgent-Tenant'] = options.tenant;
@@ -2022,6 +2055,37 @@ async function executeCloud(agentRef, file, options) {
2022
2055
  ` - Contacting the agent author to increase the timeout` +
2023
2056
  refSuffix);
2024
2057
  }
2058
+ if (errorCode === 'MISSING_SECRETS') {
2059
+ spinner?.fail('Missing workspace secrets');
2060
+ // Extract secret names from gateway message:
2061
+ // "Agent requires secret(s) not found in workspace: NAME1, NAME2. Add them in Settings > Secrets."
2062
+ const secretNames = [];
2063
+ if (message) {
2064
+ const match = message.match(/not found in workspace:\s*(.+?)\./);
2065
+ if (match) {
2066
+ secretNames.push(...match[1].split(',').map((s) => s.trim()).filter(Boolean));
2067
+ }
2068
+ }
2069
+ let hint = '';
2070
+ if (secretNames.length > 0) {
2071
+ hint += `Missing secrets:\n`;
2072
+ for (const name of secretNames) {
2073
+ hint += ` - ${name}\n`;
2074
+ }
2075
+ hint += `\nSet them with:\n`;
2076
+ for (const name of secretNames) {
2077
+ hint += ` orch secrets set ${name} <value>\n`;
2078
+ }
2079
+ }
2080
+ else {
2081
+ hint += `${message}\n\n`;
2082
+ hint += `Set missing secrets:\n`;
2083
+ hint += ` orch secrets set <NAME> <value>\n`;
2084
+ }
2085
+ hint += `\nView existing secrets:\n`;
2086
+ hint += ` orch secrets list`;
2087
+ throw new errors_1.CliError(hint + refSuffix);
2088
+ }
2025
2089
  if (response.status >= 500) {
2026
2090
  spinner?.fail(`Server error (${response.status})`);
2027
2091
  throw new errors_1.CliError(`${message}\n\n` +
@@ -2103,6 +2167,10 @@ async function executeCloud(agentRef, file, options) {
2103
2167
  if (parts.length > 0) {
2104
2168
  process.stderr.write(chalk_1.default.gray(`${parts.join(' · ')}\n`));
2105
2169
  }
2170
+ const runId = response.headers?.get?.('x-run-id');
2171
+ if (runId) {
2172
+ process.stderr.write(chalk_1.default.gray(`View logs: orch logs ${runId}\n`));
2173
+ }
2106
2174
  }
2107
2175
  }
2108
2176
  }
@@ -2191,6 +2259,10 @@ async function executeCloud(agentRef, file, options) {
2191
2259
  if (parts.length > 0) {
2192
2260
  process.stderr.write(chalk_1.default.gray(`\n${parts.join(' · ')}\n`));
2193
2261
  }
2262
+ const runId = response.headers?.get?.('x-run-id');
2263
+ if (runId) {
2264
+ process.stderr.write(chalk_1.default.gray(`View logs: orch logs ${runId}\n`));
2265
+ }
2194
2266
  }
2195
2267
  }
2196
2268
  }
@@ -0,0 +1,174 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.registerSecretsCommand = registerSecretsCommand;
7
+ const cli_table3_1 = __importDefault(require("cli-table3"));
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const config_1 = require("../lib/config");
10
+ const api_1 = require("../lib/api");
11
+ const errors_1 = require("../lib/errors");
12
+ const output_1 = require("../lib/output");
13
+ // ============================================
14
+ // HELPERS
15
+ // ============================================
16
+ const SECRET_NAME_REGEX = /^[A-Z][A-Z0-9_]*$/;
17
+ async function resolveWorkspaceId(config, slug) {
18
+ const configFile = await (0, config_1.loadConfig)();
19
+ const targetSlug = slug ?? configFile.workspace;
20
+ if (!targetSlug) {
21
+ throw new errors_1.CliError('No workspace specified. Use --workspace <slug> or run `orch workspace use <slug>` first.');
22
+ }
23
+ const response = await (0, api_1.request)(config, 'GET', '/workspaces');
24
+ const workspace = response.workspaces.find((w) => w.slug === targetSlug);
25
+ if (!workspace) {
26
+ throw new errors_1.CliError(`Workspace '${targetSlug}' not found.`);
27
+ }
28
+ return workspace.id;
29
+ }
30
+ function formatDate(iso) {
31
+ if (!iso)
32
+ return '-';
33
+ return new Date(iso).toLocaleString();
34
+ }
35
+ function validateSecretName(name) {
36
+ if (!name || name.length > 128) {
37
+ throw new errors_1.CliError('Secret name must be 1-128 characters.');
38
+ }
39
+ if (!SECRET_NAME_REGEX.test(name)) {
40
+ throw new errors_1.CliError(`Invalid secret name '${name}'.\n\n` +
41
+ 'Secret names must:\n' +
42
+ ' - Start with an uppercase letter (A-Z)\n' +
43
+ ' - Contain only uppercase letters, digits, and underscores\n\n' +
44
+ 'Examples: STRIPE_SECRET_KEY, DISCORD_TOKEN, MY_API_KEY_2');
45
+ }
46
+ }
47
+ async function findSecretByName(config, workspaceId, name) {
48
+ const result = await (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/secrets`);
49
+ return result.secrets.find((s) => s.name === name);
50
+ }
51
+ // ============================================
52
+ // COMMAND REGISTRATION
53
+ // ============================================
54
+ function registerSecretsCommand(program) {
55
+ const secrets = program
56
+ .command('secrets')
57
+ .description('Manage workspace secrets (injected as env vars into agent sandboxes)');
58
+ // orch secrets list
59
+ secrets
60
+ .command('list')
61
+ .description('List secrets in your workspace (names and metadata, never values)')
62
+ .option('--workspace <slug>', 'Workspace slug (default: current workspace)')
63
+ .option('--json', 'Output as JSON')
64
+ .action(async (options) => {
65
+ const config = await (0, config_1.getResolvedConfig)();
66
+ if (!config.apiKey) {
67
+ throw new errors_1.CliError('Missing API key. Run `orch login` first.');
68
+ }
69
+ const workspaceId = await resolveWorkspaceId(config, options.workspace);
70
+ const result = await (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/secrets`);
71
+ if (options.json) {
72
+ (0, output_1.printJson)(result);
73
+ return;
74
+ }
75
+ if (result.secrets.length === 0) {
76
+ process.stdout.write('No secrets found in this workspace.\n');
77
+ process.stdout.write(chalk_1.default.gray('\nAdd one with: orch secrets set MY_SECRET_NAME my-secret-value\n'));
78
+ return;
79
+ }
80
+ const table = new cli_table3_1.default({
81
+ head: [
82
+ chalk_1.default.bold('Name'),
83
+ chalk_1.default.bold('Type'),
84
+ chalk_1.default.bold('Description'),
85
+ chalk_1.default.bold('Updated'),
86
+ ],
87
+ });
88
+ for (const s of result.secrets) {
89
+ table.push([
90
+ s.name,
91
+ s.secret_type === 'llm_key'
92
+ ? chalk_1.default.cyan(`llm_key (${s.llm_provider ?? '?'})`)
93
+ : chalk_1.default.gray('custom'),
94
+ s.description ? s.description.slice(0, 40) + (s.description.length > 40 ? '...' : '') : chalk_1.default.gray('-'),
95
+ formatDate(s.updated_at),
96
+ ]);
97
+ }
98
+ process.stdout.write(`\n${table.toString()}\n`);
99
+ process.stdout.write(chalk_1.default.gray(`\n${result.secrets.length} secret(s)\n`));
100
+ });
101
+ // orch secrets set <NAME> <VALUE>
102
+ secrets
103
+ .command('set <name> <value>')
104
+ .description('Create or update a workspace secret')
105
+ .option('--description <text>', 'Description of what this secret is for')
106
+ .option('--workspace <slug>', 'Workspace slug (default: current workspace)')
107
+ .action(async (name, value, options) => {
108
+ const config = await (0, config_1.getResolvedConfig)();
109
+ if (!config.apiKey) {
110
+ throw new errors_1.CliError('Missing API key. Run `orch login` first.');
111
+ }
112
+ validateSecretName(name);
113
+ if (!value) {
114
+ throw new errors_1.CliError('Secret value cannot be empty.');
115
+ }
116
+ const workspaceId = await resolveWorkspaceId(config, options.workspace);
117
+ // Check if secret already exists (by name)
118
+ const existing = await findSecretByName(config, workspaceId, name);
119
+ if (existing) {
120
+ // Update existing secret
121
+ const body = { value };
122
+ if (options.description !== undefined) {
123
+ body.description = options.description;
124
+ }
125
+ const result = await (0, api_1.request)(config, 'PATCH', `/workspaces/${workspaceId}/secrets/${existing.id}`, {
126
+ body: JSON.stringify(body),
127
+ headers: { 'Content-Type': 'application/json' },
128
+ });
129
+ process.stdout.write(chalk_1.default.green('\u2713') + ` Updated secret ${chalk_1.default.bold(name)}\n`);
130
+ if (result.restarted_services && result.restarted_services.length > 0) {
131
+ process.stdout.write(chalk_1.default.yellow('\n Restarted running services that use this secret:\n'));
132
+ for (const svc of result.restarted_services) {
133
+ process.stdout.write(` - ${svc.service_name}\n`);
134
+ }
135
+ }
136
+ }
137
+ else {
138
+ // Create new secret
139
+ const body = {
140
+ name,
141
+ value,
142
+ secret_type: 'custom',
143
+ };
144
+ if (options.description !== undefined) {
145
+ body.description = options.description;
146
+ }
147
+ await (0, api_1.request)(config, 'POST', `/workspaces/${workspaceId}/secrets`, {
148
+ body: JSON.stringify(body),
149
+ headers: { 'Content-Type': 'application/json' },
150
+ });
151
+ process.stdout.write(chalk_1.default.green('\u2713') + ` Created secret ${chalk_1.default.bold(name)}\n`);
152
+ }
153
+ });
154
+ // orch secrets delete <NAME>
155
+ secrets
156
+ .command('delete <name>')
157
+ .description('Delete a workspace secret')
158
+ .option('--workspace <slug>', 'Workspace slug (default: current workspace)')
159
+ .action(async (name, options) => {
160
+ const config = await (0, config_1.getResolvedConfig)();
161
+ if (!config.apiKey) {
162
+ throw new errors_1.CliError('Missing API key. Run `orch login` first.');
163
+ }
164
+ const workspaceId = await resolveWorkspaceId(config, options.workspace);
165
+ // Resolve name → ID
166
+ const existing = await findSecretByName(config, workspaceId, name);
167
+ if (!existing) {
168
+ throw new errors_1.CliError(`Secret '${name}' not found in this workspace.\n\n` +
169
+ 'Run `orch secrets list` to see available secrets.');
170
+ }
171
+ await (0, api_1.request)(config, 'DELETE', `/workspaces/${workspaceId}/secrets/${existing.id}`);
172
+ process.stdout.write(chalk_1.default.green('\u2713') + ` Deleted secret ${chalk_1.default.bold(name)}\n`);
173
+ });
174
+ }