promigence 0.1.0-beta.15 → 0.1.0-beta.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/account.js +366 -59
  2. package/dist/agent.js +18 -5
  3. package/dist/billing.js +22 -5
  4. package/dist/bin.js +94 -14
  5. package/dist/bundle.js +15 -6
  6. package/dist/capacity.js +7 -2
  7. package/dist/commands.d.ts +51 -6
  8. package/dist/commands.js +741 -169
  9. package/dist/devcontainer.d.ts +4 -1
  10. package/dist/devcontainer.js +21 -10
  11. package/dist/dirsnap.js +208 -91
  12. package/dist/errfix.js +372 -11
  13. package/dist/generated.js +11 -5
  14. package/dist/github.js +5 -1
  15. package/dist/gitlab.js +3 -2
  16. package/dist/guide.js +44 -26
  17. package/dist/handlers.js +66 -19
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.js +1 -1
  20. package/dist/init.d.ts +1 -0
  21. package/dist/init.js +4 -1
  22. package/dist/install.js +1 -1
  23. package/dist/io.d.ts +3 -0
  24. package/dist/io.js +41 -7
  25. package/dist/live.js +86 -16
  26. package/dist/login.js +92 -20
  27. package/dist/mcp.js +40 -13
  28. package/dist/money.js +2 -0
  29. package/dist/orgs.js +91 -19
  30. package/dist/pricing.d.ts +2 -0
  31. package/dist/pricing.js +11 -5
  32. package/dist/report.js +95 -13
  33. package/dist/routes.js +3 -0
  34. package/dist/sdk/caller.js +1 -1
  35. package/dist/sdk/client.d.ts +173 -19
  36. package/dist/sdk/client.js +450 -126
  37. package/dist/sdk/command-errors.d.ts +1 -0
  38. package/dist/sdk/command-errors.js +3 -1
  39. package/dist/sdk/context.d.ts +20 -4
  40. package/dist/sdk/context.js +79 -4
  41. package/dist/sdk/core.d.ts +56 -5
  42. package/dist/sdk/core.js +44 -4
  43. package/dist/sdk/handles.js +2 -1
  44. package/dist/sdk/http2.js +27 -1
  45. package/dist/sdk/models.gen.d.ts +269 -30
  46. package/dist/sdk/proxy.js +33 -4
  47. package/dist/sdk/sandbox.d.ts +38 -13
  48. package/dist/sdk/sandbox.js +29 -5
  49. package/dist/sdk/transport-worker.js +10 -1
  50. package/dist/ssh.js +58 -9
  51. package/dist/surface.d.ts +10 -2
  52. package/dist/surface.js +145 -76
  53. package/dist/uploadguard.js +54 -8
  54. package/llms.txt +31 -20
  55. package/package.json +1 -1
package/dist/account.js CHANGED
@@ -1,11 +1,28 @@
1
- import { bool, client, KEPT_BY_HOLDERS, list, nexts, optimizeForFlag, positional, required, secretListLines, str, } from "./commands.js";
1
+ import { bool, client, KEPT_BY_HOLDERS, keyOriginText, list, nexts, optimizeForFlag, positional, required, secretListLines, str, } from "./commands.js";
2
2
  import { queuedLines } from "./capacity.js";
3
3
  import { duration } from "./report.js";
4
- import { emit, ms, okEnvelope, overMost, parseCount, parseDuration, parseUsd, usageError } from "./io.js";
4
+ import { emit, errorEnvelope, errorTextLines, ms, okEnvelope, overMost, parseCount, parseDuration, parseUsd, usageError } from "./io.js";
5
+ import { ERRORS_DOCS_URL, PromigenceError, blake3Hex } from "./sdk/index.js";
5
6
  import { sh } from "./term.js";
6
7
  import { autoReloadShort, creditsLine, resumeAllPausedForCredits } from "./billing.js";
7
8
  import { dollars } from "./money.js";
