run402 3.5.2 → 3.5.4

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 (55) hide show
  1. package/cli.mjs +20 -0
  2. package/lib/archives.mjs +55 -0
  3. package/lib/cache.mjs +10 -9
  4. package/lib/cloud.mjs +254 -0
  5. package/lib/config.mjs +6 -0
  6. package/lib/core.mjs +122 -0
  7. package/lib/deploy-v2.mjs +65 -60
  8. package/lib/doctor-source-scan.mjs +1 -1
  9. package/lib/functions.mjs +2 -1
  10. package/lib/init.mjs +8 -6
  11. package/lib/next-actions.mjs +61 -0
  12. package/lib/projects.mjs +16 -3
  13. package/lib/secrets.mjs +3 -2
  14. package/lib/subdomains.mjs +2 -1
  15. package/lib/tier.mjs +16 -5
  16. package/package.json +1 -1
  17. package/sdk/dist/credentials.d.ts +1 -1
  18. package/sdk/dist/credentials.d.ts.map +1 -1
  19. package/sdk/dist/errors.d.ts +25 -2
  20. package/sdk/dist/errors.d.ts.map +1 -1
  21. package/sdk/dist/errors.js +31 -2
  22. package/sdk/dist/errors.js.map +1 -1
  23. package/sdk/dist/index.d.ts +5 -1
  24. package/sdk/dist/index.d.ts.map +1 -1
  25. package/sdk/dist/index.js +4 -0
  26. package/sdk/dist/index.js.map +1 -1
  27. package/sdk/dist/namespaces/archives.d.ts +12 -0
  28. package/sdk/dist/namespaces/archives.d.ts.map +1 -0
  29. package/sdk/dist/namespaces/archives.js +202 -0
  30. package/sdk/dist/namespaces/archives.js.map +1 -0
  31. package/sdk/dist/namespaces/archives.types.d.ts +163 -0
  32. package/sdk/dist/namespaces/archives.types.d.ts.map +1 -0
  33. package/sdk/dist/namespaces/archives.types.js +2 -0
  34. package/sdk/dist/namespaces/archives.types.js.map +1 -0
  35. package/sdk/dist/namespaces/projects.d.ts.map +1 -1
  36. package/sdk/dist/namespaces/projects.js +3 -0
  37. package/sdk/dist/namespaces/projects.js.map +1 -1
  38. package/sdk/dist/namespaces/projects.types.d.ts +9 -0
  39. package/sdk/dist/namespaces/projects.types.d.ts.map +1 -1
  40. package/sdk/dist/namespaces/tier.d.ts +12 -1
  41. package/sdk/dist/namespaces/tier.d.ts.map +1 -1
  42. package/sdk/dist/namespaces/tier.js +4 -1
  43. package/sdk/dist/namespaces/tier.js.map +1 -1
  44. package/sdk/dist/node/archives-node.d.ts +14 -0
  45. package/sdk/dist/node/archives-node.d.ts.map +1 -0
  46. package/sdk/dist/node/archives-node.js +715 -0
  47. package/sdk/dist/node/archives-node.js.map +1 -0
  48. package/sdk/dist/node/index.d.ts +4 -1
  49. package/sdk/dist/node/index.d.ts.map +1 -1
  50. package/sdk/dist/node/index.js +3 -0
  51. package/sdk/dist/node/index.js.map +1 -1
  52. package/sdk/dist/scoped.d.ts +12 -0
  53. package/sdk/dist/scoped.d.ts.map +1 -1
  54. package/sdk/dist/scoped.js +25 -0
  55. package/sdk/dist/scoped.js.map +1 -1
package/cli.mjs CHANGED
@@ -27,6 +27,9 @@ Commands:
27
27
  tier Manage tier subscription (status, set)
28
28
  projects Manage projects (provision, list, query, inspect, delete)
29
29
  admin Platform-admin operations (lease-perpetual, archive, reactivate)
