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
@@ -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
 
@@ -54,7 +55,7 @@ Options:
54
55
  --exif-policy keep|strip v1.50: EXIF retention policy for image uploads (default keep).
55
56
  --stream NDJSON per-file progress events (for agent consumption).
56
57
  Without --stream, only the final results array is
57
- printed. --json is a deprecated alias for --stream.
58
+ printed. --json is an accepted alias for --stream.
58
59
  --prefix <p> Prefix filter (ls only)
59
60
  --limit <n> Max results (ls only; default 100, max 1000)
60
61
  --sort <key> v1.50 ls only: key:asc | createdAt:asc | createdAt:desc (default key:asc).
@@ -103,7 +104,7 @@ Options:
103
104
  from the stored bytes and the image_exif response field.
104
105
  --stream Emit NDJSON per-file progress events on stdout (for
105
106
  agent consumption). Default: emit only the final
106
- results array (also JSON). --json is a deprecated
107
+ results array (also JSON). --json is an accepted
107
108
  alias for --stream.
108
109
 
109
110
  Examples:
@@ -256,12 +257,10 @@ function parseArgs(rawArgs) {
256
257
  else if (a === "--concurrency") out.concurrency = parseIntegerFlag("--concurrency", args[++i], { min: 1 });
257
258
  else if (a === "--no-resume") out.resume = false;
258
259
  else if (a === "--stream") out.stream = true;
259
- else if (a === "--json") {
260
- out.stream = true;
261
- process.stderr.write(
262
- "# warning: `--json` on `assets put` is deprecated and will be removed in a future release. Use `--stream` (alias means the same thing — NDJSON progress events on stdout).\n",
263
- );
264
- }
260
+ // `--json` and `--stream` are permanent aliases on `assets put` (both mean
261
+ // NDJSON progress events on stdout); elsewhere `--json` is a no-op since
262
+ // stdout is already JSON.
263
+ else if (a === "--json") { out.stream = true; }
265
264
  else if (a === "--prefix") out.prefix = args[++i];
266
265
  else if (a === "--limit") out.limit = parseIntegerFlag("--limit", args[++i], { min: 1, max: 1000 });
267
266
  else if (a === "--output" || a === "-o") out.output = args[++i];
@@ -460,8 +459,11 @@ async function putOne(projectId, filePath, opts) {
460
459
  ...(opts.metadata ? { metadata: opts.metadata } : {}),
461
460
  ...(opts.exifPolicy ? { exifPolicy: opts.exifPolicy } : {}),
462
461
  });
463
- log(opts, { event: "done", ...result });
464
- 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;
465
467
  }
466
468
 
467
469
  function computeDestKey(filePath, keyOpt) {
@@ -683,6 +685,6 @@ export async function run(sub, args) {
683
685
  case "sign": await sign(defaultProject, args); break;
684
686
  case "diagnose": await diagnose(defaultProject, args); break;
685
687
  default:
686
- 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);
687
689
  }
688
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
 
