@kora-platform/cli 0.7.0-rc1 → 0.8.0-rc10

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.
Files changed (49) hide show
  1. package/README.md +21 -0
  2. package/dist/api-client.d.ts +274 -106
  3. package/dist/api-client.js +192 -167
  4. package/dist/api-types.d.ts +301 -163
  5. package/dist/artifact-api-client.d.ts +28 -1
  6. package/dist/artifact-api-client.js +33 -0
  7. package/dist/artifact-commands.d.ts +5 -0
  8. package/dist/artifact-commands.js +177 -4
  9. package/dist/audit-commands.d.ts +12 -0
  10. package/dist/audit-commands.js +74 -0
  11. package/dist/auth-commands.d.ts +1 -0
  12. package/dist/auth-commands.js +195 -32
  13. package/dist/cli-errors.d.ts +7 -1
  14. package/dist/cli-errors.js +12 -1
  15. package/dist/command-builders.d.ts +1 -0
  16. package/dist/command-builders.js +1 -0
  17. package/dist/command-flags.d.ts +1 -0
  18. package/dist/command-flags.js +7 -0
  19. package/dist/command-groups.js +10 -12
  20. package/dist/command-registry.js +595 -277
  21. package/dist/commands.js +728 -636
  22. package/dist/environment-context.d.ts +9 -0
  23. package/dist/environment-context.js +32 -0
  24. package/dist/error-code.d.ts +2 -0
  25. package/dist/error-code.js +9 -0
  26. package/dist/{integration-commands.d.ts → extension-commands.d.ts} +3 -2
  27. package/dist/extension-commands.js +446 -0
  28. package/dist/files.d.ts +44 -4
  29. package/dist/files.js +349 -26
  30. package/dist/format.d.ts +6 -0
  31. package/dist/format.js +83 -1
  32. package/dist/runner.js +28 -10
  33. package/dist/schema-registry-data.d.ts +318 -571
  34. package/dist/schema-registry-data.js +356 -698
  35. package/dist/session-store.js +80 -0
  36. package/dist/session.d.ts +1 -0
  37. package/dist/transport-refresh.d.ts +10 -0
  38. package/dist/transport-refresh.js +51 -0
  39. package/dist/transport.d.ts +31 -0
  40. package/dist/transport.js +102 -36
  41. package/dist/types.d.ts +2 -1
  42. package/dist/workspace-source.d.ts +1 -0
  43. package/dist/workspace-source.js +13 -0
  44. package/package.json +2 -1
  45. package/dist/dotenv.d.ts +0 -1
  46. package/dist/dotenv.js +0 -26
  47. package/dist/integration-api-client.d.ts +0 -29
  48. package/dist/integration-api-client.js +0 -50
  49. package/dist/integration-commands.js +0 -208
package/dist/commands.js CHANGED
@@ -1,19 +1,17 @@
1
- import { mkdir, writeFile } from "node:fs/promises";
2
- import { dirname, resolve } from "node:path";
3
1
  import { renderCompletionScript, resolveCompletionCandidates } from "./completion.js";
4
- import { executeAuthLogin, executeAuthLogout, executeAuthWhoami } from "./auth-commands.js";
2
+ import { executeAuthLogin, executeAuthLogout, executeAuthSignup, executeAuthWhoami } from "./auth-commands.js";
5
3
  import { getCliSchema, listCliSchemas } from "./schema-registry.js";
6
4
  import { createPlatformApiClient } from "./api-client.js";
7
5
  import { authProblem, genericProblem, notFoundProblem, usageProblem } from "./cli-errors.js";
8
- import { readOptionalNumberFlag, readOptionalStringFlag, readRequiredStringFlag } from "./command-flags.js";
9
- import { executeArtifactDownload, executeArtifactList, executeArtifactRead, executeArtifactUpload } from "./artifact-commands.js";
10
- import { readImportEntries, readJsonInputSpecifier, readTextInputSpecifier, parseEnvFile } from "./files.js";
6
+ import { readOptionalNumberFlag, readOptionalStringFlag, readRequiredStringFlag, readRequiredStringFlagPreservingEmpty } from "./command-flags.js";
7
+ import { executeArtifactDownload, executeArtifactArchive, executeArtifactInspect, executeArtifactInventory, executeArtifactList, executeArtifactPurge, executeArtifactRead, executeArtifactRestore, executeArtifactUpload } from "./artifact-commands.js";
8
+ import { executeAudit } from "./audit-commands.js";
9
+ import { isZipArchivePath, readArchiveBytes, readImportEntries, readJsonInputSpecifier, readWorkspaceTestEntries, readTextInputSpecifier, writeReleaseSourceFiles } from "./files.js";
11
10
  import { renderDiffSummary, renderKeyValue, renderPrettyJson, renderSuccess, renderTable } from "./format.js";
12
11
  import { buildTaskDetailPresentation } from "./task-detail.js";
13
12
  import { confirmDestructive } from "./interaction.js";