30
+ cloud Cloud portability archive export (archives create/download/status)
31
+ archives Inspect and verify portable project archives locally
32
+ core Local Run402 Core import helpers
30
33
  deploy Unified deploy operations (requires active tier)
31
34
  ci Link GitHub Actions OIDC deploy bindings
32
35
  transfer Two-party project transfer (init, preview, list, accept, cancel)
@@ -67,6 +70,8 @@ Examples:
67
70
  run402 allowance create
68
71
  run402 allowance fund
69
72
  run402 deploy apply --manifest app.json
73
+ run402 cloud archives create prj_... --wait --output ./project.r402ar --json
74
+ run402 core projects import ./project.r402ar --name imported-project --env-file ./required.env --json
70
75
  run402 jobs submit --file job.json
71
76
  run402 projects list
72
77
  run402 projects sql <project_id> "SELECT * FROM users LIMIT 5"
@@ -162,6 +167,21 @@ switch (cmd) {
162
167
  await run(sub, rest);
163
168
  break;
164
169
  }
170
+ case "cloud": {
171
+ const { run } = await import("./lib/cloud.mjs");
172
+ await run(sub, rest);
173
+ break;
174
+ }
175
+ case "archives": {
176
+ const { run } = await import("./lib/archives.mjs");
177
+ await run(sub, rest);
178
+ break;
179
+ }
180
+ case "core": {
181
+ const { run } = await import("./lib/core.mjs");
182
+ await run(sub, rest);
183
+ break;
184
+ }
165
185
  case "deploy": {
166
186
  const { run } = await import("./lib/deploy.mjs");
167
187
  await run([sub, ...rest].filter(Boolean));
@@ -0,0 +1,55 @@
1
+ import { inspectArchive, verifyArchive } from "#sdk/node";
2
+ import { reportSdkError, fail } from "./sdk-errors.mjs";
3
+ import { assertKnownFlags, hasHelp, normalizeArgv, positionalArgs } from "./argparse.mjs";
4
+
5
+ const HELP = `run402 archives — Inspect and verify portable Run402 project archives
6
+
7
+ Usage:
8
+ run402 archives inspect <archive-path> [--json]
9
+ run402 archives verify <archive-path> [--json]
10
+
11
+ Notes:
12
+ - Verification is local and offline. It does not require Cloud credentials.
13
+ - Archives are untrusted input; verify checks integrity and compatibility, not trust.
14
+ `;
15
+
16
+ export async function run(sub, rawArgs = []) {
17
+ const all = [sub, ...rawArgs].filter(Boolean);
18
+ if (hasHelp(all)) {
19
+ console.log(HELP);
20
+ return;
21
+ }
22
+ switch (sub) {
23
+ case "inspect": return inspect(rawArgs);
24
+ case "verify": return verify(rawArgs);
25
+ default:
26
+ fail({ code: "BAD_USAGE", message: "Usage: run402 archives <inspect|verify> <archive-path> [--json]" });
27
+ }
28
+ }
29
+
30
+ async function inspect(rawArgs) {
31
+ const args = normalizeArgv(rawArgs);
32
+ assertKnownFlags(args, ["--json", "--help", "-h"], []);
33
+ const archivePath = positionalArgs(args, [])[0];
34
+ if (!archivePath) fail({ code: "BAD_USAGE", message: "Usage: run402 archives inspect <archive-path> [--json]" });
35
+ try {
36
+ const result = await inspectArchive(archivePath);
37
+ console.log(JSON.stringify({ archive: result }, null, 2));
38
+ } catch (err) {
39
+ reportSdkError(err);
40
+ }
41
+ }
42
+
43
+ async function verify(rawArgs) {
44
+ const args = normalizeArgv(rawArgs);
45
+ assertKnownFlags(args, ["--json", "--help", "-h"], []);
46
+ const archivePath = positionalArgs(args, [])[0];
47
+ if (!archivePath) fail({ code: "BAD_USAGE", message: "Usage: run402 archives verify <archive-path> [--json]" });
48
+ try {
49
+ const result = await verifyArchive(archivePath);
50
+ console.log(JSON.stringify({ ok: result.ok, verified: result.ok, archive: result }, null, 2));
51
+ if (!result.ok) process.exit(1);
52
+ } catch (err) {
53
+ reportSdkError(err);
54
+ }
55
+ }
package/lib/cache.mjs CHANGED
@@ -19,6 +19,7 @@
19
19
  import { getSdk } from "./sdk.mjs";
20
20
  import { reportSdkError, fail } from "./sdk-errors.mjs";
21
21
  import { assertKnownFlags, flagValue, normalizeArgv } from "./argparse.mjs";
22
+ import { editRequestAction } from "./next-actions.mjs";
22
23
 
23
24
  // Locally-defined helpers — argparse.mjs's normalized form is a flat
24
25
  // string array; we need to identify positional args (non-flag, non-flag-
@@ -101,7 +102,7 @@ export async function run(sub, args) {
101
102
  fail({
102
103
  code: "BAD_USAGE",
103
104
  message: `Unknown cache subcommand: ${sub}`,
104
- next_actions: ["run402 cache --help"],
105
+ next_actions: [editRequestAction("run402 cache --help", "Choose a supported cache subcommand.")],
105
106
  });
106
107
  }
107
108
  }