@@ -232,18 +232,20 @@ async function createEmail(args) {
232
232
 
233
233
  async function linkWallet(args) {
234
234
  const parsedArgs = normalizeArgv(args);
235
- assertKnownFlags(parsedArgs, ["--help", "-h"]);
236
- const positionals = positionalArgs(parsedArgs);
235
+ assertKnownFlags(parsedArgs, ["--wallet", "--help", "-h"], ["--wallet"]);
236
+ const walletFlag = flagValue(parsedArgs, "--wallet");
237
+ const positionals = positionalArgs(parsedArgs, ["--wallet"]);
237
238
  const organizationId = positionals[0];
238
- const wallet = positionals[1];
239
- if (positionals.length > 2) {
240
- fail({ code: "BAD_USAGE", message: `Unexpected argument for billing link-wallet: ${positionals[2]}` });
239
+ const wallet = walletFlag ?? positionals[1];
240
+ const max = walletFlag ? 1 : 2;
241
+ if (positionals.length > max) {
242
+ fail({ code: "BAD_USAGE", message: `Unexpected argument for billing link-wallet: ${positionals[max]}` });
241
243
  }
242
244
  if (!organizationId || !wallet) {
243
245
  fail({
244
246
  code: "BAD_USAGE",
245
247
  message: "Missing <org_id> and/or <wallet_address>.",
246
- hint: "run402 billing link-wallet <org_id> <wallet_address>",
248
+ hint: "run402 billing link-wallet <org_id> --wallet <wallet_address>",
247
249
  });
248
250
  }
249
251
  try {
@@ -262,19 +264,21 @@ async function linkWallet(args) {
262
264
 
263
265
  async function autoRecharge(args) {
264
266
  const parsedArgs = normalizeArgv(args);
265
- const valueFlags = ["--threshold"];
267
+ const valueFlags = ["--threshold", "--state"];
266
268
  assertKnownFlags(parsedArgs, [...valueFlags, "--help", "-h"], valueFlags);
269
+ const stateFlag = flagValue(parsedArgs, "--state");
267
270
  const positionals = positionalArgs(parsedArgs, valueFlags);
268
271
  const organizationId = positionals[0];
269
- const state = positionals[1];
270
- if (positionals.length > 2) {
271
- fail({ code: "BAD_USAGE", message: `Unexpected argument for billing auto-recharge: ${positionals[2]}` });
272
+ const state = stateFlag ?? positionals[1];
273
+ const max = stateFlag ? 1 : 2;
274
+ if (positionals.length > max) {
275
+ fail({ code: "BAD_USAGE", message: `Unexpected argument for billing auto-recharge: ${positionals[max]}` });
272
276
  }
273
277
  if (!organizationId || !state || !["on", "off"].includes(state)) {
274
278
  fail({
275
279
  code: "BAD_USAGE",
276
280
  message: "Missing <org_id> and/or <on|off>.",
277
- hint: "run402 billing auto-recharge <org_id> <on|off> [--threshold <n>]",
281
+ hint: "run402 billing auto-recharge <org_id> --state <on|off> [--threshold <n>]",
278
282
  });
279
283
  }
280
284
  const thresholdStr = flagValue(parsedArgs, "--threshold");
@@ -355,6 +359,6 @@ export async function run(sub, args) {
355
359
  case "balance": await balance(args); break;
356
360
  case "history": await history(args); break;
357
361
  default:
358
- 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);
359
363
  }
360
364
  }
package/lib/branches.mjs CHANGED
@@ -8,19 +8,24 @@ import {
8
8
  normalizeArgv,
9
9
  parseIntegerFlag,
10
10
  positionalArgs,
11
+ resolveProjectSelector,
12
+ failUnknownSubcommand,
11
13
  } from "./argparse.mjs";
12
- import { resolveProjectId } from "./config.mjs";
13
14
 
14
15
  const HELP = `run402 branches — Contained project data branches
15
16
 
16
17
  Usage:
17
- run402 branches create [project-id] [--from-snapshot <snapshot-id>] [--name <label>] [--email-mode sandbox|off] [--enable-cron] [--ttl-days <n>] [--json]
18
- run402 branches list [project-id] [--json]
19
- run402 branches renew [project-id] <branch-project-id> [--ttl-days <n>] [--json]
20
- run402 branches delete [project-id] <branch-project-id> [--json]
18
+ run402 branches create [--project <id>] [--from-snapshot <snapshot-id>] [--name <label>] [--email-mode sandbox|off] [--enable-cron] [--ttl-days <n>] [--json]
19
+ run402 branches list [--project <id>] [--json]
20
+ run402 branches renew <branch-project-id> [--project <id>] [--ttl-days <n>] [--json]
21
+ run402 branches delete <branch-project-id> [--project <id>] [--json]
22
+
23
+ Legacy (still supported): a leading prj_... parent-project positional,
24
+ e.g. run402 branches renew prj_parent prj_branch. --project defaults to the
25
+ active project.
21
26
  `;
22
27
 
23
- const FLAG_VALUES = ["--from-snapshot", "--name", "--email-mode", "--ttl-days"];
28
+ const FLAG_VALUES = ["--project", "--from-snapshot", "--name", "--email-mode", "--ttl-days"];
24
29
  const FLAGS = new Set([...FLAG_VALUES, "--enable-cron", "--json", "--help", "-h"]);
25
30
 
26
31
  export async function run(sub, args = []) {
@@ -37,17 +42,12 @@ export async function run(sub, args = []) {
37
42
  case "renew": return renew(rest);
38
43
  case "delete": return deleteBranch(rest);
39
44
  default:
40
- fail({
41
- code: "UNKNOWN_SUBCOMMAND",
42
- message: `Unknown branches subcommand: ${sub}`,
43
- hint: "Run `run402 branches --help` for usage.",
44
- details: { command: "branches", subcommand: sub },
45
- });
45
+ failUnknownSubcommand("branches", sub);
46
46
  }
47
47
  }
48
48
 
49
49
  async function create(args) {
50
- const projectId = resolveOptionalProject(positionalArgs(args, FLAG_VALUES)[0]);
50
+ const { projectId } = resolveProjectSelector(args, { valueFlags: FLAG_VALUES });
51
51
  const emailMode = flagValue(args, "--email-mode") ?? undefined;
52
52
  if (emailMode !== undefined) assertAllowedValue(emailMode, ["sandbox", "off"], "--email-mode");
53
53
  const ttlDaysFlag = flagValue(args, "--ttl-days");
@@ -67,7 +67,7 @@ async function create(args) {
67
67
  }
68
68
 
69
69
  async function list(args) {
70
- const projectId = resolveOptionalProject(positionalArgs(args, FLAG_VALUES)[0]);
70
+ const { projectId } = resolveProjectSelector(args, { valueFlags: FLAG_VALUES });
71
71
  try {
72
72
  const result = await getSdk().branches.list(projectId);
73
73
  console.log(JSON.stringify({ project_id: projectId, ...result }, null, 2));
@@ -100,14 +100,13 @@ async function deleteBranch(args) {
100
100
  }
101
101
  }
102
102
 
103
- function resolveOptionalProject(value) {
104
- if (value && String(value).startsWith("prj_")) return value;
105
- return resolveProjectId(null);
106
- }
107
-
108
103
  function resolveProjectAndBranch(args, usage) {
109
- const pos = positionalArgs(args, FLAG_VALUES);
110
- if (pos.length === 1) return { projectId: resolveProjectId(null), branchProjectId: pos[0] };
111
- if (pos.length === 2 && pos[0].startsWith("prj_")) return { projectId: pos[0], branchProjectId: pos[1] };
112
- fail({ code: "BAD_USAGE", message: `Usage: ${usage}` });
104
+ // Canonical: `<branch-project-id> [--project <parent-id>]`. Branch ids are
105
+ // themselves prj_..., so a leading prj_ positional is only treated as the
106
+ // PARENT project when a second positional follows (requireRestPositional)
107
+ // the legacy `renew prj_parent prj_branch` form.
108
+ const { projectId, rest } = resolveProjectSelector(args, { valueFlags: FLAG_VALUES, requireRestPositional: true });
109
+ const pos = positionalArgs(rest, FLAG_VALUES);
110
+ if (pos.length !== 1) fail({ code: "BAD_USAGE", message: `Usage: ${usage}` });
111
+ return { projectId, branchProjectId: pos[0] };
113
112
  }
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";
@@ -96,6 +97,8 @@ function parseFlags(args, allowed, { repeatable = new Set() } = {}) {
96
97
  positional.push(arg);
97
98
  continue;
98
99
  }
100
+ // CLI-wide convention: --json is accepted everywhere (stdout is already JSON).
101
+ if (arg === "--json") continue;
99
102
  if (!allowed.has(arg)) {
100
103
  fail({
101
104
  code: "BAD_USAGE",
@@ -457,6 +460,6 @@ export async function run(sub, args) {
457
460
  case "revoke": await revoke(args); break;
458
461
  case "set-asset-scopes": await setAssetScopes(args); break;
459
462
  default:
460
- 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);
461
464
  }
462
465
  }
package/lib/cloud.mjs CHANGED
@@ -3,14 +3,18 @@ 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 } 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
 
10
10
  Usage:
11
- run402 cloud archives create <project-id> [options]
12
- run402 cloud archives download <project-id> <archive-id> --output <file> [--json]
13
- run402 cloud archives status <project-id> <archive-id> [--json]
11
+ run402 cloud archives create [--project <id>] [options]
12
+ run402 cloud archives download <archive-id> --output <file> [--project <id>] [--json]
13
+ run402 cloud archives status <archive-id> [--project <id>] [--json]
14
+
15
+ Legacy (still supported): a leading prj_... positional selects the project,
16
+ e.g. run402 cloud archives download <project-id> <archive-id> --output <file>.
17
+ --project defaults to the active project.
14
18
 
15
19
  Canonical agent path:
16
20
  run402 cloud archives create prj_... \\
@@ -31,6 +35,7 @@ Options for create:
31
35
  `;
32
36
 
33
37
  const FLAG_VALUES = [
38
+ "--project",
34
39
  "--scope",
35
40
  "--auth",
36
41
  "--consistency",
@@ -55,11 +60,7 @@ export async function run(sub, args = []) {
55
60
  return;
56
61
  }
57
62
  if (sub !== "archives") {
58
- fail({
59
- code: "UNKNOWN_SUBCOMMAND",
60
- message: `Unknown cloud subcommand: ${sub}`,
61
- hint: "Run `run402 cloud --help` for usage.",
62
- details: { command: "cloud", subcommand: sub },
63
+ failUnknownSubcommand("cloud", sub, {
63
64
  next_actions: [{ type: "run_command", command: "run402 cloud archives --help" }],
64
65
  });
65
66
  }
@@ -68,11 +69,7 @@ export async function run(sub, args = []) {
68
69
  if (action === "create") return create(rest);
69
70
  if (action === "download") return download(rest);
70
71
  if (action === "status") return status(rest);
71
- fail({
72
- code: "UNKNOWN_SUBCOMMAND",
73
- message: `Unknown cloud archives subcommand: ${action}`,
74
- hint: "Run `run402 cloud archives --help` for usage.",
75
- details: { command: "cloud archives", subcommand: action },
72
+ failUnknownSubcommand("cloud archives", action, {
76
73
  next_actions: [{ type: "run_command", command: "run402 cloud archives --help" }],
77
74
  });
78
75
  }
@@ -80,10 +77,10 @@ export async function run(sub, args = []) {
80
77
  async function create(rawArgs) {
81
78
  const args = normalizeArgv(rawArgs);
82
79
  assertKnownFlags(args, [...FLAGS], FLAG_VALUES);
83
- const pos = positionalArgs(args, FLAG_VALUES);
84
- const projectId = pos[0];
85
- if (!projectId) {
86
- fail({ code: "BAD_PROJECT_ID", message: "Missing project id." });
80
+ const { projectId, rest } = resolveProjectSelector(args, { valueFlags: FLAG_VALUES });
81
+ const extraPos = positionalArgs(rest, FLAG_VALUES);
82
+ if (extraPos.length > 0) {
83
+ fail({ code: "BAD_USAGE", message: `Unexpected argument for cloud archives create: ${extraPos[0]}` });
87
84
  }
88
85
  const scope = flagValue(args, "--scope") ?? "portable-runtime-v1";
89
86
  const auth = flagValue(args, "--auth") ?? "stubs";
@@ -170,12 +167,13 @@ async function create(rawArgs) {
170
167
 
171
168
  async function download(rawArgs) {
172
169
  const args = normalizeArgv(rawArgs);
173
- assertKnownFlags(args, ["--output", "--json", "--help", "-h"], ["--output"]);
174
- const pos = positionalArgs(args, ["--output"]);
175
- const [projectId, archiveId] = pos;
170
+ assertKnownFlags(args, ["--project", "--output", "--json", "--help", "-h"], ["--project", "--output"]);
171
+ const { projectId, rest } = resolveProjectSelector(args, { valueFlags: ["--project", "--output"] });
172
+ const pos = positionalArgs(rest, ["--project", "--output"]);
173
+ const [archiveId] = pos;
176
174
  const output = flagValue(args, "--output");
177
- if (!projectId || !archiveId || !output) {
178
- fail({ code: "BAD_USAGE", message: "Usage: run402 cloud archives download <project-id> <archive-id> --output <file> [--json]" });
175
+ if (!archiveId || pos.length > 1 || !output) {
176
+ fail({ code: "BAD_USAGE", message: "Usage: run402 cloud archives download <archive-id> --output <file> [--project <id>] [--json]" });
179
177
  }
180
178
  try {
181
179
  const download = await getSdk().archives.download(projectId, archiveId);
@@ -200,11 +198,12 @@ async function download(rawArgs) {
200
198
 
201
199
  async function status(rawArgs) {
202
200
  const args = normalizeArgv(rawArgs);
203
- assertKnownFlags(args, ["--json", "--help", "-h"], []);
204
- const pos = positionalArgs(args, []);
205
- const [projectId, archiveId] = pos;
206
- if (!projectId || !archiveId) {
207
- fail({ code: "BAD_USAGE", message: "Usage: run402 cloud archives status <project-id> <archive-id> [--json]" });
201
+ assertKnownFlags(args, ["--project", "--json", "--help", "-h"], ["--project"]);
202
+ const { projectId, rest } = resolveProjectSelector(args, { valueFlags: ["--project"] });
203
+ const pos = positionalArgs(rest, ["--project"]);
204
+ const [archiveId] = pos;
205
+ if (!archiveId || pos.length > 1) {
206
+ fail({ code: "BAD_USAGE", message: "Usage: run402 cloud archives status <archive-id> [--project <id>] [--json]" });
208
207
  }
209
208
  try {
210
209
  const archive = await getSdk().archives.get(projectId, archiveId);