@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.
- package/dist/commands/index.js +2 -0
- package/dist/commands/init.js +452 -16
- package/dist/commands/publish.js +138 -9
- package/dist/commands/run.js +72 -0
- package/dist/commands/secrets.js +174 -0
- package/dist/commands/templates/github-weekly-summary.js +884 -0
- package/dist/lib/api.js +15 -6
- package/package.json +1 -1
package/dist/commands/publish.js
CHANGED
|
@@ -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
|
|
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) {
|
package/dist/commands/run.js
CHANGED
|
@@ -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
|
+
}
|