@@ -119,7 +120,7 @@ async function inspect(args) {
119
120
  fail({
120
121
  code: "BAD_USAGE",
121
122
  message: "Missing URL argument.",
122
- next_actions: ["run402 cache inspect <url>"],
123
+ next_actions: [editRequestAction("run402 cache inspect <url>", "Provide the absolute URL to inspect.")],
123
124
  });
124
125
  }
125
126
  const url = positionals[0];
@@ -127,7 +128,7 @@ async function inspect(args) {
127
128
  fail({
128
129
  code: "BAD_USAGE",
129
130
  message: `URL must be absolute (got: ${url})`,
130
- next_actions: ["run402 cache inspect https://<host>/<path>"],
131
+ next_actions: [editRequestAction("run402 cache inspect https://<host>/<path>", "Use an absolute HTTP(S) URL.")],
131
132
  });
132
133
  }
133
134
 
@@ -164,7 +165,7 @@ async function invalidate(args) {
164
165
  fail({
165
166
  code: "BAD_USAGE",
166
167
  message: "--all requires --host <hostname>",
167
- next_actions: ["run402 cache invalidate --all --host <hostname>"],
168
+ next_actions: [editRequestAction("run402 cache invalidate --all --host <hostname>", "Name the host whose cache should be invalidated.")],
168
169
  });
169
170
  }
170
171
  try {
@@ -181,7 +182,7 @@ async function invalidate(args) {
181
182
  fail({
182
183
  code: "BAD_USAGE",
183
184
  message: "--prefix requires --host <hostname>",
184
- next_actions: ["run402 cache invalidate --prefix /blog/ --host <hostname>"],
185
+ next_actions: [editRequestAction("run402 cache invalidate --prefix /blog/ --host <hostname>", "Pair prefix invalidation with the owning host.")],
185
186
  });
186
187
  }
187
188
  if (!prefix.startsWith("/")) {
@@ -205,9 +206,9 @@ async function invalidate(args) {
205
206
  code: "BAD_USAGE",
206
207
  message: "Missing URL argument.",
207
208
  next_actions: [
208
- "run402 cache invalidate <url>",
209
- "run402 cache invalidate --prefix /blog/ --host <hostname>",
210
- "run402 cache invalidate --all --host <hostname>",
209
+ editRequestAction("run402 cache invalidate <url>", "Invalidate one absolute URL."),
210
+ editRequestAction("run402 cache invalidate --prefix /blog/ --host <hostname>", "Invalidate cached entries under a path prefix."),
211
+ editRequestAction("run402 cache invalidate --all --host <hostname>", "Invalidate every cached entry for a host."),
211
212
  ],
212
213
  });
213
214
  }