14
- import { executeIntegrations } from "./integration-commands.js";
15
- import { openBrowser, shouldAutoOpenBrowser } from "./browser.js";
16
- import { isInteractive } from "./prompts.js";
13
+ import { executeExtensions } from "./extension-commands.js";
14
+ import { readCliEnvironmentFlag, readRequiredCliEnvironmentFlag, resolveCliEnvironment } from "./environment-context.js";
17
15
  export async function executeParsedCommand(parsed, context) {
18
16
  const api = createPlatformApiClient({
19
17
  sessionStore: context.sessionStore
@@ -21,6 +19,8 @@ export async function executeParsedCommand(parsed, context) {
21
19
  switch (parsed.definition.id) {
22
20
  case "auth.login":
23
21
  return executeAuthLogin(parsed, context, api);
22
+ case "auth.signup":
23
+ return executeAuthSignup(parsed, context, api);
24
24
  case "auth.logout":
25
25
  return executeAuthLogout(context, api);
26
26
  case "auth.whoami":
@@ -41,10 +41,6 @@ export async function executeParsedCommand(parsed, context) {
41
41
  return executeOrgSelect(parsed, context, api);
42
42
  case "org.settings.get":
43
43
  return executeOrgSettingsGet(parsed, context, api);
44
- case "org.import":
45
- return executeOrgImport(parsed, context, api);
46
- case "org.bundle.show":
47
- return executeOrgBundleShow(parsed, context, api);
48
44
  case "org.reset":
49
45
  return executeOrgReset(parsed, context, api);
50
46
  case "status":
@@ -65,24 +61,29 @@ export async function executeParsedCommand(parsed, context) {
65
61
  return executeWorkflowContext(parsed, context, api);
66
62
  case "workflow.start":
67
63
  return executeWorkflowStart(parsed, context, api);
64
+ case "test.node":
65
+ return executeWorkflowNodeTest(parsed, context, api);
68
66
  case "release.list":
69
67
  return executeReleaseList(parsed, context, api);
70
68
  case "release.get":
71
69
  return executeReleaseGet(parsed, context, api);
72
- case "release.changes":
73
- return executeReleaseChanges(parsed, context, api);
74
- case "release.deployment.get":
75
- return executeReleaseDeploymentGet(parsed, context, api);
76
- case "release.publish":
77
- return executeReleasePublish(parsed, context, api);
78
- case "release.retry":
79
- return executeReleaseRetry(parsed, context, api);
80
- case "release.discard":
81
- return executeReleaseDiscard(parsed, context, api);
82
- case "release.restore":
83
- return executeReleaseRestore(parsed, context, api);
84
- case "release.deactivate":
85
- return executeReleaseDeactivate(parsed, context, api);
70
+ case "release.create":
71
+ return executeReleaseCreate(parsed, context, api);
72
+ case "release.validate":
73
+ return executeReleaseValidate(parsed, context, api);
74
+ case "release.source":
75
+ return executeReleaseSource(parsed, context, api);
76
+ case "environment.list":
77
+ case "environment.get":
78
+ case "environment.create":
79
+ case "environment.rename":
80
+ case "environment.archive":
81
+ case "environment.deploy":
82
+ case "environment.undeploy":
83
+ return executeEnvironment(parsed, context, api);
84
+ case "deployment.list":
85
+ case "deployment.get":
86
+ return executeDeployment(parsed, context, api);
86
87
  case "run.list":
87
88
  return executeRunList(parsed, context, api);
88
89
  case "run.get":
@@ -93,14 +94,27 @@ export async function executeParsedCommand(parsed, context) {
93
94
  return executeRunSteps(parsed, context, api);
94
95
  case "run.abort":
95
96
  return executeRunAbort(parsed, context, api);
97
+ case "audit.list":
98
+ case "audit.get":
99
+ return executeAudit(parsed, context, api, resolveOrgScope);
96
100
  case "artifact.upload":
97
101
  return executeArtifactUpload(parsed, context, api, resolveOrgScope);
98
102
  case "artifact.list":
99
103
  return executeArtifactList(parsed, context, api, resolveOrgScope);
104
+ case "artifact.inventory":
105
+ return executeArtifactInventory(parsed, context, api, resolveOrgScope);
106
+ case "artifact.inspect":
107
+ return executeArtifactInspect(parsed, context, api, resolveOrgScope);
100
108
  case "artifact.download":
101
109
  return executeArtifactDownload(parsed, context, api, resolveOrgScope);
102
110
  case "artifact.read":
103
111
  return executeArtifactRead(parsed, context, api, resolveOrgScope);
112
+ case "artifact.archive":
113
+ return executeArtifactArchive(parsed, context, api, resolveOrgScope);
114
+ case "artifact.restore":
115
+ return executeArtifactRestore(parsed, context, api, resolveOrgScope);
116
+ case "artifact.purge":
117
+ return executeArtifactPurge(parsed, context, api, resolveOrgScope);
104
118
  case "task.list":
105
119
  return executeTaskList(parsed, context, api);
106
120
  case "task.get":
@@ -119,10 +133,6 @@ export async function executeParsedCommand(parsed, context) {
119
133
  case "org-model.capabilities.get":
120
134
  case "org-model.operations.list":
121
135
  case "org-model.operations.get":
122
- case "org-model.connectors.list":
123
- case "org-model.connectors.get":
124
- case "org-model.mcp-servers.list":
125
- case "org-model.mcp-servers.get":
126
136
  return executeOrgModel(parsed, context, api);
127
137
  case "access.members.list":
128
138
  case "access.members.get":
@@ -137,36 +147,25 @@ export async function executeParsedCommand(parsed, context) {
137
147
  case "access.api-keys.create":
138
148
  case "access.api-keys.revoke":
139
149
  return executeAccess(parsed, context, api);
140
- case "integrations.list":
141
- case "integrations.diagnostics":
142
- case "integrations.get":
143
- case "integrations.connections":
144
- case "integrations.actions":
145
- case "integrations.action":
146
- return executeIntegrations(parsed, context, api, resolveOrgScope);
147
- case "mcp.list":
148
- case "mcp.get":
149
- case "mcp.create":
150
- case "mcp.update":
151
- case "mcp.delete":
152
- case "mcp.test":
153
- case "mcp.discover-tools":
154
- case "mcp.connect":
155
- case "mcp.oauth-status":
156
- case "mcp.disconnect":
157
- return executeMcp(parsed, context, api);
158
- case "skills.list":
159
- case "skills.get":
160
- case "skills.create":
161
- case "skills.upload":
162
- case "skills.download":
163
- case "skills.update":
164
- case "skills.delete":
165
- return executeSkills(parsed, context, api);
150
+ case "extensions.export":
151
+ case "extensions.validate":
152
+ case "extensions.publish":
153
+ case "extensions.install":
154
+ case "extensions.grant":
155
+ case "extensions.update":
156
+ case "extensions.built-ins.list":
157
+ case "extensions.built-ins.install":
158
+ case "extensions.list":
159
+ case "extensions.inspect":
160
+ case "extensions.detail":
161
+ case "extensions.disable":
162
+ case "extensions.enable":
163
+ case "extensions.delete":
164
+ return executeExtensions(parsed, context, api, resolveOrgScope);
166
165
  case "env.list":
167
166
  case "env.get":
167
+ case "env.set":
168
168
  case "env.replace":
169
- case "env.import":
170
169
  return executeEnv(parsed, context, api);
171
170
  case "secrets.list":
172
171
  case "secrets.set":
@@ -176,12 +175,6 @@ export async function executeParsedCommand(parsed, context) {
176
175
  case "chat.sessions.list":
177
176
  case "chat.sessions.get":
178
177
  case "chat.sessions.delete":
179
- case "chat.changes":
180
- case "chat.validation":
181
- case "chat.draft.get":
182
- case "chat.draft.apply":
183
- case "chat.draft.refresh":
184
- case "chat.workspace-state":
185
178
  return executeChat(parsed, context, api);
186
179
  case "completion.bash":
187
180
  case "completion.zsh":
@@ -191,6 +184,10 @@ export async function executeParsedCommand(parsed, context) {
191
184
  return executeSchemaList();
192
185
  case "schema.get":
193
186
  return executeSchemaGet(parsed);
187
+ case "admin.usage.summary":
188
+ case "admin.usage.workflows":
189
+ case "admin.usage.tools":
190
+ case "admin.usage.events":
194
191
  case "admin.org.deleted.list":
195
192
  case "admin.org.deleted.get":
196
193
  case "admin.org.deleted.restore":
@@ -360,35 +357,13 @@ async function executeOrgSettingsGet(parsed, context, api) {
360
357
  meta: { command: "org settings get", orgId: org.id }
361
358
  };
362
359
  }
363
- async function executeOrgImport(parsed, context, api) {
364
- const { org, session } = await resolveOrgScope(parsed, context, api);
365
- const files = await readImportEntries(readRequiredArg(parsed, "path"));
366
- const data = await api.importProject(session, org.id, { files });
367
- return {
368
- data,
369
- human: renderSuccess(`Imported ${files.length} files into ${org.slug}.`),
370
- kind: "authoring_import_apply",
371
- meta: { command: "org import", orgId: org.id }
372
- };
373
- }
374
- async function executeOrgBundleShow(parsed, context, api) {
375
- const { org, session } = await resolveOrgScope(parsed, context, api);
376
- const releaseId = readOptionalStringFlag(parsed, "release");
377
- const data = await api.getProjectWorkspace(session, org.id, releaseId);
378
- return {
379
- data,
380
- human: renderPrettyJson(data.project),
381
- kind: "authoring_bundle_show",
382
- meta: { command: "org bundle show", orgId: org.id }
383
- };
384
- }
385
360
  async function executeOrgReset(parsed, context, api) {
386
361
  const { org, session } = await resolveOrgScope(parsed, context, api);
387
- await confirmDestructive(parsed, context, `Reset the draft project for ${org.slug}?`);
362
+ await confirmDestructive(parsed, context, `Reset release, runtime, project, and chat state for ${org.slug}?`);
388
363
  await api.resetOrganizationProject(session, org.id);
389
364
  return {
390
365
  data: { reset: true },
391
- human: renderSuccess(`Reset the draft project for ${org.slug}.`),
366
+ human: renderSuccess(`Reset project state for ${org.slug}.`),
392
367
  kind: "authoring_project_reset",
393
368
  meta: { command: "org reset", orgId: org.id }
394
369
  };
@@ -399,8 +374,6 @@ async function executeStatus(parsed, context, api) {
399
374
  return {
400
375
  data,
401
376
  human: renderKeyValue([
402
- { label: "Draft changes", value: data.overview.draftChangeCount },
403
- { label: "Draft has changes", value: data.overview.draftHasChanges },
404
377
  { label: "Pending tasks", value: data.overview.pendingTaskCount },
405
378
  { label: "Runs in flight", value: data.overview.runsInFlight },
406
379
  { label: "Workflow count", value: data.overview.workflowCount }
@@ -411,12 +384,14 @@ async function executeStatus(parsed, context, api) {
411
384
  }
412
385
  async function executeActivityList(parsed, context, api) {
413
386
  const { org, session } = await resolveOrgScope(parsed, context, api);
387
+ const { environment } = resolveCliEnvironment(parsed, session);
414
388
  const focusType = readOptionalStringFlag(parsed, "focus-type");
415
389
  const focusValue = readOptionalStringFlag(parsed, "focus-value");
416
390
  if ((focusType && !focusValue) || (!focusType && focusValue)) {
417
391
  throw usageProblem("Use --focus-type and --focus-value together.", "activity list");
418
392
  }
419
393
  const data = await api.getActivity(session, org.id, {
394
+ environment,
420
395
  ...(focusType ? { focusType: focusType } : {}),
421
396
  ...(focusValue ? { focusValue } : {}),
422
397
  ...optionalNumberValue("limit", readOptionalNumberFlag(parsed, "limit")),
@@ -429,11 +404,12 @@ async function executeActivityList(parsed, context, api) {
429
404
  { key: "occurredAt", label: "Occurred" },
430
405
  { key: "kind", label: "Kind" },
431
406
  { key: "status", label: "Status" },
407
+ { key: "environmentKey", label: "Environment" },
432
408
  { key: "processName", label: "Workflow" },
433
409
  { key: "summary", label: "Summary" }
434
410
  ]),
435
411
  kind: "runtime_activity_list",
436
- meta: { command: "activity list", orgId: org.id }
412
+ meta: { command: "activity list", environment, orgId: org.id }
437
413
  };
438
414
  }
439
415
  async function executeActivityOptions(parsed, context, api) {
@@ -446,58 +422,98 @@ async function executeActivityOptions(parsed, context, api) {
446
422
  meta: { command: "activity options", orgId: org.id }
447
423
  };
448
424
  }
425
+ function readOptionalReleaseSnapshotSelector(parsed) {
426
+ const releaseId = readOptionalStringFlag(parsed, "release");
427
+ const environment = readCliEnvironmentFlag(parsed);
428
+ if (releaseId && environment) {
429
+ throw usageProblem("Use either --release or --environment, not both.", parsed.definition.id);
430
+ }
431
+ if (releaseId) {
432
+ return { releaseId };
433
+ }
434
+ if (environment) {
435
+ return { environment };
436
+ }
437
+ return undefined;
438
+ }
439
+ function readRequiredReleaseSnapshotSelector(parsed) {
440
+ const selector = readOptionalReleaseSnapshotSelector(parsed);
441
+ if (!selector) {
442
+ throw usageProblem("Provide either --release or --environment.", parsed.definition.id);
443
+ }
444
+ return selector;
445
+ }
446
+ function releaseSnapshotSelectorMeta(selector) {
447
+ if (!selector) {
448
+ return {};
449
+ }
450
+ if (typeof selector.releaseId === "string") {
451
+ return { releaseId: selector.releaseId };
452
+ }
453
+ if (typeof selector.environment === "string") {
454
+ return { environment: selector.environment };
455
+ }
456
+ return {};
457
+ }
449
458
  async function executeWorkflowList(parsed, context, api) {
450
459
  const { org, session } = await resolveOrgScope(parsed, context, api);
451
- const data = await api.listWorkflows(session, org.id, readOptionalStringFlag(parsed, "release"));
460
+ const selector = readOptionalReleaseSnapshotSelector(parsed);
461
+ const data = await api.listWorkflows(session, org.id, selector ?? { live: true });
452
462
  return {
453
463
  data,
454
464
  human: renderTable(data.processes, [
455
465
  { key: "name", label: "Name" },
466
+ { key: "environmentKey", label: "Environment" },
467
+ { key: "liveReleaseId", label: "Live release" },
456
468
  { key: "latestVersion", label: "Latest" },
457
- { key: "liveVersion", label: "Live" }
469
+ { key: "deployedVersion", label: "Deployed" }
458
470
  ]),
459
471
  kind: "authoring_workflow_list",
460
- meta: { command: "workflow list", orgId: org.id }
472
+ meta: { command: "workflow list", orgId: org.id, ...releaseSnapshotSelectorMeta(selector) }
461
473
  };
462
474
  }
463
475
  async function executeWorkflowGet(parsed, context, api) {
464
476
  const { org, session } = await resolveOrgScope(parsed, context, api);
465
- const data = await api.getWorkflow(session, org.id, readRequiredArg(parsed, "name"), readOptionalStringFlag(parsed, "release"));
477
+ const selector = readRequiredReleaseSnapshotSelector(parsed);
478
+ const data = await api.getWorkflow(session, org.id, readRequiredArg(parsed, "name"), selector);
466
479
  return {
467
480
  data,
468
481
  human: renderPrettyJson(data.process),
469
482
  kind: "authoring_workflow_get",
470
- meta: { command: "workflow get", orgId: org.id }
483
+ meta: { command: "workflow get", orgId: org.id, ...releaseSnapshotSelectorMeta(selector) }
471
484
  };
472
485
  }
473
486
  async function executeWorkflowVersionGet(parsed, context, api) {
474
487
  const { org, session } = await resolveOrgScope(parsed, context, api);
475
- const data = await api.getWorkflowVersion(session, org.id, readRequiredArg(parsed, "name"), readRequiredIntegerArg(parsed, "version"), readOptionalStringFlag(parsed, "release"));
488
+ const selector = readRequiredReleaseSnapshotSelector(parsed);
489
+ const data = await api.getWorkflowVersion(session, org.id, readRequiredArg(parsed, "name"), readRequiredIntegerArg(parsed, "version"), selector);
476
490
  return {
477
491
  data,
478
492
  human: renderPrettyJson(data.process),
479
493
  kind: "authoring_workflow_version_get",
480
- meta: { command: "workflow version get", orgId: org.id }
494
+ meta: { command: "workflow version get", orgId: org.id, ...releaseSnapshotSelectorMeta(selector) }
481
495
  };
482
496
  }
483
497
  async function executeWorkflowDependencies(parsed, context, api) {
484
498
  const { org, session } = await resolveOrgScope(parsed, context, api);
485
- const data = await api.getWorkflowDependencies(session, org.id, readRequiredArg(parsed, "name"), readRequiredIntegerArg(parsed, "version"), readOptionalStringFlag(parsed, "release"));
499
+ const selector = readRequiredReleaseSnapshotSelector(parsed);
500
+ const data = await api.getWorkflowDependencies(session, org.id, readRequiredArg(parsed, "name"), readRequiredIntegerArg(parsed, "version"), selector);
486
501
  return {
487
502
  data,
488
503
  human: renderPrettyJson(data.dependencies),
489
504
  kind: "authoring_workflow_dependencies",
490
- meta: { command: "workflow dependencies", orgId: org.id }
505
+ meta: { command: "workflow dependencies", orgId: org.id, ...releaseSnapshotSelectorMeta(selector) }
491
506
  };
492
507
  }
493
508
  async function executeWorkflowContext(parsed, context, api) {
494
509
  const { org, session } = await resolveOrgScope(parsed, context, api);
495
- const data = await api.getWorkflowContext(session, org.id, readOptionalStringFlag(parsed, "release"));
510
+ const selector = readRequiredReleaseSnapshotSelector(parsed);
511
+ const data = await api.getWorkflowContext(session, org.id, selector);
496
512
  return {
497
513
  data,
498
514
  human: renderPrettyJson(data.context),
499
515
  kind: "authoring_workflow_context",
500
- meta: { command: "workflow context", orgId: org.id }
516
+ meta: { command: "workflow context", orgId: org.id, ...releaseSnapshotSelectorMeta(selector) }
501
517
  };
502
518
  }
503
519
  async function executeWorkflowStart(parsed, context, api) {
@@ -508,10 +524,13 @@ async function executeWorkflowStart(parsed, context, api) {
508
524
  }
509
525
  const inputSpecifier = readOptionalStringFlag(parsed, "input");
510
526
  const inputData = inputSpecifier
511
- ? await readJsonInputSpecifier(inputSpecifier, context.stdin, "workflow start")
527
+ ? await readJsonInputSpecifier(inputSpecifier, context.stdin, "workflow start", { flag: "input" })
512
528
  : undefined;
513
529
  const workflowName = readRequiredArg(parsed, "name");
530
+ const resolvedEnvironment = resolveCliEnvironment(parsed, session);
531
+ announceResolvedEnvironment(parsed, context, resolvedEnvironment.environment);
514
532
  const data = await api.startWorkflow(session, org.id, workflowName, {
533
+ environment: resolvedEnvironment.environment,
515
534
  ...(inputData ? { inputData } : {}),
516
535
  startEvent: {
517
536
  ...(readOptionalStringFlag(parsed, "start-event-name")
@@ -522,24 +541,69 @@ async function executeWorkflowStart(parsed, context, api) {
522
541
  });
523
542
  return {
524
543
  data,
525
- human: renderSuccess(`Started workflow ${workflowName} as ${data.started.workflowId}.`),
544
+ human: renderSuccess(`Started workflow ${workflowName} in ${data.started.environmentKey ?? resolvedEnvironment.environment} as ${data.started.workflowId}.`),
526
545
  kind: "runtime_workflow_start",
527
- meta: { command: "workflow start", orgId: org.id }
546
+ meta: { command: "workflow start", environment: data.started.environmentKey ?? resolvedEnvironment.environment, orgId: org.id }
528
547
  };
529
548
  }
530
- async function executeReleaseList(parsed, context, api) {
549
+ async function executeWorkflowNodeTest(parsed, context, api) {
531
550
  const { org, session } = await resolveOrgScope(parsed, context, api);
532
- const data = await api.listReleases(session, org.id, {
533
- ...(parsed.flags["include-draft"] === true ? { includeDraft: true } : {}),
534
- ...optionalNumberValue("limit", readOptionalNumberFlag(parsed, "limit"))
551
+ const workflowName = readRequiredArg(parsed, "workflow-name");
552
+ const nodeId = readRequiredArg(parsed, "node-id");
553
+ const workspacePath = readOptionalStringFlag(parsed, "workspace") ?? context.cwd;
554
+ const environment = readOptionalStringFlag(parsed, "environment");
555
+ const inputSpecifier = readOptionalStringFlag(parsed, "input");
556
+ const inputData = inputSpecifier
557
+ ? await readJsonInputSpecifier(inputSpecifier, context.stdin, "test node", { flag: "input" })
558
+ : undefined;
559
+ const data = await api.testWorkflowNode(session, org.id, workflowName, nodeId, {
560
+ ...(environment ? { environment } : {}),
561
+ files: await readWorkspaceTestEntries(workspacePath),
562
+ ...(inputData ? { input: inputData } : {})
535
563
  });
564
+ const test = data.test;
536
565
  return {
537
566
  data,
538
- human: renderTable(data.releases, [
567
+ exitCode: test.status === "passed" ? 0 : 1,
568
+ human: renderWorkflowNodeTestHuman(test),
569
+ kind: "workflow_node_test",
570
+ meta: withCliSummary({
571
+ command: "test node",
572
+ nodeId,
573
+ orgId: org.id,
574
+ workflowName
575
+ }, summarizeWorkflowNodeTest(test))
576
+ };
577
+ }
578
+ async function executeReleaseList(parsed, context, api) {
579
+ const { org, session } = await resolveOrgScope(parsed, context, api);
580
+ const [data, deploymentData] = await Promise.all([
581
+ api.listReleases(session, org.id, {
582
+ ...optionalNumberValue("limit", readOptionalNumberFlag(parsed, "limit"))
583
+ }),
584
+ api.listEnvironmentDeployments(session, org.id, { limit: 100 })
585
+ ]);
586
+ return {
587
+ data: { ...data, deployments: deploymentData.deployments },
588
+ human: renderTable(data.releases.map((release) => {
589
+ const deploymentsForRelease = deploymentData.deployments.filter((deployment) => deployment.releaseId === release.id);
590
+ const latestDeployment = deploymentsForRelease[0] ?? null;
591
+ return {
592
+ createdAt: release.createdAt,
593
+ id: release.id,
594
+ state: typeof release.state === "string" ? release.state : "",
595
+ latestDeployment: latestDeployment ? `${latestDeployment.environmentKey}:${latestDeployment.status}` : "",
596
+ liveEnvironments: deploymentsForRelease
597
+ .filter((deployment) => deployment.isLive)
598
+ .map((deployment) => deployment.environmentKey)
599
+ .join(", ")
600
+ };
601
+ }), [
539
602
  { key: "id", label: "Release" },
540
- { key: "effectiveState", label: "State" },
541
- { key: "deploymentId", label: "Deployment" },
542
- { key: "publishedAt", label: "Published" }
603
+ { key: "state", label: "Artifact state" },
604
+ { key: "liveEnvironments", label: "Live environments" },
605
+ { key: "latestDeployment", label: "Latest deployment" },
606
+ { key: "createdAt", label: "Created" }
543
607
  ]),
544
608
  kind: "releases_release_list",
545
609
  meta: { command: "release list", orgId: org.id }
@@ -555,88 +619,265 @@ async function executeReleaseGet(parsed, context, api) {
555
619
  meta: { command: "release get", orgId: org.id }
556
620
  };
557
621
  }
558
- async function executeReleaseChanges(parsed, context, api) {
559
- const { org, session } = await resolveOrgScope(parsed, context, api);
560
- const data = await api.getDraftChanges(session, org.id);
561
- return {
562
- data,
563
- human: renderTable(data.changes, [
564
- { key: "changedAt", label: "Changed" },
565
- { key: "action", label: "Action" },
566
- { key: "entityType", label: "Entity" },
567
- { key: "entityKey", label: "Key" }
568
- ]),
569
- kind: "releases_release_changes",
570
- meta: { command: "release changes", orgId: org.id }
571
- };
572
- }
573
- async function executeReleaseDeploymentGet(parsed, context, api) {
622
+ async function executeReleaseValidate(parsed, context, api) {
574
623
  const { org, session } = await resolveOrgScope(parsed, context, api);
575
- const data = await api.getDeployment(session, org.id, readRequiredArg(parsed, "deployment-id"));
624
+ const releaseId = readRequiredArg(parsed, "release-id");
625
+ const environment = readOptionalStringFlag(parsed, "environment");
626
+ const data = await api.validateRelease(session, org.id, {
627
+ ...(environment ? { environment } : {}),
628
+ releaseId
629
+ });
630
+ const validation = data.validation;
631
+ const human = validation.releaseReady
632
+ ? renderSuccess(`Release ${validation.releaseId} passed release validation${environment ? ` for ${environment}` : ""}.`)
633
+ : [
634
+ `Release ${validation.releaseId} did not pass release validation${environment ? ` for ${environment}` : ""}.`,
635
+ renderTable(validation.diagnostics, [
636
+ { key: "gate", label: "Gate" },
637
+ { key: "severity", label: "Severity" },
638
+ { key: "code", label: "Code" },
639
+ { key: "path", label: "Path" },
640
+ { key: "message", label: "Message" }
641
+ ])
642
+ ].join("\n");
576
643
  return {
577
644
  data,
578
- human: renderPrettyJson(data.deployment),
579
- kind: "runtime_deployment_get",
580
- meta: { command: "release deployment get", orgId: org.id }
645
+ ...(validation.releaseReady ? {} : { exitCode: 1 }),
646
+ human,
647
+ kind: "releases_release_validate",
648
+ meta: withCliSummary({ command: "release validate", ...(environment ? { environment } : {}), orgId: org.id }, summarizeReleaseValidation(validation))
581
649
  };
582
650
  }
583
- async function executeReleasePublish(parsed, context, api) {
651
+ async function executeReleaseSource(parsed, context, api) {
584
652
  const { org, session } = await resolveOrgScope(parsed, context, api);
585
- const data = await api.publishRelease(session, org.id, readOptionalStringFlag(parsed, "reason"));
653
+ const releaseId = readRequiredArg(parsed, "release-id");
654
+ const outPath = readRequiredStringFlag(parsed, "out");
655
+ const data = await api.getReleaseSource(session, org.id, releaseId);
656
+ const files = [
657
+ ...data.source.files.map((file) => ({
658
+ content: file.content,
659
+ path: file.path
660
+ })),
661
+ ...data.source.assets.map((asset) => ({
662
+ content: asset.content,
663
+ path: asset.path
664
+ }))
665
+ ];
666
+ await writeReleaseSourceFiles(outPath, {
667
+ files,
668
+ metadata: {
669
+ fileCount: files.length,
670
+ releaseId,
671
+ source: "release"
672
+ }
673
+ });
674
+ const summary = `Release ${releaseId} source written to ${outPath}.`;
586
675
  return {
587
- data,
588
- human: renderSuccess(`Published release ${data.published.release.id}.`),
589
- kind: "releases_release_publish",
590
- meta: { command: "release publish", orgId: org.id }
676
+ data: {
677
+ fileCount: files.length,
678
+ releaseId,
679
+ out: outPath
680
+ },
681
+ human: renderSuccess(summary),
682
+ kind: "releases_release_source",
683
+ meta: withCliSummary({ command: "release source", orgId: org.id, releaseId }, summary)
591
684
  };
592
685
  }
593
- async function executeReleaseRetry(parsed, context, api) {
686
+ async function executeReleaseCreate(parsed, context, api) {
594
687
  const { org, session } = await resolveOrgScope(parsed, context, api);
595
- const data = await api.retryReleasePublication(session, org.id, readRequiredArg(parsed, "release-id"));
688
+ const workspacePath = readRequiredArg(parsed, "workspace");
689
+ if (isZipArchivePath(workspacePath)) {
690
+ const data = await api.createReleaseArchive(session, org.id, await readArchiveBytes(workspacePath, "release create"));
691
+ const summary = summarizeReleaseCreation(data.created.release.id);
692
+ return {
693
+ data,
694
+ human: renderSuccess(`${summary} Source: ${workspacePath}.`),
695
+ kind: "releases_release_create",
696
+ meta: withCliSummary({ command: "release create", orgId: org.id, releaseId: data.created.release.id }, summary)
697
+ };
698
+ }
699
+ const files = await readImportEntries(workspacePath);
700
+ const data = await api.createRelease(session, org.id, {
701
+ files
702
+ });
703
+ const summary = summarizeReleaseCreation(data.created.release.id);
596
704
  return {
597
705
  data,
598
- human: renderSuccess(`Retried release ${parsed.args["release-id"]}.`),
599
- kind: "releases_release_retry",
600
- meta: { command: "release retry", orgId: org.id }
706
+ human: renderSuccess(`${summary} Source: ${workspacePath}.`),
707
+ kind: "releases_release_create",
708
+ meta: withCliSummary({ command: "release create", orgId: org.id, releaseId: data.created.release.id }, summary)
601
709
  };
602
710
  }
603
- async function executeReleaseDiscard(parsed, context, api) {
711
+ async function executeEnvironment(parsed, context, api) {
604
712
  const { org, session } = await resolveOrgScope(parsed, context, api);
605
- await confirmDestructive(parsed, context, `Discard the draft release for ${org.slug}?`);
606
- const data = await api.discardDraft(session, org.id);
607
- return {
608
- data,
609
- human: renderSuccess(`Discarded draft release ${data.draft.id}.`),
610
- kind: "releases_release_discard",
611
- meta: { command: "release discard", orgId: org.id }
612
- };
713
+ switch (parsed.definition.id) {
714
+ case "environment.list": {
715
+ const data = await api.listEnvironments(session, org.id);
716
+ return {
717
+ data,
718
+ human: renderTable(data.environments.map((entry) => ({
719
+ currentDeploymentId: entry.environment.currentDeploymentId,
720
+ isProductionTarget: entry.environment.isProductionTarget,
721
+ key: entry.environment.key,
722
+ latestStatus: entry.latestDeployment?.status ?? "",
723
+ liveDeploymentId: entry.liveDeployment?.id ?? "",
724
+ name: entry.environment.name,
725
+ status: entry.environment.status
726
+ })), [
727
+ { key: "key", label: "Environment" },
728
+ { key: "name", label: "Name" },
729
+ { key: "status", label: "Status" },
730
+ { key: "isProductionTarget", label: "Production" },
731
+ { key: "liveDeploymentId", label: "Live deployment" },
732
+ { key: "latestStatus", label: "Latest" }
733
+ ]),
734
+ kind: "environment_list",
735
+ meta: { command: "environment list", orgId: org.id }
736
+ };
737
+ }
738
+ case "environment.get": {
739
+ const data = await api.getEnvironment(session, org.id, readRequiredArg(parsed, "environment"));
740
+ return {
741
+ data,
742
+ human: renderPrettyJson(data),
743
+ kind: "environment_get",
744
+ meta: { command: "environment get", orgId: org.id }
745
+ };
746
+ }
747
+ case "environment.create": {
748
+ const key = readRequiredArg(parsed, "environment");
749
+ const copyFrom = readOptionalStringFlag(parsed, "copy-from");
750
+ const copyFromEnvironmentId = copyFrom
751
+ ? await resolveEnvironmentIdForCli(session, org.id, copyFrom, api)
752
+ : undefined;
753
+ const data = await api.createEnvironment(session, org.id, {
754
+ ...(copyFromEnvironmentId ? { copyFromEnvironmentId } : {}),
755
+ key,
756
+ ...(readOptionalStringFlag(parsed, "name") ? { name: readOptionalStringFlag(parsed, "name") } : {})
757
+ });
758
+ return {
759
+ data,
760
+ human: renderSuccess(`Created environment ${data.environment.key}${copyFrom ? ` from ${copyFrom}` : ""}.`),
761
+ kind: "environment_create",
762
+ meta: {
763
+ command: "environment create",
764
+ ...(copyFrom ? { copyFrom } : {}),
765
+ environment: data.environment.key,
766
+ orgId: org.id
767
+ }
768
+ };
769
+ }
770
+ case "environment.rename": {
771
+ const data = await api.renameEnvironment(session, org.id, readRequiredArg(parsed, "environment"), {
772
+ name: readRequiredArg(parsed, "name")
773
+ });
774
+ return {
775
+ data,
776
+ human: renderSuccess(`Renamed environment ${data.environment.key}.`),
777
+ kind: "environment_rename",
778
+ meta: { command: "environment rename", environment: data.environment.key, orgId: org.id }
779
+ };
780
+ }
781
+ case "environment.archive": {
782
+ const environment = readRequiredArg(parsed, "environment");
783
+ await confirmDestructive(parsed, context, `Archive environment ${environment}?`);
784
+ const data = await api.archiveEnvironment(session, org.id, environment);
785
+ return {
786
+ data,
787
+ human: renderSuccess(`Archived environment ${data.environment.key}.`),
788
+ kind: "environment_archive",
789
+ meta: { command: "environment archive", environment: data.environment.key, orgId: org.id }
790
+ };
791
+ }
792
+ case "environment.deploy": {
793
+ const environment = readRequiredArg(parsed, "environment");
794
+ const releaseId = readRequiredArg(parsed, "release");
795
+ const policy = readOptionalStringFlag(parsed, "policy");
796
+ const data = await api.deployEnvironment(session, org.id, environment, {
797
+ ...(policy ? { policy } : {}),
798
+ releaseId
799
+ });
800
+ return {
801
+ data,
802
+ human: renderSuccess(`Deployed release ${releaseId} to ${environment} as ${data.deployment.id}.`),
803
+ kind: "environment_deploy",
804
+ meta: { command: "environment deploy", deploymentId: data.deployment.id, environment, orgId: org.id, releaseId }
805
+ };
806
+ }
807
+ case "environment.undeploy": {
808
+ const environment = readRequiredArg(parsed, "environment");
809
+ await confirmDestructive(parsed, context, `Undeploy the live deployment from ${environment}?`);
810
+ const policy = readOptionalStringFlag(parsed, "policy");
811
+ const data = await api.undeployEnvironment(session, org.id, environment, {
812
+ ...(policy ? { policy } : {})
813
+ });
814
+ return {
815
+ data,
816
+ human: renderSuccess(`Undeployed environment ${environment}.`),
817
+ kind: "environment_undeploy",
818
+ meta: { command: "environment undeploy", deploymentId: data.deployment.id, environment, orgId: org.id }
819
+ };
820
+ }
821
+ default:
822
+ throw genericProblem(`Unhandled environment command ${parsed.definition.id}.`, parsed.definition.id);
823
+ }
613
824
  }
614
- async function executeReleaseRestore(parsed, context, api) {
825
+ async function executeDeployment(parsed, context, api) {
615
826
  const { org, session } = await resolveOrgScope(parsed, context, api);
616
- await confirmDestructive(parsed, context, `Restore release ${parsed.args["release-id"]} into draft?`);
617
- const data = await api.restoreRelease(session, org.id, readRequiredArg(parsed, "release-id"));
618
- return {
619
- data,
620
- human: renderSuccess(`Restored release ${parsed.args["release-id"]} into draft.`),
621
- kind: "releases_release_restore",
622
- meta: { command: "release restore", orgId: org.id }
623
- };
827
+ switch (parsed.definition.id) {
828
+ case "deployment.list": {
829
+ const { environment } = resolveCliEnvironment(parsed, session);
830
+ const data = await api.listEnvironmentDeployments(session, org.id, {
831
+ environment,
832
+ ...optionalNumberValue("limit", readOptionalNumberFlag(parsed, "limit"))
833
+ });
834
+ return {
835
+ data,
836
+ human: renderTable(data.deployments.map((deployment) => ({
837
+ ...deployment,
838
+ live: deployment.isLive ? "yes" : ""
839
+ })), [
840
+ { key: "id", label: "Deployment" },
841
+ { key: "environmentKey", label: "Environment" },
842
+ { key: "live", label: "Live" },
843
+ { key: "releaseId", label: "Release" },
844
+ { key: "status", label: "Status" },
845
+ { key: "coreDeploymentId", label: "Core deployment" },
846
+ { key: "updatedAt", label: "Updated" }
847
+ ]),
848
+ kind: "deployment_list",
849
+ meta: { command: "deployment list", environment, orgId: org.id }
850
+ };
851
+ }
852
+ case "deployment.get": {
853
+ const environment = readOptionalStringFlag(parsed, "environment");
854
+ const data = await api.getEnvironmentDeployment(session, org.id, readRequiredArg(parsed, "deployment"), {
855
+ ...(environment ? { environment } : {})
856
+ });
857
+ return {
858
+ data,
859
+ human: renderPrettyJson(data.deployment),
860
+ kind: "deployment_get",
861
+ meta: { command: "deployment get", deploymentId: data.deployment.id, ...(environment ? { environment } : {}), orgId: org.id }
862
+ };
863
+ }
864
+ default:
865
+ throw genericProblem(`Unhandled deployment command ${parsed.definition.id}.`, parsed.definition.id);
866
+ }
624
867
  }
625
- async function executeReleaseDeactivate(parsed, context, api) {
626
- const { org, session } = await resolveOrgScope(parsed, context, api);
627
- await confirmDestructive(parsed, context, `Deactivate release ${parsed.args["release-id"]}?`);
628
- const policy = readOptionalStringFlag(parsed, "policy");
629
- const data = await api.deactivateRelease(session, org.id, readRequiredArg(parsed, "release-id"), policy);
630
- return {
631
- data,
632
- human: renderSuccess(`Deactivated release ${parsed.args["release-id"]}.`),
633
- kind: "releases_release_deactivate",
634
- meta: { command: "release deactivate", orgId: org.id }
635
- };
868
+ async function resolveEnvironmentIdForCli(session, orgId, environment, api) {
869
+ const data = await api.listEnvironments(session, orgId);
870
+ const match = data.environments.find((entry) => entry.environment.id === environment || entry.environment.key === environment);
871
+ if (!match) {
872
+ throw notFoundProblem(`Environment '${environment}' was not found.`, "environment create");
873
+ }
874
+ return match.environment.id;
636
875
  }
637
876
  async function executeRunList(parsed, context, api) {
638
877
  const { org, session } = await resolveOrgScope(parsed, context, api);
878
+ const resolvedEnvironment = resolveCliEnvironment(parsed, session);
639
879
  const data = await api.listRuns(session, org.id, {
880
+ environment: resolvedEnvironment.environment,
640
881
  ...optionalNumberValue("limit", readOptionalNumberFlag(parsed, "limit")),
641
882
  ...(readOptionalStringFlag(parsed, "status") ? { status: readOptionalStringFlag(parsed, "status") } : {})
642
883
  });
@@ -645,11 +886,12 @@ async function executeRunList(parsed, context, api) {
645
886
  human: renderTable(data.runs, [
646
887
  { key: "workflowId", label: "Workflow ID" },
647
888
  { key: "status", label: "Status" },
889
+ { key: "environmentKey", label: "Environment" },
648
890
  { key: "releaseId", label: "Release" },
649
891
  { key: "startTime", label: "Started" }
650
892
  ]),
651
893
  kind: "runtime_run_list",
652
- meta: { command: "run list", orgId: org.id }
894
+ meta: { command: "run list", environment: resolvedEnvironment.environment, orgId: org.id }
653
895
  };
654
896
  }
655
897
  async function executeRunGet(parsed, context, api) {
@@ -737,7 +979,7 @@ async function executeTaskGet(parsed, context, api) {
737
979
  }
738
980
  async function executeTaskComplete(parsed, context, api) {
739
981
  const { org, session } = await resolveOrgScope(parsed, context, api);
740
- const outputData = await readJsonInputSpecifier(String(parsed.flags.output), context.stdin, "task complete");
982
+ const outputData = await readJsonInputSpecifier(String(parsed.flags.output), context.stdin, "task complete", { flag: "output" });
741
983
  const data = await api.completeTask(session, org.id, readRequiredArg(parsed, "task-id"), {
742
984
  outputData,
743
985
  workflowId: String(parsed.flags.run)
@@ -753,8 +995,9 @@ async function executeOrgModel(parsed, context, api) {
753
995
  const { org, session } = await resolveOrgScope(parsed, context, api);
754
996
  const section = parsed.definition.path[1] ?? "";
755
997
  const verb = parsed.definition.path[2] ?? "";
998
+ const selector = readRequiredReleaseSnapshotSelector(parsed);
756
999
  if (section === "operations") {
757
- const editor = (await api.getOperationsEditor(session, org.id)).editor;
1000
+ const editor = (await api.getOperationsEditor(session, org.id, selector)).editor;
758
1001
  const record = verb === "get"
759
1002
  ? requireFound(editor.operations.find((entry) => entry.name === parsed.args.name), `Operation '${parsed.args.name}' was not found.`, parsed.definition.id)
760
1003
  : undefined;
@@ -764,56 +1007,20 @@ async function executeOrgModel(parsed, context, api) {
764
1007
  ? renderPrettyJson(record)
765
1008
  : renderTable(editor.operations, [
766
1009
  { key: "name", label: "Name" },
767
- { key: "connector", label: "Connector" },
768
- { key: "action", label: "Action" }
769
- ]),
770
- kind: `authoring_${sanitizeKind(section)}_${verb}`,
771
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
772
- };
773
- }
774
- if (section === "connectors") {
775
- const editor = (await api.getConnectorsEditor(session, org.id)).editor;
776
- const record = verb === "get"
777
- ? requireFound(editor.connectors.find((entry) => entry.name === parsed.args.name), `Connector '${parsed.args.name}' was not found.`, parsed.definition.id)
778
- : undefined;
779
- return {
780
- data: verb === "get" ? { connector: record } : { connectors: editor.connectors },
781
- human: verb === "get"
782
- ? renderPrettyJson(record)
783
- : renderTable(editor.connectors, [
784
- { key: "name", label: "Name" },
785
- { key: "type", label: "Type" }
786
- ]),
787
- kind: `authoring_${sanitizeKind(section)}_${verb}`,
788
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
789
- };
790
- }
791
- if (section === "mcp-servers") {
792
- const editor = (await api.getMcpServersEditor(session, org.id)).editor;
793
- const record = verb === "get"
794
- ? requireFound(editor.servers.find((entry) => entry.name === parsed.args.name), `MCP server '${parsed.args.name}' was not found.`, parsed.definition.id)
795
- : undefined;
796
- return {
797
- data: verb === "get" ? { server: record } : { servers: editor.servers },
798
- human: verb === "get"
799
- ? renderPrettyJson(record)
800
- : renderTable(editor.servers, [
801
- { key: "name", label: "Name" },
802
- { key: "transport", label: "Transport" },
803
- { key: "url", label: "URL" }
1010
+ { key: "command", label: "Command" }
804
1011
  ]),
805
1012
  kind: `authoring_${sanitizeKind(section)}_${verb}`,
806
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1013
+ meta: { command: parsed.definition.path.join(" "), orgId: org.id, ...releaseSnapshotSelectorMeta(selector) }
807
1014
  };
808
1015
  }
809
- const management = (await api.getOrganizationManagementContext(session, org.id)).context;
1016
+ const management = (await api.getOrganizationManagementContext(session, org.id, selector)).context;
810
1017
  if (section === "capabilities" && verb === "get") {
811
- const detail = await api.getCapabilityEditor(session, org.id, readRequiredArg(parsed, "name"));
1018
+ const detail = await api.getCapabilityEditor(session, org.id, readRequiredArg(parsed, "name"), selector);
812
1019
  return {
813
1020
  data: detail,
814
1021
  human: renderPrettyJson(detail.editor),
815
1022
  kind: "authoring_capabilities_get",
816
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1023
+ meta: { command: parsed.definition.path.join(" "), orgId: org.id, ...releaseSnapshotSelectorMeta(selector) }
817
1024
  };
818
1025
  }
819
1026
  const selection = selectOrgModelSection(management, section, verb === "get" ? parsed.args[section === "assignments" ? "role-name" : "name"] : undefined);
@@ -821,7 +1028,7 @@ async function executeOrgModel(parsed, context, api) {
821
1028
  data: selection.data,
822
1029
  human: selection.human,
823
1030
  kind: `authoring_${sanitizeKind(section)}_${verb}`,
824
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1031
+ meta: { command: parsed.definition.path.join(" "), orgId: org.id, ...releaseSnapshotSelectorMeta(selector) }
825
1032
  };
826
1033
  }
827
1034
  async function executeAccess(parsed, context, api) {
@@ -985,354 +1192,65 @@ async function executeAccess(parsed, context, api) {
985
1192
  throw genericProblem(`Unhandled access command ${parsed.definition.id}.`, parsed.definition.id);
986
1193
  }
987
1194
  }
988
- async function executeMcp(parsed, context, api) {
989
- const { org, session } = await resolveOrgScope(parsed, context, api);
990
- switch (parsed.definition.id) {
991
- case "mcp.list": {
992
- const data = await api.listOrgMcpServers(session, org.id);
993
- return {
994
- data,
995
- human: renderTable(data.servers, [
996
- { key: "slug", label: "Slug" },
997
- { key: "displayName", label: "Name" },
998
- { key: "status", label: "Status" },
999
- { key: "authMode", label: "Auth" },
1000
- { key: "url", label: "URL" }
1001
- ]),
1002
- kind: "mcp_servers_list",
1003
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1004
- };
1005
- }
1006
- case "mcp.get": {
1007
- const data = await api.getOrgMcpServer(session, org.id, readRequiredArg(parsed, "server"));
1008
- return {
1009
- data,
1010
- human: renderPrettyJson(data),
1011
- kind: "mcp_servers_get",
1012
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1013
- };
1014
- }
1015
- case "mcp.create": {
1016
- const authMode = readOptionalMcpAuthMode(parsed);
1017
- const timeoutMs = readOptionalNumberFlag(parsed, "timeout-ms");
1018
- const input = {
1019
- displayName: readRequiredStringFlag(parsed, "name"),
1020
- slug: readRequiredStringFlag(parsed, "slug"),
1021
- url: readRequiredStringFlag(parsed, "url"),
1022
- ...(authMode !== undefined ? { authMode } : {}),
1023
- ...(timeoutMs !== undefined ? { timeoutMs } : {})
1024
- };
1025
- const data = await api.createOrgMcpServer(session, org.id, input);
1026
- return {
1027
- data,
1028
- human: renderSuccess(`Created MCP server ${data.server.slug}.`),
1029
- kind: "mcp_servers_create",
1030
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1031
- };
1032
- }
1033
- case "mcp.update": {
1034
- const authMode = readOptionalMcpAuthMode(parsed);
1035
- const displayName = readOptionalStringFlag(parsed, "name");
1036
- const timeoutMs = readOptionalNumberFlag(parsed, "timeout-ms");
1037
- const url = readOptionalStringFlag(parsed, "url");
1038
- const disable = parsed.flags.disable === true;
1039
- const enable = parsed.flags.enable === true;
1040
- if (disable && enable) {
1041
- throw usageProblem("Use either --disable or --enable, not both.", parsed.definition.id);
1042
- }
1043
- const input = {
1044
- ...(displayName !== undefined ? { displayName } : {}),
1045
- ...(url !== undefined ? { url } : {}),
1046
- ...(authMode !== undefined ? { authMode } : {}),
1047
- ...(disable || enable ? { disabled: disable } : {}),
1048
- ...(timeoutMs !== undefined ? { timeoutMs } : {})
1049
- };
1050
- if (Object.keys(input).length === 0) {
1051
- throw usageProblem("Provide at least one MCP server field to update.", parsed.definition.id);
1052
- }
1053
- const data = await api.updateOrgMcpServer(session, org.id, readRequiredArg(parsed, "server"), input);
1054
- return {
1055
- data,
1056
- human: renderSuccess(`Updated MCP server ${data.server.slug}.`),
1057
- kind: "mcp_servers_update",
1058
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1059
- };
1060
- }
1061
- case "mcp.delete": {
1062
- const serverRef = readRequiredArg(parsed, "server");
1063
- await confirmDestructive(parsed, context, `Delete MCP server ${serverRef}?`);
1064
- await api.deleteOrgMcpServer(session, org.id, serverRef);
1065
- return {
1066
- data: {},
1067
- human: renderSuccess(`Deleted MCP server ${serverRef}.`),
1068
- kind: "mcp_servers_delete",
1069
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1070
- };
1071
- }
1072
- case "mcp.test": {
1073
- const data = await api.testOrgMcpServer(session, org.id, readRequiredArg(parsed, "server"));
1074
- if (!data.ok) {
1075
- throw genericProblem(data.server.lastErrorMessage ?? `MCP server ${data.server.slug} test failed.`, parsed.definition.id);
1076
- }
1077
- return {
1078
- data,
1079
- human: renderSuccess(`MCP server ${data.server.slug} is reachable.`),
1080
- kind: "mcp_servers_test",
1081
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1082
- };
1083
- }
1084
- case "mcp.discover-tools": {
1085
- const data = await api.discoverOrgMcpServerTools(session, org.id, readRequiredArg(parsed, "server"));
1086
- return {
1087
- data,
1088
- human: data.tools.length === 0
1089
- ? renderSuccess(`Discovered no tools for ${data.server.slug}.`)
1090
- : renderTable(data.tools, [
1091
- { key: "toolName", label: "Tool" },
1092
- { key: "description", label: "Description" },
1093
- { key: "schemaSha256", label: "Schema" }
1094
- ]),
1095
- kind: "mcp_servers_discover_tools",
1096
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1097
- };
1098
- }
1099
- case "mcp.connect": {
1100
- const serverRef = readRequiredArg(parsed, "server");
1101
- const data = await api.connectOrgMcpServerOAuth(session, org.id, serverRef);
1102
- const explicitOpen = parsed.flags.open === true;
1103
- const shouldOpen = shouldAutoOpenBrowser({
1104
- env: context.env,
1105
- explicitNoOpen: parsed.flags["no-open"] === true,
1106
- explicitOpen,
1107
- interactive: isInteractive(context),
1108
- openBrowserConfig: context.config.openBrowser
1109
- });
1110
- let opened = false;
1111
- if (shouldOpen) {
1112
- opened = await openBrowser(data.authorizationUrl);
1113
- if (explicitOpen && !opened) {
1114
- throw genericProblem(`Could not open a browser automatically. Visit ${data.authorizationUrl}`, parsed.definition.id);
1115
- }
1116
- }
1117
- const finalSession = parsed.flags.wait === true
1118
- ? await pollMcpOAuthSession(api, session, org.id, serverRef, data.sessionId)
1119
- : null;
1120
- if (finalSession && isFailedMcpOAuthSession(finalSession.session.status)) {
1121
- throw genericProblem(finalSession.session.errorMessage
1122
- ?? `MCP OAuth connection ${finalSession.session.status}.`, parsed.definition.id);
1123
- }
1124
- return {
1125
- data: finalSession ? { ...data, session: finalSession.session } : data,
1126
- human: finalSession
1127
- ? renderMcpOAuthSessionHuman(data.authorizationUrl, finalSession.session.status)
1128
- : [
1129
- "MCP OAuth authorization started.",
1130
- `Open: ${data.authorizationUrl}`,
1131
- `Session: ${data.sessionId}`
1132
- ].join("\n"),
1133
- kind: "mcp_servers_oauth_connect",
1134
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1135
- };
1136
- }
1137
- case "mcp.disconnect": {
1138
- const serverRef = readRequiredArg(parsed, "server");
1139
- await confirmDestructive(parsed, context, `Disconnect OAuth for MCP server ${serverRef}?`);
1140
- const data = await api.disconnectOrgMcpServerOAuth(session, org.id, serverRef);
1141
- return {
1142
- data,
1143
- human: renderSuccess(`Disconnected OAuth for MCP server ${data.server.slug}.`),
1144
- kind: "mcp_servers_oauth_disconnect",
1145
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1146
- };
1147
- }
1148
- case "mcp.oauth-status": {
1149
- const data = await api.getOrgMcpServerOAuthStatus(session, org.id, readRequiredArg(parsed, "server"));
1150
- return {
1151
- data,
1152
- human: renderPrettyJson(data.oauth),
1153
- kind: "mcp_servers_oauth_status",
1154
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1155
- };
1156
- }
1157
- default:
1158
- throw genericProblem(`Unhandled MCP command ${parsed.definition.id}.`, parsed.definition.id);
1159
- }
1160
- }
1161
- async function executeSkills(parsed, context, api) {
1162
- const { org, session } = await resolveOrgScope(parsed, context, api);
1163
- switch (parsed.definition.id) {
1164
- case "skills.list": {
1165
- const data = await api.listOrgSkillPackages(session, org.id);
1166
- return {
1167
- data,
1168
- human: renderTable(data.skills, [
1169
- { key: "slug", label: "Slug" },
1170
- { key: "displayName", label: "Name" },
1171
- { key: "status", label: "Status" },
1172
- { key: "currentRevision", label: "Revision" },
1173
- { key: "currentFileCount", label: "Files" },
1174
- { key: "currentPackageSha256", label: "Package SHA" },
1175
- { key: "updatedAt", label: "Updated" }
1176
- ]),
1177
- kind: "skills_list",
1178
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1179
- };
1180
- }
1181
- case "skills.get": {
1182
- const data = await api.getOrgSkillPackage(session, org.id, readRequiredArg(parsed, "skill"));
1183
- return {
1184
- data,
1185
- human: renderPrettyJson(data),
1186
- kind: "skills_get",
1187
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1188
- };
1189
- }
1190
- case "skills.create": {
1191
- const description = readOptionalStringFlag(parsed, "description");
1192
- const dir = readOptionalStringFlag(parsed, "dir");
1193
- const files = dir ? await readImportEntries(dir) : undefined;
1194
- const data = await api.createOrgSkillPackage(session, org.id, {
1195
- ...(description !== undefined ? { description } : {}),
1196
- displayName: readRequiredStringFlag(parsed, "name"),
1197
- ...(files ? { files } : {}),
1198
- slug: readRequiredStringFlag(parsed, "slug")
1199
- });
1200
- return {
1201
- data,
1202
- human: renderSuccess(`Created skill package ${data.skill.slug}.`),
1203
- kind: "skills_create",
1204
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1205
- };
1206
- }
1207
- case "skills.upload": {
1208
- const skillRef = readRequiredArg(parsed, "skill");
1209
- const files = await readImportEntries(readRequiredStringFlag(parsed, "dir"));
1210
- const data = await api.uploadOrgSkillPackageRevision(session, org.id, skillRef, { files });
1211
- return {
1212
- data,
1213
- human: renderSuccess(`Uploaded skill package ${skillRef} revision ${String(data.revision.revision)}.`),
1214
- kind: "skills_upload",
1215
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1216
- };
1217
- }
1218
- case "skills.download": {
1219
- const skillRef = readRequiredArg(parsed, "skill");
1220
- const revision = readOptionalNumberFlag(parsed, "revision");
1221
- const targetDir = readRequiredStringFlag(parsed, "dir");
1222
- const data = revision === undefined
1223
- ? await api.getOrgSkillPackage(session, org.id, skillRef)
1224
- : await api.getOrgSkillPackageRevision(session, org.id, skillRef, revision);
1225
- await writeSkillPackageFiles(targetDir, data.files);
1226
- return {
1227
- data,
1228
- human: renderSuccess(`Downloaded ${data.files.length} skill package files to ${resolve(targetDir)}.`),
1229
- kind: "skills_download",
1230
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1231
- };
1232
- }
1233
- case "skills.update": {
1234
- const displayName = readOptionalStringFlag(parsed, "name");
1235
- const description = readOptionalStringFlag(parsed, "description");
1236
- const disable = parsed.flags.disable === true;
1237
- const enable = parsed.flags.enable === true;
1238
- if (disable && enable) {
1239
- throw usageProblem("Use either --disable or --enable, not both.", parsed.definition.id);
1240
- }
1241
- const input = {
1242
- ...(displayName !== undefined ? { displayName } : {}),
1243
- ...(description !== undefined ? { description } : {}),
1244
- ...(disable || enable ? { disabled: disable } : {})
1245
- };
1246
- if (Object.keys(input).length === 0) {
1247
- throw usageProblem("Provide at least one skill package field to update.", parsed.definition.id);
1248
- }
1249
- const data = await api.updateOrgSkillPackage(session, org.id, readRequiredArg(parsed, "skill"), input);
1250
- return {
1251
- data,
1252
- human: renderSuccess(`Updated skill package ${data.skill.slug}.`),
1253
- kind: "skills_update",
1254
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1255
- };
1256
- }
1257
- case "skills.delete": {
1258
- const skillRef = readRequiredArg(parsed, "skill");
1259
- await confirmDestructive(parsed, context, `Delete skill package ${skillRef}?`);
1260
- await api.deleteOrgSkillPackage(session, org.id, skillRef);
1261
- return {
1262
- data: {},
1263
- human: renderSuccess(`Deleted skill package ${skillRef}.`),
1264
- kind: "skills_delete",
1265
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1266
- };
1267
- }
1268
- default:
1269
- throw genericProblem(`Unhandled skills command ${parsed.definition.id}.`, parsed.definition.id);
1270
- }
1271
- }
1272
- async function writeSkillPackageFiles(targetDir, files) {
1273
- const root = resolve(targetDir);
1274
- for (const file of files) {
1275
- const targetPath = resolve(root, file.path);
1276
- if (!targetPath.startsWith(`${root}/`) && targetPath !== root) {
1277
- throw usageProblem(`Refusing to write skill package path '${file.path}' outside destination.`, "skills.download");
1278
- }
1279
- await mkdir(dirname(targetPath), { recursive: true });
1280
- await writeFile(targetPath, file.content, "utf8");
1281
- }
1282
- }
1283
- async function pollMcpOAuthSession(api, session, orgId, serverRef, oauthSessionId) {
1284
- for (let attempt = 0; attempt < 60; attempt += 1) {
1285
- const result = await api.getOrgMcpServerOAuthSession(session, orgId, serverRef, oauthSessionId);
1286
- if (result.session.status !== "pending") {
1287
- return result;
1288
- }
1289
- await new Promise((resolve) => setTimeout(resolve, 2_000));
1290
- }
1291
- return await api.getOrgMcpServerOAuthSession(session, orgId, serverRef, oauthSessionId);
1292
- }
1293
- function renderMcpOAuthSessionHuman(authorizationUrl, status) {
1294
- if (status === "connected") {
1295
- return renderSuccess("MCP OAuth connection completed.");
1296
- }
1297
- if (isFailedMcpOAuthSession(status)) {
1298
- return "MCP OAuth connection failed.";
1299
- }
1300
- return [
1301
- "MCP OAuth authorization is still pending.",
1302
- `Open: ${authorizationUrl}`
1303
- ].join("\n");
1304
- }
1305
- function isFailedMcpOAuthSession(status) {
1306
- return status === "error" || status === "expired";
1307
- }
1308
1195
  async function executeEnv(parsed, context, api) {
1309
1196
  const { org, session } = await resolveOrgScope(parsed, context, api);
1310
1197
  switch (parsed.definition.id) {
1311
1198
  case "env.list": {
1312
- const data = await api.listRuntimeVariables(session, org.id);
1199
+ const environment = readCliEnvironmentFlag(parsed);
1200
+ const data = await api.listRuntimeVariables(session, org.id, {
1201
+ ...(environment ? { environment } : {})
1202
+ });
1313
1203
  return {
1314
1204
  data,
1315
1205
  human: renderTable(data.variables, [
1316
1206
  { key: "name", label: "Name" },
1317
1207
  { key: "value", label: "Value" },
1208
+ { key: "environmentKey", label: "Environment" },
1318
1209
  { key: "updatedAt", label: "Updated" }
1319
1210
  ]),
1320
1211
  kind: "authoring_env_list",
1321
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1212
+ meta: { command: parsed.definition.path.join(" "), ...(environment ? { environment } : {}), orgId: org.id }
1322
1213
  };
1323
1214
  }
1324
1215
  case "env.get": {
1325
- const variables = (await api.listRuntimeVariables(session, org.id)).variables;
1216
+ const environment = readRequiredCliEnvironmentFlag(parsed);
1217
+ const variables = (await api.listRuntimeVariables(session, org.id, {
1218
+ environment
1219
+ })).variables;
1326
1220
  const variable = requireFound(variables.find((entry) => entry.name === readRequiredArg(parsed, "name")) ?? null, `Environment variable '${parsed.args.name}' was not found.`, parsed.definition.id);
1327
1221
  return {
1328
1222
  data: { variable },
1329
1223
  human: renderPrettyJson(variable),
1330
1224
  kind: "authoring_env_get",
1331
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1225
+ meta: { command: parsed.definition.path.join(" "), environment, orgId: org.id }
1226
+ };
1227
+ }
1228
+ case "env.set": {
1229
+ const environment = readRequiredCliEnvironmentFlag(parsed);
1230
+ const name = readRequiredArg(parsed, "name");
1231
+ announceResolvedEnvironment(parsed, context, environment);
1232
+ const data = await api.upsertRuntimeVariable(session, org.id, {
1233
+ environment,
1234
+ name,
1235
+ value: readRequiredStringFlagPreservingEmpty(parsed, "value")
1236
+ });
1237
+ return {
1238
+ data: {
1239
+ variable: {
1240
+ environmentKey: data.variable.environmentKey,
1241
+ name: data.variable.name,
1242
+ status: "defined",
1243
+ updatedAt: data.variable.updatedAt
1244
+ }
1245
+ },
1246
+ human: renderSuccess(`Saved runtime variable '${data.variable.name}' in ${environment}.`),
1247
+ kind: "authoring_env_set",
1248
+ meta: { command: parsed.definition.path.join(" "), environment, orgId: org.id }
1332
1249
  };
1333
1250
  }
1334
1251
  case "env.replace": {
1335
- const body = await readJsonInputSpecifier(String(parsed.flags.file), context.stdin, parsed.definition.id);
1252
+ const environment = readRequiredCliEnvironmentFlag(parsed);
1253
+ const body = await readJsonInputSpecifier(String(parsed.flags.file), context.stdin, parsed.definition.id, { flag: "file" });
1336
1254
  const rawVariables = body.variables;
1337
1255
  if (!Array.isArray(rawVariables)) {
1338
1256
  throw usageProblem("Environment replace payload must contain a variables array.", parsed.definition.id);
@@ -1346,33 +1264,24 @@ async function executeEnv(parsed, context, api) {
1346
1264
  value: String(entry.value)
1347
1265
  };
1348
1266
  });
1349
- const current = (await api.listRuntimeVariables(session, org.id)).variables;
1267
+ const current = (await api.listRuntimeVariables(session, org.id, {
1268
+ environment
1269
+ })).variables;
1350
1270
  const diff = diffVariables(current, nextVariables);
1271
+ announceResolvedEnvironment(parsed, context, environment);
1351
1272
  if (!parsed.json) {
1352
1273
  context.stdout.write(`${renderDiffSummary(diff)}\n`);
1353
1274
  }
1354
- await confirmDestructive(parsed, context, `Replace the full environment for ${org.slug}?`);
1355
- const data = await api.replaceRuntimeVariables(session, org.id, { variables: nextVariables });
1356
- return {
1357
- data,
1358
- human: renderSuccess(`Replaced ${data.variables.length} environment variables.`),
1359
- kind: "authoring_env_replace",
1360
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1361
- };
1362
- }
1363
- case "env.import": {
1364
- const filePath = readRequiredArg(parsed, "path-to-.env");
1365
- const content = await readTextInputSpecifier(`@${filePath}`, context.stdin, parsed.definition.id);
1366
- const preview = await parseEnvFile(filePath);
1367
- const data = await api.importRuntimeVariables(session, org.id, {
1368
- content,
1369
- fileName: filePath
1275
+ await confirmDestructive(parsed, context, `Replace runtime variables for ${environment} in ${org.slug}?`);
1276
+ const data = await api.replaceRuntimeVariables(session, org.id, {
1277
+ environment,
1278
+ variables: nextVariables
1370
1279
  });
1371
1280
  return {
1372
1281
  data,
1373
- human: renderSuccess(`Imported ${data.importedCount} variables from ${filePath}. Parsed ${preview.length}.`),
1374
- kind: "authoring_env_import",
1375
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1282
+ human: renderSuccess(`Replaced ${data.variables.length} environment variables in ${environment}.`),
1283
+ kind: "authoring_env_replace",
1284
+ meta: { command: parsed.definition.path.join(" "), environment, orgId: org.id }
1376
1285
  };
1377
1286
  }
1378
1287
  default:
@@ -1383,40 +1292,50 @@ async function executeSecrets(parsed, context, api) {
1383
1292
  const { org, session } = await resolveOrgScope(parsed, context, api);
1384
1293
  switch (parsed.definition.id) {
1385
1294
  case "secrets.list": {
1386
- const data = await api.listOrgSecrets(session, org.id);
1295
+ const environment = readCliEnvironmentFlag(parsed);
1296
+ const data = await api.listOrgSecrets(session, org.id, {
1297
+ ...(environment ? { environment } : {})
1298
+ });
1387
1299
  return {
1388
1300
  data,
1389
1301
  human: renderTable(data.secrets, [
1390
1302
  { key: "name", label: "Name" },
1391
1303
  { key: "hasValue", label: "Defined" },
1304
+ { key: "environmentKey", label: "Environment" },
1392
1305
  { key: "updatedAt", label: "Updated" }
1393
1306
  ]),
1394
1307
  kind: "authoring_secrets_list",
1395
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1308
+ meta: { command: parsed.definition.path.join(" "), ...(environment ? { environment } : {}), orgId: org.id }
1396
1309
  };
1397
1310
  }
1398
1311
  case "secrets.set": {
1312
+ const environment = readRequiredCliEnvironmentFlag(parsed);
1399
1313
  const value = await readTextInputSpecifier("-", context.stdin, parsed.definition.id);
1314
+ announceResolvedEnvironment(parsed, context, environment);
1400
1315
  const data = await api.upsertOrgSecret(session, org.id, {
1316
+ environment,
1401
1317
  name: readRequiredArg(parsed, "name"),
1402
1318
  value: value.replace(/\r?\n$/u, "")
1403
1319
  });
1404
1320
  return {
1405
1321
  data,
1406
- human: renderSuccess(`Saved secret '${data.secret.name}'.`),
1322
+ human: renderSuccess(`Saved secret '${data.secret.name}' in ${environment}.`),
1407
1323
  kind: "authoring_secrets_set",
1408
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1324
+ meta: { command: parsed.definition.path.join(" "), environment, orgId: org.id }
1409
1325
  };
1410
1326
  }
1411
1327
  case "secrets.delete": {
1328
+ const environment = readRequiredCliEnvironmentFlag(parsed);
1412
1329
  const name = readRequiredArg(parsed, "name");
1413
- await confirmDestructive(parsed, context, `Delete secret ${name} from ${org.slug}?`);
1414
- const data = await api.deleteOrgSecret(session, org.id, name);
1330
+ await confirmDestructive(parsed, context, `Delete secret ${name} from ${environment} in ${org.slug}?`);
1331
+ const data = await api.deleteOrgSecret(session, org.id, name, {
1332
+ environment
1333
+ });
1415
1334
  return {
1416
1335
  data,
1417
- human: renderSuccess(data.deleted ? `Deleted secret '${name}'.` : `Secret '${name}' was not defined.`),
1336
+ human: renderSuccess(data.deleted ? `Deleted secret '${name}' from ${environment}.` : `Secret '${name}' was not defined in ${environment}.`),
1418
1337
  kind: "authoring_secrets_delete",
1419
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1338
+ meta: { command: parsed.definition.path.join(" "), environment, orgId: org.id }
1420
1339
  };
1421
1340
  }
1422
1341
  default:
@@ -1467,62 +1386,6 @@ async function executeChat(parsed, context, api) {
1467
1386
  meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1468
1387
  };
1469
1388
  }
1470
- case "chat.changes": {
1471
- const data = await api.getChatChanges(session, org.id, readRequiredArg(parsed, "session-id"));
1472
- return {
1473
- data,
1474
- human: renderPrettyJson(data.changes),
1475
- kind: "chat_changes_get",
1476
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1477
- };
1478
- }
1479
- case "chat.validation": {
1480
- const data = await api.getChatValidation(session, org.id, readRequiredArg(parsed, "session-id"));
1481
- return {
1482
- data,
1483
- human: renderPrettyJson(data.validation),
1484
- kind: "chat_validation_get",
1485
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1486
- };
1487
- }
1488
- case "chat.draft.get": {
1489
- const data = await api.getChatDraft(session, org.id, readRequiredArg(parsed, "session-id"));
1490
- return {
1491
- data,
1492
- human: renderPrettyJson(data.draft),
1493
- kind: "chat_draft_get",
1494
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1495
- };
1496
- }
1497
- case "chat.draft.apply": {
1498
- await confirmDestructive(parsed, context, `Apply draft for session ${parsed.args["session-id"]}?`);
1499
- const data = await api.applyChatDraft(session, org.id, readRequiredArg(parsed, "session-id"));
1500
- return {
1501
- data,
1502
- human: renderSuccess(`Applied draft for session ${parsed.args["session-id"]}.`),
1503
- kind: "chat_draft_apply",
1504
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1505
- };
1506
- }
1507
- case "chat.draft.refresh": {
1508
- await confirmDestructive(parsed, context, `Refresh draft for session ${parsed.args["session-id"]}?`);
1509
- const data = await api.refreshChatDraft(session, org.id, readRequiredArg(parsed, "session-id"));
1510
- return {
1511
- data,
1512
- human: renderSuccess(`Refreshed draft for session ${parsed.args["session-id"]}.`),
1513
- kind: "chat_draft_refresh",
1514
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1515
- };
1516
- }
1517
- case "chat.workspace-state": {
1518
- const data = await api.getChatWorkspaceState(session, org.id, readRequiredArg(parsed, "session-id"));
1519
- return {
1520
- data,
1521
- human: renderPrettyJson(data.workspaceState),
1522
- kind: "chat_workspace_state_get",
1523
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1524
- };
1525
- }
1526
1389
  default:
1527
1390
  throw genericProblem(`Unhandled chat command ${parsed.definition.id}.`, parsed.definition.id);
1528
1391
  }
@@ -1569,9 +1432,10 @@ async function executeCompletion(parsed) {
1569
1432
  }
1570
1433
  }
1571
1434
  async function executeSchemaGet(parsed) {
1572
- const schema = getCliSchema(readRequiredArg(parsed, "resource-name"));
1435
+ const resourceName = readRequiredArg(parsed, "resource-name");
1436
+ const schema = getCliSchema(resourceName);
1573
1437
  if (!schema) {
1574
- throw notFoundProblem(`Schema '${parsed.args["resource-name"]}' was not found.`, "schema get");
1438
+ throw notFoundProblem(`Schema '${resourceName}' was not found. Use lowercase schema names, not YAML kind names. Run \`schema list\` to see all schemas. Known names include: ${schemaNamePreview()}.`, "schema get");
1575
1439
  }
1576
1440
  return {
1577
1441
  data: { schema },
@@ -1589,9 +1453,99 @@ async function executeSchemaGet(parsed) {
1589
1453
  meta: { command: "schema get" }
1590
1454
  };
1591
1455
  }
1456
+ function schemaNamePreview() {
1457
+ const names = new Set(listCliSchemas().map((schema) => schema.name));
1458
+ return ["process", "operation", "capability", "decision", "manifest", "organization", "assignments"]
1459
+ .filter((name) => names.has(name))
1460
+ .join(", ");
1461
+ }
1592
1462
  async function executeAdmin(parsed, context, api) {
1593
1463
  const session = requireStoredSession(context.session, parsed.definition.id);
1594
1464
  switch (parsed.definition.id) {
1465
+ case "admin.usage.summary": {
1466
+ const { org, session: scopedSession } = await resolveOrgScope(parsed, context, api);
1467
+ const data = await api.getAgentUsage(scopedSession, org.id, readAgentUsageQuery(parsed));
1468
+ return {
1469
+ data,
1470
+ human: renderAgentUsageSummary(org.id, data.usage),
1471
+ kind: "admin_usage_summary",
1472
+ meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1473
+ };
1474
+ }
1475
+ case "admin.usage.workflows": {
1476
+ const { org, session: scopedSession } = await resolveOrgScope(parsed, context, api);
1477
+ const data = await api.getAgentUsage(scopedSession, org.id, readAgentUsageQuery(parsed));
1478
+ return {
1479
+ data,
1480
+ human: renderTable(data.usage.workflows.map((workflow) => ({
1481
+ cost: formatUsdMicros(workflow.usdMicros),
1482
+ executions: formatInteger(workflow.agentExecutionCount),
1483
+ model: formatProviderModel(workflow.provider, workflow.model),
1484
+ tokensIn: formatInteger(workflow.tokensIn),
1485
+ tokensOut: formatInteger(workflow.tokensOut),
1486
+ toolCalls: formatInteger(workflow.toolCallCount),
1487
+ workflow: workflow.processName
1488
+ })), [
1489
+ { key: "workflow", label: "Workflow" },
1490
+ { key: "model", label: "Model" },
1491
+ { key: "executions", label: "Executions" },
1492
+ { key: "tokensIn", label: "Tokens in" },
1493
+ { key: "tokensOut", label: "Tokens out" },
1494
+ { key: "toolCalls", label: "Tool calls" },
1495
+ { key: "cost", label: "Cost" }
1496
+ ]),
1497
+ kind: "admin_usage_workflows",
1498
+ meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1499
+ };
1500
+ }
1501
+ case "admin.usage.tools": {
1502
+ const { org, session: scopedSession } = await resolveOrgScope(parsed, context, api);
1503
+ const data = await api.getAgentUsage(scopedSession, org.id, readAgentUsageQuery(parsed));
1504
+ return {
1505
+ data,
1506
+ human: renderTable(data.usage.tools.map((tool) => ({
1507
+ calls: formatInteger(tool.callCount),
1508
+ failed: formatInteger(tool.failureCount),
1509
+ succeeded: formatInteger(tool.successCount),
1510
+ tool: tool.toolName
1511
+ })), [
1512
+ { key: "tool", label: "Tool" },
1513
+ { key: "calls", label: "Calls" },
1514
+ { key: "succeeded", label: "Succeeded" },
1515
+ { key: "failed", label: "Failed" }
1516
+ ]),
1517
+ kind: "admin_usage_tools",
1518
+ meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1519
+ };
1520
+ }
1521
+ case "admin.usage.events": {
1522
+ const { org, session: scopedSession } = await resolveOrgScope(parsed, context, api);
1523
+ const data = await api.getAgentUsage(scopedSession, org.id, readAgentUsageQuery(parsed));
1524
+ return {
1525
+ data,
1526
+ human: renderTable(data.usage.recentEvents.map((event) => ({
1527
+ cost: formatUsdMicros(event.usdMicros),
1528
+ event: event.eventId,
1529
+ model: formatProviderModel(event.provider, event.model),
1530
+ occurredAt: event.occurredAt,
1531
+ status: event.sourceStatus === "event_only" ? `${event.status} (event only)` : event.status,
1532
+ tokens: formatInteger(event.tokensIn + event.tokensOut),
1533
+ toolCalls: formatInteger(event.toolCallCount),
1534
+ workflow: event.processName
1535
+ })), [
1536
+ { key: "event", label: "Event" },
1537
+ { key: "workflow", label: "Workflow" },
1538
+ { key: "model", label: "Model" },
1539
+ { key: "status", label: "Status" },
1540
+ { key: "tokens", label: "Tokens" },
1541
+ { key: "toolCalls", label: "Tool calls" },
1542
+ { key: "cost", label: "Cost" },
1543
+ { key: "occurredAt", label: "Occurred at" }
1544
+ ]),
1545
+ kind: "admin_usage_events",
1546
+ meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1547
+ };
1548
+ }
1595
1549
  case "admin.org.deleted.list": {
1596
1550
  const data = await api.listDeletedOrganizations(session);
1597
1551
  return {
@@ -1638,6 +1592,46 @@ async function executeAdmin(parsed, context, api) {
1638
1592
  throw genericProblem(`Unhandled admin command ${parsed.definition.id}.`, parsed.definition.id);
1639
1593
  }
1640
1594
  }
1595
+ function readAgentUsageQuery(parsed) {
1596
+ const from = readOptionalStringFlag(parsed, "from");
1597
+ const to = readOptionalStringFlag(parsed, "to");
1598
+ return {
1599
+ ...(from ? { from } : {}),
1600
+ ...optionalNumberValue("limit", readOptionalNumberFlag(parsed, "limit")),
1601
+ ...(to ? { to } : {})
1602
+ };
1603
+ }
1604
+ function renderAgentUsageSummary(orgId, usage) {
1605
+ const summary = usage.summary;
1606
+ return renderKeyValue([
1607
+ { label: "Organization", value: orgId },
1608
+ { label: "From", value: summary.from },
1609
+ { label: "To", value: summary.to },
1610
+ { label: "Agent executions", value: formatInteger(summary.agentExecutionCount) },
1611
+ { label: "Tokens in", value: formatInteger(summary.tokensIn) },
1612
+ { label: "Tokens out", value: formatInteger(summary.tokensOut) },
1613
+ { label: "Tool calls", value: formatInteger(summary.toolCallCount) },
1614
+ { label: "Cost", value: formatUsdMicros(summary.usdMicros) },
1615
+ { label: "Event-only records", value: formatInteger(summary.sourceEventOnlyCount) }
1616
+ ]);
1617
+ }
1618
+ function formatProviderModel(provider, model) {
1619
+ if (provider && model) {
1620
+ return `${provider}/${model}`;
1621
+ }
1622
+ return provider ?? model ?? "";
1623
+ }
1624
+ function formatInteger(value) {
1625
+ return new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 }).format(value);
1626
+ }
1627
+ function formatUsdMicros(value) {
1628
+ return new Intl.NumberFormat(undefined, {
1629
+ currency: "USD",
1630
+ maximumFractionDigits: 4,
1631
+ minimumFractionDigits: value > 0 && value < 10_000 ? 4 : 2,
1632
+ style: "currency"
1633
+ }).format(value / 1_000_000);
1634
+ }
1641
1635
  async function resolveOrgScope(parsed, context, api) {
1642
1636
  const session = requireStoredSession(context.session, parsed.definition.id);
1643
1637
  const selector = readOptionalStringFlag(parsed, "org");
@@ -1675,16 +1669,109 @@ function isSameSessionIdentity(candidate, session) {
1675
1669
  && candidate.user.id === session.user.id
1676
1670
  && candidate.accessTokenPayload.sub === session.accessTokenPayload.sub;
1677
1671
  }
1678
- function readOptionalMcpAuthMode(parsed) {
1679
- const value = readOptionalStringFlag(parsed, "auth-mode");
1680
- if (value === undefined || value === "none" || value === "oauth") {
1681
- return value;
1682
- }
1683
- throw usageProblem("MCP auth mode must be 'none' or 'oauth'.", parsed.definition.id);
1684
- }
1685
1672
  function optionalNumberValue(key, value) {
1686
1673
  return value !== undefined ? { [key]: value } : {};
1687
1674
  }
1675
+ function withCliSummary(meta, summary) {
1676
+ const normalized = normalizeCliSummary(summary);
1677
+ return normalized ? { ...meta, summary: normalized } : meta;
1678
+ }
1679
+ function normalizeCliSummary(value) {
1680
+ const trimmed = value?.trim();
1681
+ return trimmed && trimmed.length > 0 ? trimmed : null;
1682
+ }
1683
+ function summarizeWorkflowNodeTest(test) {
1684
+ if (test.status === "passed") {
1685
+ const capturedArtifacts = Array.isArray(test.capturedArtifacts)
1686
+ ? test.capturedArtifacts.length
1687
+ : 0;
1688
+ return `Workflow-node test passed${capturedArtifacts > 0 ? ` with ${String(capturedArtifacts)} captured artifact${capturedArtifacts === 1 ? "" : "s"}` : ""}.`;
1689
+ }
1690
+ const label = test.status === "invalid" ? "invalid" : "failed";
1691
+ const error = recordField(test, "error");
1692
+ const message = typeof error.message === "string" && error.message.trim().length > 0
1693
+ ? truncateSummaryText(error.message)
1694
+ : firstDiagnosticMessage(readSummaryDiagnostics(test));
1695
+ return `Workflow-node test ${label}${message ? `: ${message}` : ""}.`;
1696
+ }
1697
+ function renderWorkflowNodeTestHuman(test) {
1698
+ const summary = summarizeWorkflowNodeTest(test);
1699
+ const capturedArtifacts = Array.isArray(test.capturedArtifacts)
1700
+ ? test.capturedArtifacts.length
1701
+ : 0;
1702
+ const logs = Array.isArray(test.logs)
1703
+ ? test.logs.length
1704
+ : 0;
1705
+ const rows = [
1706
+ { label: "Status", value: test.status },
1707
+ { label: "Workflow", value: test.workflowName },
1708
+ { label: "Node", value: test.nodeId },
1709
+ ...(typeof test.operationName === "string"
1710
+ ? [{ label: "Operation", value: test.operationName }]
1711
+ : []),
1712
+ { label: "Captured artifacts", value: capturedArtifacts },
1713
+ { label: "Logs", value: logs > 0 ? `${String(logs)} stream${logs === 1 ? "" : "s"} available` : "none" }
1714
+ ];
1715
+ const diagnostics = readSummaryDiagnostics(test);
1716
+ const error = recordField(test, "error");
1717
+ const actionable = typeof error.message === "string" && error.message.trim().length > 0
1718
+ ? truncateSummaryText(error.message)
1719
+ : firstDiagnosticMessage(diagnostics);
1720
+ return [
1721
+ summary,
1722
+ renderKeyValue(rows),
1723
+ ...(actionable && test.status !== "passed" ? [`Action: ${actionable}`] : [])
1724
+ ].filter((entry) => entry.trim().length > 0).join("\n");
1725
+ }
1726
+ function summarizeReleaseValidation(validation) {
1727
+ const diagnostics = readSummaryDiagnostics(validation);
1728
+ return validation.releaseReady === true
1729
+ ? `Release validation passed${formatDiagnosticSummarySuffix(diagnostics)}.`
1730
+ : `Release validation failed${formatDiagnosticSummarySuffix(diagnostics)}.`;
1731
+ }
1732
+ function summarizeReleaseCreation(releaseId) {
1733
+ return `Release ${releaseId} created. It is not live yet; review and deploy it from /app/releases/${releaseId}.`;
1734
+ }
1735
+ function readSummaryDiagnostics(record) {
1736
+ const diagnostics = record.diagnostics;
1737
+ return Array.isArray(diagnostics)
1738
+ ? diagnostics.filter((entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry))
1739
+ : [];
1740
+ }
1741
+ function formatDiagnosticSummarySuffix(diagnostics) {
1742
+ const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
1743
+ const warningCount = diagnostics.length - errorCount;
1744
+ const parts = [
1745
+ ...(errorCount > 0 ? [formatDiagnosticCount(errorCount, "error")] : []),
1746
+ ...(warningCount > 0 ? [formatDiagnosticCount(warningCount, "warning")] : [])
1747
+ ];
1748
+ if (parts.length === 0) {
1749
+ return "";
1750
+ }
1751
+ const message = firstDiagnosticMessage(diagnostics);
1752
+ return ` with ${parts.join(" and ")}${message ? `: ${message}` : ""}`;
1753
+ }
1754
+ function formatDiagnosticCount(count, label) {
1755
+ return `${String(count)} ${label}${count === 1 ? "" : "s"}`;
1756
+ }
1757
+ function firstDiagnosticMessage(diagnostics) {
1758
+ for (const diagnostic of diagnostics) {
1759
+ if (typeof diagnostic.message === "string" && diagnostic.message.trim().length > 0) {
1760
+ return truncateSummaryText(diagnostic.message);
1761
+ }
1762
+ }
1763
+ return null;
1764
+ }
1765
+ function truncateSummaryText(value) {
1766
+ const trimmed = value.trim();
1767
+ return trimmed.length <= 180 ? trimmed : `${trimmed.slice(0, 177)}...`;
1768
+ }
1769
+ function recordField(result, key) {
1770
+ const value = result[key];
1771
+ return typeof value === "object" && value !== null && !Array.isArray(value)
1772
+ ? value
1773
+ : {};
1774
+ }
1688
1775
  function readRequiredArg(parsed, name) {
1689
1776
  const value = parsed.args[name];
1690
1777
  if (!value) {
@@ -1692,6 +1779,11 @@ function readRequiredArg(parsed, name) {
1692
1779
  }
1693
1780
  return value;
1694
1781
  }
1782
+ function announceResolvedEnvironment(parsed, context, environment) {
1783
+ if (!parsed.json) {
1784
+ context.stdout.write(`Environment: ${environment}\n`);
1785
+ }
1786
+ }
1695
1787
  function readRequiredIntegerArg(parsed, name) {
1696
1788
  const raw = readRequiredArg(parsed, name);
1697
1789
  const parsedValue = Number(raw);