run402 4.8.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 (64) hide show
  1. package/cli.mjs +20 -8
  2. package/lib/admin.mjs +18 -25
  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 +69 -51
  7. package/lib/archives.mjs +2 -7
  8. package/lib/argparse.mjs +131 -3
  9. package/lib/asset-wire.mjs +59 -0
  10. package/lib/assets.mjs +14 -12
  11. package/lib/auth.mjs +2 -2
  12. package/lib/billing.mjs +17 -13
  13. package/lib/branches.mjs +22 -23
  14. package/lib/cache.mjs +2 -6
  15. package/lib/cdn.mjs +2 -2
  16. package/lib/ci.mjs +4 -1
  17. package/lib/cloud.mjs +27 -28
  18. package/lib/command-manifest.mjs +352 -0
  19. package/lib/contracts.mjs +49 -11
  20. package/lib/core.mjs +3 -13
  21. package/lib/credentials.mjs +3 -13
  22. package/lib/deploy-v2.mjs +21 -8
  23. package/lib/deploy.mjs +2 -5
  24. package/lib/doctor.mjs +32 -4
  25. package/lib/domains.mjs +2 -2
  26. package/lib/email.mjs +2 -2
  27. package/lib/functions.mjs +100 -55
  28. package/lib/grants.mjs +42 -22
  29. package/lib/image.mjs +2 -2
  30. package/lib/jobs.mjs +18 -11
  31. package/lib/message.mjs +2 -2
  32. package/lib/notifications.mjs +6 -12
  33. package/lib/operator.mjs +2 -7
  34. package/lib/org.mjs +88 -49
  35. package/lib/projects.mjs +68 -39
  36. package/lib/secrets.mjs +99 -41
  37. package/lib/service.mjs +3 -2
  38. package/lib/sites.mjs +2 -2
  39. package/lib/snapshots.mjs +22 -24
  40. package/lib/subdomains.mjs +2 -2
  41. package/lib/tier.mjs +2 -2
  42. package/lib/transfer.mjs +2 -1
  43. package/lib/up.mjs +12 -6
  44. package/lib/wallets.mjs +6 -9
  45. package/lib/webhook-secret.mjs +3 -8
  46. package/lib/webhooks.mjs +2 -2
  47. package/package.json +1 -1
  48. package/sdk/dist/actions.d.ts +20 -1
  49. package/sdk/dist/actions.d.ts.map +1 -1
  50. package/sdk/dist/actions.js.map +1 -1
  51. package/sdk/dist/namespaces/projects.d.ts +2 -1
  52. package/sdk/dist/namespaces/projects.d.ts.map +1 -1
  53. package/sdk/dist/namespaces/projects.js +8 -12
  54. package/sdk/dist/namespaces/projects.js.map +1 -1
  55. package/sdk/dist/node/actions-node.d.ts.map +1 -1
  56. package/sdk/dist/node/actions-node.js +114 -8
  57. package/sdk/dist/node/actions-node.js.map +1 -1
  58. package/sdk/dist/node/deploy-manifest.d.ts +36 -0
  59. package/sdk/dist/node/deploy-manifest.d.ts.map +1 -1
  60. package/sdk/dist/node/deploy-manifest.js +112 -3
  61. package/sdk/dist/node/deploy-manifest.js.map +1 -1
  62. package/sdk/dist/node/index.d.ts +1 -1
  63. package/sdk/dist/node/index.d.ts.map +1 -1
  64. package/sdk/dist/node/index.js.map +1 -1
package/cli.mjs CHANGED
@@ -79,15 +79,15 @@ Examples:
79
79
  run402 allowance fund
80
80
  run402 deploy apply --manifest app.json
81
81
  run402 apply --manifest app.json --rehearse --json
82
- run402 snapshots list prj_...
83
- run402 branches create prj_... --ttl-days 7 --json
84
- run402 cloud archives create prj_... --wait --output ./project.r402ar --json
82
+ run402 snapshots list --project prj_...
83
+ run402 branches create --project prj_... --ttl-days 7 --json
84
+ run402 cloud archives create --project prj_... --wait --output ./project.r402ar --json
85
85
  run402 core projects import ./project.r402ar --name imported-project --env-file ./required.env --json