@@ -216,7 +217,7 @@ async function invalidate(args) {
216
217
  fail({
217
218
  code: "BAD_USAGE",
218
219
  message: `URL must be absolute (got: ${url})`,
219
- next_actions: ["run402 cache invalidate https://<host>/<path>"],
220
+ next_actions: [editRequestAction("run402 cache invalidate https://<host>/<path>", "Use an absolute HTTP(S) URL.")],
220
221
  });
221
222
  }
222
223
  try {
package/lib/cloud.mjs ADDED
@@ -0,0 +1,254 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+
4
+ import { getSdk } from "./sdk.mjs";
5
+ import { fail, reportSdkError } from "./sdk-errors.mjs";
6
+ import { assertAllowedValue, assertKnownFlags, flagValue, hasHelp, normalizeArgv, parseIntegerFlag, positionalArgs } from "./argparse.mjs";
7
+
8
+ const HELP = `run402 cloud — Run402 Cloud portability commands
9
+
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]
14
+
15
+ Canonical agent path:
16
+ run402 cloud archives create prj_... \\
17
+ --scope portable-runtime-v1 --auth stubs --consistency pause-writes \\
18
+ --wait --output ./project.r402ar --json
19
+
20
+ Options for create:
21
+ --scope <scope> Archive scope. v1 supports portable-runtime-v1.
22
+ --auth <mode> Auth export mode: stubs (default) or none.
23
+ --consistency <mode> pause-writes (default) or cloud_write_pause_v1.
24
+ --idempotency-key <key> Retry-safe creation key.
25
+ --wait Poll until the archive is ready.
26
+ --output <file> Save archive bytes. Implies --wait.
27
+ --poll-interval <ms> Poll interval while waiting (default 1000).
28
+ --timeout <ms> Wait timeout (default 600000).
29
+ --json Emit final JSON on stdout.
30
+ --json-stream Emit NDJSON progress events on stdout.
31
+ `;
32
+
33
+ const FLAG_VALUES = [
34
+ "--scope",
35
+ "--auth",
36
+ "--consistency",
37
+ "--idempotency-key",
38
+ "--output",
39
+ "--poll-interval",
40
+ "--timeout",
41
+ ];
42
+ const FLAGS = new Set([
43
+ ...FLAG_VALUES,
44
+ "--wait",
45
+ "--json",
46
+ "--json-stream",
47
+ "--help",
48
+ "-h",
49
+ ]);
50
+
51
+ export async function run(sub, args = []) {
52
+ const all = [sub, ...args].filter(Boolean);
53
+ if (hasHelp(all)) {
54
+ console.log(HELP);
55
+ return;
56
+ }
57
+ if (sub !== "archives") {
58
+ fail({
59
+ code: "BAD_USAGE",
60
+ message: "Usage: run402 cloud archives <create|download|status> ...",
61
+ next_actions: [{ type: "run_command", command: "run402 cloud archives --help" }],
62
+ });
63
+ }
64
+ const action = args[0];
65
+ const rest = args.slice(1);
66
+ if (action === "create") return create(rest);
67
+ if (action === "download") return download(rest);
68
+ if (action === "status") return status(rest);
69
+ fail({
70
+ code: "BAD_USAGE",
71
+ message: "Usage: run402 cloud archives <create|download|status> ...",
72
+ next_actions: [{ type: "run_command", command: "run402 cloud archives --help" }],
73
+ });
74
+ }
75
+
76
+ async function create(rawArgs) {
77
+ const args = normalizeArgv(rawArgs);
78
+ assertKnownFlags(args, [...FLAGS], FLAG_VALUES);
79
+ const pos = positionalArgs(args, FLAG_VALUES);
80
+ const projectId = pos[0];
81
+ if (!projectId) {
82
+ fail({ code: "BAD_PROJECT_ID", message: "Missing project id." });
83
+ }
84
+ const scope = flagValue(args, "--scope") ?? "portable-runtime-v1";
85
+ const auth = flagValue(args, "--auth") ?? "stubs";
86
+ const consistency = flagValue(args, "--consistency") ?? "pause-writes";
87
+ assertAllowedValue(scope, ["portable-runtime-v1"], "--scope");
88
+ assertAllowedValue(auth, ["stubs", "none"], "--auth");
89
+ assertAllowedValue(consistency, ["pause-writes", "cloud_write_pause_v1"], "--consistency");
90
+
91
+ const output = flagValue(args, "--output");
92
+ const wait = args.includes("--wait") || Boolean(output);
93
+ const jsonStream = args.includes("--json-stream");
94
+ const json = args.includes("--json") || jsonStream || true;
95
+ const idempotencyKey = flagValue(args, "--idempotency-key") ?? undefined;
96
+ const pollIntervalMs = parseIntegerFlag("--poll-interval", flagValue(args, "--poll-interval"), { min: 100, def: 1000 });
97
+ const timeoutMs = parseIntegerFlag("--timeout", flagValue(args, "--timeout"), { min: 1000, def: 600000 });
98
+ const emit = (event) => {
99
+ if (jsonStream) console.log(JSON.stringify(event));
100
+ };
101
+
102
+ try {
103
+ const sdk = getSdk();
104
+ const created = await sdk.archives.create(projectId, {
105
+ scope,
106
+ auth,
107
+ consistency,
108
+ idempotencyKey,
109
+ });
110
+ emit(progressEvent("archive_export_created", "create", projectId, created));
111
+
112
+ let archive = created;
113
+ let outputPath = null;
114
+ let bytesWritten = 0;
115
+ if (wait) {
116
+ archive = created.status === "ready"
117
+ ? created
118
+ : await sdk.archives.wait(projectId, created.archive_id, {
119
+ pollIntervalMs,
120
+ timeoutMs,
121
+ onProgress: emit,
122
+ });
123
+ if (archive.status !== "ready") {
124
+ const result = finalCreateResult({ projectId, created, archive, outputPath, bytesWritten });
125
+ printJson(result, jsonStream);
126
+ process.exit(1);
127
+ }
128
+ if (output) {
129
+ const download = await sdk.archives.download(projectId, archive.archive_id);
130
+ outputPath = resolve(output);
131
+ mkdirSync(dirname(outputPath), { recursive: true });
132
+ writeFileSync(outputPath, download.bytes);
133
+ bytesWritten = download.bytes.byteLength;
134
+ emit({
135
+ ...progressEvent("archive_export_downloaded", "download", projectId, archive),
136
+ context: { output_path: outputPath, bytes_written: bytesWritten },
137
+ });
138
+ }
139
+ }
140
+
141
+ const result = finalCreateResult({ projectId, created, archive, outputPath, bytesWritten });
142
+ if (jsonStream) {
143
+ console.log(JSON.stringify({
144
+ event: "archive_export_complete",
145
+ stage: "complete",
146
+ resource_type: "project_archive",
147
+ resource_id: archive.archive_id,
148
+ project_id: projectId,
149
+ status: archive.status === "ready" ? "complete" : archive.status,
150
+ completed_units: archive.status === "ready" ? 1 : 0,
151
+ total_units: 1,
152
+ code: null,
153
+ message: archive.status === "ready" ? "Archive export complete." : "Archive export did not complete.",
154
+ next_action: archive.next_action,
155
+ retryable: false,
156
+ result,
157
+ }));
158
+ } else if (json) {
159
+ console.log(JSON.stringify(result, null, 2));
160
+ }
161
+ if (archive.status !== "ready") process.exit(1);
162
+ } catch (err) {
163
+ reportSdkError(err);
164
+ }
165
+ }
166
+
167
+ async function download(rawArgs) {
168
+ const args = normalizeArgv(rawArgs);
169
+ assertKnownFlags(args, ["--output", "--json", "--help", "-h"], ["--output"]);
170
+ const pos = positionalArgs(args, ["--output"]);
171
+ const [projectId, archiveId] = pos;
172
+ const output = flagValue(args, "--output");
173
+ if (!projectId || !archiveId || !output) {
174
+ fail({ code: "BAD_USAGE", message: "Usage: run402 cloud archives download <project-id> <archive-id> --output <file> [--json]" });
175
+ }
176
+ try {
177
+ const download = await getSdk().archives.download(projectId, archiveId);
178
+ const outputPath = resolve(output);
179
+ mkdirSync(dirname(outputPath), { recursive: true });
180
+ writeFileSync(outputPath, download.bytes);
181
+ console.log(JSON.stringify({
182
+ ok: true,
183
+ project_id: projectId,
184
+ archive_id: archiveId,
185
+ output_path: outputPath,
186
+ bytes_written: download.bytes.byteLength,
187
+ sha256: download.archive.sha256,
188
+ verify_command: `run402 archives verify ${JSON.stringify(outputPath)} --json`,
189
+ import_command: `run402 core projects import ${JSON.stringify(outputPath)} --name imported-project --env-file ./required.env --json`,
190
+ archive: download.archive,
191
+ }, null, 2));
192
+ } catch (err) {
193
+ reportSdkError(err);
194
+ }
195
+ }
196
+
197
+ async function status(rawArgs) {
198
+ const args = normalizeArgv(rawArgs);
199
+ assertKnownFlags(args, ["--json", "--help", "-h"], []);
200
+ const pos = positionalArgs(args, []);
201
+ const [projectId, archiveId] = pos;
202
+ if (!projectId || !archiveId) {
203
+ fail({ code: "BAD_USAGE", message: "Usage: run402 cloud archives status <project-id> <archive-id> [--json]" });
204
+ }
205
+ try {
206
+ const archive = await getSdk().archives.get(projectId, archiveId);
207
+ console.log(JSON.stringify({ archive }, null, 2));
208
+ } catch (err) {
209
+ reportSdkError(err);
210
+ }
211
+ }
212
+
213
+ function finalCreateResult({ projectId, created, archive, outputPath, bytesWritten }) {
214
+ return {
215
+ ok: archive.status === "ready",
216
+ project_id: projectId,
217
+ archive_id: archive.archive_id,
218
+ operation_id: archive.operation_id,
219
+ created_archive_id: created.archive_id,
220
+ archive_status: archive.status,
221
+ output_path: outputPath,
222
+ bytes_written: bytesWritten,
223
+ sha256: archive.sha256,
224
+ byte_count: archive.byte_count,
225
+ expires_at: archive.expires_at,
226
+ verify_command: outputPath ? `run402 archives verify ${JSON.stringify(outputPath)} --json` : null,
227
+ import_command: outputPath ? `run402 core projects import ${JSON.stringify(outputPath)} --name imported-project --env-file ./required.env --json` : null,
228
+ next_action: archive.next_action,
229
+ portability_report: archive.portability_report,
230
+ export_report: archive.export_report,
231
+ archive,
232
+ };
233
+ }
234
+
235
+ function progressEvent(event, stage, projectId, archive) {
236
+ return {
237
+ event,
238
+ stage,
239
+ resource_type: "project_archive",
240
+ resource_id: archive.archive_id,
241
+ project_id: projectId,
242
+ status: archive.status,
243
+ completed_units: archive.status === "ready" || archive.status === "failed" || archive.status === "expired" ? 1 : 0,
244
+ total_units: 1,
245
+ code: archive.status === "failed" ? archive.error?.code ?? "ARCHIVE_EXPORT_FAILED" : null,
246
+ message: `Archive export status: ${archive.status}`,
247
+ next_action: archive.next_action,
248
+ retryable: archive.status === "running",
249
+ };
250
+ }
251
+
252
+ function printJson(result, alreadyStreamed) {
253
+ if (!alreadyStreamed) console.log(JSON.stringify(result, null, 2));
254
+ }
package/lib/config.mjs CHANGED
@@ -8,6 +8,7 @@ import { readAllowance as coreReadAllowance, saveAllowance as coreSaveAllowance
8
8
  import { loadKeyStore, getProject, saveProject, updateProject, removeProject, saveKeyStore, getActiveProjectId, setActiveProjectId } from "../core-dist/keystore.js";
9
9
  import { getAllowanceAuthHeaders as coreGetAllowanceAuthHeaders } from "../core-dist/allowance-auth.js";
10
10
  import { fail } from "./sdk-errors.mjs";
11
+ import { initializeWalletAction, createProjectAction } from "./next-actions.mjs";
11
12
 
12
13
  // Wallet-dependent paths are exposed as getters (preferred — they always
13
14
  // reflect the active profile, even if some future code path imports this module
@@ -48,6 +49,7 @@ export function readAllowance() {
48
49
  message: err?.message ?? "allowance.json is malformed",
49
50
  hint: "Back up ~/.config/run402/allowance.json and run 'run402 init' to recreate it.",
50
51
  details: { path: allowanceFile() },
52
+ next_actions: [initializeWalletAction()],
51
53
  });
52
54
  }
