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.
- package/dist/account.js +366 -59
- package/dist/agent.js +18 -5
- package/dist/billing.js +22 -5
- package/dist/bin.js +94 -14
- package/dist/bundle.js +15 -6
- package/dist/capacity.js +7 -2
- package/dist/commands.d.ts +51 -6
- package/dist/commands.js +741 -169
- package/dist/devcontainer.d.ts +4 -1
- package/dist/devcontainer.js +21 -10
- package/dist/dirsnap.js +208 -91
- package/dist/errfix.js +372 -11
- package/dist/generated.js +11 -5
- package/dist/github.js +5 -1
- package/dist/gitlab.js +3 -2
- package/dist/guide.js +44 -26
- package/dist/handlers.js +66 -19
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/init.d.ts +1 -0
- package/dist/init.js +4 -1
- package/dist/install.js +1 -1
- package/dist/io.d.ts +3 -0
- package/dist/io.js +41 -7
- package/dist/live.js +86 -16
- package/dist/login.js +92 -20
- package/dist/mcp.js +40 -13
- package/dist/money.js +2 -0
- package/dist/orgs.js +91 -19
- package/dist/pricing.d.ts +2 -0
- package/dist/pricing.js +11 -5
- package/dist/report.js +95 -13
- package/dist/routes.js +3 -0
- package/dist/sdk/caller.js +1 -1
- package/dist/sdk/client.d.ts +173 -19
- package/dist/sdk/client.js +450 -126
- package/dist/sdk/command-errors.d.ts +1 -0
- package/dist/sdk/command-errors.js +3 -1
- package/dist/sdk/context.d.ts +20 -4
- package/dist/sdk/context.js +79 -4
- package/dist/sdk/core.d.ts +56 -5
- package/dist/sdk/core.js +44 -4
- package/dist/sdk/handles.js +2 -1
- package/dist/sdk/http2.js +27 -1
- package/dist/sdk/models.gen.d.ts +269 -30
- package/dist/sdk/proxy.js +33 -4
- package/dist/sdk/sandbox.d.ts +38 -13
- package/dist/sdk/sandbox.js +29 -5
- package/dist/sdk/transport-worker.js +10 -1
- package/dist/ssh.js +58 -9
- package/dist/surface.d.ts +10 -2
- package/dist/surface.js +145 -76
- package/dist/uploadguard.js +54 -8
- package/llms.txt +31 -20
- 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
|
-
|
|
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
|
|
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([
|
|
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
|
|
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
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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
|
|
127
|
-
|
|
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" ?
|
|
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 ${
|
|
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
|
-
|
|
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
|
|
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
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
504
|
-
const
|
|
505
|
-
const
|
|
506
|
-
const
|
|
507
|
-
const
|
|
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
|
|
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", "
|
|
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
|
|
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 =
|
|
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
|
-
? [[
|
|
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
|
-
|
|
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 ? []
|
|
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
|
-
...(
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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 ? [] : [`
|
|
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:
|
|
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
|
|
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
|
|
827
|
-
const
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
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
|
|
843
|
-
|
|
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();
|