86
86
  run402 jobs submit --file job.json
87
87
  run402 projects list
88
- run402 projects sql <project_id> "SELECT * FROM users LIMIT 5"
89
- run402 functions deploy <project_id> my-fn --file handler.ts
90
- run402 secrets set <project_id> API_KEY sk-1234
88
+ run402 projects sql "SELECT * FROM users LIMIT 5" --project <project_id>
89
+ run402 functions deploy my-fn --file handler.ts --project <project_id>
90
+ run402 secrets set API_KEY --value sk-1234 --project <project_id>
91
91
  run402 image generate "a startup mascot, pixel art" --output logo.png
92
92
 
93
93
  Getting started:
@@ -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 } 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
 
@@ -17,12 +17,12 @@ Subcommands:
17
17
  (\`reactivated: true\` in the response).
18
18
  Replaces the v1.56 per-project pin.
19
19
 
20
- archive <project_id> [--reason "..."] Moderate-archive a single project. Sets
20
+ archive [--project <id>] [--reason "..."] Moderate-archive a single project. Sets
21
21
  projects.archived_at = NOW(). Independent
22
22
  of organization lifecycle; the rest of the
23
23
  organization's projects keep serving.
24
24
 
25
- reactivate <project_id> Un-archive a project (flips archived_at
25
+ reactivate [--project <id>] Un-archive a project (flips archived_at
26
26
  back to NULL). In v1.57 this no longer
27
27
  touches organization-level lifecycle — to
28
28
  reactivate a grace-state account, use
@@ -65,6 +65,9 @@ Examples:
65
65
  archive: `run402 admin archive — Moderate-archive a single project
66
66
 
67
67
  Usage:
68
+ run402 admin archive [--project <id>] [--reason "..."]
69
+
70
+ Legacy (still supported):
68
71
  run402 admin archive <project_id> [--reason "..."]
69
72
 
70
73
  Options:
@@ -84,6 +87,9 @@ Examples:
84
87
  reactivate: `run402 admin reactivate — Un-archive a project
85
88
 
86
89
  Usage:
90
+ run402 admin reactivate [--project <id>]
91
+
92
+ Legacy (still supported):
87
93
  run402 admin reactivate <project_id>
88
94
 
89
95
  Notes:
@@ -100,8 +106,8 @@ Examples:
100
106
 
101
107
  const FLAGS_BY_SUB = {
102
108
  "lease-perpetual": { known: ["--enable", "--disable"], values: [] },
103
- archive: { known: ["--reason"], values: ["--reason"] },
104
- reactivate: { known: [], values: [] },
109
+ archive: { known: ["--project", "--reason"], values: ["--project", "--reason"] },
110
+ reactivate: { known: ["--project"], values: ["--project"] },
105
111
  };
106
112
 
107
113
  function validateFlags(sub, args) {
@@ -137,18 +143,12 @@ async function leasePerpetual(args) {
137
143
  }
138
144
 
139
145
  async function archive(args) {
140
- const projectId = args.find((a) => typeof a === "string" && !a.startsWith("--"));
141
- if (!projectId) {
142
- fail({
143
- code: "BAD_USAGE",
144
- message: "Missing <project_id>.",
145
- hint: "run402 admin archive <project_id> [--reason \"...\"]",
146
- });
147
- }
146
+ // Canonical: --project <id>; legacy leading prj_... positional kept.
147
+ const { projectId, rest } = resolveProjectSelector(args, { valueFlags: FLAGS_BY_SUB.archive.values });
148
148
  let reason;
149
- for (let i = 0; i < args.length; i++) {
150
- if (args[i] === "--reason" && args[i + 1] !== undefined) {
151
- reason = args[++i];
149
+ for (let i = 0; i < rest.length; i++) {
150
+ if (rest[i] === "--reason" && rest[i + 1] !== undefined) {
151
+ reason = rest[++i];
152
152
  }
153
153
  }
154
154
  try {
@@ -160,14 +160,7 @@ async function archive(args) {
160
160
  }
161
161
 
162
162
  async function reactivate(args) {
163
- const projectId = args.find((a) => typeof a === "string" && !a.startsWith("--"));
164
- if (!projectId) {
165
- fail({
166
- code: "BAD_USAGE",
167
- message: "Missing <project_id>.",
168
- hint: "run402 admin reactivate <project_id>",
169
- });
170
- }
163
+ const { projectId } = resolveProjectSelector(args, { valueFlags: FLAGS_BY_SUB.reactivate.values });
171
164
  try {
172
165
  const data = await getSdk().admin.reactivateProject(projectId);
173
166
  console.log(JSON.stringify(data, null, 2));
@@ -192,6 +185,6 @@ export async function run(sub, args) {
192
185
  case "archive": await archive(args); break;
193
186
  case "reactivate": await reactivate(args); break;
194
187
  default:
195
- 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);
196
189
  }
197
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 } 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
 
@@ -10,15 +10,20 @@ Usage:
10
10
 
11
11
  Subcommands:
12
12
  browse [--tag <tag>] Browse public apps
13
- fork <version_id> <name> [--subdomain <name>]
13
+ fork <version_id> --name <name> [--subdomain <name>]
14
14
  Fork a published app into your own project
15
- publish <id> [--description <desc>] [--tags <t1,t2>] [--visibility <v>] [--fork-allowed]
15
+ publish [--project <id>] [--description <desc>] [--tags <t1,t2>] [--visibility <v>] [--fork-allowed]
16
16
  Publish a project as an app
17
- versions <id> List published versions of a project
17
+ versions [--project <id>] List published versions of a project
18
18
  inspect <version_id> Inspect a published app version
19
- update <project_id> <version_id> [--description <desc>] [--tags <t1,t2>] [--visibility <v>] [--fork-allowed] [--no-fork]
19
+ update <version_id> [--project <id>] [--description <desc>] [--tags <t1,t2>] [--visibility <v>] [--fork-allowed] [--no-fork]
20
20
  Update a published version
21
- delete <project_id> <version_id> Delete a published version
21
+ delete <version_id> [--project <id>] Delete a published version
22
+
23
+ Legacy (still supported): the old positional forms keep working, e.g.
24
+ run402 apps fork <version_id> <name>
25
+ run402 apps publish <project_id> / versions <project_id>
26
+ run402 apps update <project_id> <version_id> / delete <project_id> <version_id>
22
27
 
23
28
  Examples:
24
29
  run402 apps browse
@@ -48,13 +53,16 @@ Examples:
48
53
  fork: `run402 apps fork — Fork a published app into your own project
49
54
 
50
55
  Usage:
56
+ run402 apps fork <version_id> --name <name> [options]
57
+
58
+ Legacy (still supported):
51
59
  run402 apps fork <version_id> <name> [options]
52
60
 
53
61
  Arguments:
54
62
  <version_id> Published version ID (e.g. ver_abc123)
55
- <name> Name for the forked project
56
63
 
57
64
  Options:
65
+ --name <name> Name for the forked project (alternative to the legacy positional)
58
66
  --subdomain <name> Claim a subdomain for the forked project
59
67
 
60
68
  Examples:
@@ -64,10 +72,13 @@ Examples:
64
72
  publish: `run402 apps publish — Publish a project as an app
65
73
 
66
74
  Usage:
67
- run402 apps publish <id> [options]
75
+ run402 apps publish [--project <id>] [options]
68
76
 
69
- Arguments:
70
- <id> Project ID to publish
77
+ Legacy (still supported):
78
+ run402 apps publish <project_id> [options]
79
+
80
+ Options (project):
81
+ --project <id> Project ID to publish (defaults to the active project)
71
82
 
72
83
  Options:
73
84
  --description <d> Human-readable description of the app
@@ -82,10 +93,12 @@ Examples:
82
93
  update: `run402 apps update — Update a published version's metadata
83
94
 
84
95
  Usage:
96
+ run402 apps update <version_id> [--project <id>] [options]
97
+
98
+ Legacy (still supported):
85
99
  run402 apps update <project_id> <version_id> [options]
86
100
 
87
101
  Arguments:
88
- <project_id> Project ID that owns the version
89
102
  <version_id> Published version ID to update
90
103
 
91
104
  Options:
@@ -114,10 +127,10 @@ Examples:
114
127
  versions: `run402 apps versions — List published versions of a project
115
128
 
116
129
  Usage:
117
- run402 apps versions <id>
130
+ run402 apps versions [--project <id>]
118
131
 
119
- Arguments:
120
- <id> Project ID (e.g. prj_abc123)
132
+ Legacy (still supported):
133
+ run402 apps versions <project_id>
121
134
 
122
135
  Examples:
123
136
  run402 apps versions prj_abc123
@@ -125,10 +138,12 @@ Examples:
125
138
  delete: `run402 apps delete — Delete a published version
126
139
 
127
140
  Usage:
141
+ run402 apps delete <version_id> [--project <id>]
142
+
143
+ Legacy (still supported):
128
144
  run402 apps delete <project_id> <version_id>
129
145
 
130
146
  Arguments:
131
- <project_id> Project ID that owns the version
132
147
  <version_id> Published version ID to delete
133
148
 
134
149
  Examples:
@@ -158,14 +173,16 @@ async function browse(args) {
158
173
 
159
174
  async function fork(versionId, name, args) {
160
175
  const parsedArgs = normalizeArgv([versionId, name, ...args].filter((arg) => arg !== undefined));
161
- const valueFlags = ["--subdomain"];
176
+ const valueFlags = ["--subdomain", "--name"];
162
177
  assertKnownFlags(parsedArgs, [...valueFlags, "--help", "-h"], valueFlags);
178
+ const nameFlag = flagValue(parsedArgs, "--name");
179
+ const expected = nameFlag ? 1 : 2;
163
180
  const positionals = positionalArgs(parsedArgs, valueFlags);
164
- if (positionals.length < 2) {
165
- fail({ code: "BAD_USAGE", message: "Missing <version_id> and/or <name>." });
181
+ if (positionals.length < expected) {
182
+ fail({ code: "BAD_USAGE", message: nameFlag ? "Missing <version_id>." : "Missing <version_id> and/or <name>." });
166
183
  }
167
- if (positionals.length > 2) {
168
- fail({ code: "BAD_USAGE", message: `Unexpected argument for apps fork: ${positionals[2]}` });
184
+ if (positionals.length > expected) {
185
+ fail({ code: "BAD_USAGE", message: `Unexpected argument for apps fork: ${positionals[expected]}` });
169
186
  }
170
187
  const opts = { subdomain: flagValue(parsedArgs, "--subdomain") ?? undefined };
171
188
  // Preserve the aggressive early exit when no allowance is configured.
@@ -174,7 +191,7 @@ async function fork(versionId, name, args) {
174
191
  try {
175
192
  const data = await getSdk().apps.fork({
176
193
  versionId: positionals[0],
177
- name: positionals[1],
194
+ name: nameFlag ?? positionals[1],
178
195
  subdomain: opts.subdomain,
179
196
  });
180
197
 
@@ -195,14 +212,12 @@ async function fork(versionId, name, args) {
195
212
 
196
213
  async function publish(projectId, args) {
197
214
  const parsedArgs = normalizeArgv([projectId, ...args].filter((arg) => arg !== undefined));
198
- const valueFlags = ["--description", "--tags", "--visibility"];
215
+ const valueFlags = ["--project", "--description", "--tags", "--visibility"];
199
216
  assertKnownFlags(parsedArgs, [...valueFlags, "--fork-allowed", "--help", "-h"], valueFlags);
200
- const positionals = positionalArgs(parsedArgs, valueFlags);
201
- if (positionals.length < 1) {
202
- fail({ code: "BAD_USAGE", message: "Missing <id>." });
203
- }
204
- if (positionals.length > 1) {
205
- fail({ code: "BAD_USAGE", message: `Unexpected argument for apps publish: ${positionals[1]}` });
217
+ const { projectId: resolvedProject, rest } = resolveProjectSelector(parsedArgs, { valueFlags });
218
+ const positionals = positionalArgs(rest, valueFlags);
219
+ if (positionals.length > 0) {
220
+ fail({ code: "BAD_USAGE", message: `Unexpected argument for apps publish: ${positionals[0]}` });
206
221
  }
207
222
  const opts = { description: undefined, tags: undefined, visibility: undefined, forkAllowed: undefined };
208
223
  opts.description = flagValue(parsedArgs, "--description") ?? undefined;
@@ -211,7 +226,7 @@ async function publish(projectId, args) {
211
226
  if (opts.visibility) assertAllowedValue(opts.visibility, ["public", "unlisted", "private"], "--visibility");
212
227
  if (parsedArgs.includes("--fork-allowed")) opts.forkAllowed = true;
213
228
  try {
214
- const data = await getSdk().apps.publish(positionals[0], {
229
+ const data = await getSdk().apps.publish(resolvedProject, {
215
230
  description: opts.description,
216
231
  tags: opts.tags,
217
232
  visibility: opts.visibility,
@@ -225,13 +240,14 @@ async function publish(projectId, args) {
225
240
 
226
241
  async function versions(projectId, args = []) {
227
242
  const parsedArgs = normalizeArgv([projectId, ...args].filter((arg) => arg !== undefined));
228
- assertKnownFlags(parsedArgs, ["--help", "-h"]);
229
- const positionals = positionalArgs(parsedArgs);
230
- if (positionals.length !== 1) {
231
- fail({ code: "BAD_USAGE", message: positionals.length === 0 ? "Missing <id>." : `Unexpected argument for apps versions: ${positionals[1]}` });
243
+ assertKnownFlags(parsedArgs, ["--project", "--help", "-h"], ["--project"]);
244
+ const { projectId: resolvedProject, rest } = resolveProjectSelector(parsedArgs, { valueFlags: ["--project"] });
245
+ const positionals = positionalArgs(rest, ["--project"]);
246
+ if (positionals.length > 0) {
247
+ fail({ code: "BAD_USAGE", message: `Unexpected argument for apps versions: ${positionals[0]}` });
232
248
  }
233
249
  try {
234
- const data = await getSdk().apps.listVersions(positionals[0]);
250
+ const data = await getSdk().apps.listVersions(resolvedProject);
235
251
  console.log(JSON.stringify(data, null, 2));
236
252
  } catch (err) {
237
253
  reportSdkError(err);
@@ -255,14 +271,15 @@ async function inspect(versionId, args = []) {
255
271
 
256
272
  async function update(projectId, versionId, args) {
257
273
  const parsedArgs = normalizeArgv([projectId, versionId, ...args].filter((arg) => arg !== undefined));
258
- const valueFlags = ["--description", "--tags", "--visibility"];
274
+ const valueFlags = ["--project", "--description", "--tags", "--visibility"];
259
275
  assertKnownFlags(parsedArgs, [...valueFlags, "--fork-allowed", "--no-fork", "--help", "-h"], valueFlags);
260
- const positionals = positionalArgs(parsedArgs, valueFlags);
261
- if (positionals.length < 2) {
262
- fail({ code: "BAD_USAGE", message: "Missing <project_id> and/or <version_id>." });
276
+ const { projectId: resolvedProject, rest } = resolveProjectSelector(parsedArgs, { valueFlags });
277
+ const positionals = positionalArgs(rest, valueFlags);
278
+ if (positionals.length < 1) {
279
+ fail({ code: "BAD_USAGE", message: "Missing <version_id>." });
263
280
  }
264
- if (positionals.length > 2) {
265
- fail({ code: "BAD_USAGE", message: `Unexpected argument for apps update: ${positionals[2]}` });
281
+ if (positionals.length > 1) {
282
+ fail({ code: "BAD_USAGE", message: `Unexpected argument for apps update: ${positionals[1]}` });
266
283
  }
267
284
  if (parsedArgs.includes("--fork-allowed") && parsedArgs.includes("--no-fork")) {
268
285
  fail({ code: "BAD_USAGE", message: "Provide either --fork-allowed or --no-fork, not both." });
@@ -275,8 +292,8 @@ async function update(projectId, versionId, args) {
275
292
  if (parsedArgs.includes("--fork-allowed")) opts.fork_allowed = true;
276
293
  if (parsedArgs.includes("--no-fork")) opts.fork_allowed = false;
277
294
  try {
278
- await getSdk().apps.updateVersion(positionals[0], positionals[1], opts);
279
- console.log(JSON.stringify({ project_id: positionals[0], version_id: positionals[1], updated: true }));
295
+ await getSdk().apps.updateVersion(resolvedProject, positionals[0], opts);
296
+ console.log(JSON.stringify({ project_id: resolvedProject, version_id: positionals[0], updated: true }));
280
297
  } catch (err) {
281
298
  reportSdkError(err);
282
299
  }
@@ -284,17 +301,18 @@ async function update(projectId, versionId, args) {
284
301
 
285
302
  async function deleteVersion(projectId, versionId, args = []) {
286
303
  const parsedArgs = normalizeArgv([projectId, versionId, ...args].filter((arg) => arg !== undefined));
287
- assertKnownFlags(parsedArgs, ["--help", "-h"]);
288
- const positionals = positionalArgs(parsedArgs);
289
- if (positionals.length < 2) {
290
- fail({ code: "BAD_USAGE", message: "Missing <project_id> and/or <version_id>." });
304
+ assertKnownFlags(parsedArgs, ["--project", "--help", "-h"], ["--project"]);
305
+ const { projectId: resolvedProject, rest } = resolveProjectSelector(parsedArgs, { valueFlags: ["--project"] });
306
+ const positionals = positionalArgs(rest, ["--project"]);
307
+ if (positionals.length < 1) {
308
+ fail({ code: "BAD_USAGE", message: "Missing <version_id>." });
291
309
  }
292
- if (positionals.length > 2) {
293
- fail({ code: "BAD_USAGE", message: `Unexpected argument for apps delete: ${positionals[2]}` });
310
+ if (positionals.length > 1) {
311
+ fail({ code: "BAD_USAGE", message: `Unexpected argument for apps delete: ${positionals[1]}` });
294
312
  }
295
313
  try {
296
- await getSdk().apps.deleteVersion(positionals[0], positionals[1]);
297
- console.log(JSON.stringify({ project_id: positionals[0], version_id: positionals[1], deleted: true }));
314
+ await getSdk().apps.deleteVersion(resolvedProject, positionals[0]);
315
+ console.log(JSON.stringify({ project_id: resolvedProject, version_id: positionals[0], deleted: true }));
298
316
  } catch (err) {
299
317
  reportSdkError(err);
300
318
  }
@@ -312,6 +330,6 @@ export async function run(sub, args) {
312
330
  case "update": await update(args[0], args[1], args.slice(2)); break;
313
331
  case "delete": await deleteVersion(args[0], args[1], args.slice(2)); break;
314
332
  default:
315
- 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);
316
334
  }
317
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 = [];
@@ -19,8 +20,14 @@ export function hasHelp(args = []) {
19
20
  return args.includes("--help") || args.includes("-h");
20
21
  }
21
22
 
23
+ // CLI-wide convention: every command accepts `--json`. Where stdout is already
24
+ // JSON (the vast majority of commands) it is a no-op; commands with a human
25
+ // default switch on it explicitly. Baking it into the baseline known set here
26
+ // makes the convention self-maintaining for future commands.
27
+ const ALWAYS_KNOWN_FLAGS = ["--json"];
28
+
22
29
  export function assertKnownFlags(args = [], knownFlags = [], flagsWithValues = []) {
23
- const known = new Set(knownFlags);
30
+ const known = new Set([...knownFlags, ...ALWAYS_KNOWN_FLAGS]);
24
31
  const valueFlags = new Set(flagsWithValues);
25
32
  for (let i = 0; i < args.length; i++) {
26
33
  const arg = args[i];
@@ -301,11 +308,78 @@ export function resolvePositionalProject(args, opts = {}) {
301
308
  return { projectId: resolveProjectId(null), rest: Array.isArray(args) ? args : [] };
302
309
  }
303
310
 
304
- function closestFlag(flag, candidates) {
311
+ // Resolve the project id for a project-scoped command from BOTH accepted
312
+ // forms — the canonical `--project <id>` flag and the legacy leading
313
+ // positional `prj_...` — with active-project fallback (CLI-wide convention).
314
+ //
315
+ // Precedence:
316
+ // 1. explicit `--project <id>` (canonical)
317
+ // 2. a leading positional starting with "prj_" (legacy compat, no warning)
318
+ // 3. the active project (`resolveProjectId(null)`)
319
+ //
320
+ // If BOTH a `--project` flag and a leading `prj_` positional are present and
321
+ // they DIFFER, fail with BAD_USAGE ("Conflicting project ids"). If they agree,
322
+ // the duplicate positional is consumed silently.
323
+ //
324
+ // Returns { projectId, rest } where `rest` is args with the project selector
325
+ // tokens (the flag+value pair and/or the leading positional) removed.
326
+ //
327
+ // Options (in addition to every `resolvePositionalProject` option, which this
328
+ // helper delegates to for the no-flag path):
329
+ // requireRestPositional: only treat a leading `prj_` positional as the
330
+ // project selector when at least one more bare positional follows (counted
331
+ // with opts.valueFlags). Needed by commands whose OWN attribute is itself a
332
+ // project id (e.g. `branches renew <branch-project-id>`).
333
+ export function resolveProjectSelector(args, opts = {}) {
334
+ const list = Array.isArray(args) ? [...args] : [];
335
+ let flagProject = null;
336
+ const flagIdx = list.indexOf("--project");
337
+ if (flagIdx !== -1) {
338
+ flagProject = flagValue(list, "--project");
339
+ list.splice(flagIdx, 2);
340
+ }
341
+
342
+ const first = list[0];
343
+ let leadingIsProject = typeof first === "string" && first.startsWith("prj_");
344
+ if (leadingIsProject && opts.requireRestPositional) {
345
+ const remainder = positionalArgs(list.slice(1), opts.valueFlags ?? []);
346
+ if (remainder.length === 0) leadingIsProject = false;
347
+ }
348
+
349
+ if (flagProject !== null) {
350
+ if (leadingIsProject) {
351
+ if (first !== flagProject) {
352
+ fail({
353
+ code: "BAD_USAGE",
354
+ message: `Conflicting project ids: --project ${flagProject} vs positional ${first}`,
355
+ hint: "Pass the project once — prefer --project <id>.",
356
+ details: { project_flag: flagProject, positional: first },
357
+ });
358
+ }
359
+ list.shift();
360
+ }
361
+ return { projectId: flagProject, rest: list };
362
+ }
363
+
364
+ if (!leadingIsProject && typeof first === "string" && first.startsWith("prj_")) {
365
+ // A lone prj_ positional deliberately NOT consumed as the selector
366
+ // (requireRestPositional): it is the command's own attribute.
367
+ return { projectId: resolveProjectId(null), rest: list };
368
+ }
369
+ return resolvePositionalProject(list, opts);
370
+ }
371
+
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) {
305
379
  let best = null;
306
380
  let bestDistance = Number.POSITIVE_INFINITY;
307
381
  for (const candidate of candidates) {
308
- const d = levenshtein(flag, candidate);
382
+ const d = levenshtein(word, candidate);
309
383
  if (d < bestDistance) {
310
384
  best = candidate;
311
385
  bestDistance = d;
@@ -315,6 +389,60 @@ function closestFlag(flag, candidates) {
315
389
  return bestDistance <= 3 ? best : null;
316
390
  }
317
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
+
318
446
  function levenshtein(a, b) {
319
447
  const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
320
448
  const curr = Array.from({ length: b.length + 1 }, () => 0);