53
55
  }
@@ -63,6 +65,7 @@ export function allowanceAuthHeaders(path) {
63
65
  code: "NO_ALLOWANCE",
64
66
  message: "No agent allowance found.",
65
67
  hint: "Run: run402 allowance create",
68
+ next_actions: [initializeWalletAction()],
66
69
  });
67
70
  }
68
71
  return headers;
@@ -80,6 +83,7 @@ export function findProject(id) {
80
83
  message: `Project ${idStr} not found in local registry.`,
81
84
  hint,
82
85
  details: { project_id: idStr, source: "local_registry" },
86
+ next_actions: [createProjectAction()],
83
87
  });
84
88
  }
85
89
  return p;
@@ -92,6 +96,7 @@ export function resolveProject(id) {
92
96
  code: "NO_ACTIVE_PROJECT",
93
97
  message: "no project specified and no active project set.",
94
98
  hint: "Run: run402 projects provision",
99
+ next_actions: [createProjectAction()],
95
100
  });
96
101
  }
97
102
  return findProject(projectId);
@@ -104,6 +109,7 @@ export function resolveProjectId(id) {
104
109
  code: "NO_ACTIVE_PROJECT",
105
110
  message: "no project specified and no active project set.",
106
111
  hint: "Run: run402 projects provision",
112
+ next_actions: [createProjectAction()],
107
113
  });
