run402 4.9.0 → 4.10.0

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 (58) hide show
  1. package/cli.mjs +14 -2
  2. package/lib/admin.mjs +2 -2
  3. package/lib/agent.mjs +2 -2
  4. package/lib/ai.mjs +2 -2
  5. package/lib/allowance.mjs +2 -2
  6. package/lib/apps.mjs +2 -2
  7. package/lib/archives.mjs +2 -7
  8. package/lib/argparse.mjs +63 -2
  9. package/lib/asset-wire.mjs +59 -0
  10. package/lib/assets.mjs +8 -4
  11. package/lib/auth.mjs +2 -2
  12. package/lib/billing.mjs +2 -2
  13. package/lib/branches.mjs +2 -6
  14. package/lib/cache.mjs +2 -6
  15. package/lib/cdn.mjs +2 -2
  16. package/lib/ci.mjs +2 -1
  17. package/lib/cloud.mjs +3 -11
  18. package/lib/contracts.mjs +2 -1
  19. package/lib/core.mjs +3 -13
  20. package/lib/credentials.mjs +3 -13
  21. package/lib/deploy.mjs +2 -5
  22. package/lib/doctor.mjs +32 -4
  23. package/lib/domains.mjs +2 -2
  24. package/lib/email.mjs +2 -2
  25. package/lib/functions.mjs +2 -2
  26. package/lib/grants.mjs +3 -2
  27. package/lib/image.mjs +2 -2
  28. package/lib/jobs.mjs +3 -2
  29. package/lib/message.mjs +2 -2
  30. package/lib/notifications.mjs +6 -12
  31. package/lib/operator.mjs +2 -7
  32. package/lib/org.mjs +2 -1
  33. package/lib/projects.mjs +2 -2
  34. package/lib/secrets.mjs +3 -3
  35. package/lib/service.mjs +3 -2
  36. package/lib/sites.mjs +2 -2
  37. package/lib/snapshots.mjs +2 -6
  38. package/lib/subdomains.mjs +2 -2
  39. package/lib/tier.mjs +2 -2
  40. package/lib/transfer.mjs +2 -1
  41. package/lib/up.mjs +12 -6
  42. package/lib/wallets.mjs +2 -6
  43. package/lib/webhook-secret.mjs +3 -8
  44. package/lib/webhooks.mjs +2 -2
  45. package/package.json +1 -1
  46. package/sdk/dist/actions.d.ts +20 -1
  47. package/sdk/dist/actions.d.ts.map +1 -1
  48. package/sdk/dist/actions.js.map +1 -1
  49. package/sdk/dist/node/actions-node.d.ts.map +1 -1
  50. package/sdk/dist/node/actions-node.js +114 -8
  51. package/sdk/dist/node/actions-node.js.map +1 -1
  52. package/sdk/dist/node/deploy-manifest.d.ts +36 -0
  53. package/sdk/dist/node/deploy-manifest.d.ts.map +1 -1
  54. package/sdk/dist/node/deploy-manifest.js +112 -3
  55. package/sdk/dist/node/deploy-manifest.js.map +1 -1
  56. package/sdk/dist/node/index.d.ts +1 -1
  57. package/sdk/dist/node/index.d.ts.map +1 -1
  58. package/sdk/dist/node/index.js.map +1 -1
package/cli.mjs CHANGED
@@ -385,11 +385,23 @@ switch (cmd) {
385
385
  }
386
386
  default: {
387
387
  const { fail } = await import("./lib/sdk-errors.mjs");
388
+ // Did-you-mean candidates: manifest families ∪ the allowlisted skipped
389
+ // families — together these cover every case in this switch (the
390
+ // cli-conventions gate keeps them in lockstep), so no second list.
391
+ const { COMMAND_MANIFEST, SKIPPED_FAMILIES } = await import("./lib/command-manifest.mjs");
392
+ const { closestWord } = await import("./lib/argparse.mjs");
393
+ const families = new Set([
394
+ ...COMMAND_MANIFEST.map((entry) => entry.path[0]),
395
+ ...Object.keys(SKIPPED_FAMILIES),
396
+ ]);
397
+ const closest = typeof cmd === "string" ? closestWord(cmd, [...families]) : null;
388
398
  fail({
389
399
  code: "UNKNOWN_COMMAND",
390
- message: `Unknown command: ${cmd}`,
400
+ message: closest
401
+ ? `Unknown command: ${cmd}. Did you mean ${closest}?`
402
+ : `Unknown command: ${cmd}`,
391
403
  hint: "Run `run402 --help` for the command list.",
392
- details: { command: cmd },
404
+ details: { command: cmd, closest: closest ? [closest] : [] },
393
405
  });
394
406
  }
395
407
  }
package/lib/admin.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { getSdk } from "./sdk.mjs";
2
2
  import { reportSdkError, fail } from "./sdk-errors.mjs";
