run402 2.0.1 → 2.2.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 (35) hide show
  1. package/README.md +11 -0
  2. package/cli.mjs +7 -0
  3. package/lib/assets.mjs +26 -210
  4. package/lib/jobs.mjs +308 -0
  5. package/package.json +1 -1
  6. package/sdk/dist/index.d.ts +3 -0
  7. package/sdk/dist/index.d.ts.map +1 -1
  8. package/sdk/dist/index.js +3 -0
  9. package/sdk/dist/index.js.map +1 -1
  10. package/sdk/dist/namespaces/assets.d.ts +13 -9
  11. package/sdk/dist/namespaces/assets.d.ts.map +1 -1
  12. package/sdk/dist/namespaces/assets.js +68 -89
  13. package/sdk/dist/namespaces/assets.js.map +1 -1
  14. package/sdk/dist/namespaces/deploy.d.ts.map +1 -1
  15. package/sdk/dist/namespaces/deploy.js +202 -2
  16. package/sdk/dist/namespaces/deploy.js.map +1 -1
  17. package/sdk/dist/namespaces/deploy.types.d.ts +102 -1
  18. package/sdk/dist/namespaces/deploy.types.d.ts.map +1 -1
  19. package/sdk/dist/namespaces/deploy.types.js.map +1 -1
  20. package/sdk/dist/namespaces/jobs.d.ts +74 -0
  21. package/sdk/dist/namespaces/jobs.d.ts.map +1 -0
  22. package/sdk/dist/namespaces/jobs.js +82 -0
  23. package/sdk/dist/namespaces/jobs.js.map +1 -0
  24. package/sdk/dist/node/assets-node.d.ts +12 -7
  25. package/sdk/dist/node/assets-node.d.ts.map +1 -1
  26. package/sdk/dist/node/assets-node.js +91 -143
  27. package/sdk/dist/node/assets-node.js.map +1 -1
  28. package/sdk/dist/node/deploy-manifest.d.ts +25 -2
  29. package/sdk/dist/node/deploy-manifest.d.ts.map +1 -1
  30. package/sdk/dist/node/deploy-manifest.js +116 -0
  31. package/sdk/dist/node/deploy-manifest.js.map +1 -1
  32. package/sdk/dist/scoped.d.ts +11 -0
  33. package/sdk/dist/scoped.d.ts.map +1 -1
  34. package/sdk/dist/scoped.js +22 -0
  35. package/sdk/dist/scoped.js.map +1 -1
package/README.md CHANGED
@@ -143,6 +143,17 @@ run402 deploy apply --manifest run402.deploy.json # manifest uses secrets.requ
143
143
 
144
144
  Secret values are write-only. `list` returns keys and timestamps only; deploy manifests should declare dependencies with `secrets.require` and never contain values.
145
145
 
146
+ ### Jobs
147
+
148
+ ```bash
149
+ run402 jobs submit --file job.json --project prj_...
150
+ run402 jobs get job_abc123 --project prj_...
151
+ run402 jobs logs job_abc123 --project prj_... --tail 100
152
+ run402 jobs cancel job_abc123 --project prj_...
153
+ ```
154
+
155
+ Jobs are fixed platform-managed runners, not arbitrary Docker execution. Submit the gateway-shaped JSON request (`job_type`, `input["input.json"]`, `max_cost_usd_micros`) and the CLI handles the required idempotency header through the SDK.
156
+
146
157
  ### Email
147
158
 
