@kora-platform/cli 0.8.0-rc3 → 0.8.0-rc6

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.
@@ -31,6 +31,12 @@ export const CLI_ALIASES = Object.freeze({
31
31
  process: ["workflow"],
32
32
  project: ["org"],
33
33
  });
34
+ const releaseReadFlag = (description, required = false) => flag("release", description, { acceptsValue: true, ...(required ? { required: true } : {}), valueType: "string" });
35
+ const releaseEnvironmentReadFlag = (description) => flag("environment", description, { acceptsValue: true, valueType: "string" });
36
+ const releaseSnapshotReadFlags = (releaseDescription, environmentDescription) => [
37
+ releaseReadFlag(releaseDescription),
38
+ releaseEnvironmentReadFlag(environmentDescription)
39
+ ];
34
40
  const RAW_CLI_COMMANDS = [
35
41
  command(["help"], "Show human or machine-readable help for a command path.", {
36
42
  labels: ["read", "chat-read", "help"],
@@ -64,13 +70,14 @@ const RAW_CLI_COMMANDS = [
64
70
  command(["auth", "whoami"], "Show the current authenticated user and selected organization.", {
65
71
  labels: ["local", "auth"]
66
72
  }),
67
- command(["auth", "login"], "Start an interactive login flow and select the active organization.", {
73
+ command(["auth", "login"], "Log in and select the active organization. Uses browser device approval with --device or when the terminal is not interactive.", {
68
74
  labels: ["credential", "auth"],
69
75
  flags: [
70
76
  flag("base-url", "Platform base URL for this login.", {
71
77
  acceptsValue: true,
72
78
  valueType: "string"
73
- })
79
+ }),
80
+ flag("device", "Use the browser device-approval flow instead of interactive email/password login.")
74
81
  ]
75
82
  }),
76
83
  command(["auth", "signup"], "Create a local account and start a CLI session.", {
@@ -182,52 +189,37 @@ const RAW_CLI_COMMANDS = [
182
189
  labels: ["read", "activity", "chat-read"],
183
190
  requiresActiveOrg: true
184
191
  }),
185
- command(["workflow", "list"], "List workflows.", {
186
- labels: ["read", "workflow"],
187
- aliases: [["process", "list"]],
188
- flags: [flag("release", "Read workflows from a release.", {
189
- acceptsValue: true,
190
- valueType: "string"
191
- })],
192
+ command(["workflow", "list"], "List live deployed workflows, filter live deployments with --environment, or list workflows from a release with --release.", {
193
+ labels: ["read", "workflow"], aliases: [["process", "list"]],
194
+ examples: ["kora workflow list", "kora workflow list --environment <environment>", "kora workflow list --release <release>"],
195
+ flags: releaseSnapshotReadFlags("Read workflows from an immutable release instead of live deployments.", "Filter live deployed workflows to one environment's live deployment."),
192
196
  requiresActiveOrg: true
193
197
  }),
194
198
  command(["workflow", "get"], "Show workflow detail.", {
195
199
  labels: ["read", "workflow"],
196
200
  aliases: [["process", "get"]],
197
201
  args: [arg("name", "Workflow name.")],
198
- flags: [flag("release", "Read workflow detail from a release.", {
199
- acceptsValue: true,
200
- valueType: "string"
201
- })],
202
+ flags: releaseSnapshotReadFlags("Read workflow detail from a release.", "Read workflow detail from an environment's live deployment release."),
202
203
  requiresActiveOrg: true
203
204
  }),
204
205
  command(["workflow", "version", "get"], "Show one workflow version.", {
205
206
  labels: ["read", "workflow"],
206
207
  aliases: [["process", "version", "get"]],
207
208
  args: [arg("name", "Workflow name."), arg("version", "Workflow version.")],
208
- flags: [flag("release", "Read workflow version from a release.", {
209
- acceptsValue: true,
210
- valueType: "string"
211
- })],
209
+ flags: releaseSnapshotReadFlags("Read workflow version from a release.", "Read workflow version from an environment's live deployment release."),
212
210
  requiresActiveOrg: true
213
211
  }),
214
212
  command(["workflow", "dependencies"], "Show workflow dependency detail.", {
215
213
  labels: ["read", "workflow"],
216
214
  aliases: [["process", "dependencies"]],
217
215
  args: [arg("name", "Workflow name."), arg("version", "Workflow version.")],
218
- flags: [flag("release", "Read workflow dependencies from a release.", {
219
- acceptsValue: true,
220
- valueType: "string"
221
- })],
216
+ flags: releaseSnapshotReadFlags("Read workflow dependencies from a release.", "Read workflow dependencies from an environment's live deployment release."),
222
217
  requiresActiveOrg: true
223
218
  }),
224
219
  command(["workflow", "context"], "Show workflow inspection context.", {
225
220
  labels: ["read", "workflow"],
226
221
  aliases: [["process", "context"]],
227
- flags: [flag("release", "Read workflow context from a release.", {
228
- acceptsValue: true,
229
- valueType: "string"
230
- })],
222
+ flags: releaseSnapshotReadFlags("Read workflow context from a release.", "Read workflow context from an environment's live deployment release."),
231
223
  requiresActiveOrg: true
232
224
  }),
233
225
  command(["workflow", "start"], "Start a workflow.", {
@@ -294,10 +286,7 @@ const RAW_CLI_COMMANDS = [
294
286
  command(["release", "create"], "Create an immutable release artifact from source files; this mutates Platform state and is not a test command.", {
295
287
  labels: ["write", "chat-write", "release"],
296
288
  args: [arg("workspace", "Path to the source folder or zip archive.")],
297
- flags: [flag("label", "Human label for the release.", {
298
- acceptsValue: true,
299
- valueType: "string"
300
- })],
289
+ flags: [],
301
290
  requiresActiveOrg: true
302
291
  }),
303
292
  command(["release", "validate"], "Validate release readiness without deploying.", {
@@ -701,51 +690,68 @@ const RAW_CLI_COMMANDS = [
701
690
  requiresActiveOrg: true
702
691
  }),
703
692
  command(["org-model", "people", "list"], "List modeled people.", {
693
+ flags: releaseSnapshotReadFlags("Read modeled people from a release.", "Read modeled people from an environment's live deployment release."),
704
694
  labels: ["read", "org-model"], requiresActiveOrg: true
705
695
  }),
706
696
  command(["org-model", "people", "get"], "Show one modeled person.", {
707
697
  labels: ["read", "org-model"],
708
698
  args: [arg("name", "Person name.")],
699
+ flags: releaseSnapshotReadFlags("Read modeled person detail from a release.", "Read modeled person detail from an environment's live deployment release."),
709
700
  requiresActiveOrg: true
710
701
  }),
711
702
  command(["org-model", "agents", "list"], "List modeled agents.", {
712
- labels: ["read", "org-model"], requiresActiveOrg: true
703
+ flags: releaseSnapshotReadFlags("Read modeled agents from a release.", "Read modeled agents from an environment's live deployment release."),
704
+ labels: ["read", "org-model"],
705
+ requiresActiveOrg: true
713
706
  }),
714
707
  command(["org-model", "agents", "get"], "Show one modeled agent.", {
715
708
  labels: ["read", "org-model"],
716
709
  args: [arg("name", "Agent name.")],
710
+ flags: releaseSnapshotReadFlags("Read modeled agent detail from a release.", "Read modeled agent detail from an environment's live deployment release."),
717
711
  requiresActiveOrg: true
718
712
  }),
719
713
  command(["org-model", "roles", "list"], "List modeled roles.", {
720
- labels: ["read", "org-model"], requiresActiveOrg: true
714
+ flags: releaseSnapshotReadFlags("Read modeled roles from a release.", "Read modeled roles from an environment's live deployment release."),
715
+ labels: ["read", "org-model"],
716
+ requiresActiveOrg: true
721
717
  }),
722
718
  command(["org-model", "roles", "get"], "Show one modeled role.", {
723
719
  labels: ["read", "org-model"],
724
720
  args: [arg("name", "Role name.")],
721
+ flags: releaseSnapshotReadFlags("Read modeled role detail from a release.", "Read modeled role detail from an environment's live deployment release."),
725
722
  requiresActiveOrg: true
726
723
  }),
727
724
  command(["org-model", "assignments", "list"], "List modeled assignments.", {
728
- labels: ["read", "org-model"], requiresActiveOrg: true
725
+ flags: releaseSnapshotReadFlags("Read modeled assignments from a release.", "Read modeled assignments from an environment's live deployment release."),
726
+ labels: ["read", "org-model"],
727
+ requiresActiveOrg: true
729
728
  }),
730
729
  command(["org-model", "assignments", "get"], "Show one modeled assignment group by role.", {
731
730
  labels: ["read", "org-model"],
732
731
  args: [arg("role-name", "Role name.")],
732
+ flags: releaseSnapshotReadFlags("Read modeled assignment detail from a release.", "Read modeled assignment detail from an environment's live deployment release."),
733
733
  requiresActiveOrg: true
734
734
  }),
735
735
  command(["org-model", "capabilities", "list"], "List capabilities.", {
736
- labels: ["read", "org-model"], requiresActiveOrg: true
736
+ flags: releaseSnapshotReadFlags("Read capabilities from a release.", "Read capabilities from an environment's live deployment release."),
737
+ labels: ["read", "org-model"],
738
+ requiresActiveOrg: true
737
739
  }),
738
740
  command(["org-model", "capabilities", "get"], "Show one capability.", {
739
741
  labels: ["read", "org-model"],
740
742
  args: [arg("name", "Capability name.")],
743
+ flags: releaseSnapshotReadFlags("Read capability detail from a release.", "Read capability detail from an environment's live deployment release."),
741
744
  requiresActiveOrg: true
742
745
  }),
743
746
  command(["org-model", "operations", "list"], "List operations.", {
744
- labels: ["read", "org-model"], requiresActiveOrg: true
747
+ flags: releaseSnapshotReadFlags("Read operations from a release.", "Read operations from an environment's live deployment release."),
748
+ labels: ["read", "org-model"],
749
+ requiresActiveOrg: true
745
750
  }),
746
751
  command(["org-model", "operations", "get"], "Show one operation.", {
747
752
  labels: ["read", "org-model"],
748
753
  args: [arg("name", "Operation name.")],
754
+ flags: releaseSnapshotReadFlags("Read operation detail from a release.", "Read operation detail from an environment's live deployment release."),
749
755
  requiresActiveOrg: true
750
756
  }),
751
757
  command(["access", "members", "list"], "List organization members.", {
package/dist/commands.js CHANGED
@@ -421,58 +421,98 @@ async function executeActivityOptions(parsed, context, api) {
421
421
  meta: { command: "activity options", orgId: org.id }
422
422
  };
423
423
  }
424
+ function readOptionalReleaseSnapshotSelector(parsed) {
425
+ const releaseId = readOptionalStringFlag(parsed, "release");
426
+ const environment = readCliEnvironmentFlag(parsed);
427
+ if (releaseId && environment) {
428
+ throw usageProblem("Use either --release or --environment, not both.", parsed.definition.id);
429
+ }
430
+ if (releaseId) {
431
+ return { releaseId };
432
+ }
433
+ if (environment) {
434
+ return { environment };
435
+ }
436
+ return undefined;
437
+ }
438
+ function readRequiredReleaseSnapshotSelector(parsed) {
439
+ const selector = readOptionalReleaseSnapshotSelector(parsed);
440
+ if (!selector) {
441
+ throw usageProblem("Provide either --release or --environment.", parsed.definition.id);
442
+ }
443
+ return selector;
444
+ }
445
+ function releaseSnapshotSelectorMeta(selector) {
446
+ if (!selector) {
447
+ return {};
448
+ }
449
+ if (typeof selector.releaseId === "string") {
450
+ return { releaseId: selector.releaseId };
451
+ }
452
+ if (typeof selector.environment === "string") {
453
+ return { environment: selector.environment };
454
+ }
455
+ return {};
456
+ }
424
457
  async function executeWorkflowList(parsed, context, api) {
425
458
  const { org, session } = await resolveOrgScope(parsed, context, api);
426
- const data = await api.listWorkflows(session, org.id, readOptionalStringFlag(parsed, "release"));
459
+ const selector = readOptionalReleaseSnapshotSelector(parsed);
460
+ const data = await api.listWorkflows(session, org.id, selector ?? { live: true });
427
461
  return {
428
462
  data,
429
463
  human: renderTable(data.processes, [
430
464
  { key: "name", label: "Name" },
465
+ { key: "environmentKey", label: "Environment" },
466
+ { key: "liveReleaseId", label: "Live release" },
431
467
  { key: "latestVersion", label: "Latest" },
432
468
  { key: "deployedVersion", label: "Deployed" }
433
469
  ]),
434
470
  kind: "authoring_workflow_list",
435
- meta: { command: "workflow list", orgId: org.id }
471
+ meta: { command: "workflow list", orgId: org.id, ...releaseSnapshotSelectorMeta(selector) }
436
472
  };
437
473
  }
438
474
  async function executeWorkflowGet(parsed, context, api) {
439
475
  const { org, session } = await resolveOrgScope(parsed, context, api);
440
- const data = await api.getWorkflow(session, org.id, readRequiredArg(parsed, "name"), readOptionalStringFlag(parsed, "release"));
476
+ const selector = readRequiredReleaseSnapshotSelector(parsed);
477
+ const data = await api.getWorkflow(session, org.id, readRequiredArg(parsed, "name"), selector);
441
478
  return {
442
479
  data,
443
480
  human: renderPrettyJson(data.process),
444
481
  kind: "authoring_workflow_get",
445
- meta: { command: "workflow get", orgId: org.id }
482
+ meta: { command: "workflow get", orgId: org.id, ...releaseSnapshotSelectorMeta(selector) }
446
483
  };
447
484
  }
448
485
  async function executeWorkflowVersionGet(parsed, context, api) {
449
486
  const { org, session } = await resolveOrgScope(parsed, context, api);
450
- const data = await api.getWorkflowVersion(session, org.id, readRequiredArg(parsed, "name"), readRequiredIntegerArg(parsed, "version"), readOptionalStringFlag(parsed, "release"));
487
+ const selector = readRequiredReleaseSnapshotSelector(parsed);
488
+ const data = await api.getWorkflowVersion(session, org.id, readRequiredArg(parsed, "name"), readRequiredIntegerArg(parsed, "version"), selector);
451
489
  return {
452
490
  data,
453
491
  human: renderPrettyJson(data.process),
454
492
  kind: "authoring_workflow_version_get",
455
- meta: { command: "workflow version get", orgId: org.id }
493
+ meta: { command: "workflow version get", orgId: org.id, ...releaseSnapshotSelectorMeta(selector) }
456
494
  };
457
495
  }
458
496
  async function executeWorkflowDependencies(parsed, context, api) {
459
497
  const { org, session } = await resolveOrgScope(parsed, context, api);
460
- const data = await api.getWorkflowDependencies(session, org.id, readRequiredArg(parsed, "name"), readRequiredIntegerArg(parsed, "version"), readOptionalStringFlag(parsed, "release"));
498
+ const selector = readRequiredReleaseSnapshotSelector(parsed);
499
+ const data = await api.getWorkflowDependencies(session, org.id, readRequiredArg(parsed, "name"), readRequiredIntegerArg(parsed, "version"), selector);
461
500
  return {
462
501
  data,
463
502
  human: renderPrettyJson(data.dependencies),
464
503
  kind: "authoring_workflow_dependencies",
465
- meta: { command: "workflow dependencies", orgId: org.id }
504
+ meta: { command: "workflow dependencies", orgId: org.id, ...releaseSnapshotSelectorMeta(selector) }
466
505
  };
467
506
  }
468
507
  async function executeWorkflowContext(parsed, context, api) {
469
508
  const { org, session } = await resolveOrgScope(parsed, context, api);
470
- const data = await api.getWorkflowContext(session, org.id, readOptionalStringFlag(parsed, "release"));
509
+ const selector = readRequiredReleaseSnapshotSelector(parsed);
510
+ const data = await api.getWorkflowContext(session, org.id, selector);
471
511
  return {
472
512
  data,
473
513
  human: renderPrettyJson(data.context),
474
514
  kind: "authoring_workflow_context",
475
- meta: { command: "workflow context", orgId: org.id }
515
+ meta: { command: "workflow context", orgId: org.id, ...releaseSnapshotSelectorMeta(selector) }
476
516
  };
477
517
  }
478
518
  async function executeWorkflowStart(parsed, context, api) {
@@ -483,7 +523,7 @@ async function executeWorkflowStart(parsed, context, api) {
483
523
  }
484
524
  const inputSpecifier = readOptionalStringFlag(parsed, "input");
485
525
  const inputData = inputSpecifier
486
- ? await readJsonInputSpecifier(inputSpecifier, context.stdin, "workflow start")
526
+ ? await readJsonInputSpecifier(inputSpecifier, context.stdin, "workflow start", { flag: "input" })
487
527
  : undefined;
488
528
  const workflowName = readRequiredArg(parsed, "name");
489
529
  const resolvedEnvironment = resolveCliEnvironment(parsed, session);
@@ -513,7 +553,7 @@ async function executeWorkflowNodeTest(parsed, context, api) {
513
553
  const environment = readOptionalStringFlag(parsed, "environment");
514
554
  const inputSpecifier = readOptionalStringFlag(parsed, "input");
515
555
  const inputData = inputSpecifier
516
- ? await readJsonInputSpecifier(inputSpecifier, context.stdin, "test node")
556
+ ? await readJsonInputSpecifier(inputSpecifier, context.stdin, "test node", { flag: "input" })
517
557
  : undefined;
518
558
  const data = await api.testWorkflowNode(session, org.id, workflowName, nodeId, {
519
559
  ...(environment ? { environment } : {}),
@@ -645,11 +685,8 @@ async function executeReleaseSource(parsed, context, api) {
645
685
  async function executeReleaseCreate(parsed, context, api) {
646
686
  const { org, session } = await resolveOrgScope(parsed, context, api);
647
687
  const workspacePath = readRequiredArg(parsed, "workspace");
648
- const label = readOptionalStringFlag(parsed, "label");
649
688
  if (isZipArchivePath(workspacePath)) {
650
- const data = await api.createReleaseArchive(session, org.id, await readArchiveBytes(workspacePath, "release create"), {
651
- ...(label ? { label } : {})
652
- });
689
+ const data = await api.createReleaseArchive(session, org.id, await readArchiveBytes(workspacePath, "release create"));
653
690
  const summary = summarizeReleaseCreation(data.created.release.id);
654
691
  return {
655
692
  data,
@@ -660,8 +697,7 @@ async function executeReleaseCreate(parsed, context, api) {
660
697
  }
661
698
  const files = await readImportEntries(workspacePath);
662
699
  const data = await api.createRelease(session, org.id, {
663
- files,
664
- ...(label ? { label } : {})
700
+ files
665
701
  });
666
702
  const summary = summarizeReleaseCreation(data.created.release.id);
667
703
  return {
@@ -702,7 +738,7 @@ async function executeEnvironment(parsed, context, api) {
702
738
  const data = await api.getEnvironment(session, org.id, readRequiredArg(parsed, "environment"));
703
739
  return {
704
740
  data,
705
- human: renderPrettyJson(data.environment),
741
+ human: renderPrettyJson(data),
706
742
  kind: "environment_get",
707
743
  meta: { command: "environment get", orgId: org.id }
708
744
  };
@@ -942,7 +978,7 @@ async function executeTaskGet(parsed, context, api) {
942
978
  }
943
979
  async function executeTaskComplete(parsed, context, api) {
944
980
  const { org, session } = await resolveOrgScope(parsed, context, api);
945
- const outputData = await readJsonInputSpecifier(String(parsed.flags.output), context.stdin, "task complete");
981
+ const outputData = await readJsonInputSpecifier(String(parsed.flags.output), context.stdin, "task complete", { flag: "output" });
946
982
  const data = await api.completeTask(session, org.id, readRequiredArg(parsed, "task-id"), {
947
983
  outputData,
948
984
  workflowId: String(parsed.flags.run)
@@ -958,8 +994,9 @@ async function executeOrgModel(parsed, context, api) {
958
994
  const { org, session } = await resolveOrgScope(parsed, context, api);
959
995
  const section = parsed.definition.path[1] ?? "";
960
996
  const verb = parsed.definition.path[2] ?? "";
997
+ const selector = readRequiredReleaseSnapshotSelector(parsed);
961
998
  if (section === "operations") {
962
- const editor = (await api.getOperationsEditor(session, org.id)).editor;
999
+ const editor = (await api.getOperationsEditor(session, org.id, selector)).editor;
963
1000
  const record = verb === "get"
964
1001
  ? requireFound(editor.operations.find((entry) => entry.name === parsed.args.name), `Operation '${parsed.args.name}' was not found.`, parsed.definition.id)
965
1002
  : undefined;
@@ -972,17 +1009,17 @@ async function executeOrgModel(parsed, context, api) {
972
1009
  { key: "command", label: "Command" }
973
1010
  ]),
974
1011
  kind: `authoring_${sanitizeKind(section)}_${verb}`,
975
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1012
+ meta: { command: parsed.definition.path.join(" "), orgId: org.id, ...releaseSnapshotSelectorMeta(selector) }
976
1013
  };
977
1014
  }
978
- const management = (await api.getOrganizationManagementContext(session, org.id)).context;
1015
+ const management = (await api.getOrganizationManagementContext(session, org.id, selector)).context;
979
1016
  if (section === "capabilities" && verb === "get") {
980
- const detail = await api.getCapabilityEditor(session, org.id, readRequiredArg(parsed, "name"));
1017
+ const detail = await api.getCapabilityEditor(session, org.id, readRequiredArg(parsed, "name"), selector);
981
1018
  return {
982
1019
  data: detail,
983
1020
  human: renderPrettyJson(detail.editor),
984
1021
  kind: "authoring_capabilities_get",
985
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1022
+ meta: { command: parsed.definition.path.join(" "), orgId: org.id, ...releaseSnapshotSelectorMeta(selector) }
986
1023
  };
987
1024
  }
988
1025
  const selection = selectOrgModelSection(management, section, verb === "get" ? parsed.args[section === "assignments" ? "role-name" : "name"] : undefined);
@@ -990,7 +1027,7 @@ async function executeOrgModel(parsed, context, api) {
990
1027
  data: selection.data,
991
1028
  human: selection.human,
992
1029
  kind: `authoring_${sanitizeKind(section)}_${verb}`,
993
- meta: { command: parsed.definition.path.join(" "), orgId: org.id }
1030
+ meta: { command: parsed.definition.path.join(" "), orgId: org.id, ...releaseSnapshotSelectorMeta(selector) }
994
1031
  };
995
1032
  }
996
1033
  async function executeAccess(parsed, context, api) {
@@ -1189,7 +1226,7 @@ async function executeEnv(parsed, context, api) {
1189
1226
  }
1190
1227
  case "env.replace": {
1191
1228
  const environment = readRequiredCliEnvironmentFlag(parsed);
1192
- const body = await readJsonInputSpecifier(String(parsed.flags.file), context.stdin, parsed.definition.id);
1229
+ const body = await readJsonInputSpecifier(String(parsed.flags.file), context.stdin, parsed.definition.id, { flag: "file" });
1193
1230
  const rawVariables = body.variables;
1194
1231
  if (!Array.isArray(rawVariables)) {
1195
1232
  throw usageProblem("Environment replace payload must contain a variables array.", parsed.definition.id);
@@ -0,0 +1,2 @@
1
+ export declare function normalizePublicErrorCode(code: string): string;
2
+ export declare function readPublicErrorCodeFromType(type: string): string;
@@ -0,0 +1,9 @@
1
+ const PUBLIC_ERROR_CODE_PATTERN = /^[a-z][a-z0-9_]*(?:\/[a-z][a-z0-9_]*)?$/u;
2
+ export function normalizePublicErrorCode(code) {
3
+ return PUBLIC_ERROR_CODE_PATTERN.test(code) ? code : "internal/error";
4
+ }
5
+ export function readPublicErrorCodeFromType(type) {
6
+ return type.startsWith("https://errors.kora.dev/")
7
+ ? type.slice("https://errors.kora.dev/".length)
8
+ : "internal/error";
9
+ }
@@ -395,11 +395,11 @@ async function readOptionalPermissions(parsed, context) {
395
395
  if (!specifier) {
396
396
  return undefined;
397
397
  }
398
- return normalizePermissions(await readJsonInputSpecifier(specifier, context.stdin, parsed.definition.id), parsed.definition.id);
398
+ return normalizePermissions(await readJsonInputSpecifier(specifier, context.stdin, parsed.definition.id, { flag: "permissions" }), parsed.definition.id);
399
399
  }
400
400
  async function readRequiredPermissions(parsed, context) {
401
401
  const specifier = readRequiredStringFlag(parsed, "permissions");
402
- return normalizePermissions(await readJsonInputSpecifier(specifier, context.stdin, parsed.definition.id), parsed.definition.id);
402
+ return normalizePermissions(await readJsonInputSpecifier(specifier, context.stdin, parsed.definition.id, { flag: "permissions" }), parsed.definition.id);
403
403
  }
404
404
  function normalizePermissions(input, instance) {
405
405
  const permissions = input.grantedPermissions && isRecord(input.grantedPermissions)
package/dist/files.d.ts CHANGED
@@ -1,5 +1,9 @@
1
+ import { Buffer } from "node:buffer";
1
2
  import type { Readable } from "node:stream";
2
- export declare function readJsonInputSpecifier(specifier: string, stdin: Readable, instance: string): Promise<Record<string, unknown>>;
3
+ interface ReadStructuredInputOptions {
4
+ flag?: string;
5
+ }
6
+ export declare function readJsonInputSpecifier(specifier: string, stdin: Readable, instance: string, options?: ReadStructuredInputOptions): Promise<Record<string, unknown>>;
3
7
  export declare function readTextInputSpecifier(specifier: string, stdin: Readable, instance: string): Promise<string>;
4
8
  export declare function readImportEntries(pathValue: string): Promise<Array<{
5
9
  content: string;
@@ -7,6 +11,12 @@ export declare function readImportEntries(pathValue: string): Promise<Array<{
7
11
  }>>;
8
12
  export declare function isZipArchivePath(pathValue: string): boolean;
9
13
  export declare function readArchiveBytes(pathValue: string, instance: string): Promise<Uint8Array>;
14
+ export declare function readLocalFileBytes(pathValue: string, instance: string, options?: {
15
+ regularFileMessage?: string;
16
+ }): Promise<{
17
+ absolutePath: string;
18
+ bytes: Buffer;
19
+ }>;
10
20
  export declare function readWorkspaceTestEntries(pathValue: string): Promise<Array<{
11
21
  content: string;
12
22
  path: string;
@@ -38,3 +48,4 @@ export declare function readPackageFileEntries(pathValue: string, instance: stri
38
48
  contentBase64: string;
39
49
  path: string;
40
50
  }>>;
51
+ export {};
package/dist/files.js CHANGED
@@ -7,14 +7,38 @@ const MAX_IMPORT_FILE_COUNT = 500;
7
7
  const MAX_IMPORT_FILE_BYTES = 1_000_000;
8
8
  const MAX_PACKAGE_FILE_BYTES = 2_000_000;
9
9
  const MAX_IMPORT_TOTAL_BYTES = 10_000_000;
10
- export async function readJsonInputSpecifier(specifier, stdin, instance) {
10
+ export async function readJsonInputSpecifier(specifier, stdin, instance, options = {}) {
11
11
  if (specifier === "-") {
12
- return readJsonObject(await readStream(stdin), instance);
12
+ return readJsonObject(await readStream(stdin), instance, {
13
+ source: "stdin",
14
+ ...options
15
+ });
13
16
  }
14
17
  if (!specifier.startsWith("@")) {
15
- throw usageProblem("Structured JSON input must use @file.json or - for stdin.", instance);
18
+ throw usageProblem("Structured JSON input must use @file.json or - for stdin.", instance, {
19
+ ...(options.flag ? { flag: options.flag } : {})
20
+ });
21
+ }
22
+ const filePath = resolve(specifier.slice(1));
23
+ let source;
24
+ try {
25
+ source = await readFile(filePath, "utf8");
26
+ }
27
+ catch (error) {
28
+ const nativeCode = readNodeErrorCode(error);
29
+ if (nativeCode && isLocalPathReadErrorCode(nativeCode)) {
30
+ throw usageProblem(`Structured JSON input file ${filePath} could not be read: ${nativeCode}.`, instance, {
31
+ filePath,
32
+ ...(options.flag ? { flag: options.flag } : {}),
33
+ nativeCode
34
+ });
35
+ }
36
+ throw error;
16
37
  }
17
- return readJsonObject(await readFile(specifier.slice(1), "utf8"), instance);
38
+ return readJsonObject(source, instance, {
39
+ filePath,
40
+ ...options
41
+ });
18
42
  }
19
43
  export async function readTextInputSpecifier(specifier, stdin, instance) {
20
44
  if (specifier === "-") {
@@ -35,12 +59,26 @@ export function isZipArchivePath(pathValue) {
35
59
  return extname(pathValue).toLowerCase() === ".zip";
36
60
  }
37
61
  export async function readArchiveBytes(pathValue, instance) {
62
+ const file = await readLocalFileBytes(pathValue, instance, {
63
+ regularFileMessage: "Archive path must be a regular .zip file, not a directory or symbolic link."
64
+ });
65
+ return file.bytes;
66
+ }
67
+ export async function readLocalFileBytes(pathValue, instance, options = {}) {
38
68
  const absolutePath = resolve(pathValue);
39
- const pathStat = await lstat(absolutePath);
69
+ const pathStat = await readLocalPathStat(absolutePath, instance);
40
70
  if (pathStat.isSymbolicLink() || !pathStat.isFile()) {
41
- throw usageProblem("Archive path must be a regular .zip file, not a directory or symbolic link.", instance);
71
+ throw usageProblem(options.regularFileMessage ?? "Path must be a regular file, not a directory or symbolic link.", instance);
72
+ }
73
+ try {
74
+ return {
75
+ absolutePath,
76
+ bytes: await readFile(absolutePath)
77
+ };
78
+ }
79
+ catch (error) {
80
+ throwLocalPathReadProblem(error, absolutePath, instance);
42
81
  }
43
- return await readFile(absolutePath);
44
82
  }
45
83
  export async function readWorkspaceTestEntries(pathValue) {
46
84
  return await readUtf8Entries(pathValue, {
@@ -50,7 +88,7 @@ export async function readWorkspaceTestEntries(pathValue) {
50
88
  }
51
89
  async function readUtf8Entries(pathValue, options) {
52
90
  const absolutePath = resolve(pathValue);
53
- const pathStat = await lstat(absolutePath);
91
+ const pathStat = await readLocalPathStat(absolutePath, options.instance);
54
92
  if (pathStat.isSymbolicLink()) {
55
93
  throw usageProblem("Import path must be a regular file or directory, not a symbolic link.", options.instance);
56
94
  }
@@ -151,7 +189,7 @@ export async function writePackageExport(outPath, envelope) {
151
189
  }
152
190
  export async function readPackageFileEntries(pathValue, instance) {
153
191
  const absolutePath = resolve(pathValue);
154
- const pathStat = await lstat(absolutePath);
192
+ const pathStat = await readLocalPathStat(absolutePath, instance);
155
193
  if (pathStat.isSymbolicLink()) {
156
194
  throw usageProblem("Package path must be a regular file or directory, not a symbolic link.", instance);
157
195
  }
@@ -161,7 +199,9 @@ export async function readPackageFileEntries(pathValue, instance) {
161
199
  totalBytes: 0
162
200
  }, instance);
163
201
  return [{
164
- contentBase64: (await readFile(absolutePath)).toString("base64"),
202
+ contentBase64: (await readLocalFileBytes(absolutePath, instance, {
203
+ regularFileMessage: "Package path must be a regular file or directory, not a symbolic link."
204
+ })).bytes.toString("base64"),
165
205
  path: basename(absolutePath)
166
206
  }];
167
207
  }
@@ -173,16 +213,24 @@ export async function readPackageFileEntries(pathValue, instance) {
173
213
  entries.sort((left, right) => left.path.localeCompare(right.path));
174
214
  return entries;
175
215
  }
176
- function readJsonObject(source, instance) {
216
+ function readJsonObject(source, instance, options) {
177
217
  let parsed;
178
218
  try {
179
219
  parsed = JSON.parse(source);
180
220
  }
181
221
  catch {
182
- throw usageProblem("Structured JSON input must be valid JSON.", instance);
222
+ const sourceLabel = "filePath" in options ? `file ${options.filePath}` : "from stdin";
223
+ throw usageProblem(`Structured JSON input ${sourceLabel} must be valid JSON.`, instance, {
224
+ ...("filePath" in options ? { filePath: options.filePath } : { source: options.source }),
225
+ ...(options.flag ? { flag: options.flag } : {})
226
+ });
183
227
  }
184
228
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
185
- throw usageProblem("Structured JSON input must be a JSON object.", instance);
229
+ const sourceLabel = "filePath" in options ? `file ${options.filePath}` : "from stdin";
230
+ throw usageProblem(`Structured JSON input ${sourceLabel} must be a JSON object.`, instance, {
231
+ ...("filePath" in options ? { filePath: options.filePath } : { source: options.source }),
232
+ ...(options.flag ? { flag: options.flag } : {})
233
+ });
186
234
  }
187
235
  return parsed;
188
236
  }
@@ -232,10 +280,10 @@ async function walkPackageDirectory(root, current, entries, counters, instance)
232
280
  if (!child.isFile()) {
233
281
  continue;
234
282
  }
235
- const childStat = await lstat(childPath);
283
+ const childStat = await readLocalPathStat(childPath, instance);
236
284
  assertPackageFileCanBeRead(childStat.size, relativePath, counters, instance);
237
285
  entries.push({
238
- contentBase64: (await readFile(childPath)).toString("base64"),
286
+ contentBase64: (await readLocalFileBytes(childPath, instance)).bytes.toString("base64"),
239
287
  path: relativePath
240
288
  });
241
289
  }
@@ -289,7 +337,13 @@ async function readLimitedUtf8File(filePath, displayPath, counters, options) {
289
337
  if (counters.fileCount >= MAX_IMPORT_FILE_COUNT) {
290
338
  throw usageProblem(`Import has more than ${String(MAX_IMPORT_FILE_COUNT)} files.`, options.instance);
291
339
  }
292
- const content = await readFile(filePath, "utf8");
340
+ let content;
341
+ try {
342
+ content = await readFile(filePath, "utf8");
343
+ }
344
+ catch (error) {
345
+ throwLocalPathReadProblem(error, filePath, options.instance);
346
+ }
293
347
  const bytes = Buffer.byteLength(content, "utf8");
294
348
  if (bytes > options.maxFileBytes) {
295
349
  throw usageProblem(`Import file ${displayPath} exceeds the per-file byte limit.`, options.instance);
@@ -320,3 +374,35 @@ function isNodeErrorWithCode(error, code) {
320
374
  "code" in error &&
321
375
  error.code === code;
322
376
  }
377
+ function readNodeErrorCode(error) {
378
+ if (typeof error !== "object" || error === null || !("code" in error)) {
379
+ return undefined;
380
+ }
381
+ const code = error.code;
382
+ return typeof code === "string" && code.length > 0 ? code : undefined;
383
+ }
384
+ function isLocalPathReadErrorCode(code) {
385
+ return code === "EACCES" ||
386
+ code === "EISDIR" ||
387
+ code === "ENOENT" ||
388
+ code === "ENOTDIR" ||
389
+ code === "EPERM";
390
+ }
391
+ async function readLocalPathStat(absolutePath, instance) {
392
+ try {
393
+ return await lstat(absolutePath);
394
+ }
395
+ catch (error) {
396
+ throwLocalPathReadProblem(error, absolutePath, instance);
397
+ }
398
+ }
399
+ function throwLocalPathReadProblem(error, absolutePath, instance) {
400
+ const nativeCode = readNodeErrorCode(error);
401
+ if (nativeCode && isLocalPathReadErrorCode(nativeCode)) {
402
+ throw usageProblem(`Local path ${absolutePath} could not be read: ${nativeCode}.`, instance, {
403
+ nativeCode,
404
+ path: absolutePath
405
+ });
406
+ }
407
+ throw error;
408
+ }