3
- import { assertKnownFlags, hasHelp, normalizeArgv, resolveProjectSelector } from "./argparse.mjs";
3
+ import { assertKnownFlags, hasHelp, normalizeArgv, resolveProjectSelector, failUnknownSubcommand } from "./argparse.mjs";
4
4
 
5
5
  const HELP = `run402 admin — Platform-admin operations (v1.57+)
6
6
 
@@ -185,6 +185,6 @@ export async function run(sub, args) {
185
185
  case "archive": await archive(args); break;
186
186
  case "reactivate": await reactivate(args); break;
187
187
  default:
188
- fail({ code: "UNKNOWN_SUBCOMMAND", message: `Unknown admin subcommand: ${sub}`, hint: "Run `run402 admin --help` for usage.", details: { command: "admin", subcommand: sub } });
188
+ failUnknownSubcommand("admin", sub);
189
189
  }
190
190
  }
package/lib/agent.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { allowanceAuthHeaders } from "./config.mjs";
2
2
  import { getSdk } from "./sdk.mjs";
3
3
  import { reportSdkError, fail } from "./sdk-errors.mjs";
4
- import { assertKnownFlags, flagValue, normalizeArgv, positionalArgs, validateWebhookUrl } from "./argparse.mjs";
4
+ import { assertKnownFlags, flagValue, normalizeArgv, positionalArgs, validateWebhookUrl, failUnknownSubcommand } from "./argparse.mjs";
5
5
 
6
6
  const HELP = `run402 agent — Manage agent identity
7
7
 
@@ -201,6 +201,6 @@ export async function run(sub, args) {
201
201
  await passkey(args);
202
202
  return;
203
203
  default:
204
- fail({ code: "UNKNOWN_SUBCOMMAND", message: `Unknown agent subcommand: ${sub}`, hint: "Run `run402 agent --help` for usage.", details: { command: "agent", subcommand: sub } });
204
+ failUnknownSubcommand("agent", sub);
205
205
  }
206
206
  }
package/lib/ai.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { resolveProjectId } from "./config.mjs";
2
2
  import { getSdk } from "./sdk.mjs";
3
3
  import { reportSdkError, fail } from "./sdk-errors.mjs";
4
- import { assertKnownFlags, flagValue, normalizeArgv, resolvePositionalProject } from "./argparse.mjs";
4
+ import { assertKnownFlags, flagValue, normalizeArgv, resolvePositionalProject, failUnknownSubcommand } from "./argparse.mjs";
5
5
 
6
6
  const HELP = `run402 ai — AI translation and moderation tools
7
7
 
@@ -230,6 +230,6 @@ export async function run(sub, args) {
230
230
  case "moderate": await moderate(args); break;
231
231
  case "usage": await usage(args); break;
232
232
  default:
233
- fail({ code: "UNKNOWN_SUBCOMMAND", message: `Unknown ai subcommand: ${sub}`, hint: "Run `run402 ai --help` for usage.", details: { command: "ai", subcommand: sub } });
233
+ failUnknownSubcommand("ai", sub);
234
234
  }
235
235
  }
package/lib/allowance.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readAllowance, saveAllowance, allowanceFile } from "./config.mjs";
2
2
  import { getSdk } from "./sdk.mjs";
3
3
  import { reportSdkError, fail } from "./sdk-errors.mjs";
4
- import { assertKnownFlags, flagValue, normalizeArgv, parseIntegerFlag, positionalArgs } from "./argparse.mjs";
4
+ import { assertKnownFlags, flagValue, normalizeArgv, parseIntegerFlag, positionalArgs, failUnknownSubcommand } from "./argparse.mjs";
5
5
 
6
6
  const HELP = `run402 allowance — Manage your agent allowance
7
7
 
@@ -347,6 +347,6 @@ export async function run(sub, args) {
347
347
  case "checkout": await checkout(args); break;
348
348
  case "history": await history(args); break;
349
349
  default:
350
- fail({ code: "UNKNOWN_SUBCOMMAND", message: `Unknown allowance subcommand: ${sub}`, hint: "Run `run402 allowance --help` for usage.", details: { command: "allowance", subcommand: sub } });
350
+ failUnknownSubcommand("allowance", sub);
351
351
  }
352
352
  }
package/lib/apps.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { allowanceAuthHeaders, saveProject } from "./config.mjs";
2
2
  import { getSdk } from "./sdk.mjs";
3
3
  import { reportSdkError, fail } from "./sdk-errors.mjs";
4
- import { assertAllowedValue, assertKnownFlags, flagValue, normalizeArgv, positionalArgs, resolveProjectSelector } from "./argparse.mjs";
4
+ import { assertAllowedValue, assertKnownFlags, flagValue, normalizeArgv, positionalArgs, resolveProjectSelector, failUnknownSubcommand } from "./argparse.mjs";
5
5
 