148
159
  ```bash
package/cli.mjs CHANGED
@@ -27,6 +27,7 @@ Commands:
27
27
  projects Manage projects (provision, list, query, inspect, delete)
28
28
  deploy Unified deploy operations (requires active tier)
29
29
  ci Link GitHub Actions OIDC deploy bindings
30
+ jobs Submit and inspect fixed platform-managed jobs
30
31
  functions Manage serverless functions (deploy, invoke, logs, list, delete)
31
32
  secrets Manage project secrets (set, list, delete)
32
33
  assets Direct-to-S3 asset storage (put, get, ls, rm, sign, diagnose) — up to 5 TiB
@@ -52,6 +53,7 @@ Examples:
52
53
  run402 allowance create
53
54
  run402 allowance fund
54
55
  run402 deploy apply --manifest app.json
56
+ run402 jobs submit --file job.json
55
57
  run402 projects list
56
58
  run402 projects sql <project_id> "SELECT * FROM users LIMIT 5"
57
59
  run402 functions deploy <project_id> my-fn --file handler.ts
@@ -129,6 +131,11 @@ switch (cmd) {
129
131
  await run(sub, rest);
130
132
  break;
131
133
  }
134
+ case "jobs": {
135
+ const { run } = await import("./lib/jobs.mjs");
136
+ await run(sub, rest);
137
+ break;
138
+ }
132
139
  case "functions": {
133
140
  const { run } = await import("./lib/functions.mjs");
134
141
  await run(sub, rest);
package/lib/assets.mjs CHANGED
@@ -19,20 +19,13 @@
19
19
  */
20
20
 
21
21
  import {
22
- createReadStream,
23
22
  statSync,
24
23
  readFileSync,
25
- writeFileSync,
26
24
  mkdirSync,
27
- chmodSync,
28
25
  existsSync,
29
- unlinkSync,
30
- readdirSync,
31
26
  createWriteStream,
32
27
  } from "node:fs";
33
- import { createHash } from "node:crypto";
34
- import { basename, dirname, join, resolve as resolvePath } from "node:path";
35
- import { homedir } from "node:os";
28
+ import { basename, dirname, resolve as resolvePath } from "node:path";
36
29
  import { pipeline } from "node:stream/promises";
37
30
 
38
31
  import { resolveProjectId } from "./config.mjs";
@@ -55,10 +48,7 @@ Options:
55
48
  --key <dest> Destination key (put only; defaults to file basename)
56
49
  --content-type <mime> MIME override for blob put (defaults to extension inference)
57
50
  --private Upload as private (not served by CDN; apikey required to read)
58
- --immutable Adds a content-hash suffix to the URL so overwrites produce distinct URLs.
59
- Requires computing SHA-256 over the file (CLI does this automatically).
60
- --concurrency N Concurrent part PUTs (default 4)
61
- --no-resume Start fresh; ignore any cached state
51
+ --immutable Append a content-hash suffix to the URL so overwrites produce distinct URLs.
62
52
  --json NDJSON progress events (for agent consumption)
63
53
  --prefix <p> Prefix filter (ls only)
64
54
  --limit <n> Max results (ls only; default 100, max 1000)
@@ -72,6 +62,11 @@ Examples:
72
62
  run402 assets ls --project prj_abc123 --prefix images/
73
63
  run402 assets rm images/logo.png --project prj_abc123
74
64
  run402 assets sign images/logo.png --project prj_abc123 --ttl 600
65
+
66
+ Note: as of v2.1.0, the CLI delegates to sdk.assets.put which routes through
67
+ the unified-apply hero. The pre-v2.1.0 --concurrency and --no-resume flags
68
+ are still accepted for backward compatibility but are ignored; resume
69
+ semantics now live at the apply-plan level (24h plan TTL).
75
70
  `;
76
71
 
