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.
- package/README.md +11 -0
- package/cli.mjs +7 -0
- package/lib/assets.mjs +26 -210
- package/lib/jobs.mjs +308 -0
- package/package.json +1 -1
- package/sdk/dist/index.d.ts +3 -0
- package/sdk/dist/index.d.ts.map +1 -1
- package/sdk/dist/index.js +3 -0
- package/sdk/dist/index.js.map +1 -1
- package/sdk/dist/namespaces/assets.d.ts +13 -9
- package/sdk/dist/namespaces/assets.d.ts.map +1 -1
- package/sdk/dist/namespaces/assets.js +68 -89
- package/sdk/dist/namespaces/assets.js.map +1 -1
- package/sdk/dist/namespaces/deploy.d.ts.map +1 -1
- package/sdk/dist/namespaces/deploy.js +202 -2
- package/sdk/dist/namespaces/deploy.js.map +1 -1
- package/sdk/dist/namespaces/deploy.types.d.ts +102 -1
- package/sdk/dist/namespaces/deploy.types.d.ts.map +1 -1
- package/sdk/dist/namespaces/deploy.types.js.map +1 -1
- package/sdk/dist/namespaces/jobs.d.ts +74 -0
- package/sdk/dist/namespaces/jobs.d.ts.map +1 -0
- package/sdk/dist/namespaces/jobs.js +82 -0
- package/sdk/dist/namespaces/jobs.js.map +1 -0
- package/sdk/dist/node/assets-node.d.ts +12 -7
- package/sdk/dist/node/assets-node.d.ts.map +1 -1
- package/sdk/dist/node/assets-node.js +91 -143
- package/sdk/dist/node/assets-node.js.map +1 -1
- package/sdk/dist/node/deploy-manifest.d.ts +25 -2
- package/sdk/dist/node/deploy-manifest.d.ts.map +1 -1
- package/sdk/dist/node/deploy-manifest.js +116 -0
- package/sdk/dist/node/deploy-manifest.js.map +1 -1
- package/sdk/dist/scoped.d.ts +11 -0
- package/sdk/dist/scoped.d.ts.map +1 -1
- package/sdk/dist/scoped.js +22 -0
- 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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
342
|
-
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
package/sdk/dist/index.d.ts
CHANGED
|
@@ -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";
|