6
6
  const HELP = `run402 apps — Browse and manage the app marketplace
7
7
 
@@ -330,6 +330,6 @@ export async function run(sub, args) {
330
330
  case "update": await update(args[0], args[1], args.slice(2)); break;
331
331
  case "delete": await deleteVersion(args[0], args[1], args.slice(2)); break;
332
332
  default:
333
- fail({ code: "UNKNOWN_SUBCOMMAND", message: `Unknown apps subcommand: ${sub}`, hint: "Run `run402 apps --help` for usage.", details: { command: "apps", subcommand: sub } });
333
+ failUnknownSubcommand("apps", sub);
334
334
  }
335
335
  }
package/lib/archives.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { inspectArchive, verifyArchive } from "#sdk/node";
2
2
  import { reportSdkError, fail } from "./sdk-errors.mjs";
3
- import { assertKnownFlags, hasHelp, normalizeArgv, positionalArgs } from "./argparse.mjs";
3
+ import { assertKnownFlags, hasHelp, normalizeArgv, positionalArgs, failUnknownSubcommand } from "./argparse.mjs";
4
4
 
5
5
  const HELP = `run402 archives — Inspect and verify portable Run402 project archives
6
6
 
@@ -23,12 +23,7 @@ export async function run(sub, rawArgs = []) {
23
23
  case "inspect": return inspect(rawArgs);
24
24
  case "verify": return verify(rawArgs);
25
25
  default:
26
- fail({
27
- code: "UNKNOWN_SUBCOMMAND",
28
- message: `Unknown archives subcommand: ${sub}`,
29
- hint: "Run `run402 archives --help` for usage.",
30
- details: { command: "archives", subcommand: sub },
31
- });
26
+ failUnknownSubcommand("archives", sub);
32
27
  }
33
28
  }
34
29
 
package/lib/argparse.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { existsSync, statSync } from "node:fs";
2
2
  import { fail } from "./sdk-errors.mjs";
3
3
  import { resolveProjectId } from "./config.mjs";
4
+ import { COMMAND_MANIFEST } from "./command-manifest.mjs";
4
5
 
