promigence 0.1.0-beta.14 → 0.1.0-beta.16

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 +359 -72
  2. package/dist/agent.js +18 -5
  3. package/dist/billing.js +274 -0
  4. package/dist/bin.js +91 -14
  5. package/dist/bundle.js +15 -6
  6. package/dist/capacity.js +7 -2
  7. package/dist/commands.d.ts +43 -6
  8. package/dist/commands.js +656 -162
  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 +289 -16
  13. package/dist/generated.js +31 -13
  14. package/dist/github.js +5 -1
  15. package/dist/gitlab.js +3 -2
  16. package/dist/guide.js +47 -27
  17. package/dist/handlers.js +76 -20
  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 +50 -12
  26. package/dist/login.js +96 -24
  27. package/dist/mcp.js +29 -11
  28. package/dist/money.js +10 -0
  29. package/dist/orgs.js +153 -12
  30. package/dist/pricing.d.ts +3 -0
  31. package/dist/pricing.js +17 -7
  32. package/dist/report.js +95 -13
  33. package/dist/routes.js +12 -4
  34. package/dist/sdk/caller.js +1 -1
  35. package/dist/sdk/client.d.ts +134 -23
  36. package/dist/sdk/client.js +201 -77
  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 +19 -4
  40. package/dist/sdk/context.js +74 -4
  41. package/dist/sdk/core.d.ts +32 -4
  42. package/dist/sdk/core.js +26 -4
  43. package/dist/sdk/http2.js +22 -1
  44. package/dist/sdk/models.gen.d.ts +663 -43
  45. package/dist/sdk/pty.d.ts +11 -14
  46. package/dist/sdk/pty.js +13 -11
  47. package/dist/sdk/sandbox.d.ts +26 -8
  48. package/dist/sdk/sandbox.js +28 -4
  49. package/dist/sdk/transport-worker.js +10 -1
  50. package/dist/ssh.js +58 -9
  51. package/dist/surface.d.ts +11 -3
  52. package/dist/surface.js +205 -76
  53. package/dist/uploadguard.js +30 -5
  54. package/llms.txt +44 -24
  55. package/package.json +1 -1