108
114
  }
109
115
  return projectId;
package/lib/core.mjs ADDED
@@ -0,0 +1,122 @@
1
+ import { importArchiveToCore } from "#sdk/node";
2
+ import { fail, reportSdkError } from "./sdk-errors.mjs";
3
+ import { assertKnownFlags, flagValue, hasHelp, normalizeArgv, positionalArgs } from "./argparse.mjs";
4
+
5
+ const HELP = `run402 core — Local Run402 Core commands
6
+
7
+ Usage:
8
+ run402 core projects import <archive-path> --name <project-name> [options]
9
+
10
+ Options:
11
+ --name <name> New Core project name (default imported-project)
12
+ --env-file <path> Env file satisfying required archive secrets.
13
+ --secret KEY=VALUE Inline secret value; repeatable. Overrides --env-file.
14
+ --core-url <url> Core gateway URL (default RUN402_CORE_URL or http://127.0.0.1:4020)
15
+ --dry-run Verify and plan without creating a Core project.
16
+ --require-runnable Block import unless required secrets are supplied.
17
+ --json Emit final JSON.
18
+ --json-stream Emit NDJSON progress events and final result event.
19
+ `;
20
+
21
+ const FLAG_VALUES = ["--name", "--env-file", "--secret", "--core-url"];
22
+ const FLAGS = new Set([...FLAG_VALUES, "--dry-run", "--require-runnable", "--json", "--json-stream", "--help", "-h"]);
23
+
24
+ export async function run(sub, args = []) {
25
+ const all = [sub, ...args].filter(Boolean);
26
+ if (hasHelp(all)) {
27
+ console.log(HELP);
28
+ return;
29
+ }
30
+ if (sub !== "projects") {
31
+ fail({ code: "BAD_USAGE", message: "Usage: run402 core projects import <archive-path> [options]" });
32
+ }
33
+ const action = args[0];
34
+ if (action === "import") return importProject(args.slice(1));
35
+ fail({ code: "BAD_USAGE", message: "Usage: run402 core projects import <archive-path> [options]" });
36
+ }
37
+
38
+ async function importProject(rawArgs) {
39
+ const args = normalizeArgv(rawArgs);
40
+ assertKnownFlags(args, [...FLAGS], FLAG_VALUES);
41
+ const archivePath = positionalArgs(args, FLAG_VALUES)[0];
42
+ if (!archivePath) {
43
+ fail({ code: "BAD_USAGE", message: "Usage: run402 core projects import <archive-path> --name <project-name> [options]" });
44
+ }
45
+ const jsonStream = args.includes("--json-stream");
46
+ const secretValues = parseSecrets(args);
47
+ const name = flagValue(args, "--name") ?? "imported-project";
48
+ const coreUrl = flagValue(args, "--core-url") ?? undefined;
49
+ const startedEvent = {
50
+ event: "core_archive_import_started",
51
+ stage: "verify",
52
+ resource_type: "project_archive",
53
+ resource_id: archivePath,
54
+ status: "running",
55
+ completed_units: 0,
56
+ total_units: 1,
57
+ code: null,
58
+ message: "Verifying archive locally before Core import.",
59
+ next_action: { type: "none" },
60
+ retryable: false,
61
+ context: { archive_path: archivePath, core_url: coreUrl ?? null, project_name: name },
62
+ };
63
+ if (jsonStream) console.log(JSON.stringify(startedEvent));
64
+ try {
65
+ const result = await importArchiveToCore({
66
+ archivePath,
67
+ name,
68
+ coreUrl,
69
+ envFile: flagValue(args, "--env-file") ?? undefined,
70
+ secretValues,
71
+ dryRun: args.includes("--dry-run"),
72
+ requireRunnable: args.includes("--require-runnable"),
73
+ });
74
+ if (jsonStream) {
75
+ console.log(JSON.stringify({
76
+ event: "core_archive_import_complete",
77
+ stage: "complete",
78
+ resource_type: "project_archive",
79
+ resource_id: archivePath,
80
+ status: result.status,
81
+ completed_units: result.status === "imported" || result.status === "dry_run" ? 1 : 0,
82
+ total_units: 1,
83
+ code: firstDiagnosticCode(result),
84
+ message: `Core archive import status: ${result.status}`,
85
+ next_action: result.next_action,
86
+ retryable: result.status === "failed" && result.diagnostics.some((d) => d.retryable),
87
+ result,
88
+ }));
89
+ } else {
90
+ console.log(JSON.stringify({ ok: result.status === "imported" || result.status === "dry_run", import: result }, null, 2));
91
+ }
92
+ if (result.status !== "imported" && result.status !== "dry_run") process.exit(1);
93
+ } catch (err) {
94
+ reportSdkError(err);
95
+ }
96
+ }
97
+
98
+ function parseSecrets(args) {
99
+ const out = {};
100
+ for (let i = 0; i < args.length; i += 1) {
101
+ if (args[i] !== "--secret") continue;
102
+ const value = args[i + 1];
103
+ if (!value || value.startsWith("--")) {
104
+ fail({ code: "BAD_FLAG", message: "--secret requires KEY=VALUE" });
105
+ }
106
+ const eq = value.indexOf("=");
107
+ if (eq <= 0) {
108
+ fail({ code: "BAD_FLAG", message: "--secret requires KEY=VALUE", details: { value } });
109
+ }
110
+ const key = value.slice(0, eq);
111
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
112
+ fail({ code: "BAD_FLAG", message: `Invalid secret env var name: ${key}` });
113
+ }
114
+ out[key] = value.slice(eq + 1);
115
+ i += 1;
116
+ }
117
+ return out;
118
+ }
119
+
120
+ function firstDiagnosticCode(result) {
121
+ return result.diagnostics.find((d) => d.severity === "blocking")?.code ?? null;
122
+ }