run402 1.69.6 → 1.69.7
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/lib/blob.mjs +85 -47
- package/lib/deploy-v2.mjs +159 -62
- package/lib/functions.mjs +42 -6
- package/package.json +1 -1
- package/sdk/dist/namespaces/blobs.d.ts +3 -2
- package/sdk/dist/namespaces/blobs.d.ts.map +1 -1
- package/sdk/dist/namespaces/blobs.js +38 -7
- package/sdk/dist/namespaces/blobs.js.map +1 -1
- package/sdk/dist/namespaces/blobs.types.d.ts +5 -6
- package/sdk/dist/namespaces/blobs.types.d.ts.map +1 -1
- package/sdk/dist/namespaces/deploy.d.ts.map +1 -1
- package/sdk/dist/namespaces/deploy.js +13 -1
- package/sdk/dist/namespaces/deploy.js.map +1 -1
- package/sdk/dist/namespaces/functions.d.ts.map +1 -1
- package/sdk/dist/namespaces/functions.js +92 -1
- package/sdk/dist/namespaces/functions.js.map +1 -1
- package/sdk/dist/node/files.d.ts +11 -5
- package/sdk/dist/node/files.d.ts.map +1 -1
- package/sdk/dist/node/files.js +31 -7
- package/sdk/dist/node/files.js.map +1 -1
package/lib/blob.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
readFileSync,
|
|
25
25
|
writeFileSync,
|
|
26
26
|
mkdirSync,
|
|
27
|
+
chmodSync,
|
|
27
28
|
existsSync,
|
|
28
29
|
unlinkSync,
|
|
29
30
|
readdirSync,
|
|
@@ -61,7 +62,7 @@ Options:
|
|
|
61
62
|
--json NDJSON progress events (for agent consumption)
|
|
62
63
|
--prefix <p> Prefix filter (ls only)
|
|
63
64
|
--limit <n> Max results (ls only; default 100, max 1000)
|
|
64
|
-
--ttl <seconds> Signed-URL TTL (sign only; default 3600, max 604800)
|
|
65
|
+
--ttl <seconds> Signed-URL TTL (sign only; default 3600, min 60, max 604800)
|
|
65
66
|
|
|
66
67
|
Examples:
|
|
67
68
|
run402 blob put ./artifact.tgz --project prj_abc123
|
|
@@ -151,7 +152,7 @@ Arguments:
|
|
|
151
152
|
|
|
152
153
|
Options:
|
|
153
154
|
--project <id> Project ID (defaults to active project)
|
|
154
|
-
--ttl <seconds> Signed-URL TTL (default 3600, max 604800)
|
|
155
|
+
--ttl <seconds> Signed-URL TTL (default 3600, min 60, max 604800)
|
|
155
156
|
|
|
156
157
|
Examples:
|
|
157
158
|
run402 blob sign reports/2025-q4.pdf --project prj_abc123 --ttl 600
|
|
@@ -184,7 +185,9 @@ Examples:
|
|
|
184
185
|
`,
|
|
185
186
|
};
|
|
186
187
|
|
|
187
|
-
|
|
188
|
+
function uploadStateDir() {
|
|
189
|
+
return join(homedir(), ".run402", "uploads");
|
|
190
|
+
}
|
|
188
191
|
|
|
189
192
|
function die(msg, exit_code = 1) {
|
|
190
193
|
fail({ code: "BAD_USAGE", message: msg, exit_code });
|
|
@@ -226,7 +229,7 @@ function parseArgs(rawArgs) {
|
|
|
226
229
|
else if (a === "--prefix") out.prefix = args[++i];
|
|
227
230
|
else if (a === "--limit") out.limit = parseIntegerFlag("--limit", args[++i], { min: 1, max: 1000 });
|
|
228
231
|
else if (a === "--output" || a === "-o") out.output = args[++i];
|
|
229
|
-
else if (a === "--ttl") out.ttl = parseIntegerFlag("--ttl", args[++i], { min:
|
|
232
|
+
else if (a === "--ttl") out.ttl = parseIntegerFlag("--ttl", args[++i], { min: 60, max: 604800 });
|
|
230
233
|
else if (!a.startsWith("--")) out.positional.push(a);
|
|
231
234
|
}
|
|
232
235
|
return out;
|
|
@@ -259,30 +262,71 @@ async function sha256File(filePath) {
|
|
|
259
262
|
return h.digest("hex");
|
|
260
263
|
}
|
|
261
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
|
+
|
|
262
283
|
function loadState(uploadId) {
|
|
263
|
-
const path = join(
|
|
284
|
+
const path = join(uploadStateDir(), `${uploadId}.json`);
|
|
264
285
|
if (!existsSync(path)) return null;
|
|
265
286
|
try { return JSON.parse(readFileSync(path, "utf8")); }
|
|
266
287
|
catch { return null; }
|
|
267
288
|
}
|
|
268
289
|
|
|
269
290
|
function saveState(state) {
|
|
270
|
-
|
|
271
|
-
|
|
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);
|
|
272
299
|
}
|
|
273
300
|
|
|
274
301
|
function removeState(uploadId) {
|
|
275
|
-
const path = join(
|
|
302
|
+
const path = join(uploadStateDir(), `${uploadId}.json`);
|
|
276
303
|
if (existsSync(path)) unlinkSync(path);
|
|
277
304
|
}
|
|
278
305
|
|
|
279
|
-
function
|
|
280
|
-
|
|
281
|
-
|
|
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)) {
|
|
282
323
|
if (!f.endsWith(".json")) continue;
|
|
283
324
|
try {
|
|
284
|
-
const s = JSON.parse(readFileSync(join(
|
|
285
|
-
if (s.project_id === projectId && s.local_path === localPath && s.key === key)
|
|
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
|
+
}
|
|
286
330
|
} catch { /* ignore */ }
|
|
287
331
|
}
|
|
288
332
|
return null;
|
|
@@ -295,16 +339,15 @@ function findResumableStateForFile(projectId, localPath, key) {
|
|
|
295
339
|
async function putOne(projectId, filePath, opts) {
|
|
296
340
|
const stat = statSync(filePath);
|
|
297
341
|
const size = stat.size;
|
|
342
|
+
const fingerprint = fileFingerprint(stat);
|
|
298
343
|
const destKey = computeDestKey(filePath, opts.key);
|
|
299
344
|
const absLocal = resolvePath(filePath);
|
|
300
345
|
|
|
301
|
-
|
|
302
|
-
const needSha = opts.immutable;
|
|
303
|
-
const sha256 = needSha ? await sha256File(filePath) : undefined;
|
|
346
|
+
const sha256 = await sha256File(filePath);
|
|
304
347
|
|
|
305
348
|
// Attempt to resume
|
|
306
349
|
let state = opts.resume
|
|
307
|
-
? findResumableStateForFile(projectId, absLocal, destKey)
|
|
350
|
+
? findResumableStateForFile(projectId, absLocal, destKey, fingerprint)
|
|
308
351
|
: null;
|
|
309
352
|
let initRes;
|
|
310
353
|
if (state) {
|
|
@@ -315,7 +358,7 @@ async function putOne(projectId, filePath, opts) {
|
|
|
315
358
|
initRes = {
|
|
316
359
|
upload_id: state.upload_id,
|
|
317
360
|
mode: poll.mode ?? state.mode,
|
|
318
|
-
parts: poll.parts ?? state.parts,
|
|
361
|
+
parts: poll.parts ?? state.parts ?? [],
|
|
319
362
|
part_count: poll.part_count ?? state.part_count,
|
|
320
363
|
part_size_bytes: poll.part_size_bytes ?? state.part_size_bytes,
|
|
321
364
|
};
|
|
@@ -345,6 +388,7 @@ async function putOne(projectId, filePath, opts) {
|
|
|
345
388
|
parts: initRes.parts,
|
|
346
389
|
parts_done: {},
|
|
347
390
|
sha256,
|
|
391
|
+
...fingerprint,
|
|
348
392
|
started_at: new Date().toISOString(),
|
|
349
393
|
};
|
|
350
394
|
if (opts.resume) saveState(state);
|
|
@@ -360,22 +404,18 @@ async function putOne(projectId, filePath, opts) {
|
|
|
360
404
|
etags[parseInt(pn, 10) - 1] = typeof pd === "string" ? { etag: pd, sha256: undefined } : pd;
|
|
361
405
|
}
|
|
362
406
|
|
|
363
|
-
// Presigned URLs are signed WITHOUT ChecksumAlgorithm (see gateway
|
|
364
|
-
// s3-presign.ts). The client-asserted sha256 declared at init is the
|
|
365
|
-
// integrity attestation — no x-amz-checksum-sha256 header on PUTs, and
|
|
366
|
-
// the gateway trusts the declared value at complete when S3 has none.
|
|
367
407
|
const todo = initRes.parts.filter((p) => !(state.parts_done || {})[String(p.part_number)]);
|
|
368
408
|
await withConcurrency(todo, opts.concurrency, async (part) => {
|
|
369
|
-
const { etag } = await putPart(filePath, part);
|
|
370
|
-
etags[part.part_number - 1] = { etag };
|
|
371
|
-
state.parts_done[String(part.part_number)] = { etag };
|
|
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 };
|
|
372
412
|
if (opts.resume) saveState(state);
|
|
373
|
-
log(opts, { event: "part", upload_id: state.upload_id, part_number: part.part_number, etag });
|
|
413
|
+
log(opts, { event: "part", upload_id: state.upload_id, part_number: part.part_number, etag, sha256: partSha256 });
|
|
374
414
|
});
|
|
375
415
|
|
|
376
416
|
// Complete
|
|
377
417
|
const body = initRes.mode === "multipart"
|
|
378
|
-
? { parts: etags.map((e, i) => ({ part_number: i + 1, etag: e.etag })) }
|
|
418
|
+
? { parts: etags.map((e, i) => ({ part_number: i + 1, etag: e.etag, sha256: e.sha256 })) }
|
|
379
419
|
: {};
|
|
380
420
|
const result = await getSdk().blobs.completeUploadSession(projectId, state.upload_id, body, {
|
|
381
421
|
contentType: opts.contentType ?? guessContentType(destKey),
|
|
@@ -399,37 +439,31 @@ async function putPart(filePath, part) {
|
|
|
399
439
|
const chunks = [];
|
|
400
440
|
for await (const c of stream) chunks.push(c);
|
|
401
441
|
const body = Buffer.concat(chunks);
|
|
442
|
+
const checksum = sha256BufferHexAndBase64(body);
|
|
402
443
|
|
|
403
|
-
const res = await fetch(part.url, {
|
|
444
|
+
const res = await fetch(part.url, {
|
|
445
|
+
method: "PUT",
|
|
446
|
+
headers: checksumHeadersForPresignedUrl(part.url, checksum.base64),
|
|
447
|
+
body,
|
|
448
|
+
});
|
|
404
449
|
if (!res.ok) {
|
|
405
450
|
const errBody = await res.text().catch(() => "");
|
|
406
451
|
throw new Error(`Part ${part.part_number} PUT failed: ${res.status} ${res.statusText}${errBody ? " — " + errBody.slice(0, 200) : ""}`);
|
|
407
452
|
}
|
|
408
453
|
const etag = res.headers.get("etag") ?? "";
|
|
409
|
-
return { etag };
|
|
454
|
+
return { etag, sha256: checksum.hex };
|
|
410
455
|
}
|
|
411
456
|
|
|
412
457
|
async function withConcurrency(items, limit, worker) {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
await
|
|
419
|
-
for (let i = running.length - 1; i >= 0; i--) {
|
|
420
|
-
if (isSettled(running[i])) running.splice(i, 1);
|
|
421
|
-
}
|
|
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);
|
|
422
464
|
}
|
|
423
465
|
}
|
|
424
|
-
await Promise.all(
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function isSettled(p) {
|
|
428
|
-
const marker = {};
|
|
429
|
-
return Promise.race([p, marker]).then(
|
|
430
|
-
(v) => v !== marker,
|
|
431
|
-
() => true,
|
|
432
|
-
);
|
|
466
|
+
await Promise.all(Array.from({ length: workerCount }, runWorker));
|
|
433
467
|
}
|
|
434
468
|
|
|
435
469
|
async function put(projectId, argv) {
|
|
@@ -464,6 +498,7 @@ async function get(projectId, argv) {
|
|
|
464
498
|
opts.project = opts.project || projectId;
|
|
465
499
|
const resolvedId = resolveProjectId(opts.project);
|
|
466
500
|
if (opts.positional.length === 0) die("Key required");
|
|
501
|
+
if (opts.positional.length > 1) die("blob get expects exactly one key");
|
|
467
502
|
if (!opts.output) die("--output <file> required");
|
|
468
503
|
const key = opts.positional[0];
|
|
469
504
|
|
|
@@ -510,6 +545,7 @@ async function rm(projectId, argv) {
|
|
|
510
545
|
opts.project = opts.project || projectId;
|
|
511
546
|
const resolvedId = resolveProjectId(opts.project);
|
|
512
547
|
if (opts.positional.length === 0) die("Key required");
|
|
548
|
+
if (opts.positional.length > 1) die("blob rm expects exactly one key");
|
|
513
549
|
const key = opts.positional[0];
|
|
514
550
|
|
|
515
551
|
try {
|
|
@@ -529,6 +565,7 @@ async function diagnose(projectId, argv) {
|
|
|
529
565
|
opts.project = opts.project || projectId;
|
|
530
566
|
const resolvedId = resolveProjectId(opts.project);
|
|
531
567
|
if (opts.positional.length === 0) die("URL required");
|
|
568
|
+
if (opts.positional.length > 1) die("blob diagnose expects exactly one URL");
|
|
532
569
|
const url = opts.positional[0];
|
|
533
570
|
|
|
534
571
|
try {
|
|
@@ -556,6 +593,7 @@ async function sign(projectId, argv) {
|
|
|
556
593
|
opts.project = opts.project || projectId;
|
|
557
594
|
const resolvedId = resolveProjectId(opts.project);
|
|
558
595
|
if (opts.positional.length === 0) die("Key required");
|
|
596
|
+
if (opts.positional.length > 1) die("blob sign expects exactly one key");
|
|
559
597
|
const key = opts.positional[0];
|
|
560
598
|
|
|
561
599
|
try {
|
package/lib/deploy-v2.mjs
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
import { getSdk } from "./sdk.mjs";
|
|
33
33
|
import { reportSdkError, fail } from "./sdk-errors.mjs";
|
|
34
34
|
import { API, allowanceAuthHeaders, getActiveProjectId, resolveProjectId } from "./config.mjs";
|
|
35
|
+
import { normalizeArgv } from "./argparse.mjs";
|
|
35
36
|
|
|
36
37
|
const APPLY_HELP = `run402 deploy apply — Unified deploy primitive (v1.34+)
|
|
37
38
|
|
|
@@ -778,19 +779,18 @@ function rejectLegacySecretManifest(spec, details) {
|
|
|
778
779
|
}
|
|
779
780
|
|
|
780
781
|
async function resumeCmd(args) {
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
}
|
|
782
|
+
const parsed = parseDeploySubcommandArgs(args, {
|
|
783
|
+
command: "deploy resume",
|
|
784
|
+
help: RESUME_HELP,
|
|
785
|
+
booleanFlags: ["--quiet"],
|
|
786
|
+
});
|
|
787
|
+
const [operationId] = expectPositionals(parsed.positionals, {
|
|
788
|
+
command: "run402 deploy resume <operation_id>",
|
|
789
|
+
min: 1,
|
|
790
|
+
max: 1,
|
|
791
|
+
missing: "Missing <operation_id>.",
|
|
792
|
+
});
|
|
793
|
+
const opts = { operationId, quiet: Boolean(parsed.flags["--quiet"]) };
|
|
794
794
|
|
|
795
795
|
allowanceAuthHeaders("/deploy/v2/operations");
|
|
796
796
|
|
|
@@ -805,12 +805,19 @@ async function resumeCmd(args) {
|
|
|
805
805
|
}
|
|
806
806
|
|
|
807
807
|
async function listCmd(args) {
|
|
808
|
-
const
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
808
|
+
const parsed = parseDeploySubcommandArgs(args, {
|
|
809
|
+
command: "deploy list",
|
|
810
|
+
help: LIST_HELP,
|
|
811
|
+
valueFlags: ["--project", "--limit"],
|
|
812
|
+
});
|
|
813
|
+
expectPositionals(parsed.positionals, {
|
|
814
|
+
command: "run402 deploy list [--project <id>] [--limit <n>]",
|
|
815
|
+
max: 0,
|
|
816
|
+
});
|
|
817
|
+
const opts = {
|
|
818
|
+
project: parsed.flags["--project"] ?? null,
|
|
819
|
+
limit: parsed.flags["--limit"] === undefined ? null : parsePositiveInt(parsed.flags["--limit"], "--limit"),
|
|
820
|
+
};
|
|
814
821
|
|
|
815
822
|
const project = resolveProjectId(opts.project);
|
|
816
823
|
allowanceAuthHeaders("/deploy/v2/operations");
|
|
@@ -826,19 +833,18 @@ async function listCmd(args) {
|
|
|
826
833
|
}
|
|
827
834
|
|
|
828
835
|
async function eventsCmd(args) {
|
|
829
|
-
const
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
}
|
|
836
|
+
const parsed = parseDeploySubcommandArgs(args, {
|
|
837
|
+
command: "deploy events",
|
|
838
|
+
help: EVENTS_HELP,
|
|
839
|
+
valueFlags: ["--project"],
|
|
840
|
+
});
|
|
841
|
+
const [operationId] = expectPositionals(parsed.positionals, {
|
|
842
|
+
command: "run402 deploy events <operation_id>",
|
|
843
|
+
min: 1,
|
|
844
|
+
max: 1,
|
|
845
|
+
missing: "Missing <operation_id>.",
|
|
846
|
+
});
|
|
847
|
+
const opts = { operationId, project: parsed.flags["--project"] ?? null };
|
|
842
848
|
|
|
843
849
|
const project = resolveProjectId(opts.project);
|
|
844
850
|
allowanceAuthHeaders("/deploy/v2/operations");
|
|
@@ -868,20 +874,24 @@ async function releaseCmd(args) {
|
|
|
868
874
|
}
|
|
869
875
|
|
|
870
876
|
async function releaseGetCmd(args) {
|
|
871
|
-
const
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
877
|
+
const parsed = parseDeploySubcommandArgs(args, {
|
|
878
|
+
command: "deploy release get",
|
|
879
|
+
help: RELEASE_GET_HELP,
|
|
880
|
+
valueFlags: ["--project", "--site-limit"],
|
|
881
|
+
});
|
|
882
|
+
const [releaseId] = expectPositionals(parsed.positionals, {
|
|
883
|
+
command: "run402 deploy release get <release_id>",
|
|
884
|
+
min: 1,
|
|
885
|
+
max: 1,
|
|
886
|
+
missing: "Missing <release_id>.",
|
|
887
|
+
});
|
|
888
|
+
const opts = {
|
|
889
|
+
releaseId,
|
|
890
|
+
project: parsed.flags["--project"] ?? null,
|
|
891
|
+
siteLimit: parsed.flags["--site-limit"] === undefined
|
|
892
|
+
? null
|
|
893
|
+
: parsePositiveInt(parsed.flags["--site-limit"], "--site-limit"),
|
|
894
|
+
};
|
|
885
895
|
|
|
886
896
|
const project = resolveProjectId(opts.project);
|
|
887
897
|
|
|
@@ -896,12 +906,21 @@ async function releaseGetCmd(args) {
|
|
|
896
906
|
}
|
|
897
907
|
|
|
898
908
|
async function releaseActiveCmd(args) {
|
|
899
|
-
const
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
909
|
+
const parsed = parseDeploySubcommandArgs(args, {
|
|
910
|
+
command: "deploy release active",
|
|
911
|
+
help: RELEASE_ACTIVE_HELP,
|
|
912
|
+
valueFlags: ["--project", "--site-limit"],
|
|
913
|
+
});
|
|
914
|
+
expectPositionals(parsed.positionals, {
|
|
915
|
+
command: "run402 deploy release active [--project <id>] [--site-limit <n>]",
|
|
916
|
+
max: 0,
|
|
917
|
+
});
|
|
918
|
+
const opts = {
|
|
919
|
+
project: parsed.flags["--project"] ?? null,
|
|
920
|
+
siteLimit: parsed.flags["--site-limit"] === undefined
|
|
921
|
+
? null
|
|
922
|
+
: parsePositiveInt(parsed.flags["--site-limit"], "--site-limit"),
|
|
923
|
+
};
|
|
905
924
|
|
|
906
925
|
const project = resolveProjectId(opts.project);
|
|
907
926
|
|
|
@@ -916,14 +935,21 @@ async function releaseActiveCmd(args) {
|
|
|
916
935
|
}
|
|
917
936
|
|
|
918
937
|
async function releaseDiffCmd(args) {
|
|
919
|
-
const
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
938
|
+
const parsed = parseDeploySubcommandArgs(args, {
|
|
939
|
+
command: "deploy release diff",
|
|
940
|
+
help: RELEASE_DIFF_HELP,
|
|
941
|
+
valueFlags: ["--project", "--from", "--to", "--limit"],
|
|
942
|
+
});
|
|
943
|
+
expectPositionals(parsed.positionals, {
|
|
944
|
+
command: "run402 deploy release diff --from <target> --to <target>",
|
|
945
|
+
max: 0,
|
|
946
|
+
});
|
|
947
|
+
const opts = {
|
|
948
|
+
project: parsed.flags["--project"] ?? null,
|
|
949
|
+
from: parsed.flags["--from"] ?? null,
|
|
950
|
+
to: parsed.flags["--to"] ?? null,
|
|
951
|
+
limit: parsed.flags["--limit"] === undefined ? null : parsePositiveInt(parsed.flags["--limit"], "--limit"),
|
|
952
|
+
};
|
|
927
953
|
if (!opts.from || !opts.to) {
|
|
928
954
|
fail({
|
|
929
955
|
code: "BAD_USAGE",
|
|
@@ -1076,9 +1102,80 @@ function redactResolveInput(input) {
|
|
|
1076
1102
|
return copy;
|
|
1077
1103
|
}
|
|
1078
1104
|
|
|
1105
|
+
function parseDeploySubcommandArgs(rawArgs, { command, help, valueFlags = [], booleanFlags = [] }) {
|
|
1106
|
+
const args = normalizeArgv(rawArgs);
|
|
1107
|
+
const valueFlagSet = new Set(valueFlags);
|
|
1108
|
+
const booleanFlagSet = new Set(booleanFlags);
|
|
1109
|
+
const numericFlagSet = new Set(["--limit", "--site-limit"]);
|
|
1110
|
+
const allowedFlags = new Set([...valueFlags, ...booleanFlags, "--help", "-h"]);
|
|
1111
|
+
const flags = {};
|
|
1112
|
+
const positionals = [];
|
|
1113
|
+
|
|
1114
|
+
for (let i = 0; i < args.length; i++) {
|
|
1115
|
+
const arg = args[i];
|
|
1116
|
+
if (arg === "--help" || arg === "-h") {
|
|
1117
|
+
console.log(help);
|
|
1118
|
+
process.exit(0);
|
|
1119
|
+
}
|
|
1120
|
+
if (valueFlagSet.has(arg)) {
|
|
1121
|
+
const value = args[i + 1];
|
|
1122
|
+
if (value === undefined || (typeof value === "string" && value.startsWith("--"))) {
|
|
1123
|
+
if (numericFlagSet.has(arg)) parsePositiveInt(value, arg);
|
|
1124
|
+
fail({
|
|
1125
|
+
code: "BAD_USAGE",
|
|
1126
|
+
message: `${arg} requires a value`,
|
|
1127
|
+
details: { flag: arg },
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
flags[arg] = value;
|
|
1131
|
+
i += 1;
|
|
1132
|
+
continue;
|
|
1133
|
+
}
|
|
1134
|
+
if (booleanFlagSet.has(arg)) {
|
|
1135
|
+
flags[arg] = true;
|
|
1136
|
+
continue;
|
|
1137
|
+
}
|
|
1138
|
+
if (typeof arg === "string" && arg.startsWith("-")) {
|
|
1139
|
+
fail({
|
|
1140
|
+
code: "BAD_USAGE",
|
|
1141
|
+
message: `Unknown flag for ${command}: ${arg}`,
|
|
1142
|
+
details: { flag: arg, allowed_flags: [...allowedFlags] },
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
positionals.push(arg);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
return { flags, positionals };
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function expectPositionals(positionals, { command, min = 0, max = min, missing = "Missing required argument." }) {
|
|
1152
|
+
if (positionals.length < min) {
|
|
1153
|
+
fail({
|
|
1154
|
+
code: "BAD_USAGE",
|
|
1155
|
+
message: missing,
|
|
1156
|
+
hint: command,
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
if (positionals.length > max) {
|
|
1160
|
+
fail({
|
|
1161
|
+
code: "BAD_USAGE",
|
|
1162
|
+
message: `Unexpected argument for ${command}: ${positionals[max]}`,
|
|
1163
|
+
hint: `Use \`${command}\`.`,
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
return positionals;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1079
1169
|
function parsePositiveInt(value, flag) {
|
|
1080
|
-
|
|
1081
|
-
|
|
1170
|
+
if (typeof value !== "string" || !/^\d+$/.test(value)) {
|
|
1171
|
+
fail({
|
|
1172
|
+
code: "BAD_USAGE",
|
|
1173
|
+
message: `${flag} must be a positive integer.`,
|
|
1174
|
+
details: { flag, value },
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
const parsed = Number.parseInt(value, 10);
|
|
1178
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1) {
|
|
1082
1179
|
fail({
|
|
1083
1180
|
code: "BAD_USAGE",
|
|
1084
1181
|
message: `${flag} must be a positive integer.`,
|
package/lib/functions.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { reportSdkError, fail } from "./sdk-errors.mjs";
|
|
|
5
5
|
import { assertKnownFlags, hasHelp, normalizeArgv, parseIntegerFlag, validateRegularFile } from "./argparse.mjs";
|
|
6
6
|
|
|
7
7
|
const FUNCTION_LOG_REQUEST_ID_RE = /^req_[A-Za-z0-9_-]{4,128}$/;
|
|
8
|
+
const FUNCTION_LOG_TAIL_MAX = 1000;
|
|
8
9
|
|
|
9
10
|
const HELP = `run402 functions — Manage serverless functions
|
|
10
11
|
|
|
@@ -180,7 +181,7 @@ async function deploy(projectId, name, args) {
|
|
|
180
181
|
if (args[i] === "--file" && args[i + 1]) opts.file = args[++i];
|
|
181
182
|
if (args[i] === "--timeout") opts.timeout = parseIntegerFlag("--timeout", args[++i], { min: 1 });
|
|
182
183
|
if (args[i] === "--memory") opts.memory = parseIntegerFlag("--memory", args[++i], { min: 1 });
|
|
183
|
-
if (args[i] === "--deps" && args[i + 1]) opts.deps = args[++i]
|
|
184
|
+
if (args[i] === "--deps" && args[i + 1]) opts.deps = parseDepsFlag(args[++i]);
|
|
184
185
|
if (args[i] === "--schedule" && i + 1 < args.length) opts.schedule = args[++i];
|
|
185
186
|
}
|
|
186
187
|
if (!opts.file) {
|
|
@@ -239,7 +240,7 @@ async function logs(projectId, name, args) {
|
|
|
239
240
|
let requestId = undefined;
|
|
240
241
|
let follow = false;
|
|
241
242
|
for (let i = 0; i < args.length; i++) {
|
|
242
|
-
if (args[i] === "--tail") tail = parseIntegerFlag("--tail", args[++i], { min: 1 });
|
|
243
|
+
if (args[i] === "--tail") tail = parseIntegerFlag("--tail", args[++i], { min: 1, max: FUNCTION_LOG_TAIL_MAX });
|
|
243
244
|
if (args[i] === "--since" && args[i + 1]) since = args[++i];
|
|
244
245
|
if (args[i] === "--request-id" && args[i + 1]) requestId = args[++i];
|
|
245
246
|
if (args[i] === "--follow") follow = true;
|
|
@@ -360,6 +361,13 @@ async function update(projectId, name, args) {
|
|
|
360
361
|
if (args[i] === "--timeout") timeout = parseIntegerFlag("--timeout", args[++i], { min: 1 });
|
|
361
362
|
if (args[i] === "--memory") memory = parseIntegerFlag("--memory", args[++i], { min: 1 });
|
|
362
363
|
}
|
|
364
|
+
if (scheduleRemove && schedule !== undefined) {
|
|
365
|
+
fail({
|
|
366
|
+
code: "BAD_USAGE",
|
|
367
|
+
message: "--schedule and --schedule-remove are mutually exclusive",
|
|
368
|
+
details: { flags: ["--schedule", "--schedule-remove"] },
|
|
369
|
+
});
|
|
370
|
+
}
|
|
363
371
|
|
|
364
372
|
const updateOpts = {};
|
|
365
373
|
if (scheduleRemove || schedule === "") {
|
|
@@ -385,8 +393,10 @@ async function update(projectId, name, args) {
|
|
|
385
393
|
}
|
|
386
394
|
}
|
|
387
395
|
|
|
388
|
-
async function list(projectId) {
|
|
396
|
+
async function list(projectId, args = []) {
|
|
389
397
|
assertRequiredProject(projectId, "run402 functions list <project_id>");
|
|
398
|
+
assertKnownFlags(args, ["--help", "-h"]);
|
|
399
|
+
assertNoExtraPositionals(args, "run402 functions list <project_id>");
|
|
390
400
|
try {
|
|
391
401
|
const data = await getSdk().functions.list(projectId);
|
|
392
402
|
console.log(JSON.stringify(data, null, 2));
|
|
@@ -395,8 +405,10 @@ async function list(projectId) {
|
|
|
395
405
|
}
|
|
396
406
|
}
|
|
397
407
|
|
|
398
|
-
async function deleteFunction(projectId, name) {
|
|
408
|
+
async function deleteFunction(projectId, name, args = []) {
|
|
399
409
|
assertRequiredProjectAndName(projectId, name, "run402 functions delete <project_id> <name>");
|
|
410
|
+
assertKnownFlags(args, ["--help", "-h"]);
|
|
411
|
+
assertNoExtraPositionals(args, "run402 functions delete <project_id> <name>");
|
|
400
412
|
try {
|
|
401
413
|
await getSdk().functions.delete(projectId, name);
|
|
402
414
|
console.log(JSON.stringify({ status: "ok", message: `Function '${name}' deleted.` }));
|
|
@@ -417,8 +429,8 @@ export async function run(sub, args) {
|
|
|
417
429
|
case "invoke": await invoke(args[0], args[1], args.slice(2)); break;
|
|
418
430
|
case "logs": await logs(args[0], args[1], args.slice(2)); break;
|
|
419
431
|
case "update": await update(args[0], args[1], args.slice(2)); break;
|
|
420
|
-
case "list": await list(args[0]); break;
|
|
421
|
-
case "delete": await deleteFunction(args[0], args[1]); break;
|
|
432
|
+
case "list": await list(args[0], args.slice(1)); break;
|
|
433
|
+
case "delete": await deleteFunction(args[0], args[1], args.slice(2)); break;
|
|
422
434
|
default:
|
|
423
435
|
console.error(`Unknown subcommand: ${sub}\n`);
|
|
424
436
|
console.log(HELP);
|
|
@@ -426,6 +438,30 @@ export async function run(sub, args) {
|
|
|
426
438
|
}
|
|
427
439
|
}
|
|
428
440
|
|
|
441
|
+
function parseDepsFlag(value) {
|
|
442
|
+
const raw = String(value);
|
|
443
|
+
const deps = raw.split(",").map((entry) => entry.trim());
|
|
444
|
+
if (deps.some((entry) => entry === "")) {
|
|
445
|
+
fail({
|
|
446
|
+
code: "BAD_USAGE",
|
|
447
|
+
message: "--deps must be a comma-separated list of non-empty package specs",
|
|
448
|
+
details: { flag: "--deps", value: raw },
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
return deps;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function assertNoExtraPositionals(args, usage) {
|
|
455
|
+
if (args.length > 0) {
|
|
456
|
+
fail({
|
|
457
|
+
code: "BAD_USAGE",
|
|
458
|
+
message: `Unexpected argument: ${args[0]}`,
|
|
459
|
+
hint: usage,
|
|
460
|
+
details: { argument: args[0] },
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
429
465
|
function assertRequiredProject(projectId, usage) {
|
|
430
466
|
if (!projectId || String(projectId).startsWith("-")) {
|
|
431
467
|
fail({
|
package/package.json
CHANGED
|
@@ -16,8 +16,9 @@ export declare class Blobs {
|
|
|
16
16
|
* presigned S3 URLs — they do NOT pass through the gateway, so uploads
|
|
17
17
|
* are not double-billed as API calls and large files stream efficiently.
|
|
18
18
|
*
|
|
19
|
-
* Pass `immutable: true` to produce a content-addressed URL
|
|
20
|
-
* computes
|
|
19
|
+
* Pass `immutable: true` to produce a content-addressed URL. The SDK always
|
|
20
|
+
* computes the SHA-256 digest required by the upload API; `immutable` only
|
|
21
|
+
* controls URL/cache semantics.
|
|
21
22
|
*
|
|
22
23
|
* @throws {ProjectNotFound} if `projectId` is not in the provider.
|
|
23
24
|
*/
|