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 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
- const UPLOAD_STATE_DIR = join(homedir(), ".run402", "uploads");
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: 1, max: 604800 });
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(UPLOAD_STATE_DIR, `${uploadId}.json`);
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
- mkdirSync(UPLOAD_STATE_DIR, { recursive: true });
271
- writeFileSync(join(UPLOAD_STATE_DIR, `${state.upload_id}.json`), JSON.stringify(state, null, 2));
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(UPLOAD_STATE_DIR, `${uploadId}.json`);
302
+ const path = join(uploadStateDir(), `${uploadId}.json`);
276
303
  if (existsSync(path)) unlinkSync(path);
277
304
  }
278
305
 
279
- function findResumableStateForFile(projectId, localPath, key) {
280
- if (!existsSync(UPLOAD_STATE_DIR)) return null;
281
- for (const f of readdirSync(UPLOAD_STATE_DIR)) {
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(UPLOAD_STATE_DIR, f), "utf8"));
285
- if (s.project_id === projectId && s.local_path === localPath && s.key === key) return s;
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
- // Compute sha256 for immutable uploads up front; otherwise lazy.
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, { method: "PUT", body });
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
- const running = [];
414
- for (const item of items) {
415
- const p = Promise.resolve().then(() => worker(item));
416
- running.push(p);
417
- if (running.length >= limit) {
418
- await Promise.race(running.map((r) => r.catch(() => {})));
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(running);
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 opts = { operationId: null, quiet: false };
782
- for (let i = 0; i < args.length; i++) {
783
- if (args[i] === "--help" || args[i] === "-h") { console.log(RESUME_HELP); process.exit(0); }
784
- if (args[i] === "--quiet") { opts.quiet = true; continue; }
785
- if (!args[i].startsWith("-") && !opts.operationId) opts.operationId = args[i];
786
- }
787
- if (!opts.operationId) {
788
- fail({
789
- code: "BAD_USAGE",
790
- message: "Missing <operation_id>.",
791
- hint: "run402 deploy resume <operation_id>",
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 opts = { project: null, limit: null };
809
- for (let i = 0; i < args.length; i++) {
810
- if (args[i] === "--help" || args[i] === "-h") { console.log(LIST_HELP); process.exit(0); }
811
- if (args[i] === "--project" && args[i + 1]) { opts.project = args[++i]; continue; }
812
- if (args[i] === "--limit") { opts.limit = parsePositiveInt(args[++i], "--limit"); continue; }
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 opts = { operationId: null, project: null };
830
- for (let i = 0; i < args.length; i++) {
831
- if (args[i] === "--help" || args[i] === "-h") { console.log(EVENTS_HELP); process.exit(0); }
832
- if (args[i] === "--project" && args[i + 1]) { opts.project = args[++i]; continue; }
833
- if (!args[i].startsWith("-") && !opts.operationId) opts.operationId = args[i];
834
- }
835
- if (!opts.operationId) {
836
- fail({
837
- code: "BAD_USAGE",
838
- message: "Missing <operation_id>.",
839
- hint: "run402 deploy events <operation_id>",
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 opts = { releaseId: null, project: null, siteLimit: null };
872
- for (let i = 0; i < args.length; i++) {
873
- if (args[i] === "--help" || args[i] === "-h") { console.log(RELEASE_GET_HELP); process.exit(0); }
874
- if (args[i] === "--project" && args[i + 1]) { opts.project = args[++i]; continue; }
875
- if (args[i] === "--site-limit" && args[i + 1]) { opts.siteLimit = parsePositiveInt(args[++i], "--site-limit"); continue; }
876
- if (!args[i].startsWith("-") && !opts.releaseId) opts.releaseId = args[i];
877
- }
878
- if (!opts.releaseId) {
879
- fail({
880
- code: "BAD_USAGE",
881
- message: "Missing <release_id>.",
882
- hint: "run402 deploy release get <release_id>",
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 opts = { project: null, siteLimit: null };
900
- for (let i = 0; i < args.length; i++) {
901
- if (args[i] === "--help" || args[i] === "-h") { console.log(RELEASE_ACTIVE_HELP); process.exit(0); }
902
- if (args[i] === "--project" && args[i + 1]) { opts.project = args[++i]; continue; }
903
- if (args[i] === "--site-limit" && args[i + 1]) { opts.siteLimit = parsePositiveInt(args[++i], "--site-limit"); continue; }
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 opts = { project: null, from: null, to: null, limit: null };
920
- for (let i = 0; i < args.length; i++) {
921
- if (args[i] === "--help" || args[i] === "-h") { console.log(RELEASE_DIFF_HELP); process.exit(0); }
922
- if (args[i] === "--project" && args[i + 1]) { opts.project = args[++i]; continue; }
923
- if (args[i] === "--from" && args[i + 1]) { opts.from = args[++i]; continue; }
924
- if (args[i] === "--to" && args[i + 1]) { opts.to = args[++i]; continue; }
925
- if (args[i] === "--limit" && args[i + 1]) { opts.limit = parsePositiveInt(args[++i], "--limit"); continue; }
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
- const parsed = Number(value);
1081
- if (!Number.isInteger(parsed) || parsed < 1) {
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].split(",");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "1.69.6",
3
+ "version": "1.69.7",
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": {
@@ -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 (the server
20
- * computes no hash — the SDK does it locally and attests via `sha256`).
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
  */