5
6
  export function normalizeArgv(argv = []) {
6
7
  const out = [];
@@ -368,11 +369,17 @@ export function resolveProjectSelector(args, opts = {}) {
368
369
  return resolvePositionalProject(list, opts);
369
370
  }
370
371
 
371
- function closestFlag(flag, candidates) {
372
+ /**
373
+ * Reusable did-you-mean helper: the closest candidate within Levenshtein
374
+ * distance ≤ 3, or null when nothing is close enough. Used for flags
375
+ * (`closestFlag`), top-level commands (cli.mjs's dispatch default), and
376
+ * subcommands (`failUnknownSubcommand`).
377
+ */
378
+ export function closestWord(word, candidates) {
372
379
  let best = null;
373
380
  let bestDistance = Number.POSITIVE_INFINITY;
374
381
  for (const candidate of candidates) {
375
- const d = levenshtein(flag, candidate);
382
+ const d = levenshtein(word, candidate);
376
383
  if (d < bestDistance) {
377
384
  best = candidate;
378
385
  bestDistance = d;
@@ -382,6 +389,60 @@ function closestFlag(flag, candidates) {
382
389
  return bestDistance <= 3 ? best : null;
383
390
  }
384
391
 
392
+ function closestFlag(flag, candidates) {
393
+ return closestWord(flag, candidates);
394
+ }
395
+
396
+ /**
397
+ * Known subcommands of a command family, derived from COMMAND_MANIFEST
398
+ * (never hand-maintained). `family` may be multi-word for nested groups
399
+ * (e.g. "cloud archives", "email webhooks").
400
+ */
401
+ export function knownSubcommands(family) {
402
+ const familyWords = String(family).split(" ").filter(Boolean);
403
+ const subs = new Set();
404
+ for (const entry of COMMAND_MANIFEST) {
405
+ if (entry.path.length <= familyWords.length) continue;
406
+ if (!familyWords.every((word, i) => entry.path[i] === word)) continue;
407
+ subs.add(entry.path[familyWords.length]);
408
+ }
409
+ return [...subs].sort();
410
+ }
411
+
412
+ /**
413
+ * Shared unknown-subcommand failure: derives the family's known
414
+ * subcommands from COMMAND_MANIFEST and adds a `Did you mean <sub>?`
415
+ * suggestion plus `details.closest` / `details.known_subcommands`.
416
+ *
417
+ * Options:
418
+ * hint override the default `Run \`run402 <family> --help\`…` line
419
+ * label display label when it differs from the manifest family
420
+ * (e.g. the email-webhooks group is dispatched as
421
+ * `run402 webhooks …` but lives at ["email","webhooks",…])
422
+ * extraSubcommands candidates handled by the module but absent from the
423
+ * manifest (nested group heads, aliases)
424
+ * next_actions forwarded to the error envelope
425
+ */
426
+ export function failUnknownSubcommand(family, sub, { hint, label, extraSubcommands = [], next_actions } = {}) {
427
+ const displayLabel = label ?? family;
428
+ const known = [...new Set([...knownSubcommands(family), ...extraSubcommands])].sort();
429
+ const closest = typeof sub === "string" ? closestWord(sub, known) : null;
430
+ fail({
431
+ code: "UNKNOWN_SUBCOMMAND",
432
+ message: closest
433
+ ? `Unknown ${displayLabel} subcommand: ${sub}. Did you mean ${closest}?`
434
+ : `Unknown ${displayLabel} subcommand: ${sub}`,
435
+ hint: hint ?? `Run \`run402 ${displayLabel} --help\` for usage.`,
436
+ details: {
437
+ command: displayLabel,
438
+ subcommand: sub,
439
+ closest: closest ? [closest] : [],
440
+ known_subcommands: known,
441
+ },
442
+ ...(next_actions ? { next_actions } : {}),
443
+ });
444
+ }
445
+
385
446
  function levenshtein(a, b) {
386
447
  const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
387
448
  const curr = Array.from({ length: b.length + 1 }, () => 0);
@@ -0,0 +1,59 @@
1
+ /**
2
+ * toWireAssetRef — project an SDK AssetRef (which carries camelCase TS
3
+ * conveniences like `cdnUrl`, `immutableUrl`, `contentSha256`, `size`) onto
4
+ * the CANONICAL gateway wire shape: snake_case keys only, no duplicate
5
+ * values under two casings.
6
+ *
7
+ * Canonical key set (gateway asset envelope):
8
+ * key, sha256, size_bytes, content_type, visibility, immutable,
9
+ * url, immutable_url, cdn_url, cdn_immutable_url, sri, etag,
10
+ * content_digest, + the v1.49/v1.50/v1.54 image fields
11
+ * (width_px, height_px, blurhash, variant_spec_version, display_url,
12
+ * display_immutable_url, variants, metadata, image_format, image_info,
13
+ * image_exif, image_exif_policy, blurhash_data_url, asset_schema).
14
+ *
15
+ * Unknown snake_case keys from newer gateways are preserved as-is; every
16
+ * camelCase key (the documented SDK aliases and any future ones) is dropped,
17
+ * with the aliases mapped back to their canonical snake twin when the snake
18
+ * form is missing from the input.
19
+ *
20
+ * The SDK keeps its camelCase conveniences for typed consumers (e.g.
21
+ * @run402/astro reads `cdnUrl`); this projector is the CLI/MCP output
22
+ * boundary only.
23
+ */
24
+
25
+ // SDK-only convenience keys that must never appear in wire output. `size`
26
+ // and `cdn` are lowercase but still SDK-only: `size` duplicates
27
+ // `size_bytes`, `cdn` is the SDK's invalidation envelope (camelCase inner
28
+ // keys, locally synthesized).
29
+ const SDK_ONLY_KEYS = new Set(["size", "cdn"]);
30
+
31
+ export function toWireAssetRef(ref) {
32
+ if (!ref || typeof ref !== "object" || Array.isArray(ref)) return ref;
33
+ const out = {};
34
+ for (const [key, value] of Object.entries(ref)) {
35
+ if (typeof value === "function") continue; // tag emitters (scriptTag, …)
36
+ if (SDK_ONLY_KEYS.has(key)) continue;
37
+ if (/[A-Z]/.test(key)) continue; // camelCase SDK aliases/conveniences
38
+ out[key] = value;
39
+ }
40
+ // Canonical fields whose only SDK source is a camelCase convenience.
41
+ if (out.content_type === undefined && typeof ref.contentType === "string") {
42
+ out.content_type = ref.contentType;
43
+ }
44
+ // SDK naming vs wire naming: `cdnUrl` is the IMMUTABLE cdn url,
45
+ // `cdnMutableUrl` the mutable one.
46
+ if (out.cdn_url === undefined && ref.cdnMutableUrl !== undefined) {
47
+ out.cdn_url = ref.cdnMutableUrl;
48
+ }
49
+ if (out.cdn_immutable_url === undefined && ref.cdnUrl !== undefined) {
50
+ out.cdn_immutable_url = ref.cdnUrl;
51
+ }
52
+ if (out.content_digest === undefined && ref.contentDigest !== undefined) {
53
+ out.content_digest = ref.contentDigest;
54
+ }
55
+ if (out.immutable === undefined) {
56
+ out.immutable = ref.immutable_url !== null && ref.immutable_url !== undefined;
57
+ }
58
+ return out;
59
+ }
package/lib/assets.mjs CHANGED
@@ -31,7 +31,8 @@ import { pipeline } from "node:stream/promises";
31
31
  import { resolveProjectId } from "./config.mjs";
32
32
  import { getSdk } from "./sdk.mjs";
33
33
  import { reportSdkError, fail } from "./sdk-errors.mjs";
34
- import { assertKnownFlags, hasHelp, normalizeArgv, parseIntegerFlag } from "./argparse.mjs";
34
+ import { assertKnownFlags, hasHelp, normalizeArgv, parseIntegerFlag, failUnknownSubcommand } from "./argparse.mjs";
35
+ import { toWireAssetRef } from "./asset-wire.mjs";
35
36
 
36
37
  const HELP = `run402 blob — Direct-to-S3 blob storage
37
38
 
@@ -458,8 +459,11 @@ async function putOne(projectId, filePath, opts) {
458
459
  ...(opts.metadata ? { metadata: opts.metadata } : {}),
459
460
  ...(opts.exifPolicy ? { exifPolicy: opts.exifPolicy } : {}),
460
461
  });
461
- log(opts, { event: "done", ...result });
462
- return result;
462
+ // Canonical wire shape only on stdout: snake_case keys, no camelCase
463
+ // duplicates (the SDK's AssetRef carries both; toWireAssetRef projects).
464
+ const wire = toWireAssetRef(result);
465
+ log(opts, { event: "done", ...wire });
466
+ return wire;
463
467
  }
464
468
 
465
469
  function computeDestKey(filePath, keyOpt) {
@@ -681,6 +685,6 @@ export async function run(sub, args) {
681
685
  case "sign": await sign(defaultProject, args); break;
682
686
  case "diagnose": await diagnose(defaultProject, args); break;
683
687
  default:
684
- fail({ code: "UNKNOWN_SUBCOMMAND", message: `Unknown assets subcommand: ${sub}`, hint: "Run `run402 assets --help` for usage.", details: { command: "assets", subcommand: sub } });
688
+ failUnknownSubcommand("assets", sub);
685
689
  }
686
690
  }
package/lib/auth.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { resolveProjectId } from "./config.mjs";
2
2
  import { getSdk } from "./sdk.mjs";
3
3
  import { reportSdkError, fail } from "./sdk-errors.mjs";
4
- import { assertKnownFlags, hasHelp, normalizeArgv } from "./argparse.mjs";
4
+ import { assertKnownFlags, hasHelp, normalizeArgv, failUnknownSubcommand } from "./argparse.mjs";
5
5
 
6
6
  const HELP = `run402 auth — Manage project user authentication
7
7
 
@@ -757,6 +757,6 @@ export async function run(sub, args) {
757
757
  case "providers": await providers(args); break;
758
758
  case "scaffold-roles": scaffoldRoles(args); break;
759
759
  default:
760
- fail({ code: "UNKNOWN_SUBCOMMAND", message: `Unknown auth subcommand: ${sub}`, hint: "Run `run402 auth --help` for usage.", details: { command: "auth", subcommand: sub } });
760
+ failUnknownSubcommand("auth", sub);
761
761
  }
762
762
  }
package/lib/billing.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { getSdk } from "./sdk.mjs";
2
2
  import { reportSdkError, fail } from "./sdk-errors.mjs";
3
- import { assertKnownFlags, flagValue, normalizeArgv, parseIntegerFlag, positionalArgs } from "./argparse.mjs";
3
+ import { assertKnownFlags, flagValue, normalizeArgv, parseIntegerFlag, positionalArgs, failUnknownSubcommand } from "./argparse.mjs";
4
4
 
5
5
  const HELP = `run402 billing — Email organizations and org checkouts
6
6
 
@@ -359,6 +359,6 @@ export async function run(sub, args) {
359
359
  case "balance": await balance(args); break;
360
360
  case "history": await history(args); break;
361
361
  default:
362
- fail({ code: "UNKNOWN_SUBCOMMAND", message: `Unknown billing subcommand: ${sub}`, hint: "Run `run402 billing --help` for usage.", details: { command: "billing", subcommand: sub } });
362
+ failUnknownSubcommand("billing", sub);
363
363
  }
364
364
  }
package/lib/branches.mjs CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  parseIntegerFlag,
10
10
  positionalArgs,
11
11
  resolveProjectSelector,
12
+ failUnknownSubcommand,
12
13
  } from "./argparse.mjs";
13
14
 
14
15
  const HELP = `run402 branches — Contained project data branches
@@ -41,12 +42,7 @@ export async function run(sub, args = []) {
41
42
  case "renew": return renew(rest);
42
43
  case "delete": return deleteBranch(rest);
43
44
  default:
44
- fail({
45
- code: "UNKNOWN_SUBCOMMAND",
46
- message: `Unknown branches subcommand: ${sub}`,
47
- hint: "Run `run402 branches --help` for usage.",
48
- details: { command: "branches", subcommand: sub },
49
- });
45
+ failUnknownSubcommand("branches", sub);
50
46
  }
51
47
  }
52
48
 
package/lib/cache.mjs CHANGED
@@ -18,7 +18,7 @@
18
18
 
19
19
  import { getSdk } from "./sdk.mjs";
20
20
  import { reportSdkError, fail } from "./sdk-errors.mjs";
21
- import { assertKnownFlags, flagValue, normalizeArgv } from "./argparse.mjs";
21
+ import { assertKnownFlags, flagValue, normalizeArgv, failUnknownSubcommand } from "./argparse.mjs";
22
22
  import { editRequestAction } from "./next-actions.mjs";
23
23
 
24
24
  // Locally-defined helpers — argparse.mjs's normalized form is a flat
@@ -99,11 +99,7 @@ export async function run(sub, args) {
99
99
  await invalidate(args);
100
100
  break;
101
101
  default:
102
- fail({
103
- code: "UNKNOWN_SUBCOMMAND",
104
- message: `Unknown cache subcommand: ${sub}`,
105
- hint: "Run `run402 cache --help` for usage.",
106
- details: { command: "cache", subcommand: sub },
102
+ failUnknownSubcommand("cache", sub, {
107
103
  next_actions: [editRequestAction("run402 cache --help", "Choose a supported cache subcommand.")],
108
104
  });
109
105
  }
package/lib/cdn.mjs CHANGED
@@ -16,7 +16,7 @@
16
16
  import { resolveProjectId } from "./config.mjs";
17
17
  import { getSdk } from "./sdk.mjs";
18
18
  import { reportSdkError, fail } from "./sdk-errors.mjs";
19
- import { assertKnownFlags, flagValue, normalizeArgv, parseIntegerFlag, positionalArgs } from "./argparse.mjs";
19
+ import { assertKnownFlags, flagValue, normalizeArgv, parseIntegerFlag, positionalArgs, failUnknownSubcommand } from "./argparse.mjs";
20
20
 
21
21
  const HELP = `run402 cdn — CloudFront CDN diagnostics for public blob URLs
22
22
 
@@ -135,6 +135,6 @@ export async function run(sub, args) {
135
135
  await waitFresh(defaultProject, args);
136
136
  break;
137
137
  default:
138
- fail({ code: "UNKNOWN_SUBCOMMAND", message: `Unknown cdn subcommand: ${sub}`, hint: "Run `run402 cdn --help` for usage.", details: { command: "cdn", subcommand: sub } });
138
+ failUnknownSubcommand("cdn", sub);
139
139
  }
140
140
  }
package/lib/ci.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { execFileSync } from "node:child_process";
2
+ import { failUnknownSubcommand } from "./argparse.mjs";
2
3
  import { randomBytes } from "node:crypto";
3
4
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
5
  import { dirname, resolve } from "node:path";
@@ -459,6 +460,6 @@ export async function run(sub, args) {
459
460
  case "revoke": await revoke(args); break;
460
461
  case "set-asset-scopes": await setAssetScopes(args); break;
461
462
  default:
462
- fail({ code: "UNKNOWN_SUBCOMMAND", message: `Unknown ci subcommand: ${sub}`, hint: "Run `run402 ci --help` for usage.", details: { command: "ci", subcommand: sub } });
463
+ failUnknownSubcommand("ci", sub);
463
464
  }
464
465
  }
package/lib/cloud.mjs CHANGED
@@ -3,7 +3,7 @@ import { dirname, resolve } from "node:path";
3
3
 
4
4
  import { getSdk } from "./sdk.mjs";
5
5
  import { fail, reportSdkError } from "./sdk-errors.mjs";
6
- import { assertAllowedValue, assertKnownFlags, flagValue, hasHelp, normalizeArgv, parseIntegerFlag, positionalArgs, resolveProjectSelector } from "./argparse.mjs";
6
+ import { assertAllowedValue, assertKnownFlags, flagValue, hasHelp, normalizeArgv, parseIntegerFlag, positionalArgs, resolveProjectSelector, failUnknownSubcommand } from "./argparse.mjs";
7
7
 
8
8
  const HELP = `run402 cloud — Run402 Cloud portability commands
9
9
 
@@ -60,11 +60,7 @@ export async function run(sub, args = []) {
60
60
  return;
61
61
  }
62
62
  if (sub !== "archives") {
63
- fail({
64
- code: "UNKNOWN_SUBCOMMAND",
65
- message: `Unknown cloud subcommand: ${sub}`,
66
- hint: "Run `run402 cloud --help` for usage.",
67
- details: { command: "cloud", subcommand: sub },
63
+ failUnknownSubcommand("cloud", sub, {
68
64
  next_actions: [{ type: "run_command", command: "run402 cloud archives --help" }],
69
65
  });
70
66
  }
@@ -73,11 +69,7 @@ export async function run(sub, args = []) {
73
69
  if (action === "create") return create(rest);
74
70
  if (action === "download") return download(rest);
75
71
  if (action === "status") return status(rest);
76
- fail({
77
- code: "UNKNOWN_SUBCOMMAND",
78
- message: `Unknown cloud archives subcommand: ${action}`,
79
- hint: "Run `run402 cloud archives --help` for usage.",
80
- details: { command: "cloud archives", subcommand: action },
72
+ failUnknownSubcommand("cloud archives", action, {
81
73
  next_actions: [{ type: "run_command", command: "run402 cloud archives --help" }],
82
74
  });
83
75
  }
package/lib/contracts.mjs CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  positionalArgs,
9
9
  resolveProjectSelector,
10
10
  validateEvmAddress,
11
+ failUnknownSubcommand,
11
12
  } from "./argparse.mjs";
12
13
 
13
14
  const HELP = `run402 contracts — KMS-backed Ethereum signers for smart-contract calls
@@ -520,6 +521,6 @@ export async function run(sub, args) {
520
521
  case "drain": { const { projectId, rest } = select(); await drain(projectId, rest[0], rest.slice(1)); break; }
521
522
  case "delete": { const { projectId, rest } = select(); await deleteSigner(projectId, rest[0], rest.slice(1)); break; }
522
523
  default:
523
- fail({ code: "UNKNOWN_SUBCOMMAND", message: `Unknown contracts subcommand: ${sub}`, hint: "Run `run402 contracts --help` for usage.", details: { command: "contracts", subcommand: sub } });
524
+ failUnknownSubcommand("contracts", sub);
524
525
  }
525
526
  }
package/lib/core.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { importArchiveToCore } from "#sdk/node";
2
2
  import { fail, reportSdkError } from "./sdk-errors.mjs";
3
- import { assertKnownFlags, flagValue, hasHelp, normalizeArgv, positionalArgs } from "./argparse.mjs";
3
+ import { assertKnownFlags, flagValue, hasHelp, normalizeArgv, positionalArgs, failUnknownSubcommand } from "./argparse.mjs";
4
4
 
5
5
  const HELP = `run402 core — Local Run402 Core commands
6
6
 
@@ -29,21 +29,11 @@ export async function run(sub, args = []) {
29
29
  return;
30
30
  }
31
31
  if (sub !== "projects") {
32
- fail({
33
- code: "UNKNOWN_SUBCOMMAND",
34
- message: `Unknown core subcommand: ${sub}`,
35
- hint: "Run `run402 core --help` for usage.",
36
- details: { command: "core", subcommand: sub },
37
- });
32
+ failUnknownSubcommand("core", sub);
38
33
  }
39
34
  const action = args[0];
40
35
  if (action === "import" || action === "apply") return importProject(args.slice(1));
41
- fail({
42
- code: "UNKNOWN_SUBCOMMAND",
43
- message: `Unknown core projects subcommand: ${action}`,
44
- hint: "Run `run402 core projects --help` for usage.",
45
- details: { command: "core projects", subcommand: action },
46
- });
36
+ failUnknownSubcommand("core projects", action, { extraSubcommands: ["apply"] });
47
37
  }
48
38
 
49
39
  async function importProject(rawArgs) {
@@ -8,7 +8,7 @@ import {
8
8
  removeProject,
9
9
  saveProject,
10
10
  } from "./config.mjs";
11
- import { assertKnownFlags, flagValue, normalizeArgv, positionalArgs } from "./argparse.mjs";
11
+ import { assertKnownFlags, flagValue, normalizeArgv, positionalArgs, failUnknownSubcommand } from "./argparse.mjs";
12
12
  import { fail } from "./sdk-errors.mjs";
13
13
 
14
14
  const HELP = `run402 credentials — Manage local credential material
@@ -245,12 +245,7 @@ async function runProjectKeys(sub, args) {
245
245
  case "export": await exportKey(args); break;
246
246
  case "remove": await remove(args); break;
247
247
  default:
248
- fail({
249
- code: "UNKNOWN_SUBCOMMAND",
250
- message: `Unknown credentials project-keys subcommand: ${sub}`,
251
- hint: "Run `run402 credentials project-keys --help` for usage.",
252
- details: { command: "credentials project-keys", subcommand: sub },
253
- });
248
+ failUnknownSubcommand("credentials project-keys", sub);
254
249
  }
255
250
  }
256
251
 
@@ -264,10 +259,5 @@ export async function run(sub, args = []) {
264
259
  await runProjectKeys(projectKeySub, rest);
265
260
  return;
266
261
  }
267
- fail({
268
- code: "UNKNOWN_SUBCOMMAND",
269
- message: `Unknown credentials subcommand: ${sub}`,
270
- hint: "Run `run402 credentials --help` for usage.",
271
- details: { command: "credentials", subcommand: sub },
272
- });
262
+ failUnknownSubcommand("credentials", sub);
273
263
  }
package/lib/deploy.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { fail } from "./sdk-errors.mjs";
1
+ import { failUnknownSubcommand } from "./argparse.mjs";
2
2
 
3
3
  const HELP = `run402 deploy — Unified deploy operations
4
4
 
@@ -65,11 +65,8 @@ export async function run(args) {
65
65
  return;
66
66
  }
67
67
  default:
68
- fail({
69
- code: "BAD_USAGE",
70
- message: `Unknown deploy subcommand: ${sub}`,
68
+ failUnknownSubcommand("deploy", sub, {
71
69
  hint: "Use `run402 deploy apply --manifest <file>` for deployments.",
72
- details: { subcommand: sub },
73
70
  });
74
71
  }
75
72
  }
package/lib/doctor.mjs CHANGED
@@ -63,6 +63,27 @@ function redactAllowanceForDiagnostics(allowance) {
63
63
  return safe;
64
64
  }
65
65
 
66
+ /**
67
+ * Compose a check-failure message context-first.
68
+ *
69
+ * The SDK kernel composes thrown messages as `<envelope message> while
70
+ * <context>`, which reads as two jammed fragments when the envelope message
71
+ * ends with a period ("…header. while checking tier status"). Strip the
72
+ * SDK's trailing ` while <context>` segment (using the error's own
73
+ * `context` field) and lead with the check label instead.
74
+ */
75
+ function describeCheckFailure(label, err) {
76
+ const raw = err instanceof Error ? err.message : String(err);
77
+ const context = typeof err?.context === "string" && err.context.length > 0 ? err.context : null;
78
+ let reason = raw;
79
+ if (context) {
80
+ const marker = ` while ${context}`;
81
+ const idx = raw.indexOf(marker);
82
+ if (idx !== -1) reason = (raw.slice(0, idx) + raw.slice(idx + marker.length)).trim();
83
+ }
84
+ return `${label} failed: ${reason}`;
85
+ }
86
+
66
87
  export async function run(sub, args = []) {
67
88
  const all = [sub, ...args].filter(Boolean);
68
89
  if (all.includes("--help") || all.includes("-h")) {
@@ -111,9 +132,11 @@ export async function run(sub, args = []) {
111
132
  }
112
133
 
113
134
  // 2. Allowance.
135
+ let allowanceConfigured = false;
114
136
  try {
115
137
  const allowance = readAllowance();
116
138
  if (allowance) {
139
+ allowanceConfigured = true;
117
140
  checks.push({
118
141
  name: "allowance",
119
142
  status: "ok",
@@ -152,8 +175,13 @@ export async function run(sub, args = []) {
152
175
  name: "projects",
153
176
  status: "ok",
154
177
  value: { project_count: projectCount },
178
+ // State-aware parenthetical: only claim the wallet is set up when the
179
+ // allowance check above actually passed; pre-init installs are pointed
180
+ // at `run402 init` first.
155
181
  ...(projectCount === 0 && {
156
- hint: "No projects yet — run 'run402 projects provision' to create one (wallet is already set up).",
182
+ hint: allowanceConfigured
183
+ ? "No projects yet — run 'run402 projects provision' to create one (wallet is already set up)."
184
+ : "No projects yet — run 'run402 init' to set up the wallet first, then 'run402 projects provision'.",
157
185
  }),
158
186
  });
159
187
  } catch (err) {
@@ -224,7 +252,7 @@ export async function run(sub, args = []) {
224
252
  checks.push({
225
253
  name: "tier",
226
254
  status: "error",
227
- message: err instanceof Error ? err.message : String(err),
255
+ message: describeCheckFailure("tier status check", err),
228
256
  });
229
257
  }
230
258
 
@@ -324,13 +352,13 @@ export async function run(sub, args = []) {
324
352
  checks.push({
325
353
  name: "operator_health",
326
354
  status: "skipped",
327
- message: err instanceof Error ? err.message : String(err),
355
+ message: describeCheckFailure("operator status check", err),
328
356
  ...(verbose && { hint: "GET /agent/v1/operator/status not reachable; requires v1.55+ gateway." }),
329
357
  });
330
358
  checks.push({
331
359
  name: "runtime_staleness",
332
360
  status: "skipped",
333
- message: err instanceof Error ? err.message : String(err),
361
+ message: describeCheckFailure("operator status check", err),
334
362
  });
335
363
  }
336
364