promigence 0.1.0-beta.15 → 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 +303 -50
- package/dist/agent.js +18 -5
- package/dist/billing.js +12 -4
- package/dist/bin.js +89 -13
- package/dist/bundle.js +15 -6
- package/dist/capacity.js +7 -2
- package/dist/commands.d.ts +43 -6
- package/dist/commands.js +654 -160
- package/dist/devcontainer.d.ts +4 -1
- package/dist/devcontainer.js +21 -10
- package/dist/dirsnap.js +208 -91
- package/dist/errfix.js +277 -10
- package/dist/generated.js +8 -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 +50 -12
- package/dist/login.js +92 -20
- package/dist/mcp.js +29 -11
- package/dist/money.js +2 -0
- package/dist/orgs.js +55 -18
- package/dist/pricing.d.ts +2 -0
- package/dist/pricing.js +11 -5
- package/dist/report.js +95 -13
- package/dist/routes.js +1 -0
- package/dist/sdk/caller.js +1 -1
- package/dist/sdk/client.d.ts +50 -9
- package/dist/sdk/client.js +76 -44
- 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 +24 -4
- package/dist/sdk/http2.js +22 -1
- package/dist/sdk/models.gen.d.ts +64 -7
- 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 +10 -2
- package/dist/surface.js +125 -70
- package/dist/uploadguard.js +30 -5
- 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,13 @@ 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
|
+
export function sessionLimit(seconds) {
|
|
73
|
+
return seconds % 3600 === 0 ? `${seconds / 3600} h per sandbox` : `${span(seconds)} per sandbox`;
|
|
74
|
+
}
|
|
51
75
|
export function planName(me, b) {
|
|
52
76
|
if (typeof me?.plan === "string" && me.plan)
|
|
53
77
|
return me.plan;
|
|
@@ -66,6 +90,34 @@ function span(seconds) {
|
|
|
66
90
|
return `${Math.floor(s / 60)}m${s % 60 ? `${s % 60}s` : ""}`;
|
|
67
91
|
return `${s}s`;
|
|
68
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
|
+
}
|
|
69
121
|
const status = async (ctx) => {
|
|
70
122
|
const c = client(ctx);
|
|
71
123
|
let failure;
|
|
@@ -88,12 +140,17 @@ const status = async (ctx) => {
|
|
|
88
140
|
...(conc ? { concurrency: conc } : {}),
|
|
89
141
|
...(me?.spend === undefined ? {} : { spend: me.spend }),
|
|
90
142
|
}, next);
|
|
91
|
-
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})` : ""}`];
|
|
92
147
|
for (const s of services) {
|
|
93
148
|
lines.push(` ${s.name}: ${s.state ?? "?"}`);
|
|
94
149
|
}
|
|
95
150
|
if (!services.length)
|
|
96
151
|
lines.push(" (no service reported)");
|
|
152
|
+
if (checks)
|
|
153
|
+
lines.push(...checks.lines);
|
|
97
154
|
if (conc)
|
|
98
155
|
lines.push(` your concurrency: ${concurrencyText(conc)}`);
|
|
99
156
|
else if (!c.authenticated)
|
|
@@ -104,28 +161,40 @@ const status = async (ctx) => {
|
|
|
104
161
|
};
|
|
105
162
|
const me = async (ctx) => {
|
|
106
163
|
const c = client(ctx);
|
|
107
|
-
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
|
+
]);
|
|
108
169
|
const conc = await concurrencyOf(c, info);
|
|
109
170
|
const plan = planName(info);
|
|
110
171
|
const trial = info.trial;
|
|
111
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"]);
|
|
112
173
|
const org = info.org && info.org !== info.org_id ? `${info.org} (${info.org_id})` : (info.org_id ?? "org");
|
|
113
|
-
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}` : ""}`];
|
|
114
180
|
if (info.user && typeof info.user === "object" && info.user.email)
|
|
115
181
|
lines.push(` signed up as ${info.user.email}`);
|
|
116
|
-
|
|
117
|
-
|
|
182
|
+
const shown = planDisplay(info, bill) ?? plan;
|
|
183
|
+
if (shown)
|
|
184
|
+
lines.push(` plan: ${shown}${trial?.on_trial ? " (trial)" : ""}`);
|
|
118
185
|
if (conc)
|
|
119
186
|
lines.push(` concurrency: ${concurrencyText(conc)}`);
|
|
120
|
-
|
|
187
|
+
const billCredits = creditsLine(bill?.credits);
|
|
188
|
+
if (billCredits)
|
|
189
|
+
lines.push(` ${billCredits}`);
|
|
190
|
+
else if (trial) {
|
|
121
191
|
const available = trial.credits_available ?? trial.credits_remaining;
|
|
122
192
|
const held = trial.credits_reserved ?? 0;
|
|
123
193
|
lines.push(` credits: ${dollars(available)} available${held > 0 ? ` (${dollars(held)} held by running work)` : ""}`);
|
|
124
194
|
}
|
|
125
195
|
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(" · ")})` : ""}`);
|
|
196
|
+
const month = bill?.usage?.total;
|
|
197
|
+
lines.push(` ${spendLine(info.spend, info.spend_detail?.by_kind, typeof month === "number" ? month : undefined)}`);
|
|
129
198
|
}
|
|
130
199
|
if (info.spend === undefined && info.trial === undefined) {
|
|
131
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)");
|
|
@@ -137,7 +206,7 @@ const me = async (ctx) => {
|
|
|
137
206
|
const lim = info.limits ?? {};
|
|
138
207
|
const shape = [
|
|
139
208
|
typeof lim.max_sandbox_vcpus === "number" ? `sandboxes up to ${lim.max_sandbox_vcpus} vCPU` : "",
|
|
140
|
-
typeof lim.max_session_s === "number" ?
|
|
209
|
+
typeof lim.max_session_s === "number" ? sessionLimit(lim.max_session_s) : "",
|
|
141
210
|
].filter(Boolean);
|
|
142
211
|
if (shape.length)
|
|
143
212
|
lines.push(` limits: ${shape.join(" · ")}`);
|
|
@@ -146,12 +215,22 @@ const me = async (ctx) => {
|
|
|
146
215
|
lines.push(` ${head}:`, ...rest.map((l) => ` ${l}`));
|
|
147
216
|
}
|
|
148
217
|
return emit(ctx.mode, okEnvelope(ctx.cmd.name, {
|
|
149
|
-
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 }) } } : {}),
|
|
150
221
|
...(outcomes ? { outcomes } : {}),
|
|
151
222
|
...(plan ? { plan } : {}),
|
|
152
223
|
...(conc ? { concurrency: { in_use: conc.in_use, limit: conc.limit, source: conc.source, ...(conc.at_least ? { at_least: true } : {}) } } : {}),
|
|
153
224
|
}, next), lines, 0, ctx.pointer);
|
|
154
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
|
+
}
|
|
155
234
|
export function outcomeLines(o) {
|
|
156
235
|
const usd = (n) => `$${n.toFixed(2)}`;
|
|
157
236
|
const lines = [`this month (since ${o.since.slice(0, 10)})`];
|
|
@@ -161,7 +240,7 @@ export function outcomeLines(o) {
|
|
|
161
240
|
}
|
|
162
241
|
lines.push(` runs ${o.runs.toLocaleString("en-US")}`);
|
|
163
242
|
const ready = o.ready_ms_p50 ?? null;
|
|
164
|
-
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)`}`);
|
|
165
244
|
if (o.failures_ours > 0)
|
|
166
245
|
lines.push(` failures of ours ${o.failures_ours}, not billed${o.failures_ours_usd > 0 ? ` (${usd(o.failures_ours_usd)} given back)` : ""}`);
|
|
167
246
|
const speedup = o.batch_speedup ?? null;
|
|
@@ -177,6 +256,15 @@ export function outcomeLines(o) {
|
|
|
177
256
|
lines.push(` paused, not billed ${hours.toFixed(1)} h${paidNot === null ? "" : ` (${usd(paidNot)} you did not pay)`}`);
|
|
178
257
|
return lines;
|
|
179
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
|
+
}
|
|
180
268
|
const billingUsage = async (ctx) => {
|
|
181
269
|
const limit = limitOf(ctx, 20, 1000);
|
|
182
270
|
const wantLedger = bool(ctx, "--ledger");
|
|
@@ -184,13 +272,14 @@ const billingUsage = async (ctx) => {
|
|
|
184
272
|
if (cursor !== undefined && !wantLedger)
|
|
185
273
|
throw usageError("--cursor pages the ledger", "Pass it with --ledger.");
|
|
186
274
|
const c = client(ctx);
|
|
187
|
-
const [b, me] = await Promise.all([
|
|
275
|
+
const [b, me, prices] = await Promise.all([
|
|
188
276
|
c.billing.usage().catch((e) => {
|
|
189
277
|
if (wantLedger && e?.code === "forbidden")
|
|
190
278
|
return undefined;
|
|
191
279
|
throw e;
|
|
192
280
|
}),
|
|
193
281
|
c.me().catch(() => undefined),
|
|
282
|
+
livePricing(c).catch(() => undefined),
|
|
194
283
|
]);
|
|
195
284
|
const page = wantLedger ? await c.billing.ledgerPage({ limit, ...(cursor === undefined ? {} : { cursor }) }) : undefined;
|
|
196
285
|
const entries = page?.items ?? [];
|
|
@@ -233,9 +322,9 @@ const billingUsage = async (ctx) => {
|
|
|
233
322
|
...(wantLedger ? { ledger: entries, next_cursor: page?.nextCursor ?? null } : {}),
|
|
234
323
|
}, next);
|
|
235
324
|
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 ${
|
|
325
|
+
const lines = [`promigence billing usage: ${usd(usage.total)} (${period}, plan ${planDisplay(me, b) ?? "?"}${onTrial ? ", on the trial" : ""})`];
|
|
237
326
|
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`}` +
|
|
327
|
+
`${row.gib === undefined ? "" : ` · ${row.gib} GiB`}${row.entries === undefined ? "" : ` · ${row.entries} ${row.entries === 1 ? "entry" : "entries"}`}` +
|
|
239
328
|
`${row.episodes === undefined ? "" : ` · ${row.episodes} episodes`}`;
|
|
240
329
|
for (const [k, v] of Object.entries(usage)) {
|
|
241
330
|
if (k === "total" || v === null || typeof v !== "object")
|
|
@@ -248,7 +337,8 @@ const billingUsage = async (ctx) => {
|
|
|
248
337
|
const r = inner;
|
|
249
338
|
if (!r.amount && !r.seconds && !r.count && !r.entries && !r.episodes)
|
|
250
339
|
continue;
|
|
251
|
-
|
|
340
|
+
const label = k === "other" ? kindWords(name) : `${k} ${name}`;
|
|
341
|
+
lines.push(` ${label}: ${detail(r)}${k === "sandboxes" ? belowList(prices, name, r) : ""}`);
|
|
252
342
|
}
|
|
253
343
|
continue;
|
|
254
344
|
}
|
|
@@ -492,22 +582,37 @@ const snapshotAlias = async (ctx) => {
|
|
|
492
582
|
if (clear && name) {
|
|
493
583
|
throw usageError("--clear takes no name", `Use either ${sh `promigence snapshot alias ${arg} NAME`} or ${sh `promigence snapshot alias ${arg} --clear`}.`);
|
|
494
584
|
}
|
|
495
|
-
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 } : {});
|
|
496
590
|
const next = nexts(["promigence snapshot list", "your snapshots and their names"]);
|
|
497
|
-
|
|
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);
|
|
498
594
|
};
|
|
499
595
|
const runList = async (ctx) => {
|
|
500
596
|
const wanted = stateFlag(ctx, RUN_STATES);
|
|
597
|
+
const labels = Object.entries(labelsOf(ctx));
|
|
501
598
|
const limit = limitOf(ctx, 20, 500);
|
|
502
599
|
const cursor = str(ctx, "--cursor");
|
|
503
|
-
const
|
|
504
|
-
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));
|
|
505
605
|
const runs = matched.slice(0, limit);
|
|
506
606
|
const more = runs.length === matched.length ? page.nextCursor : null;
|
|
507
|
-
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
|
+
};
|
|
508
613
|
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 ?? ""}`),
|
|
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)}`),
|
|
511
616
|
...(runs.length ? [] : [" (no runs)"]),
|
|
512
617
|
...(more ? [` more: --cursor ${more}`] : []),
|
|
513
618
|
];
|
|
@@ -531,7 +636,14 @@ function labelsOf(ctx) {
|
|
|
531
636
|
}
|
|
532
637
|
return out;
|
|
533
638
|
}
|
|
534
|
-
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
|
+
}
|
|
535
647
|
export function pauseReasonText(reason) {
|
|
536
648
|
if (reason === "spend_cap")
|
|
537
649
|
return "spend cap reached";
|
|
@@ -543,8 +655,9 @@ const sandboxList = async (ctx) => {
|
|
|
543
655
|
const labels = labelsOf(ctx);
|
|
544
656
|
const states = list(ctx, "--state").flatMap((s) => s.split(",")).map((s) => s.trim()).filter(Boolean);
|
|
545
657
|
const cursor = str(ctx, "--cursor");
|
|
658
|
+
const limit = limitOf(ctx, 50);
|
|
546
659
|
const page = await client(ctx).sandboxes.listPage({
|
|
547
|
-
limit
|
|
660
|
+
limit,
|
|
548
661
|
...(Object.keys(labels).length ? { labels } : {}),
|
|
549
662
|
...(states.length ? { state: states } : {}),
|
|
550
663
|
...(cursor === undefined ? {} : { cursor }),
|
|
@@ -564,10 +677,10 @@ const sandboxList = async (ctx) => {
|
|
|
564
677
|
held = { count, atLeast: live.nextCursor !== null };
|
|
565
678
|
}
|
|
566
679
|
}
|
|
567
|
-
const heldCmd =
|
|
680
|
+
const heldCmd = ["promigence sandbox list", sandboxListFlags({ states: HELD_STATES, labels })].filter(Boolean).join(" ");
|
|
568
681
|
const firstLive = sandboxes.find((s) => s.state !== "ended");
|
|
569
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
|
|
570
|
-
? [[
|
|
683
|
+
? [[["promigence sandbox list", sandboxListFlags({ limit, states, labels }), sh `--cursor ${page.nextCursor}`].filter(Boolean).join(" "), "the next page"]]
|
|
571
684
|
: []), ...(firstLive ? [["promigence sandbox kill SID", "end one: put its id from this list in place of SID"]] : []));
|
|
572
685
|
const shown = Object.entries(labels).map(([k, v]) => `${k}=${v}`).join(" ");
|
|
573
686
|
const lines = [
|
|
@@ -577,7 +690,7 @@ const sandboxList = async (ctx) => {
|
|
|
577
690
|
return ` ${s.sandbox_id} ${s.state}${why ? ` (${why})` : ""}${s.outcome ? ` (${s.outcome})` : ""} ${s.run_id ?? ""}`;
|
|
578
691
|
}),
|
|
579
692
|
...(sandboxes.length ? [] : [" (no sandboxes)"]),
|
|
580
|
-
...(page.nextCursor ? [` more: --cursor ${page.nextCursor}`] : []),
|
|
693
|
+
...(page.nextCursor ? [` more: ${sandboxListFlags({ limit, states, labels })} --cursor ${page.nextCursor}`] : []),
|
|
581
694
|
...(held ? [` ${held.atLeast ? "at least " : ""}${held.count} live or paused not on this page: ${heldCmd}`] : []),
|
|
582
695
|
];
|
|
583
696
|
return emit(ctx.mode, okEnvelope(ctx.cmd.name, {
|
|
@@ -642,10 +755,29 @@ export function sandboxFactLines(s, facts, nowMs = Date.now()) {
|
|
|
642
755
|
if (elapsed !== undefined && facts.perSecond !== undefined) {
|
|
643
756
|
const secs = Math.ceil(elapsed / 1000);
|
|
644
757
|
const rate = `$${facts.perSecond.toFixed(9).replace(/0+$/, "").replace(/\.$/, "")}`;
|
|
645
|
-
|
|
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})`);
|
|
646
760
|
}
|
|
647
761
|
return out;
|
|
648
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
|
+
}
|
|
649
781
|
export function supportStopLines(s) {
|
|
650
782
|
if (s.outcome !== "stopped_by_support")
|
|
651
783
|
return [];
|
|
@@ -668,11 +800,15 @@ const sandboxInfo = async (ctx) => {
|
|
|
668
800
|
: [];
|
|
669
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
|
|
670
802
|
? (s.run_id ? [[sh `promigence run report ${s.run_id} --sandbox ${sid}`, "how it ended, in its run's report"]] : [])
|
|
671
|
-
: 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"]]));
|
|
672
807
|
const lines = [
|
|
673
808
|
`sandbox ${sid}: ${s.state}${why ? ` (${why})` : ""}${s.outcome ? ` (${s.outcome})` : ""}${s.run_id ? ` · run ${s.run_id}` : ""}`,
|
|
674
809
|
...supportStopLines(s),
|
|
675
810
|
...(why ? [" its files and state are kept, and paused time is not billed"] : []),
|
|
811
|
+
...pausedLines(sid, s),
|
|
676
812
|
...queuedLines([s]),
|
|
677
813
|
...(s.snapshot_id ? [` snapshot ${s.snapshot_id}`] : []),
|
|
678
814
|
...sandboxFactLines(s, facts),
|
|
@@ -682,15 +818,22 @@ const sandboxInfo = async (ctx) => {
|
|
|
682
818
|
...(Number(s.auto_pause_s ?? 0) > 0 ? [` pauses itself after ${s.auto_pause_s} s idle`] : []),
|
|
683
819
|
...execs.map((e) => ` ${e.exec_id} exit ${e.exit_code ?? "—"} ${(e.argv ?? []).join(" ")}`),
|
|
684
820
|
...(execs.length ? [] : [" (no execs recorded)"]),
|
|
821
|
+
...(ended ? [] : [` background commands (exec --background): ${sh `promigence exec ps --sandbox ${sid}`}`]),
|
|
685
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;
|
|
686
830
|
return emit(ctx.mode, okEnvelope(ctx.cmd.name, {
|
|
687
831
|
sandbox: s,
|
|
688
832
|
...(facts.tier ? { tier: facts.tier } : {}),
|
|
689
|
-
...(
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
? { 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"] }
|
|
694
837
|
: {}),
|
|
695
838
|
}, next), lines, 0, ctx.pointer);
|
|
696
839
|
};
|
|
@@ -705,7 +848,7 @@ const sandboxPause = async (ctx) => {
|
|
|
705
848
|
`sandbox ${sid}: paused${res.already_paused ? " (already was)" : ""}`,
|
|
706
849
|
" it holds no capacity and bills no seconds while parked",
|
|
707
850
|
...(took === undefined ? [] : [` took ${ms(took)}`]),
|
|
708
|
-
...(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`]),
|
|
709
852
|
...(killed > 0 ? [` ended ${killed} background process${killed === 1 ? "" : "es"} (started with exec --background)`] : []),
|
|
710
853
|
...(res.warnings ?? []).map((w) => ` warning: ${w}`),
|
|
711
854
|
];
|
|
@@ -789,7 +932,7 @@ const sandboxAutopause = async (ctx) => {
|
|
|
789
932
|
throw usageError("--mode and --off ask for opposite things", "--off removes the idle rule; --mode says what idle means under one.");
|
|
790
933
|
}
|
|
791
934
|
const rule = (s, m) => s > 0
|
|
792
|
-
? `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)" : ""}`
|
|
793
936
|
: "never pauses itself on idle";
|
|
794
937
|
const next = nexts([sh `promigence sandbox info ${sid}`, "the sandbox as it stands now"]);
|
|
795
938
|
if (after === undefined && !off && mode === undefined) {
|
|
@@ -818,18 +961,122 @@ const sandboxAutopause = async (ctx) => {
|
|
|
818
961
|
];
|
|
819
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);
|
|
820
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
|
+
}
|
|
821
1038
|
const sandboxProbe = async (ctx) => {
|
|
822
1039
|
const sid = positional(ctx, "a sandbox id", "promigence sandbox probe sbx_01");
|
|
823
1040
|
const paths = list(ctx, "--path");
|
|
824
|
-
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));
|
|
825
1044
|
const ok = res.ok !== false;
|
|
826
|
-
const
|
|
827
|
-
const
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
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);
|
|
833
1080
|
};
|
|
834
1081
|
const filesRm = async (ctx) => {
|
|
835
1082
|
const path = positional(ctx, "a path", "promigence files rm /work/out.log --sandbox sbx_01");
|
|
@@ -839,8 +1086,14 @@ const filesRm = async (ctx) => {
|
|
|
839
1086
|
const sid = required(ctx, "--sandbox", "Pass --sandbox sbx_<id> — the sandbox the path is in.");
|
|
840
1087
|
const recursive = bool(ctx, "--recursive");
|
|
841
1088
|
const res = await client(ctx).sandboxes.files.delete(sid, path, { recursive });
|
|
842
|
-
const
|
|
843
|
-
|
|
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);
|
|
844
1097
|
};
|
|
845
1098
|
const secretsList = async (ctx) => {
|
|
846
1099
|
const { secrets } = await client(ctx).secrets.list();
|
package/dist/agent.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_SNAPSHOT, PromigenceError, checkImageRef, checkRepoUrl, imageSnapshot, } from "./sdk/index.js";
|
|
2
2
|
import { snapshotArg } from "./bundle.js";
|
|
3
|
-
import { bool, client, nexts, spendCap, str, waitBound, } from "./commands.js";
|
|
3
|
+
import { bool, client, list, nexts, spendCap, str, waitBound, } from "./commands.js";
|
|
4
4
|
import { emit, errorEnvelope, errorTextLines, okEnvelope, parseDuration, usageError, write, } from "./io.js";
|
|
5
5
|
import { findCommand } from "./surface.js";
|
|
6
6
|
import { LIVE_HANDLERS } from "./live.js";
|
|
@@ -75,7 +75,7 @@ const OPENCODE_PROVIDER_VAR = {
|
|
|
75
75
|
const OWN_PROVIDER = {
|
|
76
76
|
claude: { id: "anthropic", name: "Anthropic" }, codex: { id: "openai", name: "OpenAI" },
|
|
77
77
|
};
|
|
78
|
-
function withModel(which, agent, model) {
|
|
78
|
+
function withModel(which, agent, model, keySecret) {
|
|
79
79
|
if (model === undefined)
|
|
80
80
|
return { agent };
|
|
81
81
|
if (!/^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,127}$/.test(model)) {
|
|
@@ -95,7 +95,11 @@ function withModel(which, agent, model) {
|
|
|
95
95
|
if (slash <= 0 || slash === model.length - 1) {
|
|
96
96
|
throw usageError(`opencode takes --model as provider/model, got '${model}'`, "Name the provider first, e.g. --model xai/grok-4-0709 or --model anthropic/claude-sonnet-4-5.");
|
|
97
97
|
}
|
|
98
|
-
const
|
|
98
|
+
const provider = model.slice(0, slash).toLowerCase();
|
|
99
|
+
const v = OPENCODE_PROVIDER_VAR[provider];
|
|
100
|
+
if (v === undefined && keySecret === undefined) {
|
|
101
|
+
throw usageError(`opencode's provider '${provider}' (--model ${model}) is not one whose key variable is known here`, `Name its key: --key-secret NAME=VAR, the stored secret and the variable opencode reads for ${provider}; or a provider known here: ${Object.keys(OPENCODE_PROVIDER_VAR).join(", ")}.`);
|
|
102
|
+
}
|
|
99
103
|
return { agent: v ? { ...agent, defaultSecret: v, keyVars: [v] } : agent, model };
|
|
100
104
|
}
|
|
101
105
|
const an = (word) => (/^[AEIOU]/.test(word) ? "an" : "a");
|
|
@@ -220,7 +224,7 @@ const agentStart = async (ctx) => {
|
|
|
220
224
|
if (known === undefined) {
|
|
221
225
|
throw usageError(`unknown agent '${which}'`, `Pick one of ${AGENT_NAMES.join(" | ")}.`);
|
|
222
226
|
}
|
|
223
|
-
const { agent, model } = withModel(which, known, str(ctx, "--model"));
|
|
227
|
+
const { agent, model } = withModel(which, known, str(ctx, "--model"), str(ctx, "--key-secret"));
|
|
224
228
|
const key = keyBindings(agent, str(ctx, "--key-secret"), which);
|
|
225
229
|
const fromEnv = str(ctx, "--key-from-env");
|
|
226
230
|
let keyValue;
|
|
@@ -231,6 +235,15 @@ const agentStart = async (ctx) => {
|
|
|
231
235
|
}
|
|
232
236
|
}
|
|
233
237
|
checkSource(ctx);
|
|
238
|
+
const labels = {};
|
|
239
|
+
for (const l of list(ctx, "--label")) {
|
|
240
|
+
const i = l.indexOf("=");
|
|
241
|
+
if (i < 1)
|
|
242
|
+
throw usageError(`--label: '${l}' is not NAME=VALUE`, "Pass --label name=value (repeatable).");
|
|
243
|
+
if (l.slice(0, i) === "agent")
|
|
244
|
+
throw usageError("--label agent=… is the agent's own label", "Name another label: agent start sets agent=NAME itself.");
|
|
245
|
+
labels[l.slice(0, i)] = l.slice(i + 1);
|
|
246
|
+
}
|
|
234
247
|
const repo = str(ctx, "--repo");
|
|
235
248
|
if (repo !== undefined)
|
|
236
249
|
checkRepo(repo);
|
|
@@ -254,7 +267,7 @@ const agentStart = async (ctx) => {
|
|
|
254
267
|
const snapshot = await snapshotFor(ctx, c);
|
|
255
268
|
const started = await c.runs.start({
|
|
256
269
|
snapshot, count: 1, timeoutMs, spendCap: await spendCap(ctx, c), secrets: key.bindings,
|
|
257
|
-
labels: { agent: which },
|
|
270
|
+
labels: { ...labels, agent: which },
|
|
258
271
|
});
|
|
259
272
|
const runId = started.run_id;
|
|
260
273
|
const bound = waitBound(ctx);
|