8
- const usd = (n) => n === undefined ? "—" : `$${n.toFixed(n < 0.01 && n > 0 ? 4 : 2)}`;
9
+ import { livePricing } from "./pricing.js";
10
+ const usd = (n) => n === undefined ? "—" : n < 0 ? `-${usd(-n)}` : `$${n.toFixed(n < 0.01 && n > 0 ? 4 : 2)}`;
11
+ export function keyAccess(prefix) {
12
+ if (!prefix)
13
+ return undefined;
14
+ return prefix.startsWith("prom_read_") ? "read" : prefix.startsWith("prom_run_") ? "run" : prefix.startsWith("prom_api_") ? "full" : undefined;
15
+ }
16
+ export const KIND_WORDS = {
17
+ sandbox: "sandboxes", build: "builds", egress: "data out", storage: "storage",
18
+ failure_credit: "credited back for failures of ours",
19
+ };
20
+ export const kindWords = (k) => KIND_WORDS[k] ?? k.replace(/_/g, " ");
21
+ export function spendLine(spend, byKind, monthTotal) {
22
+ const parts = Object.entries(byKind ?? {}).filter(([, v]) => v !== 0).sort(([, a], [, b]) => b - a);
23
+ const month = monthTotal === undefined ? "" : `this month ${usd(monthTotal)} (the bill: promigence billing usage) · `;
24
+ return `spent: ${month}all time ${usd(spend)}${parts.length ? ` (${parts.map(([k, v]) => `${kindWords(k)} ${usd(v)}`).join(" · ")})` : ""}`;
25
+ }
9
26
  export const PAYMENT_METHOD_NAMES = {
10
27
  apple_pay: "Apple Pay",
11
28
  google_pay: "Google Pay",
@@ -48,6 +65,28 @@ export async function concurrencyOf(c, me) {
48
65
  export function concurrencyText(c) {
49
66
  return `${c.in_use}${c.at_least ? "+" : ""} of ${c.limit} in use (paused sandboxes are not counted)`;
50
67
  }
68
+ export function planDisplay(me, b) {
69
+ const name = b?.plan_status?.name;
70
+ return typeof name === "string" && name ? name : planName(me, b);
71
+ }
72
+ function gbText(bytes) {
73
+ const r = Math.round((bytes / 2 ** 30) * 10) / 10;
74
+ return Number.isInteger(r) ? String(r) : r.toFixed(1);
75
+ }
76
+ export function storageLine(me) {
77
+ const s = me?.storage;
78
+ if (!s || typeof s.used_bytes !== "number")
79
+ return undefined;
80
+ const lim = me?.limits ?? {};
81
+ const of = (used, max) => `${gbText(used)} GB${typeof max === "number" ? ` of ${max} GB` : ""}`;
82
+ const capMax = lim.max_capture_storage_gb;
83
+ const captures = typeof s.captures_bytes === "number" ? s.captures_bytes : 0;
84
+ return `storage: ${of(s.used_bytes, lim.max_storage_gb)} used · saved failures ${of(captures, capMax)}` +
85
+ (typeof capMax === "number" ? ", the oldest removed at that limit" : "");
86
+ }
87
+ export function sessionLimit(seconds) {
88
+ return seconds % 3600 === 0 ? `${seconds / 3600} h per sandbox` : `${span(seconds)} per sandbox`;
89
+ }
51
90
  export function planName(me, b) {
52
91
  if (typeof me?.plan === "string" && me.plan)
53
92
  return me.plan;
@@ -66,6 +105,34 @@ function span(seconds) {
66
105
  return `${Math.floor(s / 60)}m${s % 60 ? `${s % 60}s` : ""}`;
67
106
  return `${s}s`;
68
107
  }
108
+ function clock(at) {
109
+ const d = new Date(at);
110
+ if (Number.isNaN(d.getTime()))
111
+ return { hm: at, day: "" };
112
+ const MON = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
113
+ return { hm: d.toISOString().slice(11, 16), day: `${d.getUTCDate()} ${MON[d.getUTCMonth()]}` };
114
+ }
115
+ export function checksSummary(raw) {
116
+ if (raw === null || typeof raw !== "object")
117
+ return undefined;
118
+ const c = raw;
119
+ if (typeof c.success_pct !== "number" || typeof c.checks !== "number")
120
+ return undefined;
121
+ const lines = [` checks, last 24 h: ${c.success_pct.toFixed(1)}% passed (${typeof c.ok === "number" ? c.ok : "?"} of ${c.checks})`];
122
+ const failures = (Array.isArray(c.failures) ? c.failures : [])
123
+ .map((f) => (f && typeof f === "object" ? String(f.at ?? "") : ""))
124
+ .filter(Boolean)
125
+ .sort();
126
+ if (failures.length) {
127
+ const [a, b] = [clock(failures[0]), clock(failures[failures.length - 1])];
128
+ const span = a.day === b.day ? `${a.hm} to ${b.hm} UTC on ${a.day}` : `${a.hm} UTC ${a.day} to ${b.hm} UTC ${b.day}`;
129
+ lines.push(` failed checks: ${failures.length}, ${failures.length === 1 ? `at ${a.hm} UTC on ${a.day}` : span}`);
130
+ }
131
+ const lastFailed = c.last?.ok === false;
132
+ if (lastFailed)
133
+ lines.push(" the last check failed: new sandboxes may not start right now");
134
+ return { lines, lastFailed };
135
+ }
69
136
  const status = async (ctx) => {
70
137
  const c = client(ctx);
71
138
  let failure;
@@ -88,12 +155,17 @@ const status = async (ctx) => {
88
155
  ...(conc ? { concurrency: conc } : {}),
89
156
  ...(me?.spend === undefined ? {} : { spend: me.spend }),
90
157
  }, next);
91
- const lines = [`promigence status: ${health?.ok ?? svc ? "up" : "no answer"}${health?.version ? ` (api ${health.version})` : ""}`];
158
+ const checks = checksSummary(svc?.checks);
159
+ const up = health?.ok ?? Boolean(svc);
160
+ const head = !up ? "no answer" : checks?.lastFailed ? "up, but the last check failed" : "up";
161
+ const lines = [`promigence status: ${head}${health?.version ? ` (api ${health.version})` : ""}`];
92
162
  for (const s of services) {
93
163
  lines.push(` ${s.name}: ${s.state ?? "?"}`);
94
164
  }
95
165
  if (!services.length)
96
166
  lines.push(" (no service reported)");
167
+ if (checks)
168
+ lines.push(...checks.lines);
97
169
  if (conc)
98
170
  lines.push(` your concurrency: ${concurrencyText(conc)}`);
99
171
  else if (!c.authenticated)
@@ -104,28 +176,40 @@ const status = async (ctx) => {
104
176
  };
105
177
  const me = async (ctx) => {
106
178
  const c = client(ctx);
107
- const [info, outcomes] = await Promise.all([c.me(), c.outcomes().catch(() => undefined)]);
179
+ const [info, outcomes, bill, keys] = await Promise.all([
180
+ c.me(), c.outcomes().catch(() => undefined), c.billing.usage().catch(() => undefined),
181
+ c.request({ method: "GET", path: "/keys" })
182
+ .catch(() => undefined),
183
+ ]);
108
184
  const conc = await concurrencyOf(c, info);
109
185
  const plan = planName(info);
110
186
  const trial = info.trial;
111
187
  const next = nexts(["promigence billing usage", "this month's spend by tier, and the credit that paid it"], ["promigence status", "is the service up, and how much of your limit is in use"]);
112
188
  const org = info.org && info.org !== info.org_id ? `${info.org} (${info.org_id})` : (info.org_id ?? "org");
113
- const lines = [`promigence me: ${org}${info.key?.key_id ? ` · key ${info.key.key_id}` : ""}`];
189
+ const from = keyOriginText(c.keyOrigin);
190
+ const row = keys?.keys?.find((k) => k.current === true || (info.key?.key_id !== undefined && k.key_id === info.key.key_id));
191
+ const access = row?.access ?? keyAccess(info.key?.prefix);
192
+ const expires = row === undefined ? undefined : (row.expires_at ?? null);
193
+ const keyWords = [access ? `${access} access` : "", expires === undefined ? "" : `expires ${expires ?? "never"}`].filter(Boolean).join(", ");
194
+ const lines = [`promigence me: ${org}${info.key?.key_id ? ` · key ${info.key.key_id}${keyWords ? ` (${keyWords})` : ""}` : ""}${from ? ` from ${from}` : ""}`];
114
195
  if (info.user && typeof info.user === "object" && info.user.email)
115
196
  lines.push(` signed up as ${info.user.email}`);
116
- if (plan)
117
- lines.push(` plan: ${plan}${trial?.on_trial ? " (trial)" : ""}`);
197
+ const shown = planDisplay(info, bill) ?? plan;
198
+ if (shown)
199
+ lines.push(` plan: ${shown}${trial?.on_trial ? " (trial)" : ""}`);
118
200
  if (conc)
119
201
  lines.push(` concurrency: ${concurrencyText(conc)}`);
120
- if (trial) {
202
+ const billCredits = creditsLine(bill);
203
+ if (billCredits)
204
+ lines.push(` ${billCredits}`);
205
+ else if (trial) {
121
206
  const available = trial.credits_available ?? trial.credits_remaining;
122
207
  const held = trial.credits_reserved ?? 0;
123
208
  lines.push(` credits: ${dollars(available)} available${held > 0 ? ` (${dollars(held)} held by running work)` : ""}`);
124
209
  }
125
210
  if (typeof info.spend === "number") {
126
- const kinds = Object.entries(info.spend_detail?.by_kind ?? {}).filter(([, v]) => v > 0);
127
- const kindName = { sandbox: "sandboxes", build: "builds", egress: "data out", storage: "storage" };
128
- lines.push(` spent: ${usd(info.spend)}${kinds.length ? ` (${kinds.map(([k, v]) => `${kindName[k] ?? k} ${usd(v)}`).join(" · ")})` : ""}`);
211
+ const month = bill?.usage?.total;
212
+ lines.push(` ${spendLine(info.spend, info.spend_detail?.by_kind, typeof month === "number" ? month : undefined)}`);
129
213
  }
130
214
  if (info.spend === undefined && info.trial === undefined) {
131
215
  lines.push(" spend and credit: shown to an owner's key of the default project (or `promigence billing usage --ledger` for this key's project)");
@@ -137,21 +221,34 @@ const me = async (ctx) => {
137
221
  const lim = info.limits ?? {};
138
222
  const shape = [
139
223
  typeof lim.max_sandbox_vcpus === "number" ? `sandboxes up to ${lim.max_sandbox_vcpus} vCPU` : "",
140
- typeof lim.max_session_s === "number" ? `sessions up to ${span(lim.max_session_s)}` : "",
224
+ typeof lim.max_session_s === "number" ? sessionLimit(lim.max_session_s) : "",
141
225
  ].filter(Boolean);
142
226
  if (shape.length)
143
227
  lines.push(` limits: ${shape.join(" · ")}`);
228
+ const stored = storageLine(info);
229
+ if (stored)
230
+ lines.push(` ${stored}`);
144
231
  if (outcomes && outcomes.sandboxes > 0) {
145
232
  const [head, ...rest] = outcomeLines(outcomes);
146
233
  lines.push(` ${head}:`, ...rest.map((l) => ` ${l}`));
147
234
  }
148
235
  return emit(ctx.mode, okEnvelope(ctx.cmd.name, {
149
- me: info,
236
+ me: { ...info, default_spend_cap: typeof cap === "number" && cap > 0 ? cap : null },
237
+ ...(c.keyOrigin ? { credential: { ...c.keyOrigin, from: from ?? null } } : {}),
238
+ ...(info.key?.key_id ? { key: { key_id: info.key.key_id, access: access ?? null, ...(expires === undefined ? {} : { expires_at: expires }) } } : {}),
150
239
  ...(outcomes ? { outcomes } : {}),
151
240
  ...(plan ? { plan } : {}),
152
241
  ...(conc ? { concurrency: { in_use: conc.in_use, limit: conc.limit, source: conc.source, ...(conc.at_least ? { at_least: true } : {}) } } : {}),
153
242
  }, next), lines, 0, ctx.pointer);
154
243
  };
244
+ export function spentLine(total, byKind) {
245
+ const name = {
246
+ sandbox: "sandboxes", build: "builds", egress: "data out", storage: "storage", failure_credit: "credited back",
247
+ };
248
+ const parts = Object.entries(byKind ?? {}).filter(([, v]) => typeof v === "number" && Math.abs(v) >= 0.005);
249
+ const money = (v) => (v < 0 ? `-${usd(-v)}` : usd(v));
250
+ return `${usd(total)}${parts.length ? ` (${parts.map(([k, v]) => `${name[k] ?? k.replace(/_/g, " ")} ${money(v)}`).join(" · ")})` : ""}`;
251
+ }
155
252
  export function outcomeLines(o) {
156
253
  const usd = (n) => `$${n.toFixed(2)}`;
157
254
  const lines = [`this month (since ${o.since.slice(0, 10)})`];
@@ -161,7 +258,7 @@ export function outcomeLines(o) {
161
258
  }
162
259
  lines.push(` runs ${o.runs.toLocaleString("en-US")}`);
163
260
  const ready = o.ready_ms_p50 ?? null;
164
- lines.push(` sandboxes ${o.sandboxes.toLocaleString("en-US")}${ready === null ? "" : `, ready in ${ms(ready)} (median)`}`);
261
+ lines.push(` sandboxes ${o.sandboxes.toLocaleString("en-US")}${ready === null ? "" : `, ready in ${ms(ready)} on our side (median, from the request to a ready sandbox)`}`);
165
262
  if (o.failures_ours > 0)
166
263
  lines.push(` failures of ours ${o.failures_ours}, not billed${o.failures_ours_usd > 0 ? ` (${usd(o.failures_ours_usd)} given back)` : ""}`);
167
264
  const speedup = o.batch_speedup ?? null;
@@ -177,6 +274,15 @@ export function outcomeLines(o) {
177
274
  lines.push(` paused, not billed ${hours.toFixed(1)} h${paidNot === null ? "" : ` (${usd(paidNot)} you did not pay)`}`);
178
275
  return lines;
179
276
  }
277
+ export function belowList(prices, tier, row) {
278
+ const rate = prices?.tiers.find((t) => t.name === tier)?.per_second_usd;
279
+ if (rate === undefined || row.amount === undefined || !row.seconds)
280
+ return "";
281
+ const list = row.seconds * rate;
282
+ if (row.amount >= list - 0.005)
283
+ return "";
284
+ return ` (${usd(list)} at this tier's rate; part of it billed at a lower rate, such as preemptible)`;
285
+ }
180
286
  const billingUsage = async (ctx) => {
181
287
  const limit = limitOf(ctx, 20, 1000);
182
288
  const wantLedger = bool(ctx, "--ledger");
@@ -184,13 +290,14 @@ const billingUsage = async (ctx) => {
184
290
  if (cursor !== undefined && !wantLedger)
185
291
  throw usageError("--cursor pages the ledger", "Pass it with --ledger.");
186
292
  const c = client(ctx);
187
- const [b, me] = await Promise.all([
293
+ const [b, me, prices] = await Promise.all([
188
294
  c.billing.usage().catch((e) => {
189
295
  if (wantLedger && e?.code === "forbidden")
190
296
  return undefined;
191
297
  throw e;
192
298
  }),
193
299
  c.me().catch(() => undefined),
300
+ livePricing(c).catch(() => undefined),
194
301
  ]);
195
302
  const page = wantLedger ? await c.billing.ledgerPage({ limit, ...(cursor === undefined ? {} : { cursor }) }) : undefined;
196
303
  const entries = page?.items ?? [];
@@ -233,9 +340,9 @@ const billingUsage = async (ctx) => {
233
340
  ...(wantLedger ? { ledger: entries, next_cursor: page?.nextCursor ?? null } : {}),
234
341
  }, next);
235
342
  const period = b.period?.start ? `${b.period.start.slice(0, 10)} → ${(b.period.end ?? "").slice(0, 10)}` : "this month";
236
- const lines = [`promigence billing usage: ${usd(usage.total)} (${period}, plan ${plan ?? "?"}${onTrial ? ", on the trial" : ""})`];
343
+ const lines = [`promigence billing usage: ${usd(usage.total)} (${period}, plan ${planDisplay(me, b) ?? "?"}${onTrial ? ", on the trial" : ""})`];
237
344
  const detail = (row) => `${usd(row.amount)}${row.seconds === undefined ? "" : ` · ${row.seconds}s`}${row.count === undefined ? "" : ` · ${row.count}`}` +
238
- `${row.gib === undefined ? "" : ` · ${row.gib} GiB`}${row.entries === undefined ? "" : ` · ${row.entries} entries`}` +
345
+ `${row.gib === undefined ? "" : ` · ${row.gib} GiB`}${row.entries === undefined ? "" : ` · ${row.entries} ${row.entries === 1 ? "entry" : "entries"}`}` +
239
346
  `${row.episodes === undefined ? "" : ` · ${row.episodes} episodes`}`;
240
347
  for (const [k, v] of Object.entries(usage)) {
241
348
  if (k === "total" || v === null || typeof v !== "object")
@@ -248,7 +355,8 @@ const billingUsage = async (ctx) => {
248
355
  const r = inner;
249
356
  if (!r.amount && !r.seconds && !r.count && !r.entries && !r.episodes)
250
357
  continue;
251
- lines.push(` ${k} ${name}: ${detail(r)}`);
358
+ const label = k === "other" ? kindWords(name) : `${k} ${name}`;
359
+ lines.push(` ${label}: ${detail(r)}${k === "sandboxes" ? belowList(prices, name, r) : ""}`);
252
360
  }
253
361
  continue;
254
362
  }
@@ -260,7 +368,7 @@ const billingUsage = async (ctx) => {
260
368
  lines.push(` paid from credits: ${usd(usage["credits_applied"])}`);
261
369
  }
262
370
  const trial = b.trial;
263
- const credits = creditsLine(b.credits) ?? (trial?.credits_remaining === undefined ? undefined : `credits: ${dollars(trial.credits_remaining)} available`);
371
+ const credits = creditsLine(b) ?? (trial?.credits_remaining === undefined ? undefined : `credits: ${dollars(trial.credits_remaining)} available`);
264
372
  if (credits)
265
373
  lines.push(` ${credits}`);
266
374
  if (b.auto_reload)
@@ -461,20 +569,54 @@ function stateFlag(ctx, states) {
461
569
  }
462
570
  const BUILD_STATES = ["queued", "running", "verified", "built", "rejected", "failed"];
463
571
  const RUN_STATES = ["running", "completed", "cap_killed", "aborted"];
572
+ export const FILTER_PAGES = 10;
573
+ export async function filteredPage(page, keep, limit, cursor) {
574
+ const rows = [];
575
+ const from = (at) => (at === undefined ? {} : { cursor: at });
576
+ let at = cursor;
577
+ let read = 0;
578
+ for (let n = 0; n < FILTER_PAGES; n++) {
579
+ const p = await page({ limit: 500, ...from(at) });
580
+ for (let i = 0; i < p.items.length; i++) {
581
+ const row = p.items[i];
582
+ if (!keep(row))
583
+ continue;
584
+ if (rows.length < limit) {
585
+ rows.push(row);
586
+ continue;
587
+ }
588
+ read += i;
589
+ const nextCursor = i === 0 ? at ?? null : (await page({ limit: i, ...from(at) })).nextCursor ?? at ?? null;
590
+ return { rows, nextCursor, read, bounded: false };
591
+ }
592
+ read += p.items.length;
593
+ if (!p.nextCursor)
594
+ return { rows, nextCursor: null, read, bounded: false };
595
+ at = p.nextCursor;
596
+ }
597
+ return { rows, nextCursor: at ?? null, read, bounded: true };
598
+ }
599
+ function moreLines(more, filtered, rows, limit, read, what, bounded = false) {
600
+ if (!more)
601
+ return [];
602
+ return [filtered && (bounded || rows < limit) ? ` the newest ${read} ${what} were read; older ones: --cursor ${more}` : ` more: --cursor ${more}`];
603
+ }
464
604
  const snapshotBuilds = async (ctx) => {
465
605
  const wanted = stateFlag(ctx, BUILD_STATES);
466
606
  const limit = limitOf(ctx, 20, 500);
467
607
  const cursor = str(ctx, "--cursor");
468
- const page = await client(ctx).snapshots.buildsPage({ limit: wanted === undefined ? limit : 500, ...(cursor === undefined ? {} : { cursor }) });
469
- const matched = wanted === undefined ? page.items : page.items.filter((b) => b.state === wanted);
470
- const builds = matched.slice(0, limit);
471
- const more = builds.length === matched.length ? page.nextCursor : null;
608
+ const c = client(ctx);
609
+ const found = wanted === undefined
610
+ ? await c.snapshots.buildsPage({ limit, ...(cursor === undefined ? {} : { cursor }) }).then((p) => ({ rows: p.items, nextCursor: p.nextCursor, read: p.items.length, bounded: false }))
611
+ : await filteredPage((o) => c.snapshots.buildsPage(o), (b) => b.state === wanted, limit, cursor);
612
+ const builds = found.rows;
613
+ const more = found.nextCursor;
472
614
  const next = nexts(...(more ? [[`${sh `promigence snapshot builds --limit ${String(limit)}`}${wanted ? sh ` --state ${wanted}` : ""}${sh ` --cursor ${more}`}`, "the next page"]] : []), ...(builds.length ? [[sh `promigence snapshot status ${builds[0].build_id} --log 40`, "the newest build, with the tail of its log"]] : []));
473
615
  const lines = [
474
616
  `promigence snapshot builds: ${builds.length}${wanted ? ` in state ${wanted}` : ""}`,
475
617
  ...builds.map((b) => ` ${b.build_id} ${b.state} ${b.snapshot_id ?? "—"}${buildAliasNote(b)}${b.error?.code ? ` ${b.error.code}` : ""}`),
476
618
  ...(builds.length ? [] : [" (no builds)"]),
477
- ...(more ? [` more: --cursor ${more}`] : []),
619
+ ...moreLines(more, wanted !== undefined, builds.length, limit, found.read, "builds", found.bounded),
478
620
  ];
479
621
  return emit(ctx.mode, okEnvelope(ctx.cmd.name, { builds, count: builds.length, next_cursor: more }, next), lines, 0, ctx.pointer);
480
622
  };
@@ -492,24 +634,41 @@ const snapshotAlias = async (ctx) => {
492
634
  if (clear && name) {
493
635
  throw usageError("--clear takes no name", `Use either ${sh `promigence snapshot alias ${arg} NAME`} or ${sh `promigence snapshot alias ${arg} --clear`}.`);
494
636
  }
495
- const res = await client(ctx).snapshots.alias(arg, clear ? null : name);
637
+ const moveAlias = bool(ctx, "--move-alias");
638
+ if (moveAlias && clear) {
639
+ throw usageError("--move-alias takes a name, not --clear", `Use ${sh `promigence snapshot alias ${arg} NAME --move-alias`}.`);
640
+ }
641
+ const res = await client(ctx).snapshots.alias(arg, clear ? null : name, moveAlias ? { moveAlias: true } : {});
496
642
  const next = nexts(["promigence snapshot list", "your snapshots and their names"]);
497
- return emit(ctx.mode, okEnvelope(ctx.cmd.name, { snapshot_id: res.snapshot_id, alias: res.alias ?? null }, next), [`snapshot ${res.snapshot_id}: ${clear ? "alias cleared" : `alias ${res.alias}`}`], 0, ctx.pointer);
643
+ const said = res.warnings;
644
+ const warnings = Array.isArray(said) ? said.filter((w) => typeof w === "string") : [];
645
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, { snapshot_id: res.snapshot_id, alias: res.alias ?? null, ...(warnings.length ? { warnings } : {}) }, next), [`snapshot ${res.snapshot_id}: ${clear ? "alias cleared" : `alias ${res.alias}`}`, ...warnings.map((w) => ` warning: ${w}`)], 0, ctx.pointer);
498
646
  };
499
647
  const runList = async (ctx) => {
500
648
  const wanted = stateFlag(ctx, RUN_STATES);
649
+ const labels = Object.entries(labelsOf(ctx));
501
650
  const limit = limitOf(ctx, 20, 500);
502
651
  const cursor = str(ctx, "--cursor");
503
- const page = await client(ctx).runs.listPage({ limit: wanted === undefined ? limit : 500, ...(cursor === undefined ? {} : { cursor }) });
504
- const matched = wanted === undefined ? page.items : page.items.filter((r) => r.state === wanted);
505
- const runs = matched.slice(0, limit);
506
- const more = runs.length === matched.length ? page.nextCursor : null;
507
- const next = nexts(...(more ? [[`${sh `promigence run list --limit ${String(limit)}`}${wanted ? sh ` --state ${wanted}` : ""}${sh ` --cursor ${more}`}`, "the next page"]] : []), ...(runs.length ? [[sh `promigence run report ${runs[0].run_id}`, "the newest run's validity report"]] : []));
652
+ const filtering = wanted !== undefined || labels.length > 0;
653
+ const labelsOfRun = (r) => (r.labels ?? {});
654
+ const labelled = (r) => labels.every(([k, v]) => labelsOfRun(r)[k] === v);
655
+ const c = client(ctx);
656
+ const found = filtering
657
+ ? await filteredPage((o) => c.runs.listPage(o), (r) => (wanted === undefined || r.state === wanted) && labelled(r), limit, cursor)
658
+ : await c.runs.listPage({ limit, ...(cursor === undefined ? {} : { cursor }) }).then((p) => ({ rows: p.items, nextCursor: p.nextCursor, read: p.items.length, bounded: false }));
659
+ const runs = found.rows;
660
+ const more = found.nextCursor;
661
+ const labelArgs = labels.map(([k, v]) => sh ` --label ${`${k}=${v}`}`).join("");
662
+ const next = nexts(...(more ? [[`${sh `promigence run list --limit ${String(limit)}`}${wanted ? sh ` --state ${wanted}` : ""}${labelArgs}${sh ` --cursor ${more}`}`, "the next page"]] : []), ...(runs.length ? [[sh `promigence run report ${runs[0].run_id}`, "the newest run's validity report"]] : []));
663
+ const labelText = (r) => {
664
+ const own = Object.entries(labelsOfRun(r));
665
+ return own.length ? ` ${own.map(([k, v]) => `${k}=${v}`).join(",")}` : "";
666
+ };
508
667
  const lines = [
509
- `promigence run list: ${runs.length}${wanted ? ` in state ${wanted}` : ""}`,
510
- ...runs.map((r) => ` ${r.run_id} ${r.state} ${r.count ?? "?"}× ${r.snapshot_id || r.snapshot || "—"} ${r.started_at ?? ""}`),
668
+ `promigence run list: ${runs.length}${wanted ? ` in state ${wanted}` : ""}${labels.length ? ` with ${labels.map(([k, v]) => `${k}=${v}`).join(", ")}` : ""}`,
669
+ ...runs.map((r) => ` ${r.run_id} ${r.state} ${r.count ?? "?"}× ${r.snapshot_id || r.snapshot || "—"} ${r.started_at ?? ""}${labelText(r)}`),
511
670
  ...(runs.length ? [] : [" (no runs)"]),
512
- ...(more ? [` more: --cursor ${more}`] : []),
671
+ ...moreLines(more, filtering, runs.length, limit, found.read, "runs", found.bounded),
513
672
  ];
514
673
  return emit(ctx.mode, okEnvelope(ctx.cmd.name, { runs, count: runs.length, next_cursor: more }, next), lines, 0, ctx.pointer);
515
674
  };
@@ -531,7 +690,14 @@ function labelsOf(ctx) {
531
690
  }
532
691
  return out;
533
692
  }
534
- const HELD_STATES = ["queued", "placing", "ready", "running", "paused"];
693
+ const HELD_STATES = ["queued", "ready", "running", "paused"];
694
+ export function sandboxListFlags(f) {
695
+ return [
696
+ ...(f.limit === undefined ? [] : [sh `--limit ${String(f.limit)}`]),
697
+ ...(f.states?.length ? [sh `--state ${f.states.join(",")}`] : []),
698
+ ...Object.entries(f.labels ?? {}).map(([k, v]) => sh `--label ${`${k}=${v}`}`),
699
+ ].join(" ");
700
+ }
535
701
  export function pauseReasonText(reason) {
536
702
  if (reason === "spend_cap")
537
703
  return "spend cap reached";
@@ -543,8 +709,9 @@ const sandboxList = async (ctx) => {
543
709
  const labels = labelsOf(ctx);
544
710
  const states = list(ctx, "--state").flatMap((s) => s.split(",")).map((s) => s.trim()).filter(Boolean);
545
711
  const cursor = str(ctx, "--cursor");
712
+ const limit = limitOf(ctx, 50);
546
713
  const page = await client(ctx).sandboxes.listPage({
547
- limit: limitOf(ctx, 50),
714
+ limit,
548
715
  ...(Object.keys(labels).length ? { labels } : {}),
549
716
  ...(states.length ? { state: states } : {}),
550
717
  ...(cursor === undefined ? {} : { cursor }),
@@ -564,10 +731,10 @@ const sandboxList = async (ctx) => {
564
731
  held = { count, atLeast: live.nextCursor !== null };
565
732
  }
566
733
  }
567
- const heldCmd = sh `promigence sandbox list --state ${HELD_STATES.join(",")}`;
734
+ const heldCmd = ["promigence sandbox list", sandboxListFlags({ states: HELD_STATES, labels })].filter(Boolean).join(" ");
568
735
  const firstLive = sandboxes.find((s) => s.state !== "ended");
569
736
  const next = nexts(...(held ? [[heldCmd, "the sandboxes still held (live or paused) that this page does not show"]] : []), ...(sandboxes.length ? [[sh `promigence sandbox info ${sandboxes[0].sandbox_id}`, "one sandbox: its run, its state and the execs it has run"]] : []), ...(page.nextCursor
570
- ? [[sh `promigence sandbox list --cursor ${page.nextCursor}`, "the next page"]]
737
+ ? [[["promigence sandbox list", sandboxListFlags({ limit, states, labels }), sh `--cursor ${page.nextCursor}`].filter(Boolean).join(" "), "the next page"]]
571
738
  : []), ...(firstLive ? [["promigence sandbox kill SID", "end one: put its id from this list in place of SID"]] : []));
572
739
  const shown = Object.entries(labels).map(([k, v]) => `${k}=${v}`).join(" ");
573
740
  const lines = [
@@ -577,7 +744,7 @@ const sandboxList = async (ctx) => {
577
744
  return ` ${s.sandbox_id} ${s.state}${why ? ` (${why})` : ""}${s.outcome ? ` (${s.outcome})` : ""} ${s.run_id ?? ""}`;
578
745
  }),
579
746
  ...(sandboxes.length ? [] : [" (no sandboxes)"]),
580
- ...(page.nextCursor ? [` more: --cursor ${page.nextCursor}`] : []),
747
+ ...(page.nextCursor ? [` more: ${sandboxListFlags({ limit, states, labels })} --cursor ${page.nextCursor}`] : []),
581
748
  ...(held ? [` ${held.atLeast ? "at least " : ""}${held.count} live or paused not on this page: ${heldCmd}`] : []),
582
749
  ];
583
750
  return emit(ctx.mode, okEnvelope(ctx.cmd.name, {
@@ -642,10 +809,29 @@ export function sandboxFactLines(s, facts, nowMs = Date.now()) {
642
809
  if (elapsed !== undefined && facts.perSecond !== undefined) {
643
810
  const secs = Math.ceil(elapsed / 1000);
644
811
  const rate = `$${facts.perSecond.toFixed(9).replace(/0+$/, "").replace(/\.$/, "")}`;
645
- out.push(` cost so far: about ${usd(secs * facts.perSecond)} (${secs} s at ${rate}/s)`);
812
+ const billed = typeof s["cost_usd"] === "number" && s["state"] !== "ended" ? `; billed so far ${usd(s["cost_usd"])}` : "";
813
+ out.push(` cost so far: about ${usd(secs * facts.perSecond)} (${secs} s at ${rate}/s${billed})`);
646
814
  }
647
815
  return out;
648
816
  }
817
+ export function timeLeftMs(s) {
818
+ if (s.state === "ended" || typeof s.timeout_ms !== "number" || typeof s.elapsed_ms !== "number")
819
+ return undefined;
820
+ return Math.max(0, s.timeout_ms - s.elapsed_ms);
821
+ }
822
+ export function pausedLines(sid, s) {
823
+ if (s.state !== "paused" || pauseReasonText(s.pause_reason))
824
+ return [];
825
+ const wakes = Number(s.auto_pause_s ?? 0) > 0;
826
+ return [
827
+ wakes
828
+ ? " it has an idle rule, so the next request (exec, files, ssh) wakes it, whether it paused itself when idle or was paused by hand"
829
+ : ` it stays paused until \`${sh `promigence sandbox resume ${sid}`}\` (it has no idle rule, so a request does not wake it)`,
830
+ s.pause_expires_at
831
+ ? ` its files and state are kept until ${s.pause_expires_at} unless it is resumed before then`
832
+ : " its files and state are kept at least 30 days from the pause; resuming starts them again",
833
+ ];
834
+ }
649
835
  export function supportStopLines(s) {
650
836
  if (s.outcome !== "stopped_by_support")
651
837
  return [];
@@ -668,11 +854,15 @@ const sandboxInfo = async (ctx) => {
668
854
  : [];
669
855
  const next = nexts(...limitNext, ...(execs.length ? [[sh `promigence exec output ${execs[execs.length - 1].exec_id} --sandbox ${sid}`, "what its last command printed"]] : []), ...(ended
670
856
  ? (s.run_id ? [[sh `promigence run report ${s.run_id} --sandbox ${sid}`, "how it ended, in its run's report"]] : [])
671
- : why ? [] : [[sh `promigence exec --sandbox ${sid} -- CMD`, "run something in it"]]));
857
+ : why ? []
858
+ : s.state === "paused" && !(Number(s.auto_pause_s ?? 0) > 0)
859
+ ? [[sh `promigence sandbox resume ${sid}`, "bring it back; a request does not wake it"]]
860
+ : [[sh `promigence exec --sandbox ${sid} -- CMD`, "run something in it"]]));
672
861
  const lines = [
673
862
  `sandbox ${sid}: ${s.state}${why ? ` (${why})` : ""}${s.outcome ? ` (${s.outcome})` : ""}${s.run_id ? ` · run ${s.run_id}` : ""}`,
674
863
  ...supportStopLines(s),
675
864
  ...(why ? [" its files and state are kept, and paused time is not billed"] : []),
865
+ ...pausedLines(sid, s),
676
866
  ...queuedLines([s]),
677
867
  ...(s.snapshot_id ? [` snapshot ${s.snapshot_id}`] : []),
678
868
  ...sandboxFactLines(s, facts),
@@ -682,15 +872,22 @@ const sandboxInfo = async (ctx) => {
682
872
  ...(Number(s.auto_pause_s ?? 0) > 0 ? [` pauses itself after ${s.auto_pause_s} s idle`] : []),
683
873
  ...execs.map((e) => ` ${e.exec_id} exit ${e.exit_code ?? "—"} ${(e.argv ?? []).join(" ")}`),
684
874
  ...(execs.length ? [] : [" (no execs recorded)"]),
875
+ ...(ended ? [] : [` background commands (exec --background): ${sh `promigence exec ps --sandbox ${sid}`}`]),
685
876
  ];
877
+ const left = timeLeftMs(s);
878
+ const billed = typeof s["cost_usd"] === "number" ? s["cost_usd"] : undefined;
879
+ const costSoFar = ended
880
+ ? billed
881
+ : facts.perSecond !== undefined && typeof s.elapsed_ms === "number"
882
+ ? Math.round(Math.ceil(s.elapsed_ms / 1000) * facts.perSecond * 1e6) / 1e6
883
+ : undefined;
686
884
  return emit(ctx.mode, okEnvelope(ctx.cmd.name, {
687
885
  sandbox: s,
688
886
  ...(facts.tier ? { tier: facts.tier } : {}),
689
- ...(facts.perSecond !== undefined && typeof s.elapsed_ms === "number"
690
- ? { cost_so_far_usd: Math.round(Math.ceil(s.elapsed_ms / 1000) * facts.perSecond * 1e6) / 1e6 }
691
- : {}),
692
- ...(ended && typeof s["cost_usd"] === "number"
693
- ? { cost_usd: s["cost_usd"], billable_seconds: s["billable_seconds"] }
887
+ ...(left === undefined ? {} : { time_left_ms: left }),
888
+ ...(costSoFar === undefined ? {} : { cost_so_far_usd: costSoFar }),
889
+ ...(ended && billed !== undefined
890
+ ? { cost_usd: billed, billable_seconds: s["billable_seconds"] }
694
891
  : {}),
695
892
  }, next), lines, 0, ctx.pointer);
696
893
  };
@@ -705,7 +902,7 @@ const sandboxPause = async (ctx) => {
705
902
  `sandbox ${sid}: paused${res.already_paused ? " (already was)" : ""}`,
706
903
  " it holds no capacity and bills no seconds while parked",
707
904
  ...(took === undefined ? [] : [` took ${ms(took)}`]),
708
- ...(bytes?.allocated === undefined ? [] : [` parked image: ${Math.round(bytes.allocated / 1048576)} MiB`]),
905
+ ...(bytes?.allocated === undefined ? [] : [` holds ${bytes.allocated >= 1073741824 ? `${(bytes.allocated / 1073741824).toFixed(1)} GiB` : `${Math.round(bytes.allocated / 1048576)} MiB`} of storage while paused`]),
709
906
  ...(killed > 0 ? [` ended ${killed} background process${killed === 1 ? "" : "es"} (started with exec --background)`] : []),
710
907
  ...(res.warnings ?? []).map((w) => ` warning: ${w}`),
711
908
  ];
@@ -789,7 +986,7 @@ const sandboxAutopause = async (ctx) => {
789
986
  throw usageError("--mode and --off ask for opposite things", "--off removes the idle rule; --mode says what idle means under one.");
790
987
  }
791
988
  const rule = (s, m) => s > 0
792
- ? `pauses itself after ${s} s idle${m === "activity" ? " (activity: no CPU, no bytes, no open connection)" : ""}`
989
+ ? `pauses itself after ${s} s idle${m === "activity" ? " (activity: also CPU under 5% of one core, no network bytes, no outside connection)" : ""}`
793
990
  : "never pauses itself on idle";
794
991
  const next = nexts([sh `promigence sandbox info ${sid}`, "the sandbox as it stands now"]);
795
992
  if (after === undefined && !off && mode === undefined) {
@@ -818,18 +1015,122 @@ const sandboxAutopause = async (ctx) => {
818
1015
  ];
819
1016
  return emit(ctx.mode, okEnvelope(ctx.cmd.name, { ...res, sandbox_id: sid, auto_pause_s: secs, auto_pause_mode: m, ...(pausedNow ? { paused: true } : {}) }, pausedNow ? nexts([sh `promigence sandbox resume ${sid}`, "bring it back: with the rule off, a request no longer wakes it"]) : next), lines, 0, ctx.pointer);
820
1017
  };
1018
+ export const PROBE_DIGEST_MAX_BYTES = 16 * 1024 * 1024;
1019
+ export async function pathNow(c, sid, path) {
1020
+ try {
1021
+ const { stat } = await c.sandboxes.files.stat(sid, path);
1022
+ if (stat.is_dir)
1023
+ return { kind: "dir" };
1024
+ if (stat.is_symlink)
1025
+ return { kind: "symlink" };
1026
+ if (stat.size > PROBE_DIGEST_MAX_BYTES)
1027
+ return { kind: "file", size: stat.size };
1028
+ const bytes = await c.sandboxes.files.get(sid, path);
1029
+ return { kind: "file", size: bytes.length, digest: `blake3:${blake3Hex(bytes)}` };
1030
+ }
1031
+ catch (e) {
1032
+ return e?.code === "not_found" ? { kind: "missing" } : undefined;
1033
+ }
1034
+ }
1035
+ export function ownDigests(paths) {
1036
+ const out = new Map();
1037
+ for (const p of paths) {
1038
+ const at = p.indexOf("=");
1039
+ if (at > 0)
1040
+ out.set(p.slice(0, at), p.slice(at + 1));
1041
+ }
1042
+ return out;
1043
+ }
1044
+ export function ownPathOf(bad, own) {
1045
+ for (const p of own.keys()) {
1046
+ const root = p.length > 1 ? p.replace(/\/+$/, "") : p;
1047
+ if (bad === p || bad === root || bad.startsWith(root === "/" ? "/" : `${root}/`))
1048
+ return p;
1049
+ }
1050
+ return undefined;
1051
+ }
1052
+ function nowWords(now) {
1053
+ if (!now)
1054
+ return undefined;
1055
+ if (now.kind === "missing")
1056
+ return "it does not exist";
1057
+ if (now.kind === "dir")
1058
+ return "it is a directory";
1059
+ if (now.kind === "symlink")
1060
+ return "it is a symbolic link";
1061
+ return now.digest ? `it is ${now.digest} now` : `it is a file of ${now.size} bytes`;
1062
+ }
1063
+ async function probeRefusal(ctx, c, sid, e) {
1064
+ if (!(e instanceof PromigenceError) || e.code !== "usage")
1065
+ throw e;
1066
+ const later = (path, digest) => sh `promigence sandbox probe ${sid} --path ${`${path}=${digest}`}`;
1067
+ if (/has nothing to compare against/.test(e.message)) {
1068
+ throw usageError(e.message, `Name the files to check: ${sh `promigence sandbox probe ${sid} --path /work/FILE`} answers with the file's digest now, ` +
1069
+ "and --path /work/FILE=<that digest> checks later that it has not changed.");
1070
+ }
1071
+ const m = /^(.+): the snapshot recorded no digest for this path\b/.exec(e.message);
1072
+ if (!m)
1073
+ throw e;
1074
+ const path = m[1];
1075
+ const now = await pathNow(c, sid, path);
1076
+ const digest = now?.kind === "file" ? now.digest : undefined;
1077
+ const fix = digest
1078
+ ? `Its digest now is ${digest}. To check later that it has not changed: ${later(path, digest)}`
1079
+ : now?.kind === "missing"
1080
+ ? `It does not exist in sandbox ${sid}: check the path (${sh `promigence files ls / --sandbox ${sid}`}).`
1081
+ : now?.kind === "file"
1082
+ ? `It is over ${PROBE_DIGEST_MAX_BYTES / 1048576} MiB, too large to read back here: name it as PATH=blake3:<hex> with a digest you computed (b3sum).`
1083
+ : `Name it as PATH=blake3:<hex> (${nowWords(now) ?? "the digest of the file's bytes"}).`;
1084
+ const err = usageError(e.message, fix);
1085
+ const next = digest ? nexts([later(path, digest), "check later that the file has not changed"]) : nexts([sh `promigence sandbox info ${sid}`, "the sandbox and its snapshot"]);
1086
+ return emit(ctx.mode, {
1087
+ ...errorEnvelope(ctx.cmd.name, err, next),
1088
+ sandbox_id: sid,
1089
+ ...(digest ? { digests: [{ path, digest }] } : {}),
1090
+ }, errorTextLines(ctx.cmd.name, err, 2, next), 2, ctx.pointer);
1091
+ }
821
1092
  const sandboxProbe = async (ctx) => {
822
1093
  const sid = positional(ctx, "a sandbox id", "promigence sandbox probe sbx_01");
823
1094
  const paths = list(ctx, "--path");
824
- const res = await client(ctx).sandboxes.probe(sid, ...(paths.length ? [{ paths }] : []));
1095
+ const own = ownDigests(paths);
1096
+ const c = client(ctx);
1097
+ const res = await c.sandboxes.probe(sid, ...(paths.length ? [{ paths }] : [])).catch((e) => probeRefusal(ctx, c, sid, e));
825
1098
  const ok = res.ok !== false;
826
- const env = { ...okEnvelope(ctx.cmd.name, { ...res, sandbox_id: sid }, nexts([sh `promigence sandbox info ${sid}`, "the sandbox, its run and the commands it ran"])), ok };
827
- const lines = [
828
- `sandbox ${sid}: ${ok ? "matches the verified environment" : "DIFFERS from the verified environment"}`,
829
- ...(res.first_bad_path ? [` first difference at ${res.first_bad_path}`] : []),
830
- ...(res.ms === undefined ? [] : [` checked in ${ms(res.ms)}`]),
831
- ];
832
- return emit(ctx.mode, env, lines, ok ? 0 : 3, ctx.pointer);
1099
+ const infoNext = nexts([sh `promigence sandbox info ${sid}`, "the sandbox, its run and the commands it ran"]);
1100
+ const took = res.ms === undefined ? [] : [` checked in ${ms(res.ms)}`];
1101
+ if (ok) {
1102
+ const allOwn = paths.length > 0 && own.size === paths.length;
1103
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, { ...res, sandbox_id: sid }, infoNext), [
1104
+ `sandbox ${sid}: ${allOwn ? "every path matches the digest you gave" : "matches the verified environment"}`,
1105
+ ...took,
1106
+ ], 0, ctx.pointer);
1107
+ }
1108
+ const bad = res.first_bad_path ?? undefined;
1109
+ const mine = bad === undefined ? undefined : ownPathOf(bad, own);
1110
+ const now = bad === undefined ? undefined : await pathNow(c, sid, bad);
1111
+ const words = nowWords(now);
1112
+ const where = bad === undefined ? "" : bad;
1113
+ const err = mine !== undefined
1114
+ ? new PromigenceError({
1115
+ code: "digest_mismatch", exitCode: 4, docs: `${ERRORS_DOCS_URL}#digest_mismatch`,
1116
+ message: now?.kind === "missing"
1117
+ ? `${where} does not exist in sandbox ${sid}, so it cannot match the digest you gave${mine !== bad ? ` for ${mine}` : ""}`
1118
+ : `${mine !== bad ? `${mine} does not match the digest you gave, first at ${where}` : `${where} does not match the digest you gave`} (${own.get(mine)})${words ? `: ${words}` : ""}`,
1119
+ fix: now?.kind === "missing"
1120
+ ? `Nothing failed on our side. Check the path (${sh `promigence files ls / --sandbox ${sid}`}), or what should have made the file.`
1121
+ : "Nothing failed on our side: the file is not what that digest says. Check the digest (blake3 of the file's bytes) and the path, or what produced the file.",
1122
+ })
1123
+ : new PromigenceError({
1124
+ code: "integrity_mismatch", exitCode: 3, docs: `${ERRORS_DOCS_URL}#integrity_mismatch`,
1125
+ message: `sandbox ${sid} differs from its verified snapshot${where ? ` at ${where}` : ""}${words ? ` (${words})` : ""}`,
1126
+ fix: "If a command you ran changed it, that is expected. If nothing did, this sandbox is not the verified environment: " +
1127
+ "start a new one from the same snapshot and probe it; if that one differs too, rebuild the snapshot and tell support@promigence.ai the sandbox id.",
1128
+ });
1129
+ return emit(ctx.mode, {
1130
+ ...errorEnvelope(ctx.cmd.name, err, infoNext),
1131
+ sandbox_id: sid, checked: res.checked, ms: res.ms, first_bad_path: bad ?? null,
1132
+ difference: { path: bad ?? null, ...(mine === undefined ? { against: "snapshot" } : { against: "your_digest", digest_given: own.get(mine) }), now: now ?? null },
1133
+ }, [...errorTextLines(ctx.cmd.name, err, err.cliExit, infoNext), ...took], err.cliExit, ctx.pointer);
833
1134
  };
834
1135
  const filesRm = async (ctx) => {
835
1136
  const path = positional(ctx, "a path", "promigence files rm /work/out.log --sandbox sbx_01");
@@ -839,8 +1140,14 @@ const filesRm = async (ctx) => {
839
1140
  const sid = required(ctx, "--sandbox", "Pass --sandbox sbx_<id> — the sandbox the path is in.");
840
1141
  const recursive = bool(ctx, "--recursive");
841
1142
  const res = await client(ctx).sandboxes.files.delete(sid, path, { recursive });
842
- const next = nexts([sh `promigence files ls --sandbox ${sid}`, "what is left"]);
843
- return emit(ctx.mode, okEnvelope(ctx.cmd.name, { sandbox_id: sid, path, deleted: res.deleted, existed: res.existed, recursive }, next), [`${sid}:${path}: ${res.deleted ? "deleted" : "not deleted"}${res.existed === false ? " (it was not there)" : ""}`], 0, ctx.pointer);
1143
+ const gone = res.existed === false;
1144
+ const dir = path.replace(/\/+$/, "").replace(/\/[^/]*$/, "") || "/";
1145
+ const next = gone
1146
+ ? nexts([sh `promigence files ls ${dir} --sandbox ${sid}`, "check the path: what is in its directory"])
1147
+ : nexts([sh `promigence files ls --sandbox ${sid}`, "what is left"]);
1148
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, { sandbox_id: sid, path, deleted: res.deleted, existed: res.existed, recursive }, next), [gone
1149
+ ? `${sid}:${path}: not there, nothing was deleted (check the path)`
1150
+ : `${sid}:${path}: ${res.deleted ? "deleted" : "not deleted"}`], 0, ctx.pointer);
844
1151
  };
845
1152
  const secretsList = async (ctx) => {
846
1153
  const { secrets } = await client(ctx).secrets.list();