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.
- package/dist/account.js +359 -72
- package/dist/agent.js +18 -5
- package/dist/billing.js +274 -0
- package/dist/bin.js +91 -14
- package/dist/bundle.js +15 -6
- package/dist/capacity.js +7 -2
- package/dist/commands.d.ts +43 -6
- package/dist/commands.js +656 -162
- package/dist/devcontainer.d.ts +4 -1
- package/dist/devcontainer.js +21 -10
- package/dist/dirsnap.js +208 -91
- package/dist/errfix.js +289 -16
- package/dist/generated.js +31 -13
- package/dist/github.js +5 -1
- package/dist/gitlab.js +3 -2
- package/dist/guide.js +47 -27
- package/dist/handlers.js +76 -20
- 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 +50 -12
- package/dist/login.js +96 -24
- package/dist/mcp.js +29 -11
- package/dist/money.js +10 -0
- package/dist/orgs.js +153 -12
- package/dist/pricing.d.ts +3 -0
- package/dist/pricing.js +17 -7
- package/dist/report.js +95 -13
- package/dist/routes.js +12 -4
- package/dist/sdk/caller.js +1 -1
- package/dist/sdk/client.d.ts +134 -23
- package/dist/sdk/client.js +201 -77
- package/dist/sdk/command-errors.d.ts +1 -0
- package/dist/sdk/command-errors.js +3 -1
- package/dist/sdk/context.d.ts +19 -4
- package/dist/sdk/context.js +74 -4
- package/dist/sdk/core.d.ts +32 -4
- package/dist/sdk/core.js +26 -4
- package/dist/sdk/http2.js +22 -1
- package/dist/sdk/models.gen.d.ts +663 -43
- package/dist/sdk/pty.d.ts +11 -14
- package/dist/sdk/pty.js +13 -11
- package/dist/sdk/sandbox.d.ts +26 -8
- package/dist/sdk/sandbox.js +28 -4
- package/dist/sdk/transport-worker.js +10 -1
- package/dist/ssh.js +58 -9
- package/dist/surface.d.ts +11 -3
- package/dist/surface.js +205 -76
- package/dist/uploadguard.js +30 -5
- package/llms.txt +44 -24
- 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
|
-
|
|
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
|
|
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([
|
|
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
|
|
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
|
-
|
|
115
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
lines.push(`
|
|
121
|
-
|
|
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
|
|
125
|
-
|
|
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" ?
|
|
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
|
|
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 ${
|
|
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
|
-
|
|
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
|
|
350
|
+
lines.push(` paid from credits: ${usd(usage["credits_applied"])}`);
|
|
259
351
|
}
|
|
260
352
|
const trial = b.trial;
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
:
|
|
329
|
-
|
|
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
|
|
334
|
-
const
|
|
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
|
|
382
|
-
`${o.qualifying_plans.length ? `
|
|
383
|
-
`${o.hold_days > 0 ? `${o.hold_days} day
|
|
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
|
|
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
|
|
409
|
-
`${r.qualifying_plans.length ? `
|
|
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
|
|
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
|
-
|
|
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
|
|
476
|
-
const
|
|
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
|
|
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", "
|
|
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
|
|
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 =
|
|
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
|
-
? [[
|
|
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
|
-
|
|
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
|
|
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 ? []
|
|
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
|
-
...(
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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 ? [] : [`
|
|
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:
|
|
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
|
|
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
|
|
793
|
-
const
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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
|
|
809
|
-
|
|
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();
|