package/dist/account.js CHANGED
@@ -1,9 +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
- const usd = (n) => n === undefined ? "—" : `$${n.toFixed(n < 0.01 && n > 0 ? 4 : 2)}`;
7
+ import { autoReloadShort, creditsLine, resumeAllPausedForCredits } from "./billing.js";
8
+ import { dollars } from "./money.js";
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
+ }
7
26
  export const PAYMENT_METHOD_NAMES = {
8
27
  apple_pay: "Apple Pay",
9
28
  google_pay: "Google Pay",
@@ -46,6 +65,13 @@ export async function concurrencyOf(c, me) {
46
65
  export function concurrencyText(c) {
47
66
  return `${c.in_use}${c.at_least ? "+" : ""} of ${c.limit} in use (paused sandboxes are not counted)`;
48
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
+ export function sessionLimit(seconds) {
73
+ return seconds % 3600 === 0 ? `${seconds / 3600} h per sandbox` : `${span(seconds)} per sandbox`;
74
+ }
49
75
  export function planName(me, b) {
50
76
  if (typeof me?.plan === "string" && me.plan)
51
77
  return me.plan;
@@ -64,6 +90,34 @@ function span(seconds) {
64
90
  return `${Math.floor(s / 60)}m${s % 60 ? `${s % 60}s` : ""}`;
65
91
  return `${s}s`;
66
92
  }
93
+ function clock(at) {
94
+ const d = new Date(at);
95
+ if (Number.isNaN(d.getTime()))
96
+ return { hm: at, day: "" };
97
+ const MON = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
98
+ return { hm: d.toISOString().slice(11, 16), day: `${d.getUTCDate()} ${MON[d.getUTCMonth()]}` };
99
+ }
100
+ export function checksSummary(raw) {
101
+ if (raw === null || typeof raw !== "object")
102
+ return undefined;
103
+ const c = raw;
104
+ if (typeof c.success_pct !== "number" || typeof c.checks !== "number")
105
+ return undefined;
106
+ const lines = [` checks, last 24 h: ${c.success_pct.toFixed(1)}% passed (${typeof c.ok === "number" ? c.ok : "?"} of ${c.checks})`];
107
+ const failures = (Array.isArray(c.failures) ? c.failures : [])
108
+ .map((f) => (f && typeof f === "object" ? String(f.at ?? "") : ""))
109
+ .filter(Boolean)
110
+ .sort();
111
+ if (failures.length) {
112
+ const [a, b] = [clock(failures[0]), clock(failures[failures.length - 1])];
113
+ 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}`;
114
+ lines.push(` failed checks: ${failures.length}, ${failures.length === 1 ? `at ${a.hm} UTC on ${a.day}` : span}`);
115
+ }
116
+ const lastFailed = c.last?.ok === false;
117
+ if (lastFailed)
118
+ lines.push(" the last check failed: new sandboxes may not start right now");
119
+ return { lines, lastFailed };
120
+ }
67
121
  const status = async (ctx) => {
68
122
  const c = client(ctx);
69
123
  let failure;
@@ -86,12 +140,17 @@ const status = async (ctx) => {
86
140
  ...(conc ? { concurrency: conc } : {}),
87
141
  ...(me?.spend === undefined ? {} : { spend: me.spend }),
88
142
  }, next);
89
- const lines = [`promigence status: ${health?.ok ?? svc ? "up" : "no answer"}${health?.version ? ` (api ${health.version})` : ""}`];
143
+ const checks = checksSummary(svc?.checks);
144
+ const up = health?.ok ?? Boolean(svc);
145
+ const head = !up ? "no answer" : checks?.lastFailed ? "up, but the last check failed" : "up";
146
+ const lines = [`promigence status: ${head}${health?.version ? ` (api ${health.version})` : ""}`];
90
147
  for (const s of services) {
91
148
  lines.push(` ${s.name}: ${s.state ?? "?"}`);
92
149
  }
93
150
  if (!services.length)
94
151
  lines.push(" (no service reported)");
152
+ if (checks)
153
+ lines.push(...checks.lines);
95
154
  if (conc)
96
155
  lines.push(` your concurrency: ${concurrencyText(conc)}`);
97
156
  else if (!c.authenticated)
@@ -102,28 +161,40 @@ const status = async (ctx) => {
102
161
  };
103
162
  const me = async (ctx) => {
104
163
  const c = client(ctx);
105
- const [info, outcomes] = await Promise.all([c.me(), c.outcomes().catch(() => undefined)]);
164
+ const [info, outcomes, bill, keys] = await Promise.all([
165
+ c.me(), c.outcomes().catch(() => undefined), c.billing.usage().catch(() => undefined),
166
+ c.request({ method: "GET", path: "/keys" })
167
+ .catch(() => undefined),
168
+ ]);
106
169
  const conc = await concurrencyOf(c, info);
107
170
  const plan = planName(info);
108
171
  const trial = info.trial;
109
172
  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"]);
110
173
  const org = info.org && info.org !== info.org_id ? `${info.org} (${info.org_id})` : (info.org_id ?? "org");
111
- const lines = [`promigence me: ${org}${info.key?.key_id ? ` · key ${info.key.key_id}` : ""}`];
174
+ const from = keyOriginText(c.keyOrigin);
175
+ const row = keys?.keys?.find((k) => k.current === true || (info.key?.key_id !== undefined && k.key_id === info.key.key_id));
176
+ const access = row?.access ?? keyAccess(info.key?.prefix);
177
+ const expires = row === undefined ? undefined : (row.expires_at ?? null);
178
+ const keyWords = [access ? `${access} access` : "", expires === undefined ? "" : `expires ${expires ?? "never"}`].filter(Boolean).join(", ");
179
+ const lines = [`promigence me: ${org}${info.key?.key_id ? ` · key ${info.key.key_id}${keyWords ? ` (${keyWords})` : ""}` : ""}${from ? ` from ${from}` : ""}`];
112
180
  if (info.user && typeof info.user === "object" && info.user.email)
113
181
  lines.push(` signed up as ${info.user.email}`);
114
- if (plan)
115
- lines.push(` plan: ${plan}${trial?.on_trial ? " (trial)" : ""}`);
182
+ const shown = planDisplay(info, bill) ?? plan;
183
+ if (shown)
184
+ lines.push(` plan: ${shown}${trial?.on_trial ? " (trial)" : ""}`);
116
185
  if (conc)
117
186
  lines.push(` concurrency: ${concurrencyText(conc)}`);
118
- if (trial?.on_trial) {
119
- const left = trial.credits_available ?? trial.credits_remaining;
120
- lines.push(` trial credit: ${usd(left)} left of ${usd(trial.credits_granted)}` +
121
- `${trial.credits_reserved ? ` (${usd(trial.credits_reserved)} held for running sandboxes)` : ""}`);
187
+ const billCredits = creditsLine(bill?.credits);
188
+ if (billCredits)
189
+ lines.push(` ${billCredits}`);
190
+ else if (trial) {
191
+ const available = trial.credits_available ?? trial.credits_remaining;
192
+ const held = trial.credits_reserved ?? 0;
193
+ lines.push(` credits: ${dollars(available)} available${held > 0 ? ` (${dollars(held)} held by running work)` : ""}`);
122
194
  }
123
195
  if (typeof info.spend === "number") {
124
- const kinds = Object.entries(info.spend_detail?.by_kind ?? {}).filter(([, v]) => v > 0);
125
- const kindName = { sandbox: "sandboxes", build: "builds", egress: "data out", storage: "storage" };
126
- lines.push(` spent: ${usd(info.spend)}${kinds.length ? ` (${kinds.map(([k, v]) => `${kindName[k] ?? k} ${usd(v)}`).join(" · ")})` : ""}`);
196
+ const month = bill?.usage?.total;
197
+ lines.push(` ${spendLine(info.spend, info.spend_detail?.by_kind, typeof month === "number" ? month : undefined)}`);
127
198
  }
128
199
  if (info.spend === undefined && info.trial === undefined) {
129
200
  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)");
@@ -135,7 +206,7 @@ const me = async (ctx) => {
135
206
  const lim = info.limits ?? {};
136
207
  const shape = [
137
208
  typeof lim.max_sandbox_vcpus === "number" ? `sandboxes up to ${lim.max_sandbox_vcpus} vCPU` : "",
138
- typeof lim.max_session_s === "number" ? `sessions up to ${span(lim.max_session_s)}` : "",
209
+ typeof lim.max_session_s === "number" ? sessionLimit(lim.max_session_s) : "",
139
210
  ].filter(Boolean);
140
211
  if (shape.length)
141
212
  lines.push(` limits: ${shape.join(" · ")}`);
@@ -144,12 +215,22 @@ const me = async (ctx) => {
144
215
  lines.push(` ${head}:`, ...rest.map((l) => ` ${l}`));
145
216
  }
146
217
  return emit(ctx.mode, okEnvelope(ctx.cmd.name, {
147
- me: info,
218
+ me: { ...info, default_spend_cap: typeof cap === "number" && cap > 0 ? cap : null },
219
+ ...(c.keyOrigin ? { credential: { ...c.keyOrigin, from: from ?? null } } : {}),
220
+ ...(info.key?.key_id ? { key: { key_id: info.key.key_id, access: access ?? null, ...(expires === undefined ? {} : { expires_at: expires }) } } : {}),
148
221
  ...(outcomes ? { outcomes } : {}),
149
222
  ...(plan ? { plan } : {}),
150
223
  ...(conc ? { concurrency: { in_use: conc.in_use, limit: conc.limit, source: conc.source, ...(conc.at_least ? { at_least: true } : {}) } } : {}),
151
224
  }, next), lines, 0, ctx.pointer);
152
225
  };
226
+ export function spentLine(total, byKind) {
227
+ const name = {
228
+ sandbox: "sandboxes", build: "builds", egress: "data out", storage: "storage", failure_credit: "credited back",
229
+ };
230
+ const parts = Object.entries(byKind ?? {}).filter(([, v]) => typeof v === "number" && Math.abs(v) >= 0.005);
231
+ const money = (v) => (v < 0 ? `-${usd(-v)}` : usd(v));
232
+ return `${usd(total)}${parts.length ? ` (${parts.map(([k, v]) => `${name[k] ?? k.replace(/_/g, " ")} ${money(v)}`).join(" · ")})` : ""}`;
233
+ }
153
234
  export function outcomeLines(o) {
154
235
  const usd = (n) => `$${n.toFixed(2)}`;
155
236
  const lines = [`this month (since ${o.since.slice(0, 10)})`];
@@ -159,7 +240,7 @@ export function outcomeLines(o) {
159
240
  }
160
241
  lines.push(` runs ${o.runs.toLocaleString("en-US")}`);
161
242
  const ready = o.ready_ms_p50 ?? null;
162
- lines.push(` sandboxes ${o.sandboxes.toLocaleString("en-US")}${ready === null ? "" : `, ready in ${ms(ready)} (median)`}`);
243
+ 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)`}`);
163
244
  if (o.failures_ours > 0)
164
245
  lines.push(` failures of ours ${o.failures_ours}, not billed${o.failures_ours_usd > 0 ? ` (${usd(o.failures_ours_usd)} given back)` : ""}`);
165
246
  const speedup = o.batch_speedup ?? null;
@@ -175,6 +256,15 @@ export function outcomeLines(o) {
175
256
  lines.push(` paused, not billed ${hours.toFixed(1)} h${paidNot === null ? "" : ` (${usd(paidNot)} you did not pay)`}`);
176
257
  return lines;
177
258
  }
259
+ export function belowList(prices, tier, row) {
260
+ const rate = prices?.tiers.find((t) => t.name === tier)?.per_second_usd;
261
+ if (rate === undefined || row.amount === undefined || !row.seconds)
262
+ return "";
263
+ const list = row.seconds * rate;
264
+ if (row.amount >= list - 0.005)
265
+ return "";
266
+ return ` (${usd(list)} at this tier's rate; part of it billed at a lower rate, such as preemptible)`;
267
+ }
178
268
  const billingUsage = async (ctx) => {
179
269
  const limit = limitOf(ctx, 20, 1000);
180
270
  const wantLedger = bool(ctx, "--ledger");
@@ -182,13 +272,14 @@ const billingUsage = async (ctx) => {
182
272
  if (cursor !== undefined && !wantLedger)
183
273
  throw usageError("--cursor pages the ledger", "Pass it with --ledger.");
184
274
  const c = client(ctx);
185
- const [b, me] = await Promise.all([
275
+ const [b, me, prices] = await Promise.all([
186
276
  c.billing.usage().catch((e) => {
187
277
  if (wantLedger && e?.code === "forbidden")
188
278
  return undefined;
189
279
  throw e;
190
280
  }),
191
281
  c.me().catch(() => undefined),
282
+ livePricing(c).catch(() => undefined),
192
283
  ]);
193
284
  const page = wantLedger ? await c.billing.ledgerPage({ limit, ...(cursor === undefined ? {} : { cursor }) }) : undefined;
194
285
  const entries = page?.items ?? [];
@@ -213,7 +304,7 @@ const billingUsage = async (ctx) => {
213
304
  ? [["promigence me", "your limits, concurrency and trial credit"]]
214
305
  : b.card_on_file
215
306
  ? [["promigence billing portal", "the card on file and past invoices"]]
216
- : [["promigence billing card", "an owner adds a card (nothing is charged now)"]]), ...(moreLedger.length ? moreLedger : [["promigence billing usage --ledger", "this key's project's metered rows"]]));
307
+ : [["promigence billing card", "an owner adds a card"]]), ...(moreLedger.length ? moreLedger : [["promigence billing usage --ledger", "this key's project's metered rows"]]));
217
308
  const plan = planName(me, b);
218
309
  const onTrial = (b.trial?.on_trial ?? me?.trial?.on_trial) === true;
219
310
  const env = okEnvelope(ctx.cmd.name, {
@@ -231,9 +322,9 @@ const billingUsage = async (ctx) => {
231
322
  ...(wantLedger ? { ledger: entries, next_cursor: page?.nextCursor ?? null } : {}),
232
323
  }, next);
233
324
  const period = b.period?.start ? `${b.period.start.slice(0, 10)} → ${(b.period.end ?? "").slice(0, 10)}` : "this month";
234
- const lines = [`promigence billing usage: ${usd(usage.total)} (${period}, plan ${plan ?? "?"}${onTrial ? ", on the trial" : ""})`];
325
+ const lines = [`promigence billing usage: ${usd(usage.total)} (${period}, plan ${planDisplay(me, b) ?? "?"}${onTrial ? ", on the trial" : ""})`];
235
326
  const detail = (row) => `${usd(row.amount)}${row.seconds === undefined ? "" : ` · ${row.seconds}s`}${row.count === undefined ? "" : ` · ${row.count}`}` +
236
- `${row.gib === undefined ? "" : ` · ${row.gib} GiB`}${row.entries === undefined ? "" : ` · ${row.entries} entries`}` +
327
+ `${row.gib === undefined ? "" : ` · ${row.gib} GiB`}${row.entries === undefined ? "" : ` · ${row.entries} ${row.entries === 1 ? "entry" : "entries"}`}` +
237
328
  `${row.episodes === undefined ? "" : ` · ${row.episodes} episodes`}`;
238
329
  for (const [k, v] of Object.entries(usage)) {
239
330
  if (k === "total" || v === null || typeof v !== "object")
@@ -246,7 +337,8 @@ const billingUsage = async (ctx) => {
246
337
  const r = inner;
247
338
  if (!r.amount && !r.seconds && !r.count && !r.entries && !r.episodes)
248
339
  continue;
249
- lines.push(` ${k} ${name}: ${detail(r)}`);
340
+ const label = k === "other" ? kindWords(name) : `${k} ${name}`;
341
+ lines.push(` ${label}: ${detail(r)}${k === "sandboxes" ? belowList(prices, name, r) : ""}`);
250
342
  }
251
343
  continue;
252
344
  }
@@ -255,22 +347,26 @@ const billingUsage = async (ctx) => {
255
347
  lines.push(` ${k}: ${detail(row)}`);
256
348
  }
257
349
  if (typeof usage["credits_applied"] === "number" && usage["credits_applied"] > 0) {
258
- lines.push(` paid by trial credits: ${usd(usage["credits_applied"])} · to the card: ${usd(usage["billable"])}`);
350
+ lines.push(` paid from credits: ${usd(usage["credits_applied"])}`);
259
351
  }
260
352
  const trial = b.trial;
261
- if (trial?.credits_remaining !== undefined)
262
- lines.push(` trial credits left: ${usd(trial.credits_remaining)}`);
263
- lines.push(` card: ${b.card_on_file ? paymentMethodText(b.card) : `none on file${billingOff ? " (card payments are not open yet)" : ""}`}`);
353
+ const credits = creditsLine(b.credits) ?? (trial?.credits_remaining === undefined ? undefined : `credits: ${dollars(trial.credits_remaining)} available`);
354
+ if (credits)
355
+ lines.push(` ${credits}`);
356
+ if (b.auto_reload)
357
+ lines.push(` auto-reload: ${autoReloadShort(b.auto_reload)}`);
358
+ lines.push(` card: ${b.card_on_file ? paymentMethodText(b.card) : `none on file${billingOff ? " (payments are not open yet)" : ""}`}`);
264
359
  if (b.card_note)
265
360
  lines.push(` ${b.card_note}`);
266
- if (b.upcoming_invoice?.amount_due !== undefined && b.upcoming_invoice?.amount_due !== null) {
361
+ const invoiceComes = b.plan_status === undefined || !!b.plan_status.renews_at || b.model === "postpaid";
362
+ if (invoiceComes && b.upcoming_invoice?.amount_due !== undefined && b.upcoming_invoice?.amount_due !== null) {
267
363
  lines.push(` next invoice: ${usd(b.upcoming_invoice.amount_due)}`);
268
364
  }
269
- else if (b.upcoming_invoice_error) {
365
+ else if (invoiceComes && b.upcoming_invoice_error) {
270
366
  lines.push(` next invoice: not available (${b.upcoming_invoice_error})`);
271
367
  }
272
368
  if (b.suspended)
273
- lines.push(` SUSPENDED: ${b.suspend_reason ?? "see promigence billing card"}`);
369
+ lines.push(` SUSPENDED: ${b.suspend_reason ?? "see promigence billing"}`);
274
370
  for (const e of entries) {
275
371
  lines.push(` ${e.created_at ?? ""} ${e.kind}${e.tier ? ` ${e.tier}` : ""} ${usd(e.amount)}${e.seconds ? ` · ${e.seconds}s` : ""}${e.run_id ? ` · ${e.run_id}` : ""}`);
276
372
  }
@@ -294,12 +390,19 @@ const CREDIT_WHAT = {
294
390
  from_promigence: "Credit from Promigence",
295
391
  service_restart: "Credit for a service restart",
296
392
  confirmation_credit: "Credit for the $1 confirmation",
393
+ credits_bought: "Credits bought",
394
+ auto_reload: "Auto-reload",
395
+ credit_taken_back: "Credit taken back",
396
+ credit_regranted: "Credit given back",
297
397
  };
298
398
  const CREDIT_ENDED = { used: "used", expired: "expired", taken_back: "taken back" };
299
399
  const PAYMENT_WHAT = {
300
400
  invoice_paid: "Invoice paid",
301
401
  payment_failed: "Payment failed",
302
402
  refund: "Refund",
403
+ credits_bought: "Credits bought",
404
+ auto_reload: "Auto-reload",
405
+ dispute_repaid: "Repayment",
303
406
  };
304
407
  const PAYMENT_STATUS = {
305
408
  paid: "paid",
@@ -323,15 +426,30 @@ export function historyLines(entries) {
323
426
  if (!p)
324
427
  return ` ${day} (a row this version cannot show: see --json)`;
325
428
  const check = p.kind === "method_check";
429
+ const planFee = p.kind === "plan_invoice";
326
430
  const what = check
327
431
  ? `$${Number.isInteger(p.amount_usd) ? String(p.amount_usd) : p.amount_usd.toFixed(2)} to confirm ${p.method || "your payment method"}`
328
- : (wordFor(PAYMENT_WHAT, p.kind) ?? "Payment");
329
- const detail = [p.plan ? `${p.plan} plan` : null, p.invoice_number ? `invoice ${p.invoice_number}` : null]
432
+ : planFee
433
+ ? `Plan: ${p.plan ?? "your plan"}`
434
+ : (wordFor(PAYMENT_WHAT, p.kind) ?? "Payment");
435
+ const detail = [p.plan && !planFee ? `${p.plan} plan` : null, p.invoice_number ? `invoice ${p.invoice_number}` : null]
330
436
  .filter(Boolean)
331
437
  .join(", ");
332
438
  const status = check && p.status === "failed" ? "failed" : (wordFor(PAYMENT_STATUS, p.status) ?? p.status);
333
- const url = typeof p.invoice_url === "string" && p.invoice_url.startsWith("https://") ? p.invoice_url : null;
334
- const link = url && !check ? ` · ${unpaid(p.status) ? "pay at" : "receipt"} ${url}` : "";
439
+ const https = (u) => (typeof u === "string" && u.startsWith("https://") ? u : null);
440
+ const linked = [
441
+ https(p.invoice_pdf) ? `invoice ${p.invoice_pdf}` : null,
442
+ https(p.receipt_url) ? `receipt ${p.receipt_url}` : null,
443
+ https(p.pay_url) ? `pay at ${p.pay_url}` : null,
444
+ ].filter(Boolean);
445
+ const url = https(p.invoice_url);
446
+ const link = check
447
+ ? ""
448
+ : linked.length
449
+ ? ` · ${linked.join(" · ")}`
450
+ : url
451
+ ? ` · ${unpaid(p.status) ? "pay at" : "receipt"} ${url}`
452
+ : "";
335
453
  return ` ${day} ${what}${detail ? ` (${detail})` : ""} ${usd(p.amount_usd)} ${status}${link}`;
336
454
  });
337
455
  }
@@ -378,11 +496,11 @@ const referrals = async (ctx) => {
378
496
  const o = r.offer;
379
497
  lines.push(`promigence referrals: ${o.headline}`);
380
498
  lines.push(` your code: ${r.code} (a friend signs up with: ${sh `promigence login --ref ${r.code}`})`);
381
- lines.push(` they get ${usd(o.give_usd)} of compute credit when they pay their first plan invoice` +
382
- `${o.qualifying_plans.length ? ` on ${o.qualifying_plans.join(" / ")}` : ""}; you get ${usd(o.get_usd)} ` +
383
- `${o.hold_days > 0 ? `${o.hold_days} day(s) later` : "then"}`);
499
+ lines.push(` they get ${usd(o.give_usd)} of compute credit once they first pay at least ${usd(o.give_usd)}, for credits or a plan` +
500
+ `${o.qualifying_plans.length ? ` (plans: ${o.qualifying_plans.join(" / ")})` : ""}; you get ${usd(o.get_usd)} ` +
501
+ `${o.hold_days > 0 ? `${o.hold_days} ${o.hold_days === 1 ? "day" : "days"} later` : "then"}`);
384
502
  if (o.credit_expiry_days !== null)
385
- lines.push(` credit lasts ${o.credit_expiry_days} day(s) from its grant`);
503
+ lines.push(` credit lasts ${o.credit_expiry_days} ${o.credit_expiry_days === 1 ? "day" : "days"} from its grant`);
386
504
  if (o.ends_at)
387
505
  lines.push(` offer ends ${o.ends_at}`);
388
506
  }
@@ -405,8 +523,8 @@ const referralsApply = async (ctx) => {
405
523
  ? [`promigence referrals apply: ${r.code} recorded; it is being checked before any credit is granted`]
406
524
  : [
407
525
  `promigence referrals apply: ${r.code} applied`,
408
- ` you get ${usd(r.reward_usd)} of compute credit when you pay your first plan invoice` +
409
- `${r.qualifying_plans.length ? ` on ${r.qualifying_plans.join(" / ")}` : ""}`,
526
+ ` you get ${usd(r.reward_usd)} of compute credit once you first pay at least ${usd(r.reward_usd)}, for credits or a plan` +
527
+ `${r.qualifying_plans.length ? ` (plans: ${r.qualifying_plans.join(" / ")})` : ""}`,
410
528
  ];
411
529
  return emit(ctx.mode, okEnvelope(ctx.cmd.name, { ...r }, next), lines, 0, ctx.pointer);
412
530
  };
@@ -464,22 +582,37 @@ const snapshotAlias = async (ctx) => {
464
582
  if (clear && name) {
465
583
  throw usageError("--clear takes no name", `Use either ${sh `promigence snapshot alias ${arg} NAME`} or ${sh `promigence snapshot alias ${arg} --clear`}.`);
466
584
  }
467
- const res = await client(ctx).snapshots.alias(arg, clear ? null : name);
585
+ const moveAlias = bool(ctx, "--move-alias");
586
+ if (moveAlias && clear) {
587
+ throw usageError("--move-alias takes a name, not --clear", `Use ${sh `promigence snapshot alias ${arg} NAME --move-alias`}.`);
588
+ }
589
+ const res = await client(ctx).snapshots.alias(arg, clear ? null : name, moveAlias ? { moveAlias: true } : {});
468
590
  const next = nexts(["promigence snapshot list", "your snapshots and their names"]);
469
- 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);
591
+ const said = res.warnings;
592
+ const warnings = Array.isArray(said) ? said.filter((w) => typeof w === "string") : [];
593
+ 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);
470
594
  };
471
595
  const runList = async (ctx) => {
472
596
  const wanted = stateFlag(ctx, RUN_STATES);
597
+ const labels = Object.entries(labelsOf(ctx));
473
598
  const limit = limitOf(ctx, 20, 500);
474
599
  const cursor = str(ctx, "--cursor");
475
- const page = await client(ctx).runs.listPage({ limit: wanted === undefined ? limit : 500, ...(cursor === undefined ? {} : { cursor }) });
476
- const matched = wanted === undefined ? page.items : page.items.filter((r) => r.state === wanted);
600
+ const filtering = wanted !== undefined || labels.length > 0;
601
+ const page = await client(ctx).runs.listPage({ limit: filtering ? 500 : limit, ...(cursor === undefined ? {} : { cursor }) });
602
+ const labelsOfRun = (r) => (r.labels ?? {});
603
+ const labelled = (r) => labels.every(([k, v]) => labelsOfRun(r)[k] === v);
604
+ const matched = page.items.filter((r) => (wanted === undefined || r.state === wanted) && labelled(r));
477
605
  const runs = matched.slice(0, limit);
478
606
  const more = runs.length === matched.length ? page.nextCursor : null;
479
- 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"]] : []));
607
+ const labelArgs = labels.map(([k, v]) => sh ` --label ${`${k}=${v}`}`).join("");
608
+ 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"]] : []));
609
+ const labelText = (r) => {
610
+ const own = Object.entries(labelsOfRun(r));
611
+ return own.length ? ` ${own.map(([k, v]) => `${k}=${v}`).join(",")}` : "";
612
+ };
480
613
  const lines = [
481
- `promigence run list: ${runs.length}${wanted ? ` in state ${wanted}` : ""}`,
482
- ...runs.map((r) => ` ${r.run_id} ${r.state} ${r.count ?? "?"}× ${r.snapshot_id || r.snapshot || "—"} ${r.started_at ?? ""}`),
614
+ `promigence run list: ${runs.length}${wanted ? ` in state ${wanted}` : ""}${labels.length ? ` with ${labels.map(([k, v]) => `${k}=${v}`).join(", ")}` : ""}`,
615
+ ...runs.map((r) => ` ${r.run_id} ${r.state} ${r.count ?? "?"}× ${r.snapshot_id || r.snapshot || "—"} ${r.started_at ?? ""}${labelText(r)}`),
483
616
  ...(runs.length ? [] : [" (no runs)"]),
484
617
  ...(more ? [` more: --cursor ${more}`] : []),
485
618
  ];
@@ -503,7 +636,14 @@ function labelsOf(ctx) {
503
636
  }
504
637
  return out;
505
638
  }
506
- const HELD_STATES = ["queued", "placing", "ready", "running", "paused"];
639
+ const HELD_STATES = ["queued", "ready", "running", "paused"];
640
+ export function sandboxListFlags(f) {
641
+ return [
642
+ ...(f.limit === undefined ? [] : [sh `--limit ${String(f.limit)}`]),
643
+ ...(f.states?.length ? [sh `--state ${f.states.join(",")}`] : []),
644
+ ...Object.entries(f.labels ?? {}).map(([k, v]) => sh `--label ${`${k}=${v}`}`),
645
+ ].join(" ");
646
+ }
507
647
  export function pauseReasonText(reason) {
508
648
  if (reason === "spend_cap")
509
649
  return "spend cap reached";
@@ -515,8 +655,9 @@ const sandboxList = async (ctx) => {
515
655
  const labels = labelsOf(ctx);
516
656
  const states = list(ctx, "--state").flatMap((s) => s.split(",")).map((s) => s.trim()).filter(Boolean);
517
657
  const cursor = str(ctx, "--cursor");
658
+ const limit = limitOf(ctx, 50);
518
659
  const page = await client(ctx).sandboxes.listPage({
519
- limit: limitOf(ctx, 50),
660
+ limit,
520
661
  ...(Object.keys(labels).length ? { labels } : {}),
521
662
  ...(states.length ? { state: states } : {}),
522
663
  ...(cursor === undefined ? {} : { cursor }),
@@ -536,10 +677,10 @@ const sandboxList = async (ctx) => {
536
677
  held = { count, atLeast: live.nextCursor !== null };
537
678
  }
538
679
  }
539
- const heldCmd = sh `promigence sandbox list --state ${HELD_STATES.join(",")}`;
680
+ const heldCmd = ["promigence sandbox list", sandboxListFlags({ states: HELD_STATES, labels })].filter(Boolean).join(" ");
540
681
  const firstLive = sandboxes.find((s) => s.state !== "ended");
541
682
  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
542
- ? [[sh `promigence sandbox list --cursor ${page.nextCursor}`, "the next page"]]
683
+ ? [[["promigence sandbox list", sandboxListFlags({ limit, states, labels }), sh `--cursor ${page.nextCursor}`].filter(Boolean).join(" "), "the next page"]]
543
684
  : []), ...(firstLive ? [["promigence sandbox kill SID", "end one: put its id from this list in place of SID"]] : []));
544
685
  const shown = Object.entries(labels).map(([k, v]) => `${k}=${v}`).join(" ");
545
686
  const lines = [
@@ -549,7 +690,7 @@ const sandboxList = async (ctx) => {
549
690
  return ` ${s.sandbox_id} ${s.state}${why ? ` (${why})` : ""}${s.outcome ? ` (${s.outcome})` : ""} ${s.run_id ?? ""}`;
550
691
  }),
551
692
  ...(sandboxes.length ? [] : [" (no sandboxes)"]),
552
- ...(page.nextCursor ? [` more: --cursor ${page.nextCursor}`] : []),
693
+ ...(page.nextCursor ? [` more: ${sandboxListFlags({ limit, states, labels })} --cursor ${page.nextCursor}`] : []),
553
694
  ...(held ? [` ${held.atLeast ? "at least " : ""}${held.count} live or paused not on this page: ${heldCmd}`] : []),
554
695
  ];
555
696
  return emit(ctx.mode, okEnvelope(ctx.cmd.name, {
@@ -614,10 +755,29 @@ export function sandboxFactLines(s, facts, nowMs = Date.now()) {
614
755
  if (elapsed !== undefined && facts.perSecond !== undefined) {
615
756
  const secs = Math.ceil(elapsed / 1000);
616
757
  const rate = `$${facts.perSecond.toFixed(9).replace(/0+$/, "").replace(/\.$/, "")}`;
617
- out.push(` cost so far: about ${usd(secs * facts.perSecond)} (${secs} s at ${rate}/s)`);
758
+ const billed = typeof s["cost_usd"] === "number" && s["state"] !== "ended" ? `; billed so far ${usd(s["cost_usd"])}` : "";
759
+ out.push(` cost so far: about ${usd(secs * facts.perSecond)} (${secs} s at ${rate}/s${billed})`);
618
760
  }
619
761
  return out;
620
762
  }
763
+ export function timeLeftMs(s) {
764
+ if (s.state === "ended" || typeof s.timeout_ms !== "number" || typeof s.elapsed_ms !== "number")
765
+ return undefined;
766
+ return Math.max(0, s.timeout_ms - s.elapsed_ms);
767
+ }
768
+ export function pausedLines(sid, s) {
769
+ if (s.state !== "paused" || pauseReasonText(s.pause_reason))
770
+ return [];
771
+ const wakes = Number(s.auto_pause_s ?? 0) > 0;
772
+ return [
773
+ wakes
774
+ ? " it paused itself when idle: the next request (exec, files, ssh) wakes it"
775
+ : ` it stays paused until \`${sh `promigence sandbox resume ${sid}`}\` (it has no idle rule, so a request does not wake it)`,
776
+ s.pause_expires_at
777
+ ? ` its files and state are kept until ${s.pause_expires_at} unless it is resumed before then`
778
+ : " its files and state are kept at least 30 days from the pause; resuming starts them again",
779
+ ];
780
+ }
621
781
  export function supportStopLines(s) {
622
782
  if (s.outcome !== "stopped_by_support")
623
783
  return [];
@@ -636,15 +796,19 @@ const sandboxInfo = async (ctx) => {
636
796
  const limitNext = s.pause_reason === "spend_cap"
637
797
  ? [[sh `promigence sandbox resume ${sid} --cap USD`, "raise its run's cap and go on (paused time is not billed)"]]
638
798
  : s.pause_reason === "credits_exhausted"
639
- ? [["promigence billing card", "add a card, then resume it"], [sh `promigence sandbox resume ${sid}`, "go on once credits are added"]]
799
+ ? [["promigence billing buy USD", "add credits, then resume it"], [sh `promigence sandbox resume ${sid}`, "go on once credits are added"]]
640
800
  : [];
641
801
  const next = nexts(...limitNext, ...(execs.length ? [[sh `promigence exec output ${execs[execs.length - 1].exec_id} --sandbox ${sid}`, "what its last command printed"]] : []), ...(ended
642
802
  ? (s.run_id ? [[sh `promigence run report ${s.run_id} --sandbox ${sid}`, "how it ended, in its run's report"]] : [])
643
- : why ? [] : [[sh `promigence exec --sandbox ${sid} -- CMD`, "run something in it"]]));
803
+ : why ? []
804
+ : s.state === "paused" && !(Number(s.auto_pause_s ?? 0) > 0)
805
+ ? [[sh `promigence sandbox resume ${sid}`, "bring it back; a request does not wake it"]]
806
+ : [[sh `promigence exec --sandbox ${sid} -- CMD`, "run something in it"]]));
644
807
  const lines = [
645
808
  `sandbox ${sid}: ${s.state}${why ? ` (${why})` : ""}${s.outcome ? ` (${s.outcome})` : ""}${s.run_id ? ` · run ${s.run_id}` : ""}`,
646
809
  ...supportStopLines(s),
647
810
  ...(why ? [" its files and state are kept, and paused time is not billed"] : []),
811
+ ...pausedLines(sid, s),
648
812
  ...queuedLines([s]),
649
813
  ...(s.snapshot_id ? [` snapshot ${s.snapshot_id}`] : []),
650
814
  ...sandboxFactLines(s, facts),
@@ -654,15 +818,22 @@ const sandboxInfo = async (ctx) => {
654
818
  ...(Number(s.auto_pause_s ?? 0) > 0 ? [` pauses itself after ${s.auto_pause_s} s idle`] : []),
655
819
  ...execs.map((e) => ` ${e.exec_id} exit ${e.exit_code ?? "—"} ${(e.argv ?? []).join(" ")}`),
656
820
  ...(execs.length ? [] : [" (no execs recorded)"]),
821
+ ...(ended ? [] : [` background commands (exec --background): ${sh `promigence exec ps --sandbox ${sid}`}`]),
657
822
  ];
823
+ const left = timeLeftMs(s);
824
+ const billed = typeof s["cost_usd"] === "number" ? s["cost_usd"] : undefined;
825
+ const costSoFar = ended
826
+ ? billed
827
+ : facts.perSecond !== undefined && typeof s.elapsed_ms === "number"
828
+ ? Math.round(Math.ceil(s.elapsed_ms / 1000) * facts.perSecond * 1e6) / 1e6
829
+ : undefined;
658
830
  return emit(ctx.mode, okEnvelope(ctx.cmd.name, {
659
831
  sandbox: s,
660
832
  ...(facts.tier ? { tier: facts.tier } : {}),
661
- ...(facts.perSecond !== undefined && typeof s.elapsed_ms === "number"
662
- ? { cost_so_far_usd: Math.round(Math.ceil(s.elapsed_ms / 1000) * facts.perSecond * 1e6) / 1e6 }
663
- : {}),
664
- ...(ended && typeof s["cost_usd"] === "number"
665
- ? { cost_usd: s["cost_usd"], billable_seconds: s["billable_seconds"] }
833
+ ...(left === undefined ? {} : { time_left_ms: left }),
834
+ ...(costSoFar === undefined ? {} : { cost_so_far_usd: costSoFar }),
835
+ ...(ended && billed !== undefined
836
+ ? { cost_usd: billed, billable_seconds: s["billable_seconds"] }
666
837
  : {}),
667
838
  }, next), lines, 0, ctx.pointer);
668
839
  };
@@ -677,13 +848,19 @@ const sandboxPause = async (ctx) => {
677
848
  `sandbox ${sid}: paused${res.already_paused ? " (already was)" : ""}`,
678
849
  " it holds no capacity and bills no seconds while parked",
679
850
  ...(took === undefined ? [] : [` took ${ms(took)}`]),
680
- ...(bytes?.allocated === undefined ? [] : [` parked image: ${Math.round(bytes.allocated / 1048576)} MiB`]),
851
+ ...(bytes?.allocated === undefined ? [] : [` holds ${bytes.allocated >= 1073741824 ? `${(bytes.allocated / 1073741824).toFixed(1)} GiB` : `${Math.round(bytes.allocated / 1048576)} MiB`} of storage while paused`]),
681
852
  ...(killed > 0 ? [` ended ${killed} background process${killed === 1 ? "" : "es"} (started with exec --background)`] : []),
682
853
  ...(res.warnings ?? []).map((w) => ` warning: ${w}`),
683
854
  ];
684
855
  return emit(ctx.mode, okEnvelope(ctx.cmd.name, { ...res, sandbox_id: sid, state: "paused" }, next), lines, 0, ctx.pointer);
685
856
  };
686
857
  const sandboxResume = async (ctx) => {
858
+ if (bool(ctx, "--all-paused-for-credits")) {
859
+ if (ctx.args[0] !== undefined) {
860
+ throw usageError("--all-paused-for-credits takes no sandbox id", "Pass a sandbox id, or --all-paused-for-credits alone.");
861
+ }
862
+ return resumeAllPausedForCredits(ctx);
863
+ }
687
864
  const sid = positional(ctx, "a sandbox id", "promigence sandbox resume sbx_01");
688
865
  const optimizeFor = optimizeForFlag(ctx);
689
866
  const cap = str(ctx, "--cap");
@@ -755,7 +932,7 @@ const sandboxAutopause = async (ctx) => {
755
932
  throw usageError("--mode and --off ask for opposite things", "--off removes the idle rule; --mode says what idle means under one.");
756
933
  }
757
934
  const rule = (s, m) => s > 0
758
- ? `pauses itself after ${s} s idle${m === "activity" ? " (activity: no CPU, no bytes, no open connection)" : ""}`
935
+ ? `pauses itself after ${s} s idle${m === "activity" ? " (activity: also CPU under 5% of one core, no network bytes, no outside connection)" : ""}`
759
936
  : "never pauses itself on idle";
760
937
  const next = nexts([sh `promigence sandbox info ${sid}`, "the sandbox as it stands now"]);
761
938
  if (after === undefined && !off && mode === undefined) {
@@ -784,18 +961,122 @@ const sandboxAutopause = async (ctx) => {
784
961
  ];
785
962
  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);
786
963
  };
964
+ export const PROBE_DIGEST_MAX_BYTES = 16 * 1024 * 1024;
965
+ export async function pathNow(c, sid, path) {
966
+ try {
967
+ const { stat } = await c.sandboxes.files.stat(sid, path);
968
+ if (stat.is_dir)
969
+ return { kind: "dir" };
970
+ if (stat.is_symlink)
971
+ return { kind: "symlink" };
972
+ if (stat.size > PROBE_DIGEST_MAX_BYTES)
973
+ return { kind: "file", size: stat.size };
974
+ const bytes = await c.sandboxes.files.get(sid, path);
975
+ return { kind: "file", size: bytes.length, digest: `blake3:${blake3Hex(bytes)}` };
976
+ }
977
+ catch (e) {
978
+ return e?.code === "not_found" ? { kind: "missing" } : undefined;
979
+ }
980
+ }
981
+ export function ownDigests(paths) {
982
+ const out = new Map();
983
+ for (const p of paths) {
984
+ const at = p.indexOf("=");
985
+ if (at > 0)
986
+ out.set(p.slice(0, at), p.slice(at + 1));
987
+ }
988
+ return out;
989
+ }
990
+ export function ownPathOf(bad, own) {
991
+ for (const p of own.keys()) {
992
+ const root = p.length > 1 ? p.replace(/\/+$/, "") : p;
993
+ if (bad === p || bad === root || bad.startsWith(root === "/" ? "/" : `${root}/`))
994
+ return p;
995
+ }
996
+ return undefined;
997
+ }
998
+ function nowWords(now) {
999
+ if (!now)
1000
+ return undefined;
1001
+ if (now.kind === "missing")
1002
+ return "it does not exist";
1003
+ if (now.kind === "dir")
1004
+ return "it is a directory";
1005
+ if (now.kind === "symlink")
1006
+ return "it is a symbolic link";
1007
+ return now.digest ? `it is ${now.digest} now` : `it is a file of ${now.size} bytes`;
1008
+ }
1009
+ async function probeRefusal(ctx, c, sid, e) {
1010
+ if (!(e instanceof PromigenceError) || e.code !== "usage")
1011
+ throw e;
1012
+ const later = (path, digest) => sh `promigence sandbox probe ${sid} --path ${`${path}=${digest}`}`;
1013
+ if (/has nothing to compare against/.test(e.message)) {
1014
+ throw usageError(e.message, `Name the files to check: ${sh `promigence sandbox probe ${sid} --path /work/FILE`} answers with the file's digest now, ` +
1015
+ "and --path /work/FILE=<that digest> checks later that it has not changed.");
1016
+ }
1017
+ const m = /^(.+): the snapshot recorded no digest for this path\b/.exec(e.message);
1018
+ if (!m)
1019
+ throw e;
1020
+ const path = m[1];
1021
+ const now = await pathNow(c, sid, path);
1022
+ const digest = now?.kind === "file" ? now.digest : undefined;
1023
+ const fix = digest
1024
+ ? `Its digest now is ${digest}. To check later that it has not changed: ${later(path, digest)}`
1025
+ : now?.kind === "missing"
1026
+ ? `It does not exist in sandbox ${sid}: check the path (${sh `promigence files ls / --sandbox ${sid}`}).`
1027
+ : now?.kind === "file"
1028
+ ? `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).`
1029
+ : `Name it as PATH=blake3:<hex> (${nowWords(now) ?? "the digest of the file's bytes"}).`;
1030
+ const err = usageError(e.message, fix);
1031
+ 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"]);
1032
+ return emit(ctx.mode, {
1033
+ ...errorEnvelope(ctx.cmd.name, err, next),
1034
+ sandbox_id: sid,
1035
+ ...(digest ? { digests: [{ path, digest }] } : {}),
1036
+ }, errorTextLines(ctx.cmd.name, err, 2, next), 2, ctx.pointer);
1037
+ }
787
1038
  const sandboxProbe = async (ctx) => {
788
1039
  const sid = positional(ctx, "a sandbox id", "promigence sandbox probe sbx_01");
789
1040
  const paths = list(ctx, "--path");
790
- const res = await client(ctx).sandboxes.probe(sid, ...(paths.length ? [{ paths }] : []));
1041
+ const own = ownDigests(paths);
1042
+ const c = client(ctx);
1043
+ const res = await c.sandboxes.probe(sid, ...(paths.length ? [{ paths }] : [])).catch((e) => probeRefusal(ctx, c, sid, e));
791
1044
  const ok = res.ok !== false;
792
- 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 };
793
- const lines = [
794
- `sandbox ${sid}: ${ok ? "matches the verified environment" : "DIFFERS from the verified environment"}`,
795
- ...(res.first_bad_path ? [` first difference at ${res.first_bad_path}`] : []),
796
- ...(res.ms === undefined ? [] : [` checked in ${ms(res.ms)}`]),
797
- ];
798
- return emit(ctx.mode, env, lines, ok ? 0 : 3, ctx.pointer);
1045
+ const infoNext = nexts([sh `promigence sandbox info ${sid}`, "the sandbox, its run and the commands it ran"]);
1046
+ const took = res.ms === undefined ? [] : [` checked in ${ms(res.ms)}`];
1047
+ if (ok) {
1048
+ const allOwn = paths.length > 0 && own.size === paths.length;
1049
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, { ...res, sandbox_id: sid }, infoNext), [
1050
+ `sandbox ${sid}: ${allOwn ? "every path matches the digest you gave" : "matches the verified environment"}`,
1051
+ ...took,
1052
+ ], 0, ctx.pointer);
1053
+ }
1054
+ const bad = res.first_bad_path ?? undefined;
1055
+ const mine = bad === undefined ? undefined : ownPathOf(bad, own);
1056
+ const now = bad === undefined ? undefined : await pathNow(c, sid, bad);
1057
+ const words = nowWords(now);
1058
+ const where = bad === undefined ? "" : bad;
1059
+ const err = mine !== undefined
1060
+ ? new PromigenceError({
1061
+ code: "digest_mismatch", exitCode: 4, docs: `${ERRORS_DOCS_URL}#digest_mismatch`,
1062
+ message: now?.kind === "missing"
1063
+ ? `${where} does not exist in sandbox ${sid}, so it cannot match the digest you gave${mine !== bad ? ` for ${mine}` : ""}`
1064
+ : `${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}` : ""}`,
1065
+ fix: now?.kind === "missing"
1066
+ ? `Nothing failed on our side. Check the path (${sh `promigence files ls / --sandbox ${sid}`}), or what should have made the file.`
1067
+ : "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.",
1068
+ })
1069
+ : new PromigenceError({
1070
+ code: "integrity_mismatch", exitCode: 3, docs: `${ERRORS_DOCS_URL}#integrity_mismatch`,
1071
+ message: `sandbox ${sid} differs from its verified snapshot${where ? ` at ${where}` : ""}${words ? ` (${words})` : ""}`,
1072
+ fix: "If a command you ran changed it, that is expected. If nothing did, this sandbox is not the verified environment: " +
1073
+ "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.",
1074
+ });
1075
+ return emit(ctx.mode, {
1076
+ ...errorEnvelope(ctx.cmd.name, err, infoNext),
1077
+ sandbox_id: sid, checked: res.checked, ms: res.ms, first_bad_path: bad ?? null,
1078
+ difference: { path: bad ?? null, ...(mine === undefined ? { against: "snapshot" } : { against: "your_digest", digest_given: own.get(mine) }), now: now ?? null },
1079
+ }, [...errorTextLines(ctx.cmd.name, err, err.cliExit, infoNext), ...took], err.cliExit, ctx.pointer);
799
1080
  };
800
1081
  const filesRm = async (ctx) => {
801
1082
  const path = positional(ctx, "a path", "promigence files rm /work/out.log --sandbox sbx_01");
@@ -805,8 +1086,14 @@ const filesRm = async (ctx) => {
805
1086
  const sid = required(ctx, "--sandbox", "Pass --sandbox sbx_<id> — the sandbox the path is in.");
806
1087
  const recursive = bool(ctx, "--recursive");
807
1088
  const res = await client(ctx).sandboxes.files.delete(sid, path, { recursive });
808
- const next = nexts([sh `promigence files ls --sandbox ${sid}`, "what is left"]);
809
- 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);
1089
+ const gone = res.existed === false;
1090
+ const dir = path.replace(/\/+$/, "").replace(/\/[^/]*$/, "") || "/";
1091
+ const next = gone
1092
+ ? nexts([sh `promigence files ls ${dir} --sandbox ${sid}`, "check the path: what is in its directory"])
1093
+ : nexts([sh `promigence files ls --sandbox ${sid}`, "what is left"]);
1094
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, { sandbox_id: sid, path, deleted: res.deleted, existed: res.existed, recursive }, next), [gone
1095
+ ? `${sid}:${path}: not there, nothing was deleted (check the path)`
1096
+ : `${sid}:${path}: ${res.deleted ? "deleted" : "not deleted"}`], 0, ctx.pointer);
810
1097
  };
811
1098
  const secretsList = async (ctx) => {
812
1099
  const { secrets } = await client(ctx).secrets.list();