77
72
  const SUB_HELP = {
@@ -89,15 +84,13 @@ Options:
89
84
  --content-type <mime> MIME override; defaults to inferring from the destination key extension
90
85
  --private Upload as private (not served by CDN; apikey required to read)
91
86
  --immutable Append content-hash suffix so overwrites produce distinct URLs
92
- --concurrency N Concurrent part PUTs for multipart uploads (default 4)
93
- --no-resume Ignore any cached resumable-upload state and start fresh
94
87
  --json Emit NDJSON progress events on stdout (for agent consumption)
95
88
 
96
89
  Examples:
97
90
  run402 assets put ./artifact.tgz --project prj_abc123
98
91
  run402 assets put ./dist/**/*.png --project prj_abc123 --key assets/
99
92
  run402 assets put ./asset --project prj_abc123 --key assets/logo --content-type image/svg+xml
100
- run402 assets put huge.bin --project prj_abc123 --immutable --concurrency 8
93
+ run402 assets put huge.bin --project prj_abc123 --immutable
101
94
  `,
102
95
  get: `run402 assets get — Download a blob by key
103
96
 
@@ -185,10 +178,6 @@ Examples:
185
178
  `,
186
179
  };
187
180
 
188
- function uploadStateDir() {
189
- return join(homedir(), ".run402", "uploads");
190
- }
191
-
192
181
  function die(msg, exit_code = 1) {
193
182
  fail({ code: "BAD_USAGE", message: msg, exit_code });
194
183
  }
@@ -255,173 +244,34 @@ function parseContentTypeFlag(name, value) {
255
244
  return raw;
256
245
  }
257
246
 
258
- async function sha256File(filePath) {
259
- const h = createHash("sha256");
260
- const stream = createReadStream(filePath);
261
- for await (const chunk of stream) h.update(chunk);
262
- return h.digest("hex");
263
- }
264
-
265
- function sha256BufferHexAndBase64(body) {
266
- const digest = createHash("sha256").update(body).digest();
267
- return {
268
- hex: digest.toString("hex"),
269
- base64: digest.toString("base64"),
270
- };
271
- }
272
-
273
- function checksumHeadersForPresignedUrl(url, checksumBase64) {
274
- let urlHasChecksum = false;
275
- try {
276
- urlHasChecksum = new URL(url).searchParams.has("x-amz-checksum-sha256");
277
- } catch {
278
- urlHasChecksum = false;
279
- }
280
- return urlHasChecksum ? {} : { "x-amz-checksum-sha256": checksumBase64 };
281
- }
282
-
283
- function loadState(uploadId) {
284
- const path = join(uploadStateDir(), `${uploadId}.json`);
285
- if (!existsSync(path)) return null;
286
- try { return JSON.parse(readFileSync(path, "utf8")); }
287
- catch { return null; }
288
- }
289
-
290
- function saveState(state) {
291
- const dir = uploadStateDir();
292
- mkdirSync(dir, { recursive: true, mode: 0o700 });
293
- chmodSync(dir, 0o700);
294
- const diskState = { ...state };
295
- delete diskState.parts;
296
- const path = join(dir, `${state.upload_id}.json`);
297
- writeFileSync(path, JSON.stringify(diskState, null, 2), { mode: 0o600 });
298
- chmodSync(path, 0o600);
299
- }
300
-
301
- function removeState(uploadId) {
302
- const path = join(uploadStateDir(), `${uploadId}.json`);
303
- if (existsSync(path)) unlinkSync(path);
304
- }
305
-
306
- function fileFingerprint(stat) {
307
- return {
308
- file_size: stat.size,
309
- file_mtime_ms: stat.mtimeMs,
310
- };
311
- }
312
-
313
- function stateMatchesFile(state, fingerprint) {
314
- return state.file_size === fingerprint.file_size &&
315
- typeof state.file_mtime_ms === "number" &&
316
- state.file_mtime_ms === fingerprint.file_mtime_ms;
317
- }
318
-
319
- function findResumableStateForFile(projectId, localPath, key, fingerprint) {
320
- const dir = uploadStateDir();
321
- if (!existsSync(dir)) return null;
322
- for (const f of readdirSync(dir)) {
323
- if (!f.endsWith(".json")) continue;
324
- try {
325
- const s = JSON.parse(readFileSync(join(dir, f), "utf8"));
326
- if (s.project_id === projectId && s.local_path === localPath && s.key === key) {
327
- if (stateMatchesFile(s, fingerprint)) return s;
328
- removeState(s.upload_id);
329
- }
330
- } catch { /* ignore */ }
331
- }
332
- return null;
333
- }
334
-
335
247
  // ---------------------------------------------------------------------------
336
248
  // put
337
249
  // ---------------------------------------------------------------------------
338
250
 
339
251
  async function putOne(projectId, filePath, opts) {
340
252
  const stat = statSync(filePath);
341
- const size = stat.size;
342
- const fingerprint = fileFingerprint(stat);
343
- const destKey = computeDestKey(filePath, opts.key);
344
- const absLocal = resolvePath(filePath);
345
-
346
- const sha256 = await sha256File(filePath);
347
-
348
- // Attempt to resume
349
- let state = opts.resume
350
- ? findResumableStateForFile(projectId, absLocal, destKey, fingerprint)
351
- : null;
352
- let initRes;
353
- if (state) {
354
- // Re-poll the session; if it's still active, resume. Otherwise start fresh.
355
- const poll = await getSdk().assets.getUploadSession(projectId, state.upload_id);
356
- if (poll.status === "active") {
357
- log(opts, { event: "resume", upload_id: state.upload_id, key: destKey });
358
- initRes = {
359
- upload_id: state.upload_id,
360
- mode: poll.mode ?? state.mode,
361
- parts: poll.parts ?? state.parts ?? [],
362
- part_count: poll.part_count ?? state.part_count,
363
- part_size_bytes: poll.part_size_bytes ?? state.part_size_bytes,
364
- };
365
- } else {
366
- removeState(state.upload_id);
367
- state = null;
368
- }
253
+ if (!stat.isFile()) {
254
+ die(`Not a regular file: ${filePath}`);
369
255
  }
256
+ const destKey = computeDestKey(filePath, opts.key);
370
257
 
371
- if (!state) {
372
- initRes = await getSdk().assets.initUploadSession(projectId, {
373
- key: destKey,
374
- size_bytes: size,
375
- content_type: opts.contentType ?? guessContentType(destKey),
376
- visibility: opts.private ? "private" : "public",
377
- immutable: opts.immutable,
378
- sha256,
379
- });
380
- state = {
381
- upload_id: initRes.upload_id,
382
- project_id: projectId,
383
- local_path: absLocal,
384
- key: destKey,
385
- mode: initRes.mode,
386
- part_size_bytes: initRes.part_size_bytes,
387
- part_count: initRes.part_count,
388
- parts: initRes.parts,
389
- parts_done: {},
390
- sha256,
391
- ...fingerprint,
392
- started_at: new Date().toISOString(),
393
- };
394
- if (opts.resume) saveState(state);
395
- }
396
-
397
- // Upload parts with concurrency limit. For single-PUT mode part_count=1 and
398
- // this loop runs once.
399
- const etags = Array(initRes.part_count);
400
- for (const pn of Object.keys(state.parts_done || {})) {
401
- const pd = state.parts_done[pn];
402
- // Legacy resume state stored just the etag string; new code stores
403
- // { etag, sha256 }. Normalize on load.
404
- etags[parseInt(pn, 10) - 1] = typeof pd === "string" ? { etag: pd, sha256: undefined } : pd;
405
- }
406
-
407
- const todo = initRes.parts.filter((p) => !(state.parts_done || {})[String(p.part_number)]);
408
- await withConcurrency(todo, opts.concurrency, async (part) => {
409
- const { etag, sha256: partSha256 } = await putPart(filePath, part);
410
- etags[part.part_number - 1] = { etag, sha256: partSha256 };
411
- state.parts_done[String(part.part_number)] = { etag, sha256: partSha256 };
412
- if (opts.resume) saveState(state);
413
- log(opts, { event: "part", upload_id: state.upload_id, part_number: part.part_number, etag, sha256: partSha256 });
414
- });
415
-
416
- // Complete
417
- const body = initRes.mode === "multipart"
418
- ? { parts: etags.map((e, i) => ({ part_number: i + 1, etag: e.etag, sha256: e.sha256 })) }
419
- : {};
420
- const result = await getSdk().assets.completeUploadSession(projectId, state.upload_id, body, {
258
+ // v2.1.0: the legacy /storage/v1/uploads* session API is gone. The CLI
259
+ // now delegates to `sdk.assets.put`, which routes through the
260
+ // unified-apply hero (apply/v1/plans -> content/v1/plans -> S3 PUT ->
261
+ // commit).
262
+ //
263
+ // Trade-off vs v2.0.x: resumable uploads via persisted state under
264
+ // ~/.run402/uploads/ are no longer supported. Resume semantics now live
265
+ // at the apply-plan level (24h plan TTL); a future CLI redesign can
266
+ // expose that. The --concurrency and --no-resume flags are accepted but
267
+ // ignored — the SDK upload paths handle parallelism internally.
268
+ log(opts, { event: "start", key: destKey, size_bytes: stat.size });
269
+ const bytes = new Uint8Array(readFileSync(filePath));
270
+ const result = await getSdk().assets.put(projectId, destKey, { bytes }, {
421
271
  contentType: opts.contentType ?? guessContentType(destKey),
272
+ visibility: opts.private ? "private" : "public",
273
+ immutable: opts.immutable,
422
274
  });
423
-
424
- removeState(state.upload_id);
425
275
  log(opts, { event: "done", ...result });
426
276
  return result;
427
277
  }
@@ -432,40 +282,6 @@ function computeDestKey(filePath, keyOpt) {
432
282
  return keyOpt;
433
283
  }
434
284
 
435
- async function putPart(filePath, part) {
436
- const start = part.byte_start ?? 0;
437
- const end = part.byte_end ?? (statSync(filePath).size - 1);
438
- const stream = createReadStream(filePath, { start, end });
439
- const chunks = [];
440
- for await (const c of stream) chunks.push(c);
441
- const body = Buffer.concat(chunks);
442
- const checksum = sha256BufferHexAndBase64(body);
443
-
444
- const res = await fetch(part.url, {
445
- method: "PUT",
446
- headers: checksumHeadersForPresignedUrl(part.url, checksum.base64),
447
- body,
448
- });
449
- if (!res.ok) {
450
- const errBody = await res.text().catch(() => "");
451
- throw new Error(`Part ${part.part_number} PUT failed: ${res.status} ${res.statusText}${errBody ? " — " + errBody.slice(0, 200) : ""}`);
452
- }
453
- const etag = res.headers.get("etag") ?? "";
454
- return { etag, sha256: checksum.hex };
455
- }
456
-
457
- async function withConcurrency(items, limit, worker) {
458
- let index = 0;
459
- const workerCount = Math.min(limit, items.length);
460
- async function runWorker() {
461
- while (index < items.length) {
462
- const item = items[index++];
463
- await worker(item);
464
- }
465
- }
466
- await Promise.all(Array.from({ length: workerCount }, runWorker));
467
- }
468
-
469
285
  async function put(projectId, argv) {
470
286
  const opts = parseArgs(argv);
471
287
  opts.project = opts.project || projectId;
package/lib/jobs.mjs ADDED
@@ -0,0 +1,308 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ import { getSdk } from "./sdk.mjs";
4
+ import { resolveProjectId } from "./config.mjs";
5
+ import { reportSdkError, fail } from "./sdk-errors.mjs";
6
+ import {
7
+ assertKnownFlags,
8
+ flagValue,
9
+ normalizeArgv,
10
+ parseIntegerFlag,
11
+ requirePositionalCount,
12
+ validateRegularFile,
13
+ } from "./argparse.mjs";
14
+
15
+ const HELP = `run402 jobs — Submit and inspect fixed platform-managed jobs
16
+
17
+ Usage:
18
+ run402 jobs <subcommand> [args...] [options]
19
+
20
+ Subcommands:
21
+ submit --file <path>|--stdin Submit a managed job request
22
+ get <job_id> Get a job run
23
+ logs <job_id> Read job logs
24
+ cancel <job_id> Cancel a queued or running job
25
+
26
+ Examples:
27
+ run402 jobs submit --file job.json
28
+ cat job.json | run402 jobs submit --stdin --project prj_abc123
29
+ run402 jobs get job_abc123
30
+ run402 jobs logs job_abc123 --tail 100
31
+ run402 jobs cancel job_abc123
32
+
33
+ Notes:
34
+ - --project defaults to the active project from 'run402 projects use'
35
+ - Submit requests must match the gateway jobs API shape
36
+ `;
37
+
38
+ const SUB_HELP = {
39
+ submit: `run402 jobs submit — Submit a managed job request
40
+
41
+ Usage:
42
+ run402 jobs submit --file <path> [--project <id>]
43
+ run402 jobs submit --stdin [--project <id>]
44
+
45
+ Options:
46
+ --file <path> Read the JSON submit request from a file
47
+ --stdin Read the JSON submit request from stdin
48
+ --project <id> Project ID (defaults to the active project)
49
+
50
+ Example request:
51
+ {
52
+ "job_type": "kysigned.fflonk_prove.v0_17_0",
53
+ "input": { "input.json": {} },
54
+ "max_cost_usd_micros": 50000
55
+ }
56
+ `,
57
+ get: `run402 jobs get — Get a managed job run
58
+
59
+ Usage:
60
+ run402 jobs get <job_id> [--project <id>]
61
+
62
+ Options:
63
+ --project <id> Project ID (defaults to the active project)
64
+ `,
65
+ logs: `run402 jobs logs — Read managed job logs
66
+
67
+ Usage:
68
+ run402 jobs logs <job_id> [--project <id>] [--tail <n>] [--since <epoch_ms>]
69
+
70
+ Options:
71
+ --project <id> Project ID (defaults to the active project)
72
+ --tail <n> Maximum entries to return (gateway max: 1000)
73
+ --since <ms> Only include logs at or after this epoch millisecond timestamp
74
+ `,
75
+ cancel: `run402 jobs cancel — Cancel a managed job run
76
+
77
+ Usage:
78
+ run402 jobs cancel <job_id> [--project <id>]
79
+
80
+ Options:
81
+ --project <id> Project ID (defaults to the active project)
82
+ `,
83
+ };
84
+
85
+ const PROJECT_FLAGS = ["--project"];
86
+
87
+ function parseJsonRequest(raw, source) {
88
+ let parsed;
89
+ try {
90
+ parsed = JSON.parse(raw);
91
+ } catch (err) {
92
+ fail({
93
+ code: "BAD_JSON",
94
+ message: `${source} is not valid JSON`,
95
+ details: {
96
+ source,
97
+ parse_error: err instanceof Error ? err.message : String(err),
98
+ },
99
+ });
100
+ }
101
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
102
+ fail({
103
+ code: "BAD_JSON",
104
+ message: `${source} must contain a JSON object`,
105
+ details: { source },
106
+ });
107
+ }
108
+ return parsed;
109
+ }
110
+
111
+ async function readStdinText() {
112
+ if (process.stdin?.isTTY) {
113
+ fail({
114
+ code: "BAD_USAGE",
115
+ message: "Missing JSON request on stdin.",
116
+ hint: "Pipe a job request JSON object, or use --file <path>.",
117
+ });
118
+ }
119
+ const chunks = [];
120
+ for await (const chunk of process.stdin) {
121
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
122
+ }
123
+ if (chunks.length === 0) {
124
+ fail({
125
+ code: "BAD_USAGE",
126
+ message: "Missing JSON request on stdin.",
127
+ hint: "Pipe a job request JSON object, or use --file <path>.",
128
+ });
129
+ }
130
+ return Buffer.concat(chunks).toString("utf-8");
131
+ }
132
+
133
+ async function readSubmitRequest(args) {
134
+ const file = flagValue(args, "--file");
135
+ const stdin = args.includes("--stdin");
136
+ if (file && stdin) {
137
+ fail({
138
+ code: "BAD_USAGE",
139
+ message: "Provide exactly one request source.",
140
+ hint: "Use either --file <path> or --stdin.",
141
+ });
142
+ }
143
+ if (!file && !stdin) {
144
+ fail({
145
+ code: "BAD_USAGE",
146
+ message: "Missing job request source.",
147
+ hint: "Use --file <path> or --stdin.",
148
+ });
149
+ }
150
+ if (file) {
151
+ validateRegularFile(file, "--file");
152
+ return parseJsonRequest(readFileSync(file, "utf-8"), file);
153
+ }
154
+ return parseJsonRequest(await readStdinText(), "stdin");
155
+ }
156
+
157
+ async function submit(args = []) {
158
+ const parsed = normalizeArgv(args);
159
+ const valueFlags = ["--file", "--project"];
160
+ assertKnownFlags(parsed, ["--file", "--stdin", "--project", "--help", "-h"], valueFlags);
161
+ requirePositionalCount(parsed, valueFlags, {
162
+ max: 0,
163
+ command: "run402 jobs submit",
164
+ });
165
+ const projectId = resolveProjectId(flagValue(parsed, "--project"));
166
+ const request = await readSubmitRequest(parsed);
167
+
168
+ try {
169
+ const result = await getSdk().jobs.submit(projectId, request);
170
+ console.log(JSON.stringify(result, null, 2));
171
+ } catch (err) {
172
+ reportSdkError(err);
173
+ }
174
+ }
175
+
176
+ async function get(jobId, args = []) {
177
+ const parsed = normalizeArgv(args);
178
+ assertKnownFlags(parsed, ["--project", "--help", "-h"], PROJECT_FLAGS);
179
+ if (!jobId) {
180
+ fail({
181
+ code: "BAD_USAGE",
182
+ message: "Missing job_id.",
183
+ hint: "Use `run402 jobs get <job_id>`.",
184
+ });
185
+ }
186
+ requirePositionalCount(parsed, PROJECT_FLAGS, {
187
+ max: 0,
188
+ command: "run402 jobs get <job_id>",
189
+ });
190
+ const projectId = resolveProjectId(flagValue(parsed, "--project"));
191
+
192
+ try {
193
+ const result = await getSdk().jobs.get(projectId, jobId);
194
+ console.log(JSON.stringify(result, null, 2));
195
+ } catch (err) {
196
+ reportSdkError(err);
197
+ }
198
+ }
199
+
200
+ async function logs(jobId, args = []) {
201
+ const parsed = normalizeArgv(args);
202
+ const valueFlags = ["--project", "--tail", "--since"];
203
+ assertKnownFlags(parsed, ["--project", "--tail", "--since", "--help", "-h"], valueFlags);
204
+ if (!jobId) {
205
+ fail({
206
+ code: "BAD_USAGE",
207
+ message: "Missing job_id.",
208
+ hint: "Use `run402 jobs logs <job_id>`.",
209
+ });
210
+ }
211
+ requirePositionalCount(parsed, valueFlags, {
212
+ max: 0,
213
+ command: "run402 jobs logs <job_id>",
214
+ });
215
+ const projectId = resolveProjectId(flagValue(parsed, "--project"));
216
+ const opts = {};
217
+ const tail = flagValue(parsed, "--tail");
218
+ const since = flagValue(parsed, "--since");
219
+ if (tail !== null) opts.tail = parseIntegerFlag("--tail", tail, { min: 1, max: 1000 });
220
+ if (since !== null) opts.since = parseIntegerFlag("--since", since, { min: 0 });
221
+
222
+ try {
223
+ const result = await getSdk().jobs.logs(projectId, jobId, opts);
224
+ console.log(JSON.stringify(result, null, 2));
225
+ } catch (err) {
226
+ reportSdkError(err);
227
+ }
228
+ }
229
+
230
+ async function cancel(jobId, args = []) {
231
+ const parsed = normalizeArgv(args);
232
+ assertKnownFlags(parsed, ["--project", "--help", "-h"], PROJECT_FLAGS);
233
+ if (!jobId) {
234
+ fail({
235
+ code: "BAD_USAGE",
236
+ message: "Missing job_id.",
237
+ hint: "Use `run402 jobs cancel <job_id>`.",
238
+ });
239
+ }
240
+ requirePositionalCount(parsed, PROJECT_FLAGS, {
241
+ max: 0,
242
+ command: "run402 jobs cancel <job_id>",
243
+ });
244
+ const projectId = resolveProjectId(flagValue(parsed, "--project"));
245
+
246
+ try {
247
+ const result = await getSdk().jobs.cancel(projectId, jobId);
248
+ console.log(JSON.stringify(result, null, 2));
249
+ } catch (err) {
250
+ reportSdkError(err);
251
+ }
252
+ }
253
+
254
+ function splitJobIdArg(args = [], valueFlags = []) {
255
+ const flagsWithValues = new Set(valueFlags);
256
+ for (let i = 0; i < args.length; i += 1) {
257
+ const arg = args[i];
258
+ if (flagsWithValues.has(arg)) {
259
+ i += 1;
260
+ continue;
261
+ }
262
+ if (typeof arg === "string" && arg.startsWith("-")) continue;
263
+ return {
264
+ jobId: arg,
265
+ rest: [...args.slice(0, i), ...args.slice(i + 1)],
266
+ };
267
+ }
268
+ return { jobId: undefined, rest: args };
269
+ }
270
+
271
+ export async function run(sub, args = []) {
272
+ if (!sub || sub === "--help" || sub === "-h") {
273
+ console.log(HELP);
274
+ process.exit(0);
275
+ }
276
+ if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) {
277
+ console.log(SUB_HELP[sub] || HELP);
278
+ process.exit(0);
279
+ }
280
+
281
+ switch (sub) {
282
+ case "submit":
283
+ await submit(args);
284
+ break;
285
+ case "get": {
286
+ const parsed = normalizeArgv(args);
287
+ const { jobId, rest } = splitJobIdArg(parsed, PROJECT_FLAGS);
288
+ await get(jobId, rest);
289
+ break;
290
+ }
291
+ case "logs": {
292
+ const parsed = normalizeArgv(args);
293
+ const { jobId, rest } = splitJobIdArg(parsed, ["--project", "--tail", "--since"]);
294
+ await logs(jobId, rest);
295
+ break;
296
+ }
297
+ case "cancel": {
298
+ const parsed = normalizeArgv(args);
299
+ const { jobId, rest } = splitJobIdArg(parsed, PROJECT_FLAGS);
300
+ await cancel(jobId, rest);
301
+ break;
302
+ }
303
+ default:
304
+ console.error(`Unknown subcommand: ${sub}\n`);
305
+ console.log(HELP);
306
+ process.exit(1);
307
+ }
308
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "2.0.1",
3
+ "version": "2.2.0",
4
4
  "description": "CLI for Run402 — provision Postgres databases, deploy static sites, generate images, and manage wallets via x402 and MPP micropayments.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,6 +26,7 @@ import { Contracts } from "./namespaces/contracts.js";
26
26
  import { Admin } from "./namespaces/admin.js";
27
27
  import { Deploy } from "./namespaces/deploy.js";
28
28
  import { Ci } from "./namespaces/ci.js";
29
+ import { Jobs } from "./namespaces/jobs.js";
29
30
  import type { ContentSource, FileSet } from "./namespaces/deploy.types.js";
30
31
  import { ScopedRun402 } from "./scoped.js";
31
32
  export interface Run402Options {
@@ -70,6 +71,7 @@ export declare class Run402 {
70
71
  */
71
72
  readonly _applyEngine: Deploy;
72
73
  readonly ci: Ci;
74
+ readonly jobs: Jobs;
73
75
  constructor(opts: Run402Options);
74
76
  /**
75
77
  * Return a project-scoped sub-client where every project-id-bearing namespace
@@ -151,6 +153,7 @@ export type { ByteReader } from "./namespaces/deploy.js";
151
153
  export type * from "./namespaces/domains.js";
152
154
  export type * from "./namespaces/email.js";
153
155
  export type * from "./namespaces/functions.types.js";
156
+ export type * from "./namespaces/jobs.js";
154
157
  export type * from "./namespaces/projects.types.js";
155
158
  export type * from "./namespaces/secrets.js";
156
159
  export type * from "./namespaces/sender-domain.js";