promigence 0.1.0-beta.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/NOTICE +14 -0
  2. package/README.md +88 -0
  3. package/dist/account.js +850 -0
  4. package/dist/agent.js +374 -0
  5. package/dist/argv.js +159 -0
  6. package/dist/bin.js +332 -0
  7. package/dist/bundle.js +133 -0
  8. package/dist/capacity.js +69 -0
  9. package/dist/commands.d.ts +110 -0
  10. package/dist/commands.js +2675 -0
  11. package/dist/connectreq.js +125 -0
  12. package/dist/credfile.js +188 -0
  13. package/dist/credits.js +197 -0
  14. package/dist/devcontainer.d.ts +99 -0
  15. package/dist/devcontainer.js +744 -0
  16. package/dist/device.js +129 -0
  17. package/dist/dirsnap.js +378 -0
  18. package/dist/errfix.js +262 -0
  19. package/dist/generated.d.ts +35 -0
  20. package/dist/generated.js +307 -0
  21. package/dist/github.js +307 -0
  22. package/dist/gitlab.js +210 -0
  23. package/dist/guide.d.ts +19 -0
  24. package/dist/guide.js +366 -0
  25. package/dist/handlers.js +511 -0
  26. package/dist/index.d.ts +14 -0
  27. package/dist/index.js +7 -0
  28. package/dist/init.d.ts +40 -0
  29. package/dist/init.js +352 -0
  30. package/dist/install.d.ts +23 -0
  31. package/dist/install.js +127 -0
  32. package/dist/io.d.ts +37 -0
  33. package/dist/io.js +318 -0
  34. package/dist/live.js +469 -0
  35. package/dist/login.js +1063 -0
  36. package/dist/mcp.js +889 -0
  37. package/dist/money.js +5 -0
  38. package/dist/orgs.js +475 -0
  39. package/dist/pricing.d.ts +52 -0
  40. package/dist/pricing.js +181 -0
  41. package/dist/report.js +132 -0
  42. package/dist/routes.js +135 -0
  43. package/dist/runners-github.js +257 -0
  44. package/dist/runners-gitlab.js +125 -0
  45. package/dist/runners-spec.js +137 -0
  46. package/dist/runners.js +1458 -0
  47. package/dist/sdk/blake3.d.ts +10 -0
  48. package/dist/sdk/blake3.js +111 -0
  49. package/dist/sdk/bundle.d.ts +96 -0
  50. package/dist/sdk/bundle.js +227 -0
  51. package/dist/sdk/ca-guard.js +166 -0
  52. package/dist/sdk/caller.d.ts +73 -0
  53. package/dist/sdk/caller.js +47 -0
  54. package/dist/sdk/client.d.ts +3015 -0
  55. package/dist/sdk/client.js +2583 -0
  56. package/dist/sdk/command-errors.d.ts +41 -0
  57. package/dist/sdk/command-errors.js +36 -0
  58. package/dist/sdk/context.d.ts +119 -0
  59. package/dist/sdk/context.js +588 -0
  60. package/dist/sdk/core.d.ts +285 -0
  61. package/dist/sdk/core.js +266 -0
  62. package/dist/sdk/credstore.d.ts +90 -0
  63. package/dist/sdk/credstore.js +206 -0
  64. package/dist/sdk/dispatcher.js +86 -0
  65. package/dist/sdk/github.d.ts +158 -0
  66. package/dist/sdk/github.js +78 -0
  67. package/dist/sdk/gitlab.d.ts +104 -0
  68. package/dist/sdk/gitlab.js +20 -0
  69. package/dist/sdk/handles.d.ts +82 -0
  70. package/dist/sdk/handles.js +135 -0
  71. package/dist/sdk/hint.js +7 -0
  72. package/dist/sdk/http2.js +1921 -0
  73. package/dist/sdk/index.d.ts +34 -0
  74. package/dist/sdk/index.js +15 -0
  75. package/dist/sdk/keyed.gen.js +7 -0
  76. package/dist/sdk/models.gen.d.ts +6162 -0
  77. package/dist/sdk/models.gen.js +1 -0
  78. package/dist/sdk/proxy.js +249 -0
  79. package/dist/sdk/pty.d.ts +171 -0
  80. package/dist/sdk/pty.js +496 -0
  81. package/dist/sdk/sandbox.d.ts +537 -0
  82. package/dist/sdk/sandbox.js +725 -0
  83. package/dist/sdk/ssh.d.ts +25 -0
  84. package/dist/sdk/ssh.js +114 -0
  85. package/dist/sdk/trace.js +80 -0
  86. package/dist/sdk/transport-worker.js +466 -0
  87. package/dist/sdk/warm.js +40 -0
  88. package/dist/ssh.js +267 -0
  89. package/dist/surface.d.ts +95 -0
  90. package/dist/surface.js +1452 -0
  91. package/dist/templates.js +53 -0
  92. package/dist/term.js +50 -0
  93. package/dist/timing.js +71 -0
  94. package/dist/untar.js +324 -0
  95. package/dist/uploadguard.js +75 -0
  96. package/llms.txt +209 -0
  97. package/package.json +52 -0
@@ -0,0 +1,2675 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { createHash, randomBytes } from "node:crypto";
3
+ import { closeSync, constants as fsConstants, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, renameSync, rmSync, statSync, writeSync, } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { basename, dirname, join, resolve } from "node:path";
6
+ import { DEFAULT_API_URL, DEFAULT_SNAPSHOT, ERRORS_DOCS_URL, MAX_OUTPUT_BYTES_DEFAULT, CONTEXT_GIT_RE, PromigenceClient, checkRepoUrl, withoutPin, PromigenceError, SpendCapError, TimeoutError, checkImageRef, contextRelative, credentialsPath, FILE_PUT_CHUNK_BYTES, imageSnapshot, isWaiting, execOutputBytes, listContext, packContext, readCredentials, } from "./sdk/index.js";
7
+ import { fmtMs, recordTiming } from "./timing.js";
8
+ import { capacityLine, queuedClause, queuedLines, statusLine } from "./capacity.js";
9
+ import { DevcontainerError, detectOne, load, planJson, planLines, planSetup, resolveGiven, snapshotEnv, snapshotUsers, translate, workspaceRootFor, wrapCommand, } from "./devcontainer.js";
10
+ import { detect, ecosystemOfCommands, imageCreateCommand, readConfig } from "./init.js";
11
+ import { CREDENTIAL_REMEDY, confirmOverride, credentialRefusal, emitUploadList, localTarNoMetadataArgs } from "./uploadguard.js";
12
+ import { emit, errorEnvelope, errorTextLines, exitWith, forkLine, forkSummary, gradeLine, ms, noCommandFix, okEnvelope, parseMaxOutput, recordNotice, sandboxGrade, parseCommandTimeout, parseCount, parseDuration, parseUsd, pickExit, reportSummaryLines, toPromigenceError, totalsLine, usageError, write, writeBytes, } from "./io.js";
13
+ import { buildPrice, buildPriceWords } from "./pricing.js";
14
+ import { DOCS_URL, WAIT_TIMEOUT_DEFAULT } from "./surface.js";
15
+ import { escapeLine, fieldForTerminal, forTerminal, jsonLine, sh, shellArg } from "./term.js";
16
+ import { snapshotArg } from "./bundle.js";
17
+ import { loginMintedKey, revokeReplacedKey, storeCredentials, storedWhere } from "./credfile.js";
18
+ import { isSecureUrl, tlsChecksOff } from "./login.js";
19
+ import { duration, reportLines, reproCommand, sandboxLines } from "./report.js";
20
+ import { ERROR_BY_CODE } from "./generated.js";
21
+ import { lookupError } from "./guide.js";
22
+ import { usd as moneyUsd } from "./money.js";
23
+ import { execBackground } from "./live.js";
24
+ import { dirSnapshotCreate } from "./dirsnap.js";
25
+ import { githubCreate } from "./github.js";
26
+ import { untarSafe, UnsafeArchiveError } from "./untar.js";
27
+ export function str(ctx, name) {
28
+ const v = ctx.flags.get(name);
29
+ if (!v || v.length === 0)
30
+ return undefined;
31
+ const last = v[v.length - 1];
32
+ if (last === true) {
33
+ throw usageError(`${name} needs a value`, `Pass ${name} <value> — or ${name}=<value> if the value starts with '--'; see 'promigence ${ctx.cmd.name} --help'.`);
34
+ }
35
+ return last;
36
+ }
37
+ export function list(ctx, name) {
38
+ return (ctx.flags.get(name) ?? []).filter((v) => typeof v === "string");
39
+ }
40
+ export function bool(ctx, name) {
41
+ return ctx.flags.has(name);
42
+ }
43
+ export function required(ctx, name, hint) {
44
+ const v = str(ctx, name);
45
+ if (v === undefined)
46
+ throw usageError(`${name} is required`, hint);
47
+ return v;
48
+ }
49
+ export function clientOpts(ctx) {
50
+ return {
51
+ cliErrors: true,
52
+ ...(ctx.apiKey === undefined ? {} : { apiKey: ctx.apiKey }),
53
+ ...(ctx.requestTimeoutMs === undefined ? {} : { requestTimeoutMs: ctx.requestTimeoutMs }),
54
+ };
55
+ }
56
+ export function client(ctx) {
57
+ return new PromigenceClient({ ...clientOpts(ctx), onNotice: recordNotice, onTiming: recordTiming });
58
+ }
59
+ export async function serverLocation(ctx) {
60
+ const h = await client(ctx).health({ timeoutMs: 3000 }).catch(() => undefined);
61
+ return typeof h?.location === "string" && h.location !== "" ? h.location : undefined;
62
+ }
63
+ export const nexts = (...items) => items.map(([cmd, why]) => ({ cmd, why }));
64
+ export const KEPT_BY_HOLDERS = "sandboxes that were already given its value keep it until they end; it stays masked in their output";
65
+ export function waitBound(ctx) {
66
+ const v = str(ctx, "--wait-timeout") ?? WAIT_TIMEOUT_DEFAULT;
67
+ return parseDuration(v, "--wait-timeout");
68
+ }
69
+ export async function spendCap(ctx, c) {
70
+ const flag = str(ctx, "--cap");
71
+ if (flag !== undefined)
72
+ return parseUsd(flag, "--cap");
73
+ const env = process.env["PROMIGENCE_SPEND_CAP"];
74
+ if (env !== undefined && env !== "")
75
+ return parseUsd(env, "PROMIGENCE_SPEND_CAP");
76
+ const me = await c.me();
77
+ if (typeof me.default_spend_cap === "number" && me.default_spend_cap > 0)
78
+ return me.default_spend_cap;
79
+ throw usageError("no spend cap: every run is capped before it starts", "Pass --cap USD (e.g. --cap 1), set PROMIGENCE_SPEND_CAP, or ask for an org default_spend_cap (promigence me).");
80
+ }
81
+ function autoIdempotencyKey(ctx) {
82
+ if (bool(ctx, "--fresh"))
83
+ return undefined;
84
+ const explicit = str(ctx, "--idempotency-key");
85
+ if (explicit !== undefined)
86
+ return explicit;
87
+ const h = createHash("sha256");
88
+ h.update(process.argv.slice(2).join(" "));
89
+ h.update(process.cwd());
90
+ try {
91
+ h.update(readFileSync("promigence.toml"));
92
+ }
93
+ catch {
94
+ h.update("");
95
+ }
96
+ h.update(ctx.apiKey ?? process.env["PROMIGENCE_API_KEY"] ?? readCredentials().api_key ?? "");
97
+ h.update(String(Math.floor(Date.now() / 600_000)));
98
+ return `hk-cli-${h.digest("hex").slice(0, 32)}`;
99
+ }
100
+ export function replayNote(started, explicitKey) {
101
+ if (started.idempotent_replay !== true)
102
+ return undefined;
103
+ const age = started.idempotent_replay_age_s;
104
+ const when = age === undefined ? "earlier"
105
+ : age < 60 ? `${age}s ago`
106
+ : `${Math.round(age / 60)} min ago`;
107
+ return explicitKey
108
+ ? `same --idempotency-key as ${when}: returning that run's sandboxes (a new key starts new ones)`
109
+ : `same command as ${when}: returning that run's sandboxes (pass --fresh for new ones)`;
110
+ }
111
+ export function buildReplayNote(started, explicitKey) {
112
+ if (started.idempotent_replay !== true)
113
+ return undefined;
114
+ const age = started.idempotent_replay_age_s;
115
+ const when = age === undefined ? "earlier" : age < 60 ? `${age}s ago` : `${Math.round(age / 60)} min ago`;
116
+ return explicitKey
117
+ ? `same --idempotency-key as ${when}: reusing build ${started.build_id ?? "?"} (nothing charged; a new key starts a new build)`
118
+ : `same command as ${when}: reusing build ${started.build_id ?? "?"} (nothing charged; pass --fresh for a new build)`;
119
+ }
120
+ function replayFields(started) {
121
+ if (started.idempotent_replay === undefined)
122
+ return {};
123
+ return {
124
+ idempotent_replay: started.idempotent_replay,
125
+ ...(started.idempotent_replay ? { replayed: true } : {}),
126
+ ...(started.idempotent_replay_age_s === undefined ? {} : { idempotent_replay_age_s: started.idempotent_replay_age_s }),
127
+ };
128
+ }
129
+ export function optimizeForFlag(ctx) {
130
+ const v = str(ctx, "--optimize-for");
131
+ if (v === undefined)
132
+ return undefined;
133
+ if (v !== "start" && v !== "compute") {
134
+ throw usageError(`--optimize-for: '${v}' is not a choice`, "Pass --optimize-for start | compute (default start).");
135
+ }
136
+ return v;
137
+ }
138
+ async function runStartOpts(ctx, c, resolved) {
139
+ const snapshot = resolved ?? await snapshotArg(ctx, c);
140
+ const count = parseCount(required(ctx, "--count", "Pass --count N (number of sandboxes to fork)."), "--count");
141
+ const opts = { snapshot, count, spendCap: await spendCap(ctx, c) };
142
+ const timeout = str(ctx, "--timeout");
143
+ opts.timeoutMs = timeout === undefined ? 15 * 60_000 : parseDuration(timeout, "--timeout");
144
+ if (bool(ctx, "--allow-partial"))
145
+ opts.allowPartial = true;
146
+ if (bool(ctx, "--verify-each"))
147
+ opts.verifyEach = true;
148
+ const optimizeFor = optimizeForFlag(ctx);
149
+ if (optimizeFor !== undefined)
150
+ opts.optimizeFor = optimizeFor;
151
+ const network = str(ctx, "--network");
152
+ const allow = list(ctx, "--allow").flatMap((h) => h.split(",")).map((h) => h.trim()).filter(Boolean);
153
+ if (network !== undefined) {
154
+ if (network !== "none" && network !== "egress") {
155
+ throw usageError(`--network: '${network}' is not a mode`, "Pass --network none | egress (default egress), or --allow HOST to name what the sandbox may reach.");
156
+ }
157
+ opts.network = network;
158
+ }
159
+ if (allow.length) {
160
+ if (network !== undefined && network !== "egress") {
161
+ throw usageError(`--allow cannot be combined with --network ${network}`, "Use --allow HOST on its own: it already runs the sandbox behind an outbound allow-list.");
162
+ }
163
+ opts.network = { allow };
164
+ }
165
+ const secrets = list(ctx, "--secret");
166
+ if (secrets.length)
167
+ opts.secrets = secrets;
168
+ const labels = {};
169
+ for (const l of list(ctx, "--label")) {
170
+ const i = l.indexOf("=");
171
+ if (i < 1)
172
+ throw usageError(`--label: '${l}' is not NAME=VALUE`, "Pass --label name=value (repeatable).");
173
+ labels[l.slice(0, i)] = l.slice(i + 1);
174
+ }
175
+ if (Object.keys(labels).length)
176
+ opts.labels = labels;
177
+ const grade = gradeOpts(ctx);
178
+ if (grade)
179
+ opts.grade = grade;
180
+ if (bool(ctx, "--grade-output"))
181
+ opts.gradeOutput = true;
182
+ return opts;
183
+ }
184
+ export function gradeOpts(ctx) {
185
+ const cmd = str(ctx, "--grade");
186
+ const holdout = str(ctx, "--holdout");
187
+ const score = str(ctx, "--grade-score");
188
+ const timeout = str(ctx, "--grade-timeout");
189
+ if (cmd === undefined) {
190
+ if (holdout !== undefined || score !== undefined || timeout !== undefined) {
191
+ throw usageError("--holdout / --grade-score / --grade-timeout need --grade", "Pass --grade 'pytest -q tests/' alongside them.");
192
+ }
193
+ return undefined;
194
+ }
195
+ const out = { cmd };
196
+ if (holdout !== undefined)
197
+ out.holdoutCmd = holdout;
198
+ if (timeout !== undefined)
199
+ out.timeoutMs = parseDuration(timeout, "--grade-timeout");
200
+ if (score !== undefined) {
201
+ if (score !== "exit" && score !== "last_line_json" && score !== "tap") {
202
+ throw usageError(`--grade-score: '${score}' is not a mode`, "Pass --grade-score exit | last_line_json | tap (default exit).");
203
+ }
204
+ out.score = score;
205
+ }
206
+ return out;
207
+ }
208
+ function reportExits(report) {
209
+ const t = report.totals;
210
+ const codes = [];
211
+ if (!t)
212
+ return codes;
213
+ if (t.env_failed > 0 || (t.grader_failed ?? 0) > 0)
214
+ codes.push(3);
215
+ if (t.not_started > 0 || t.cap_killed > 0 || t.expired > 0 || (t.stopped_by_support ?? 0) > 0)
216
+ codes.push(12);
217
+ if (t.task_failed > 0 || t.snapshot_failed > 0)
218
+ codes.push(4);
219
+ return codes;
220
+ }
221
+ async function readStdin() {
222
+ const chunks = [];
223
+ for await (const chunk of process.stdin)
224
+ chunks.push(Buffer.from(chunk));
225
+ return Buffer.concat(chunks).toString("utf8");
226
+ }
227
+ const authLogin = async (ctx) => {
228
+ const fromEnv = str(ctx, "--from-env");
229
+ const onArgv = str(ctx, "--api-key");
230
+ const warnings = [];
231
+ let key;
232
+ if (onArgv !== undefined) {
233
+ key = onArgv;
234
+ warnings.push("--api-key puts the key on argv, where `ps` and shell history can read it; prefer --from-env VAR or stdin.");
235
+ }
236
+ else if (fromEnv !== undefined) {
237
+ key = process.env[fromEnv];
238
+ if (!key) {
239
+ throw usageError(`--from-env ${fromEnv}: the variable is empty or unset`, `Export it first: export ${fromEnv}=prom_api_... && ${sh `promigence auth login --from-env ${fromEnv}`}.`);
240
+ }
241
+ }
242
+ else {
243
+ if (process.stdin.isTTY) {
244
+ throw usageError("no key given and stdin is a TTY (promigence never prompts)", 'Pipe it: printf %s "$PROMIGENCE_API_KEY" | promigence auth login, or use --from-env PROMIGENCE_API_KEY.');
245
+ }
246
+ key = (await readStdin()).trim();
247
+ if (!key)
248
+ throw usageError("stdin was empty", 'Pipe the key: printf %s "$PROMIGENCE_API_KEY" | promigence auth login.');
249
+ }
250
+ key = key.trim();
251
+ const path = credentialsPath();
252
+ const existing = readCredentials();
253
+ const previous = loginMintedKey();
254
+ const body = { api_key: key };
255
+ const apiUrl = process.env["PROMIGENCE_API_URL"] || existing.api_url;
256
+ const bound = apiUrl && apiUrl.replace(/\/+$/, "") !== DEFAULT_API_URL.replace(/\/+$/, "") ? apiUrl : undefined;
257
+ if (bound && !isSecureUrl(bound)) {
258
+ const from = process.env["PROMIGENCE_API_URL"] ? "PROMIGENCE_API_URL" : `the api_url in ${path}`;
259
+ await write(process.stderr, `promigence auth login: warning: ${bound} (from ${from}) is not https; every request `
260
+ + "will carry this key in the clear. Use the https URL unless this is a private benchmark network.\n");
261
+ }
262
+ const revokes = previous !== undefined && previous.apiKey !== key;
263
+ if (bound)
264
+ body["api_url"] = bound;
265
+ const stored = storeCredentials(body);
266
+ warnings.push(...stored.warnings);
267
+ let revoked;
268
+ if (previous && revokes && tlsChecksOff()) {
269
+ warnings.push(`the key this replaced (${previous.keyId}) was not revoked: TLS certificate checks are off `
270
+ + `(NODE_TLS_REJECT_UNAUTHORIZED), so it was not sent; revoke it with \`${sh `promigence keys revoke ${previous.keyId}`}\` once they are on`);
271
+ }
272
+ else if (previous && revokes) {
273
+ const r = await revokeReplacedKey(previous, "this replaced", key);
274
+ revoked = r.revoked;
275
+ if (r.warning)
276
+ warnings.push(r.warning);
277
+ }
278
+ const next = nexts(["promigence me", "confirm the key, org limits and default spend cap"], ["promigence examples", "what you can fork with no build (the built-in base)"]);
279
+ const fields = {
280
+ credentials: path, store: stored.store, mode: stored.store === "file" ? "0600" : null, replaced: Boolean(existing.api_key),
281
+ api_url: (bound ?? DEFAULT_API_URL).replace(/\/+$/, ""), revoked_key_id: revoked ?? null,
282
+ };
283
+ if (stored.note)
284
+ fields["store_note"] = stored.note;
285
+ if (warnings.length)
286
+ fields["warnings"] = warnings;
287
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, fields, next), [
288
+ `promigence auth login: key stored in ${storedWhere(stored)}, sent only to ${(bound ?? DEFAULT_API_URL).replace(/\/+$/, "")}`,
289
+ ...(stored.note ? [` note: ${stored.note}`] : []),
290
+ ...(revoked ? [` replaced key ${revoked} revoked`] : []),
291
+ ...warnings.map((w) => ` warning: ${w}`),
292
+ ` next: ${next[0].cmd}`,
293
+ ], 0, ctx.pointer);
294
+ };
295
+ const secretsSet = async (ctx) => {
296
+ const c = client(ctx);
297
+ if (bool(ctx, "--list")) {
298
+ const { secrets } = await c.secrets.list();
299
+ const next = nexts(["promigence run --count 1 --cap 1 --secret NAME -- CMD", "use one in a run"]);
300
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, { secrets }, next), [
301
+ `promigence secrets: ${secrets.length} stored`,
302
+ ...secrets.flatMap(secretListLines),
303
+ ], 0, false);
304
+ }
305
+ const name = positional(ctx, "a secret name", "promigence secrets set OPENAI_API_KEY --from-env OPENAI_API_KEY");
306
+ if (ctx.args.length > 1) {
307
+ throw usageError("a value on the command line is refused (it would be kept in your shell history)", `Use --from-env VAR, --from-file PATH, or pipe it on stdin: printf %s "$KEY" | ${sh `promigence secrets set ${name}`}.`);
308
+ }
309
+ if (bool(ctx, "--delete")) {
310
+ const res = await c.secrets.delete(name);
311
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, { name: res.name, deleted: res.deleted, note: KEPT_BY_HOLDERS }, []), [`${sh `promigence secrets set ${name} --delete`}: removed`, KEPT_BY_HOLDERS], 0, false);
312
+ }
313
+ const fromEnv = str(ctx, "--from-env");
314
+ const fromFile = str(ctx, "--from-file");
315
+ if (fromEnv !== undefined && fromFile !== undefined) {
316
+ throw usageError("--from-env and --from-file are alternatives", "Pick one source for the value.");
317
+ }
318
+ let value;
319
+ let source;
320
+ if (fromEnv !== undefined) {
321
+ value = process.env[fromEnv];
322
+ source = `env:${fromEnv}`;
323
+ if (!value) {
324
+ throw usageError(`--from-env ${fromEnv}: the variable is empty or unset`, `Export it first: export ${fromEnv}=... && ${sh `promigence secrets set ${name} --from-env ${fromEnv}`}.`);
325
+ }
326
+ }
327
+ else if (fromFile !== undefined) {
328
+ source = `file:${fromFile}`;
329
+ try {
330
+ value = readFileSync(fromFile, "utf8");
331
+ }
332
+ catch (err) {
333
+ throw usageError(`--from-file ${fromFile}: ${err?.message ?? "cannot read"}`, "Point it at a readable file containing only the value.");
334
+ }
335
+ }
336
+ else {
337
+ source = "stdin";
338
+ if (process.stdin.isTTY) {
339
+ throw usageError("no value given and stdin is a TTY (promigence never prompts)", `Pipe it: printf %s "$OPENAI_API_KEY" | ${sh `promigence secrets set ${name}`}, or use --from-env OPENAI_API_KEY.`);
340
+ }
341
+ value = await readStdin();
342
+ if (!value.trim()) {
343
+ throw usageError("stdin was empty", `Pipe the value: printf %s "$KEY" | ${sh `promigence secrets set ${name}`}.`);
344
+ }
345
+ }
346
+ value = value.replace(/\r?\n$/, "");
347
+ const hosts = list(ctx, "--host");
348
+ const headers = list(ctx, "--header");
349
+ const noRaw = bool(ctx, "--no-raw");
350
+ const opts = {
351
+ ...(hosts.length ? { hosts } : {}),
352
+ ...(headers.length ? { headers } : {}),
353
+ ...(noRaw ? { raw: "refused" } : {}),
354
+ };
355
+ const res = await c.secrets.set(name, value, opts);
356
+ const next = nexts([`${sh `promigence run --count 1 --cap 1 --secret ${name}`} -- CMD`, "give it to every sandbox of a run"], ["promigence secrets set --list", "what this org has stored"]);
357
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, {
358
+ name: res.name, bytes: res.bytes, created: res.created ?? true, source,
359
+ ...(res.hosts?.length
360
+ ? {
361
+ hosts: res.hosts, placeholder: res.placeholder, raw: res.raw,
362
+ ...(res.urls?.length ? { urls: res.urls } : {}),
363
+ ...(res.env && Object.keys(res.env).length ? { env: res.env } : {}),
364
+ }
365
+ : {}),
366
+ }, next), [
367
+ `${sh `promigence secrets set ${name}`}: stored ${res.bytes} bytes from ${source}`,
368
+ ...(res.hosts?.length
369
+ ? [
370
+ ` bound to ${res.hosts.join(", ")}: the value stays out of the sandbox unless a run asks for it (the variable holds ${res.placeholder ?? "something else"})`,
371
+ ...boundUseLines(res),
372
+ ...(res.raw === "refused" ? [" its value can never be given to a sandbox"] : []),
373
+ ]
374
+ : [" the value is encrypted at rest and redacted wherever it appears verbatim in logs, reports and command output",
375
+ " redaction is best effort (an encoded or altered value prints in clear): never print a secret"]),
376
+ ` next: ${next[0].cmd}`,
377
+ ], 0, false);
378
+ };
379
+ function boundUseLines(res) {
380
+ const hosts = res.hosts ?? [];
381
+ const urls = res.urls ?? [];
382
+ if (urls.length !== hosts.length)
383
+ return [];
384
+ const lines = [
385
+ " requests the sandbox sends to this address are authenticated with it:",
386
+ ...urls.map((u) => ` ${urlLine(u, res.env)}`),
387
+ ` a request sent straight to ${hosts.map((h) => `https://${h}`).join(" or ")} is not authenticated: send it to the address above`,
388
+ ...(res.scan_responses === true ? [" the host's answers never show the value: an echo service shows asterisks instead"] : []),
389
+ ];
390
+ return lines;
391
+ }
392
+ function urlLine(u, env) {
393
+ const vars = Object.entries(env ?? {})
394
+ .filter(([, v]) => v === u || v.startsWith(`${u}/`))
395
+ .map(([k]) => k);
396
+ return `${u}/<path>${vars.length ? ` (${vars.join(", ")} in the sandbox already points here)` : ""}`;
397
+ }
398
+ export function secretListLines(s) {
399
+ return [
400
+ ` ${s.name} (${s.bytes} bytes, updated ${s.updated_at ?? "?"})${boundTo(s)}`,
401
+ ...(s.urls ?? []).map((u) => ` in requests sent to ${urlLine(u, s.env)}`),
402
+ ];
403
+ }
404
+ function boundTo(s) {
405
+ if (!s.hosts?.length)
406
+ return "";
407
+ return ` → bound to ${s.hosts.join(", ")}${s.raw === "refused" ? ", value never given" : ""}`;
408
+ }
409
+ function buildFields(b) {
410
+ const out = { build_id: b.build_id, state: b.state };
411
+ if (b.snapshot_id)
412
+ out["snapshot_id"] = b.snapshot_id;
413
+ if (b.alias)
414
+ out["alias"] = b.alias;
415
+ if (b.snapshot_deleted === true)
416
+ out["snapshot_deleted"] = true;
417
+ if (b.tier_assigned)
418
+ out["tier_assigned"] = b.tier_assigned;
419
+ if (b.verification)
420
+ out["verification"] = b.verification;
421
+ if (b.error)
422
+ out["error_detail"] = b.error;
423
+ if (b.deduped)
424
+ out["deduped"] = true;
425
+ if (b.joined)
426
+ out["joined"] = true;
427
+ if (b.warnings?.length)
428
+ out["warnings"] = b.warnings;
429
+ if (b.source)
430
+ out["source"] = b.source;
431
+ if (typeof b.verified === "boolean")
432
+ out["verified"] = b.verified;
433
+ for (const k of ["build_price", "per_second_usd", "max_seconds", "billable_seconds", "cost_usd"]) {
434
+ if (typeof b[k] === "number")
435
+ out[k] = b[k];
436
+ }
437
+ const ab = b;
438
+ if (ab.alias_requested !== undefined)
439
+ out["alias_requested"] = ab.alias_requested;
440
+ if (ab.alias_applied !== undefined)
441
+ out["alias_applied"] = ab.alias_applied;
442
+ return out;
443
+ }
444
+ function isBuilding(b) {
445
+ return b.state === "queued" || b.state === "running";
446
+ }
447
+ export function buildPriceOf(b) {
448
+ return typeof b.build_price === "number" && typeof b.per_second_usd === "number" && typeof b.max_seconds === "number"
449
+ ? { most_usd: b.build_price, per_second_usd: b.per_second_usd, max_s: b.max_seconds }
450
+ : undefined;
451
+ }
452
+ function buildExit(b) {
453
+ if (b.state === "verified" || b.state === "built")
454
+ return 0;
455
+ if (b.state === "rejected")
456
+ return 6;
457
+ if (b.state === "failed")
458
+ return 3;
459
+ return 0;
460
+ }
461
+ export function verifyTiming(v) {
462
+ if (!v)
463
+ return undefined;
464
+ const cold = typeof v["verify_cold_ms"] === "number" && v["verify_cold_ms"] > 0 ? v["verify_cold_ms"] : undefined;
465
+ const warmRaw = v["verify_warm_builder"]?.median_ms;
466
+ const warm = typeof warmRaw === "number" && warmRaw > 0 ? warmRaw : undefined;
467
+ const parts = [
468
+ ...(cold === undefined ? [] : [`cold ${ms(cold)}`]),
469
+ ...(warm === undefined ? [] : [`warm ${ms(warm)}`]),
470
+ ];
471
+ return parts.length ? parts.join(" · ") : undefined;
472
+ }
473
+ function buildLines(b) {
474
+ const note = b.deduped ? " (deduped: an identical build already ran)"
475
+ : b.idempotent_replay === true ? " (reused: the same request already built it)" : "";
476
+ const unverified = b.state === "built" || (b.state === "verified" && b.verified === false);
477
+ const shown = unverified ? "built (no --verify: not verified)" : b.state;
478
+ const lines = [`build ${b.build_id}: ${shown}${note}`];
479
+ if (b.snapshot_id)
480
+ lines.push(` snapshot ${b.snapshot_id}${b.snapshot_deleted === true ? " (deleted)" : b.alias ? ` (alias ${b.alias})` : ""} · tier ${b.tier_assigned ?? "?"}`);
481
+ if (b.source?.kind === "image")
482
+ lines.push(` image ${b.source.image ?? b.source.digest ?? "?"}`);
483
+ const timing = verifyTiming(b.verification);
484
+ if (timing)
485
+ lines.push(` verify: ${timing}`);
486
+ const cost = typeof b.cost_usd === "number" && !isBuilding(b) ? b.cost_usd : undefined;
487
+ const ran = typeof b.billable_seconds === "number" ? ` for the ${b.billable_seconds} s it ran` : "";
488
+ if (b.error) {
489
+ lines.push(` ${b.error.guard ? `guard ${b.error.guard}: ` : ""}${b.error.message ?? b.error.code ?? "rejected"}`);
490
+ const stored = `nothing was stored${b.error.stored === false ? "" : "?"}`;
491
+ lines.push(cost !== undefined && cost > 0 ? ` ${stored}; billed ${usd(cost)}${ran}` : ` ${stored} and nothing was charged`);
492
+ }
493
+ else if (cost !== undefined && cost > 0) {
494
+ lines.push(` billed ${usd(cost)}${ran}`);
495
+ }
496
+ for (const w of b.warnings ?? [])
497
+ lines.push(` warning: ${w}`);
498
+ if (unverified)
499
+ lines.push(" add --verify CMD to have the build check the environment, and a fork re-check it on demand (snapshot verify)");
500
+ return lines;
501
+ }
502
+ async function emitBuild(ctx, b, extra = {}, more = []) {
503
+ const exit = buildExit(b);
504
+ const next = b.snapshot_deleted === true
505
+ ? nexts(["promigence snapshot list", "this build's snapshot was deleted; the snapshots you have now"])
506
+ : b.snapshot_id && (b.state === "verified" || b.state === "built" || (!isBuilding(b) && b.state !== "rejected" && b.state !== "failed"))
507
+ ? nexts([sh `promigence run --snapshot ${b.snapshot_id} --count 2 --cap 1 -- CMD`, "fork it and run a command in every sandbox"], [sh `promigence snapshot inspect ${b.snapshot_id}`, "the manifest: provenance and verification"])
508
+ : b.state === "running"
509
+ ? nexts([sh `promigence snapshot status ${b.build_id} --wait`, "keep waiting; the build is untouched"])
510
+ : nexts([sh `promigence snapshot status ${b.build_id} --log 80`, "the tail of the build log that failed: fix what it names, then build again"]);
511
+ const want = b.alias_requested;
512
+ if (b.alias_applied === false && want && b.snapshot_id) {
513
+ next.unshift({ cmd: sh `promigence snapshot alias ${b.snapshot_id} ${want}`, why: `the alias ${want} was not applied: move it to this snapshot` });
514
+ }
515
+ const env = { ...okEnvelope(ctx.cmd.name, { ...buildFields(b), ...extra }, next), ok: exit === 0 };
516
+ if (exit !== 0) {
517
+ const code = b.error?.code ?? (b.state === "rejected" ? "snapshot_rejected" : "build_failed");
518
+ env.error = {
519
+ code,
520
+ message: `build ${b.build_id} ${b.state}: ${b.error?.guard ? `guard ${b.error.guard}: ` : ""}${b.error?.message ?? "no snapshot was stored"}`,
521
+ fix: `Read the log (${sh `promigence snapshot status ${b.build_id} --log 80`}), fix what it names, and build again.`,
522
+ docs: DOCS_URL,
523
+ guide: sh `promigence agent-guide --section errors --code ${code}`,
524
+ };
525
+ }
526
+ return emit(ctx.mode, env, [...buildLines(b), ...more, ...(env.error ? [` fix: ${env.error.fix}`] : []), ` next: ${next[0].cmd}`], exit, ctx.pointer);
527
+ }
528
+ export function buildLogLines(log, state) {
529
+ if (log === undefined)
530
+ return [" log: could not be read right now; try again in a moment"];
531
+ if (log.length === 0) {
532
+ return [state === "queued" ? " log: none yet, the build has not started" : " log: no output of your own was kept for this build"];
533
+ }
534
+ return [` log (your build's own output, last ${log.length} line${log.length === 1 ? "" : "s"}):`, ...log.map((l) => ` ${l}`)];
535
+ }
536
+ function buildWaitExpired(ctx, b, boundMs) {
537
+ const next = nexts([sh `promigence snapshot status ${b.build_id} --wait`, "keep waiting; the build is untouched"], [sh `promigence snapshot status ${b.build_id} --log 80`, "what the builder is doing right now"]);
538
+ const err = new TimeoutError(`wait of ${boundMs}ms expired; the build is still running and nothing was cancelled`, `Run the id-based command: ${sh `promigence snapshot status ${b.build_id} --wait`}.`);
539
+ const env = errorEnvelope(ctx.cmd.name, err, next);
540
+ env["build_id"] = b.build_id;
541
+ env["state"] = b.state;
542
+ env["cancelled"] = false;
543
+ return emit(ctx.mode, env, errorTextLines(ctx.cmd.name, err, 11, next), 11, ctx.pointer);
544
+ }
545
+ function devcontainerPlan(ctx, form) {
546
+ const flag = ctx.flags.get("--devcontainer");
547
+ const asked = flag !== undefined && flag.length > 0;
548
+ const given = asked ? flag[flag.length - 1] : undefined;
549
+ if (bool(ctx, "--no-devcontainer")) {
550
+ if (asked)
551
+ throw usageError("--devcontainer and --no-devcontainer contradict each other", "Pass one of them.");
552
+ return undefined;
553
+ }
554
+ if (!asked && form !== "repo")
555
+ return undefined;
556
+ try {
557
+ const file = given === undefined || given === true ? detectOne(process.cwd(), asked) : resolveGiven(given);
558
+ if (file === undefined)
559
+ return undefined;
560
+ const loaded = load(file);
561
+ const override = str(ctx, "--image");
562
+ if (override !== undefined)
563
+ checkImageRef(override);
564
+ return translate(loaded, { workspaceRoot: workspaceRootFor(file), imageOverride: override, allowLocalEnv: bool(ctx, "--devcontainer-local-env") });
565
+ }
566
+ catch (e) {
567
+ if (e instanceof DevcontainerError) {
568
+ throw usageError(`devcontainer (${e.kind}): ${e.message}`, `${e.fix} Or pass --no-devcontainer to ignore the file.`);
569
+ }
570
+ throw e;
571
+ }
572
+ }
573
+ const CONFIG_FILE = "promigence.toml";
574
+ function configFlags(ctx) {
575
+ const sourced = ["--image", "--dockerfile", "--repo", "--commit", "--devcontainer"].some((f) => ctx.flags.has(f));
576
+ if (sourced || !existsSync(CONFIG_FILE))
577
+ return false;
578
+ let cfg;
579
+ try {
580
+ cfg = readConfig(readFileSync(CONFIG_FILE, "utf8"));
581
+ }
582
+ catch (e) {
583
+ throw usageError(`${CONFIG_FILE}: ${e.message}`, "Fix that line (`promigence init --force` rewrites the file), or pass the source yourself: --repo U --commit SHA --verify CMD --tier T.");
584
+ }
585
+ const set = (flag, v) => {
586
+ if (v === undefined || ctx.flags.has(flag))
587
+ return;
588
+ ctx.flags.set(flag, typeof v === "string" ? [v] : [...v]);
589
+ };
590
+ set("--repo", cfg.repo);
591
+ set("--commit", cfg.commit);
592
+ if (cfg.setup?.length)
593
+ set("--setup", cfg.setup);
594
+ set("--verify", cfg.verify);
595
+ set("--tier", cfg.tier);
596
+ set("--alias", cfg.alias);
597
+ return true;
598
+ }
599
+ function snapshotForm(ctx) {
600
+ if (ctx.flags.has("--devcontainer") && ctx.flags.has("--dockerfile")) {
601
+ throw usageError("--devcontainer and --dockerfile name the same thing twice", "The devcontainer already says which Dockerfile builds it: pass --devcontainer alone, or --dockerfile alone.");
602
+ }
603
+ if (ctx.flags.has("--devcontainer") && !ctx.flags.has("--image") && !ctx.flags.has("--repo") && !ctx.flags.has("--commit")) {
604
+ return "image";
605
+ }
606
+ const forms = [
607
+ ["--image", ctx.flags.has("--image")],
608
+ ["--dockerfile", ctx.flags.has("--dockerfile")],
609
+ ["--repo/--commit", ctx.flags.has("--repo") || ctx.flags.has("--commit")],
610
+ ].filter(([, present]) => present).map(([n]) => n);
611
+ if (forms.length > 1) {
612
+ throw usageError(`snapshot create takes one of --image, --dockerfile or --repo/--commit, got ${forms.join(" + ")}`, "Image: promigence snapshot create --image REF@sha256:<digest> [--setup CMD] [--verify CMD]. " +
613
+ "Dockerfile: promigence snapshot create --dockerfile ./Dockerfile [--context DIR|URL@SHA]. " +
614
+ "Repo: promigence snapshot create --repo U --commit SHA --verify CMD --tier T.");
615
+ }
616
+ if (forms.length === 0) {
617
+ throw usageError(`snapshot create needs --image, --dockerfile or --repo/--commit (or a ${CONFIG_FILE} here)`, "Pass --image registry/name@sha256:<digest>, --dockerfile ./Dockerfile, or --repo https://github.com/org/app --commit <40-char sha>; or run `promigence init` in the repo first.");
618
+ }
619
+ const form = forms[0] === "--image" ? "image" : forms[0] === "--dockerfile" ? "dockerfile" : "repo";
620
+ if (form === "repo" && !ctx.flags.has("--devcontainer") && ctx.flags.has("--start")) {
621
+ throw usageError("--start needs --image or --dockerfile", "A start process is part of an image snapshot: pass --image REF@sha256:<digest> or --dockerfile ./Dockerfile.");
622
+ }
623
+ if (form !== "dockerfile" && ctx.flags.has("--context")) {
624
+ throw usageError("--context needs --dockerfile", "A build context belongs to a Dockerfile: pass --dockerfile ./Dockerfile --context DIR.");
625
+ }
626
+ return form;
627
+ }
628
+ function alsoRepos(ctx) {
629
+ const pin = /^(.*)@([0-9a-f]{40})$/i;
630
+ return list(ctx, "--also-repo").map((raw) => {
631
+ const token = raw.trim();
632
+ const cut = token.lastIndexOf(":");
633
+ let head = token;
634
+ let path;
635
+ if (cut > 0 && pin.test(token.slice(0, cut))) {
636
+ head = token.slice(0, cut);
637
+ path = token.slice(cut + 1);
638
+ }
639
+ const m = pin.exec(head);
640
+ if (!m || !m[1]) {
641
+ throw usageError(`--also-repo must be URL@<40-char sha>[:PATH], got ${JSON.stringify(raw)}`, "A branch or a short sha is not a pin, and a world that cannot be rebuilt is not " +
642
+ "reproducible: pass --also-repo https://github.com/org/lib@$(git -C ../lib rev-parse HEAD). " +
643
+ "Add :/work/lib or :vendor/lib to choose where it lands.");
644
+ }
645
+ return { repo: m[1], commit: m[2].toLowerCase(), ...(path ? { path } : {}) };
646
+ });
647
+ }
648
+ function refuseRepoCredentials(ctx) {
649
+ checkRepoUrl(str(ctx, "--repo"), "--repo");
650
+ const pin = /^(.*)@([0-9a-f]{40})$/i;
651
+ for (const raw of list(ctx, "--also-repo")) {
652
+ const token = raw.trim();
653
+ const cut = token.lastIndexOf(":");
654
+ const head = cut > 0 && pin.test(token.slice(0, cut)) ? token.slice(0, cut) : token;
655
+ checkRepoUrl(withoutPin(head), "--also-repo");
656
+ }
657
+ const context = str(ctx, "--context")?.trim();
658
+ if (context !== undefined && (context.includes("://") || /^[^/:]*@[^/:]*:/.test(context))) {
659
+ checkRepoUrl(withoutPin(context), "--context");
660
+ }
661
+ }
662
+ async function packedContext(ctx, dir, path) {
663
+ const listed = listContext(dir, { dockerfile: path });
664
+ if (bool(ctx, "--list-files")) {
665
+ return emitUploadList(ctx, {
666
+ kind: "dockerfile", root: dir, paths: listed.paths, files: listed.files, bytes: listed.bytes,
667
+ secrets: listed.secrets, notes: listed.notes, ignoreFile: listed.ignoreFile,
668
+ });
669
+ }
670
+ if (listed.notes.length) {
671
+ await write(process.stderr, ctx.mode === "json"
672
+ ? jsonLine(process.stderr, { hint: "context", notes: listed.notes })
673
+ : listed.notes.map((n) => fieldForTerminal(process.stderr, ` note: ${n}`)).join("\n") + "\n");
674
+ }
675
+ if (listed.secrets.length && !bool(ctx, "--allow-secrets"))
676
+ throw credentialRefusal(listed.secrets, CREDENTIAL_REMEDY.dockerfile);
677
+ const secretsAllowed = await confirmOverride(ctx, {
678
+ flag: "--allow-secrets", what: "files that look like credentials", items: listed.secrets,
679
+ });
680
+ return { context: packContext(dir, { dockerfile: path, allowSecrets: true }), secretsAllowed };
681
+ }
682
+ async function dockerfileOpts(ctx) {
683
+ const df = required(ctx, "--dockerfile", "Pass --dockerfile ./Dockerfile.");
684
+ const context = str(ctx, "--context");
685
+ if (context !== undefined && CONTEXT_GIT_RE.test(context.trim())) {
686
+ if (df.startsWith("/") || df.split("/").includes("..")) {
687
+ throw usageError(`--dockerfile must be a path inside the repository, got ${JSON.stringify(df)}`, "With --context URL@SHA the Dockerfile is read from that repository: pass --dockerfile Dockerfile or --dockerfile ci/Dockerfile.");
688
+ }
689
+ return { context: context.trim(), path: df };
690
+ }
691
+ if (context !== undefined && /^(?:https?:\/\/|ssh:\/\/|git:\/\/|git@)/i.test(context.trim())) {
692
+ throw usageError(`--context ${JSON.stringify(context)} is a URL but is not pinned to a full 40-character sha`, "A branch or a tag is not a pin, and a snapshot that cannot be rebuilt is not reproducible: " +
693
+ "pass --context URL@<40-char sha> (git rev-parse HEAD).");
694
+ }
695
+ const dir = context ?? (dirname(df) || ".");
696
+ let path;
697
+ try {
698
+ path = contextRelative(dir, df);
699
+ }
700
+ catch (e) {
701
+ throw usageError(`--dockerfile ${JSON.stringify(df)} is not inside --context ${JSON.stringify(dir)}`, `${e instanceof Error ? e.message : String(e)}. Pass --context DIR with the Dockerfile underneath it.`);
702
+ }
703
+ if (!existsSync(df)) {
704
+ throw usageError(`no Dockerfile at ${JSON.stringify(df)}`, "Pass --dockerfile with the path to your Dockerfile, or --context URL@SHA to build one from a repository.");
705
+ }
706
+ const packed = await packedContext(ctx, dir, path);
707
+ return { context: packed.context, path, ...(packed.secretsAllowed.length ? { secretsAllowed: packed.secretsAllowed } : {}) };
708
+ }
709
+ async function devcontainerCreate(ctx, plan) {
710
+ let source;
711
+ let label;
712
+ let secretsAllowed = [];
713
+ const localEnvBaked = await confirmOverride(ctx, {
714
+ flag: "--devcontainer-local-env", what: "values from your environment, baked into the snapshot",
715
+ items: plan.localEnvBaked ?? [],
716
+ });
717
+ if (plan.source.kind === "image") {
718
+ source = { image: plan.source.image };
719
+ label = plan.source.image;
720
+ }
721
+ else {
722
+ if (!existsSync(plan.source.dockerfile)) {
723
+ throw usageError(`the devcontainer names a Dockerfile that is not here: ${plan.source.dockerfile}`, "Check `build.dockerfile` and `build.context` in the devcontainer.json, relative to the file itself.");
724
+ }
725
+ const path = contextRelative(plan.source.context, plan.source.dockerfile);
726
+ const packed = await packedContext(ctx, plan.source.context, path);
727
+ source = { dockerfile: { context: packed.context, path } };
728
+ secretsAllowed = packed.secretsAllowed;
729
+ label = `${path} in ${plan.source.context}`;
730
+ }
731
+ const repo = str(ctx, "--repo");
732
+ const commit = str(ctx, "--commit");
733
+ if (repo !== undefined && commit === undefined) {
734
+ throw usageError("--repo needs --commit", "Pass --commit <40-char sha> (git rev-parse HEAD) — a branch is not a pin.");
735
+ }
736
+ if (commit !== undefined && !/^[0-9a-f]{40}$/i.test(commit)) {
737
+ throw usageError(`--commit must be a full 40-character sha, got ${JSON.stringify(commit)}`, "A branch is not a pin: pass --commit $(git rev-parse HEAD).");
738
+ }
739
+ const setup = planSetup(plan, { repo, commit, userSetup: list(ctx, "--setup") });
740
+ const env = snapshotEnv(plan);
741
+ const users = snapshotUsers(plan);
742
+ const opts = {
743
+ ...source, ...(setup.length ? { setup } : {}), ...(env ? { env } : {}), ...(users ?? {}),
744
+ };
745
+ const verify = str(ctx, "--verify");
746
+ if (verify !== undefined)
747
+ opts.verify = { cmd: wrapCommand(plan, verify) };
748
+ const start = str(ctx, "--start");
749
+ if (start !== undefined)
750
+ opts.start = wrapCommand(plan, start);
751
+ const tier = str(ctx, "--tier") ?? plan.tier;
752
+ if (tier !== undefined)
753
+ opts.tier = tier;
754
+ const json = planJson(plan, setup);
755
+ const submitted = {
756
+ build_price: null,
757
+ devcontainer: json,
758
+ ...(repo === undefined ? {} : { repo, commit }),
759
+ ...(tier === undefined ? {} : { requested_tier: tier }),
760
+ ...(secretsAllowed.length ? { secret_files_allowed: secretsAllowed } : {}),
761
+ ...(localEnvBaked.length ? { local_env_baked: localEnvBaked } : {}),
762
+ };
763
+ const lines = planLines(plan, setup);
764
+ if (repo !== undefined && commit !== undefined) {
765
+ lines.splice(1, 0, ` repo ${repo}@${commit.slice(0, 7)} → ${plan.workspaceFolder}`);
766
+ }
767
+ return { opts, label, submitted, tier, lines, json };
768
+ }
769
+ const snapshotCreate = async (ctx) => {
770
+ if (bool(ctx, "--dir"))
771
+ return dirSnapshotCreate(ctx);
772
+ if (configFlags(ctx)) {
773
+ await write(process.stderr, ctx.mode === "json"
774
+ ? jsonLine(process.stderr, { hint: "config", file: CONFIG_FILE })
775
+ : `building what ${CONFIG_FILE} describes (flags you pass win)\n`);
776
+ }
777
+ refuseRepoCredentials(ctx);
778
+ const gh = await githubCreate(ctx);
779
+ const form = gh ? "repo" : snapshotForm(ctx);
780
+ const plan = gh ? undefined : devcontainerPlan(ctx, form);
781
+ const setup = list(ctx, "--setup");
782
+ const alias = str(ctx, "--alias");
783
+ let opts;
784
+ let label;
785
+ let submitted;
786
+ let tier;
787
+ if (gh !== undefined) {
788
+ ({ opts, label, submitted, tier } = gh);
789
+ }
790
+ else if (plan !== undefined) {
791
+ const built = await devcontainerCreate(ctx, plan);
792
+ opts = built.opts;
793
+ label = built.label;
794
+ submitted = built.submitted;
795
+ tier = built.tier;
796
+ await write(process.stderr, ctx.mode === "json"
797
+ ? jsonLine(process.stderr, { hint: "devcontainer", devcontainer: built.json })
798
+ : built.lines.join("\n") + "\n");
799
+ }
800
+ else if (form === "repo") {
801
+ const repo = required(ctx, "--repo", "Pass --repo https://github.com/org/app.");
802
+ const commit = required(ctx, "--commit", "Pass --commit <40-char sha> (git rev-parse HEAD) — a branch is not a pin.");
803
+ const verify = required(ctx, "--verify", "Pass --verify 'bun typecheck' — the command that must exit 0 or nothing is stored.");
804
+ tier = required(ctx, "--tier", "Pass --tier xsmall|small|medium|large-16|large|xlarge|2xlarge (see 'promigence agent-guide --section cost').");
805
+ opts = { repo, commit, setup, verify: { cmd: verify }, tier: tier };
806
+ label = `${repo}@${commit.slice(0, 7)}`;
807
+ submitted = { build_price: null, repo, commit, requested_tier: tier };
808
+ }
809
+ else {
810
+ let source;
811
+ if (form === "image") {
812
+ const image = required(ctx, "--image", "Pass --image python:3.12 or registry/name@sha256:<digest>.");
813
+ checkImageRef(image);
814
+ source = { image };
815
+ label = image;
816
+ submitted = { build_price: null, image };
817
+ }
818
+ else {
819
+ const { secretsAllowed, ...dockerfile } = await dockerfileOpts(ctx);
820
+ source = { dockerfile };
821
+ const where = dockerfile.context.startsWith("tar;") ? (str(ctx, "--context") ?? dirname(str(ctx, "--dockerfile") ?? ".")) : dockerfile.context;
822
+ label = `${dockerfile.path ?? "Dockerfile"} in ${where}`;
823
+ submitted = {
824
+ build_price: null, dockerfile: { path: dockerfile.path ?? "Dockerfile", context: where },
825
+ ...(secretsAllowed?.length ? { secret_files_allowed: secretsAllowed } : {}),
826
+ };
827
+ }
828
+ const verify = str(ctx, "--verify");
829
+ const start = str(ctx, "--start");
830
+ tier = str(ctx, "--tier");
831
+ opts = { ...source, ...(setup.length ? { setup } : {}) };
832
+ if (verify !== undefined)
833
+ opts.verify = { cmd: verify };
834
+ if (start !== undefined)
835
+ opts.start = start;
836
+ if (tier !== undefined)
837
+ opts.tier = tier;
838
+ if (tier !== undefined)
839
+ submitted["requested_tier"] = tier;
840
+ }
841
+ const sources = alsoRepos(ctx);
842
+ if (sources.length) {
843
+ if (opts.repo === undefined) {
844
+ throw usageError("--also-repo needs --repo/--commit", "An image or a Dockerfile already carries its tree and may not have git in it: clone " +
845
+ "the extra repository in a --setup step, or build from --repo U --commit SHA --also-repo …");
846
+ }
847
+ opts.sources = sources;
848
+ submitted["sources"] = sources;
849
+ }
850
+ const c = client(ctx);
851
+ if (alias !== undefined)
852
+ opts.alias = alias;
853
+ const grade = gradeOpts(ctx);
854
+ if (grade)
855
+ opts.grade = grade;
856
+ if (bool(ctx, "--grade-output")) {
857
+ if (!grade) {
858
+ throw usageError("--grade-output needs --grade", "Pass --grade 'pytest -q tests/' alongside it: it keeps what that grader prints.");
859
+ }
860
+ opts.gradeOutput = true;
861
+ }
862
+ const key = autoIdempotencyKey(ctx);
863
+ if (key !== undefined)
864
+ opts.idempotencyKey = key;
865
+ let started;
866
+ try {
867
+ started = await c.snapshots.create(opts);
868
+ }
869
+ catch (e) {
870
+ if (plan?.source.kind === "dockerfile" && form === "repo" && e instanceof PromigenceError &&
871
+ e.code === "usage" && (e.message.includes("from a Dockerfile") || e.message.includes("Dockerfile builds"))) {
872
+ const repo = str(ctx, "--repo") ?? "<url>";
873
+ const commit = str(ctx, "--commit") ?? "<sha>";
874
+ const verify = str(ctx, "--verify") ?? "<test>";
875
+ const cmd = "promigence snapshot create --no-devcontainer --image <an image with git and your toolchain> "
876
+ + sh `--setup ${`git clone ${repo} /work/repo && git -C /work/repo checkout ${commit}`} --verify ${`cd /work/repo && ${verify}`}`;
877
+ throw usageError(e.message, `${e.fix} Or build it without the devcontainer's Dockerfile, from an image: ${cmd}`);
878
+ }
879
+ if (form === "repo" && plan === undefined && e instanceof PromigenceError && e.code === "usage" &&
880
+ /from a git repository[\s\S]*switched off/i.test(e.message)) {
881
+ const cmd = imageCreateCommand({
882
+ repo: str(ctx, "--repo"), commit: str(ctx, "--commit"), setup, verify: str(ctx, "--verify"), tier,
883
+ }, ecosystemOfCommands([...setup, str(ctx, "--verify") ?? ""]) ?? detect(process.cwd()).ecosystem);
884
+ const err = usageError("building a snapshot straight from a repository is not open in this beta; nothing was built or charged", `Build it from an image instead, with the clone as the first setup step: ${cmd}`);
885
+ const next = nexts([cmd, "build and verify the same environment from an image"]);
886
+ return emit(ctx.mode, errorEnvelope(ctx.cmd.name, err, next), errorTextLines(ctx.cmd.name, err, 2, next), 2, ctx.pointer);
887
+ }
888
+ throw e;
889
+ }
890
+ const reused = started.deduped === true;
891
+ const joinedBuild = started.joined === true;
892
+ const replay = buildReplayNote(started, str(ctx, "--idempotency-key") !== undefined);
893
+ const free = reused || joinedBuild || replay !== undefined;
894
+ const builtAt = tier ?? (typeof started["tier"] === "string" ? started["tier"] : undefined);
895
+ const perSecond = buildPriceOf(started);
896
+ const flat = !free && perSecond === undefined && typeof started.build_price === "number" ? started.build_price : undefined;
897
+ const said = free || flat !== undefined ? perSecond : (perSecond ?? await buildPrice(c, builtAt));
898
+ const charged = free ? 0 : flat ?? said?.most_usd ?? 0;
899
+ submitted["build_price"] = charged;
900
+ if (!free && said !== undefined)
901
+ Object.assign(submitted, { per_second_usd: said.per_second_usd, max_seconds: said.max_s });
902
+ if (free)
903
+ submitted["charged"] = false;
904
+ Object.assign(submitted, replayFields(started));
905
+ await write(process.stderr, ctx.mode === "json"
906
+ ? JSON.stringify({
907
+ hint: "build_price", build_price: charged, unit: "USD", ...(tier === undefined ? {} : { tier }),
908
+ ...(!free && said !== undefined ? { per_second_usd: said.per_second_usd, max_seconds: said.max_s } : {}),
909
+ ...(reused ? { deduped: true } : {}), ...(joinedBuild ? { joined: true } : {}),
910
+ ...(replay === undefined ? {} : { replayed: true }),
911
+ }) + "\n"
912
+ : replay !== undefined
913
+ ? `build_price: $0.00 - ${replay}\n`
914
+ : reused
915
+ ? `build_price: $0.00 - an identical build already ran (${started.build_id}); reusing it, nothing charged\n`
916
+ : joinedBuild
917
+ ? `build_price: $0.00 - joining an identical build already running (${started.build_id}); it is charged once\n`
918
+ : flat !== undefined
919
+ ? `build_price: $${flat.toFixed(2)} (tier ${tier ?? "default"}) - building ${label}\n`
920
+ : `build_price: ${buildPriceWords(said, builtAt ?? "default")} - building ${label}\n`);
921
+ if (sources.length && ctx.mode !== "json" && !reused) {
922
+ await write(process.stderr, sources
923
+ .map((s) => ` also ${s.repo}@${s.commit.slice(0, 7)} -> ${s.path ?? "beside the workspace, under its own name"}`)
924
+ .join("\n") + "\n");
925
+ }
926
+ if (started.deduped || started.state === "verified" || started.state === "built" || (started.snapshot_id && !isBuilding(started))) {
927
+ return emitBuild(ctx, started, submitted);
928
+ }
929
+ const joined = started.joined === true;
930
+ const what = joined
931
+ ? `build ${started.build_id}: joined an identical build in progress (${label}, tier ${tier ?? "default"})`
932
+ : replay !== undefined
933
+ ? `build ${started.build_id}: started by the same request earlier, ${started.state} (${label}, tier ${tier ?? "default"})`
934
+ : `build ${started.build_id} started (${label}, tier ${tier ?? "default"})`;
935
+ const warned = (started.warnings ?? []).map((w) => ` warning: ${w}`);
936
+ if (!bool(ctx, "--wait")) {
937
+ const next = nexts([sh `promigence snapshot status ${started.build_id} --wait`, "wait for the build (bounded; nothing is cancelled on expiry)"], [sh `promigence snapshot status ${started.build_id} --log 80`, "the tail of the build log"]);
938
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, { ...buildFields(started), ...submitted }, next), [what, ...warned, ` next: ${next[0].cmd}`], 0, ctx.pointer);
939
+ }
940
+ await write(process.stderr, ctx.mode === "json"
941
+ ? jsonLine(process.stderr, { hint: joined ? "joined" : "build started", build_id: started.build_id, ...(warned.length ? { warnings: started.warnings } : {}) })
942
+ : [what, ...warned, ` waiting; Ctrl-C stops the wait, not the build (${sh `promigence snapshot status ${started.build_id} --wait`})`]
943
+ .map((l) => fieldForTerminal(process.stderr, l)).join("\n") + "\n");
944
+ const interrupted = (sig) => {
945
+ const follow = sh `promigence snapshot status ${started.build_id} --wait`;
946
+ const err = new PromigenceError({
947
+ code: "aborted_by_client",
948
+ message: `interrupted by ${sig}; build ${started.build_id} goes on and nothing was cancelled (it is billed per second until it ends)`,
949
+ fix: `Follow it: ${follow}.`,
950
+ exitCode: 130,
951
+ });
952
+ const next = nexts([follow, "keep waiting for the build"], [sh `promigence snapshot status ${started.build_id} --log 80`, "what the builder is doing"]);
953
+ const env = errorEnvelope(ctx.cmd.name, err, next);
954
+ env["build_id"] = started.build_id;
955
+ env["cancelled"] = false;
956
+ void emit(ctx.mode, env, errorTextLines(ctx.cmd.name, err, 130, next), 130, false);
957
+ };
958
+ for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"])
959
+ process.once(sig, () => interrupted(sig));
960
+ const bound = waitBound(ctx);
961
+ const waited = await c.snapshots.waitBuild(started.build_id, { waitTimeoutMs: bound });
962
+ if (waited.timedOut)
963
+ return buildWaitExpired(ctx, waited.build, bound);
964
+ const done = replay === undefined ? waited.build : { ...waited.build, idempotent_replay: true };
965
+ return emitBuild(ctx, done, { ...submitted, waited_ms: waited.waitedMs, ...(joined ? { joined: true } : {}) });
966
+ };
967
+ const snapshotStatus = async (ctx) => {
968
+ const buildId = positional(ctx, "a build id", "promigence snapshot status bld_01 --wait");
969
+ const logLines = str(ctx, "--log");
970
+ const logCount = logLines === undefined ? undefined : parseCount(logLines, "--log");
971
+ const c = client(ctx);
972
+ let build = await c.snapshots.status(buildId);
973
+ let waitedMs;
974
+ if (bool(ctx, "--wait") && (build.state === "running" || build.state === "queued")) {
975
+ const bound = waitBound(ctx);
976
+ const waited = await c.snapshots.waitBuild(buildId, { waitTimeoutMs: bound });
977
+ if (waited.timedOut)
978
+ return buildWaitExpired(ctx, waited.build, bound);
979
+ build = waited.build;
980
+ waitedMs = waited.waitedMs;
981
+ }
982
+ const extra = {};
983
+ if (waitedMs !== undefined)
984
+ extra["waited_ms"] = waitedMs;
985
+ let more = [];
986
+ if (logCount !== undefined) {
987
+ const log = await c.snapshots.buildLog(buildId, logCount).catch(() => undefined);
988
+ extra["log"] = log ?? [];
989
+ more = buildLogLines(log, build.state);
990
+ }
991
+ return emitBuild(ctx, build, extra, more);
992
+ };
993
+ export function snapshotSource(s) {
994
+ const live = s.verification?.["from_live_sandbox"];
995
+ if (typeof live === "string" && live !== "")
996
+ return `live snapshot of ${live}`;
997
+ const repo = s.repo ?? "";
998
+ const commit = (s.commit ?? "").slice(0, 7);
999
+ if (repo === "" && commit === "")
1000
+ return "-";
1001
+ return commit === "" ? repo : `${repo}@${commit}`;
1002
+ }
1003
+ const snapshotList = async (ctx) => {
1004
+ const snapshots = await client(ctx).snapshots.list();
1005
+ const next = snapshots.length
1006
+ ? nexts([sh `promigence snapshot inspect ${snapshots[0].snapshot_id}`, "the manifest of the newest one"])
1007
+ : nexts(["promigence snapshot create --image IMAGE:TAG --verify CMD --alias NAME --wait", "build your first snapshot from an image of your choice (a paid build: `promigence pricing`; add --setup steps)"]);
1008
+ const lines = snapshots.length
1009
+ ? snapshots.map((s) => ` ${s.snapshot_id} ${s.alias ?? "-"} ${s.tier ?? "?"} ${snapshotSource(s)}${s.unavailable ? " UNAVAILABLE" : ""}`)
1010
+ : [" (no snapshots yet)"];
1011
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, { snapshots }, next), [`snapshots: ${snapshots.length}`, ...lines], 0, ctx.pointer);
1012
+ };
1013
+ const snapshotInspect = async (ctx) => {
1014
+ const id = positional(ctx, "a snapshot id or alias", "promigence snapshot inspect snap_01");
1015
+ const c = client(ctx);
1016
+ const snapshot = await c.snapshots.get(id);
1017
+ const base = snapshot.source?.kind === "base";
1018
+ const integrity = base ? undefined : await c.snapshots.manifest(id).then((m) => m.integrity).catch(() => undefined);
1019
+ const unavailable = typeof snapshot.unavailable === "string" && snapshot.unavailable !== "" ? snapshot.unavailable : undefined;
1020
+ const next = unavailable
1021
+ ? nexts([sh `promigence snapshot delete ${snapshot.snapshot_id}`, "remove it: it cannot be started again"], ["promigence sandbox snapshot <sandbox_id>", "take a new snapshot of a running sandbox"])
1022
+ : nexts([sh `promigence run --snapshot ${snapshot.snapshot_id} --count 2 --cap 1 -- CMD`, "fork it"], [`promigence snapshot list`, "every snapshot in your org"]);
1023
+ const timing = verifyTiming(snapshot.verification);
1024
+ const lines = base ? [
1025
+ `snapshot ${snapshot.snapshot_id} · tier ${snapshot.tier ?? "?"}`,
1026
+ ` ${String(snapshot.source?.["description"] ?? "the built-in base")}`,
1027
+ " every sandbox created without --snapshot starts from it",
1028
+ ] : [
1029
+ `snapshot ${snapshot.snapshot_id}${snapshot.alias ? ` (alias ${snapshot.alias})` : ""}`,
1030
+ ` ${snapshotSource(snapshot)} · tier ${snapshot.tier ?? "?"} · verified ${snapshot.verified === false ? "no" : "yes"}`,
1031
+ ...(snapshot.verify?.cmd ? [` verify: ${snapshot.verify.cmd}${timing ? ` · ${timing}` : ""}`] : []),
1032
+ ...(snapshot.build_id ? [` build ${snapshot.build_id}`] : []),
1033
+ ...(integrity?.summary ? [` integrity: ${integrity.summary}`] : []),
1034
+ ...(unavailable ? [` UNAVAILABLE: ${unavailable}`] : []),
1035
+ ];
1036
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, { snapshot, ...(integrity ? { integrity } : {}) }, next), lines, 0, ctx.pointer);
1037
+ };
1038
+ const fork = async (ctx) => {
1039
+ const c = client(ctx);
1040
+ const opts = await runStartOpts(ctx, c, str(ctx, "--snapshot") === undefined ? DEFAULT_SNAPSHOT : undefined);
1041
+ const key = autoIdempotencyKey(ctx);
1042
+ if (key !== undefined)
1043
+ opts.idempotencyKey = key;
1044
+ const streaming = wantsStream(ctx);
1045
+ const execSpec = opts.exec;
1046
+ if (streaming)
1047
+ delete opts.exec;
1048
+ const started = await c.runs.start(opts);
1049
+ const runId = started.run_id;
1050
+ ctx.startedRunId = runId;
1051
+ const replayed = replayNote(started, str(ctx, "--idempotency-key") !== undefined);
1052
+ const only = started.sandboxes.length === 1 ? started.sandboxes[0].sandbox_id : undefined;
1053
+ const execNext = nexts(only !== undefined
1054
+ ? [sh `promigence exec --sandbox ${only} -- CMD`, "run a command in the sandbox"]
1055
+ : [sh `promigence exec --run ${runId} --all -- CMD`, "run a command in every ready sandbox"], [sh `promigence run report ${runId} --json`, "the validity report (env vs task failures)"], [sh `promigence run kill ${runId}`, "kill every sandbox of this run"]);
1056
+ if (bool(ctx, "--no-wait")) {
1057
+ const env = okEnvelope(ctx.cmd.name, {
1058
+ run_id: runId,
1059
+ requested: { snapshot: opts.snapshot, count: opts.count, spend_cap: opts.spendCap },
1060
+ sandboxes: started.sandboxes,
1061
+ waited: false,
1062
+ ...replayFields(started),
1063
+ }, execNext);
1064
+ return emit(ctx.mode, env, [
1065
+ ...(replayed ? [replayed] : []),
1066
+ `run ${runId}: ${started.sandboxes.length} sandboxes ${replayed ? "" : "queued "}(--no-wait)`,
1067
+ ...queuedLines(started.sandboxes),
1068
+ ...sandboxIdLines(started.sandboxes),
1069
+ ` next: ${execNext[0].cmd}`,
1070
+ ], 0, ctx.pointer);
1071
+ }
1072
+ const bound = waitBound(ctx);
1073
+ const status = ctx.mode === "text" ? statusLine() : undefined;
1074
+ const total = started.sandboxes.length;
1075
+ let readyCount = 0;
1076
+ let lastCapacity;
1077
+ const paint = () => {
1078
+ const line = capacityLine(lastCapacity, readyCount, total);
1079
+ if (line)
1080
+ status?.set(line);
1081
+ else if (readyCount >= total)
1082
+ status?.clear();
1083
+ };
1084
+ status?.set(`capacity requested: ${total} sandbox${total === 1 ? "" : "es"}`);
1085
+ const waited = await c.runs.waitReady(runId, {
1086
+ waitTimeoutMs: bound,
1087
+ ...(status ? {
1088
+ onCapacity: (cap) => { lastCapacity = cap; paint(); },
1089
+ onReady: () => { readyCount += 1; paint(); },
1090
+ } : {}),
1091
+ });
1092
+ status?.clear();
1093
+ if (waited.timedOut)
1094
+ return waitExpired(ctx, runId, bound, waited.sandboxes);
1095
+ const report = await c.runs.report(runId).catch(() => undefined);
1096
+ const up = waited.sandboxes.filter((s) => s.state === "ready" || s.state === "running").length;
1097
+ const fields = {
1098
+ run_id: runId,
1099
+ requested: { snapshot: opts.snapshot, count: opts.count, spend_cap: opts.spendCap },
1100
+ up,
1101
+ sandboxes: waited.sandboxes,
1102
+ waited_ms: waited.waitedMs,
1103
+ };
1104
+ Object.assign(fields, replayFields(started));
1105
+ if (report) {
1106
+ fields["fork"] = forkSummary(report.fork);
1107
+ fields["totals"] = report.totals;
1108
+ }
1109
+ const exit = report ? pickExit(reportExits(report)) : 0;
1110
+ const env = { ...okEnvelope(ctx.cmd.name, fields, execNext), ok: exit === 0 };
1111
+ const failure = report ? runError(report, exit) : undefined;
1112
+ if (failure)
1113
+ env.error = failure;
1114
+ return emit(ctx.mode, env, [
1115
+ ...(replayed ? [replayed] : []),
1116
+ `${up} of ${opts.count} sandbox${opts.count === 1 ? "" : "es"} ready in ${readyLine(report?.fork?.ready_ms?.p50, waited.waitedMs)} (run ${runId})`,
1117
+ ...sandboxIdLines(waited.sandboxes),
1118
+ ...(report && bool(ctx, "--verbose") ? [` ${forkLine(report.fork)}`, ` ${totalsLine(report.totals)}`] : []),
1119
+ ...(report && exit !== 0 ? failureLines(report, new Map(), false) : []),
1120
+ ` next: ${execNext[0].cmd}`,
1121
+ ], exit, ctx.pointer);
1122
+ };
1123
+ export function readyLine(ourMs, totalMs) {
1124
+ return ourMs === undefined ? `${totalMs} ms` : `${fmtMs(ourMs)} on our side · ${fmtMs(totalMs)} total`;
1125
+ }
1126
+ export function sandboxIdLines(sandboxes, cap = 10) {
1127
+ const lines = sandboxes.slice(0, cap).map((s) => ` ${s.sandbox_id} ${s.state}`);
1128
+ if (sandboxes.length > cap)
1129
+ lines.push(` … ${sandboxes.length - cap} more`);
1130
+ return lines;
1131
+ }
1132
+ function waitExpired(ctx, runId, boundMs, sandboxes) {
1133
+ const next = nexts([sh `promigence run status ${runId} --wait`, "keep waiting; the run is untouched"], [sh `promigence run report ${runId} --json`, "the validity report so far"], [sh `promigence run kill ${runId}`, "kill the run yourself (we did not)"]);
1134
+ const why = queuedClause(sandboxes);
1135
+ const running = ctx.cmd.name === "run" && sandboxes.some((s) => s.state !== "ended");
1136
+ const err = running
1137
+ ? new TimeoutError(`run ${runId} is still going after the ${duration(boundMs)} wait (--wait-timeout); nothing was stopped${why ? `. ${why}` : ""}`, `It goes on, and bills, until each sandbox's command ends or reaches its --timeout. Follow it: ` +
1138
+ `${sh `promigence run status ${runId} --wait`}, then ${sh `promigence run report ${runId}`}; ` +
1139
+ `or wait longer in one call with --wait-timeout (e.g. 20m).`)
1140
+ : new TimeoutError(`wait of ${boundMs}ms expired; nothing was killed${why ? `. ${why}` : ""}`, `Run the id-based command: ${sh `promigence run status ${runId} --wait`} (or ${sh `promigence run report ${runId}`}).`);
1141
+ const env = errorEnvelope(ctx.cmd.name, err, next);
1142
+ env["run_id"] = runId;
1143
+ env["sandboxes"] = sandboxes;
1144
+ env["killed"] = false;
1145
+ if (running)
1146
+ env["still_running"] = true;
1147
+ return emit(ctx.mode, env, errorTextLines(ctx.cmd.name, err, 11, next), 11, ctx.pointer);
1148
+ }
1149
+ export function envError(code, message, fix) {
1150
+ return { code, message, fix, docs: `${ERRORS_DOCS_URL}#${code}`, guide: sh `promigence agent-guide --section errors --code ${code}` };
1151
+ }
1152
+ export function execOutcome(res, sandboxId, timeoutMs, words = "cli") {
1153
+ const sig = res["signal"];
1154
+ const signal = typeof sig === "number" && sig > 0 ? sig : undefined;
1155
+ const timedOut = res.timed_out === true || res.reason === "timeout";
1156
+ const oom = res.oom_killed === true || res.reason === "out_of_memory";
1157
+ const own = typeof res.exit_code === "number" ? res.exit_code : undefined;
1158
+ const exitCode = own ?? (signal !== undefined ? 128 + signal : null);
1159
+ const fields = { exit_code: exitCode };
1160
+ if (signal !== undefined)
1161
+ fields["signal"] = signal;
1162
+ if (timedOut)
1163
+ fields["timed_out"] = true;
1164
+ if (oom)
1165
+ fields["oom_killed"] = true;
1166
+ if (res.reason !== undefined)
1167
+ fields["reason"] = res.reason;
1168
+ for (const k of ["wall_ms", "cpu_ms"]) {
1169
+ const v = res[k];
1170
+ if (typeof v === "number" && Number.isFinite(v))
1171
+ fields[k] = Math.round(v);
1172
+ }
1173
+ if (exitCode === 0 && !timedOut && !oom)
1174
+ return { exitCode, fields };
1175
+ const code = timedOut ? "task_timeout" : oom ? "oom_over_tier" : "nonzero_exit";
1176
+ const bound = timeoutMs === undefined ? "" : words === "mcp" ? ` (timeout_s ${Math.round(timeoutMs / 1000)})` : ` (--timeout ${duration(timeoutMs)})`;
1177
+ const message = timedOut
1178
+ ? `the command was killed at its timeout${bound}`
1179
+ : oom ? "the command was killed for running out of memory"
1180
+ : own === undefined && signal !== undefined ? `the command was killed by signal ${signal}`
1181
+ : exitCode === null ? "the command ended without an exit code"
1182
+ : `the command exited ${exitCode}`;
1183
+ const fix = code === "nonzero_exit"
1184
+ ? `The environment was verified; the command failed. Read its stdout and stderr${res.exec_id ? ` (again: ${sh `promigence exec output ${res.exec_id} --sandbox ${sandboxId}`})` : ""}.`
1185
+ : code === "task_timeout"
1186
+ ? `Pass a longer ${words === "mcp" ? "timeout_s" : "--timeout"} (billed per second used, so a longer bound only costs what runs), or shorten the command.`
1187
+ : ERROR_BY_CODE[code]?.fix ?? "";
1188
+ return { exitCode, fields, error: envError(code, message, fix) };
1189
+ }
1190
+ function execArgv(ctx) {
1191
+ if (ctx.cmdTail.length === 0) {
1192
+ const example = ctx.cmd.name === "run"
1193
+ ? "promigence run --count 2 --cap 1 -- python3 -c 'print(6*7)'"
1194
+ : "promigence exec --sandbox sbx_01 -- bun typecheck";
1195
+ throw usageError("no command: everything after `--` is the command to run", noCommandFix(`Write the command after --, e.g. ${example}.`));
1196
+ }
1197
+ return ctx.cmdTail;
1198
+ }
1199
+ function wantsStream(ctx) {
1200
+ if (bool(ctx, "--no-stream"))
1201
+ return false;
1202
+ if (bool(ctx, "--stream"))
1203
+ return true;
1204
+ return Boolean(process.stdout.isTTY);
1205
+ }
1206
+ let followKill;
1207
+ const FOLLOW_MAX_OUTPUT = 8 * 1024 * 1024;
1208
+ const FOLLOW_RECONNECT_MS = 10 * 60_000;
1209
+ async function followExec(c, sandboxId, execOpts) {
1210
+ let rec;
1211
+ try {
1212
+ rec = await c.sandboxes.execBackground(sandboxId, {
1213
+ argv: execOpts.argv,
1214
+ ...(execOpts.secrets ? { secrets: execOpts.secrets } : {}),
1215
+ ...(execOpts.timeoutMs === undefined ? {} : { timeoutMs: execOpts.timeoutMs }),
1216
+ maxOutputBytes: execOpts.maxOutputBytes ?? FOLLOW_MAX_OUTPUT,
1217
+ follow: true,
1218
+ });
1219
+ }
1220
+ catch (err) {
1221
+ const e = toPromigenceError(err);
1222
+ if (e.code === "usage" && /follow/.test(e.message))
1223
+ return undefined;
1224
+ throw err;
1225
+ }
1226
+ const eid = rec.exec_id;
1227
+ followKill = () => Promise.race([
1228
+ c.sandboxes.execs.forget(sandboxId, eid).then(() => undefined, () => undefined),
1229
+ new Promise((r) => setTimeout(r, 2000)),
1230
+ ]);
1231
+ const offset = { stdout: 0, stderr: 0 };
1232
+ let last = rec;
1233
+ let lostAt;
1234
+ const say = (s) => write(process.stderr, s);
1235
+ const pump = async (fd) => {
1236
+ for (let backoff = 500;;) {
1237
+ try {
1238
+ const r = await c.sandboxes.execs.read(sandboxId, eid, fd, { offset: offset[fd], waitMs: 25_000, untilOutput: true });
1239
+ if (lostAt !== undefined) {
1240
+ lostAt = undefined;
1241
+ await say("promigence: reconnected; the command's output continues from where it was\n");
1242
+ }
1243
+ backoff = 500;
1244
+ if (r.skipped > 0) {
1245
+ await say(`\npromigence: ${r.skipped} bytes of the command's ${fd} were not kept while this connection was down (the newest 8 MiB are)\n`);
1246
+ }
1247
+ if (r.data.length)
1248
+ await writeBytes(fd === "stdout" ? process.stdout : process.stderr, r.data);
1249
+ offset[fd] = r.offset + r.skipped + r.data.length;
1250
+ last = r.record;
1251
+ if (r.eof || (r.record.state !== "running" && r.data.length === 0))
1252
+ return;
1253
+ }
1254
+ catch (err) {
1255
+ const e = toPromigenceError(err);
1256
+ if (e.code !== "unreachable" && e.code !== "timeout")
1257
+ throw err;
1258
+ if (lostAt === undefined) {
1259
+ lostAt = Date.now();
1260
+ await say(`\npromigence: connection lost; the command keeps running in the sandbox. Reconnecting… ` +
1261
+ `(or later: ${sh `promigence exec output ${eid} --sandbox ${sandboxId} --follow`})\n`);
1262
+ }
1263
+ else if (Date.now() - lostAt > FOLLOW_RECONNECT_MS) {
1264
+ throw err;
1265
+ }
1266
+ await new Promise((r) => setTimeout(r, backoff));
1267
+ backoff = Math.min(backoff * 2, 5_000);
1268
+ }
1269
+ }
1270
+ };
1271
+ try {
1272
+ await Promise.all([pump("stdout"), pump("stderr")]);
1273
+ }
1274
+ finally {
1275
+ followKill = undefined;
1276
+ }
1277
+ const fin = last.state === "running" || !last.exit ? await c.sandboxes.execs.get(sandboxId, eid, { waitMs: 5_000 }) : last;
1278
+ await c.sandboxes.execs.forget(sandboxId, eid).catch(() => undefined);
1279
+ const x = fin.exit;
1280
+ const signal = typeof x?.signal === "number" ? x.signal : undefined;
1281
+ return {
1282
+ exec_id: eid,
1283
+ exit_code: x?.exit_code ?? null,
1284
+ ...(signal === undefined ? {} : { signal }),
1285
+ ...(x?.oom_killed ? { reason: "out_of_memory" } : signal !== undefined ? { reason: "signal" } : {}),
1286
+ ...(typeof x?.wall_ms === "number" ? { wall_ms: x.wall_ms } : {}),
1287
+ ...(typeof x?.cpu_ms === "number" ? { cpu_ms: x.cpu_ms } : {}),
1288
+ truncated: false,
1289
+ };
1290
+ }
1291
+ async function streamExec(ctx, c, sandboxId, execOpts) {
1292
+ const ndjson = ctx.mode === "json";
1293
+ const dec = { stdout: new TextDecoder(), stderr: new TextDecoder() };
1294
+ const out = (fd, text) => {
1295
+ void write(process.stdout, jsonLine(process.stdout, { stream: fd, data: text }));
1296
+ };
1297
+ const raw = (fd, b) => {
1298
+ void writeBytes(fd === "stdout" ? process.stdout : process.stderr, b);
1299
+ };
1300
+ try {
1301
+ return await c.sandboxes.execStream(sandboxId, execOpts, {
1302
+ stdout: (b) => (ndjson ? out("stdout", dec.stdout.decode(b, { stream: true })) : raw("stdout", b)),
1303
+ stderr: (b) => (ndjson ? out("stderr", dec.stderr.decode(b, { stream: true })) : raw("stderr", b)),
1304
+ });
1305
+ }
1306
+ finally {
1307
+ if (ndjson) {
1308
+ for (const fd of ["stdout", "stderr"]) {
1309
+ const tail = dec[fd].decode();
1310
+ if (tail)
1311
+ out(fd, tail);
1312
+ }
1313
+ }
1314
+ }
1315
+ }
1316
+ function asText(res) {
1317
+ const out = { ...res };
1318
+ for (const fd of ["stdout", "stderr"]) {
1319
+ const k = `${fd}_b64`;
1320
+ if (typeof res[k] !== "string")
1321
+ continue;
1322
+ const bytes = execOutputBytes(res, fd);
1323
+ let text;
1324
+ try {
1325
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
1326
+ delete out[k];
1327
+ }
1328
+ catch {
1329
+ text = new TextDecoder().decode(bytes);
1330
+ }
1331
+ out[fd] = text;
1332
+ }
1333
+ return out;
1334
+ }
1335
+ async function emitChild(res, exit, notice) {
1336
+ if (typeof res.stdout_b64 === "string" || typeof res.stderr_b64 === "string") {
1337
+ await writeBytes(process.stdout, execOutputBytes(res, "stdout"));
1338
+ await writeBytes(process.stderr, execOutputBytes(res, "stderr"));
1339
+ if (notice)
1340
+ await write(process.stderr, notice);
1341
+ return exitWith(exit);
1342
+ }
1343
+ if (res.stdout)
1344
+ await write(process.stdout, res.stdout.endsWith("\n") ? res.stdout : res.stdout + "\n");
1345
+ if (res.stderr)
1346
+ await write(process.stderr, res.stderr.endsWith("\n") ? res.stderr : res.stderr + "\n");
1347
+ if (notice)
1348
+ await write(process.stderr, notice);
1349
+ return exitWith(exit);
1350
+ }
1351
+ export function execTimingLine(exitCode, wallMs, totalMs, isTTY = Boolean(process.stderr.isTTY), env = process.env) {
1352
+ if (!isTTY || env["PROMIGENCE_QUIET"] === "1" || typeof wallMs !== "number" || !Number.isFinite(wallMs))
1353
+ return "";
1354
+ return `promigence: exit ${exitCode ?? "none"} · ran ${fmtMs(wallMs)} on our side · ${fmtMs(totalMs)} round trip\n`;
1355
+ }
1356
+ export function endNotice(res, timeoutMs) {
1357
+ const sig = typeof res["signal"] === "number" ? ` (signal ${res["signal"]})` : "";
1358
+ switch (res.reason) {
1359
+ case "timeout": return `promigence exec: the command was killed at its timeout (--timeout ${timeoutMs === undefined ? "15m" : duration(timeoutMs)})\n`;
1360
+ case "out_of_memory": return "promigence exec: the command was killed: out of memory (a larger --tier has more)\n";
1361
+ case "signal": return `promigence exec: the command was killed by a signal${sig}\n`;
1362
+ default: return "";
1363
+ }
1364
+ }
1365
+ export const MAX_OUTPUT_BYTES_PIPED = 32 * 1024 * 1024;
1366
+ function endsWithNewline(res) {
1367
+ if (typeof res.stdout_b64 === "string") {
1368
+ const b = execOutputBytes(res, "stdout");
1369
+ return b.length === 0 || b[b.length - 1] === 0x0a;
1370
+ }
1371
+ const t = res.stdout ?? "";
1372
+ return t === "" || t.endsWith("\n");
1373
+ }
1374
+ export function truncationNotice(kept, maxOutputBytes) {
1375
+ return `promigence exec: output cut to the ${kept} ${bytes(maxOutputBytes ?? MAX_OUTPUT_BYTES_DEFAULT)} ` +
1376
+ "(--max-output; at most 32m); re-run with a larger --max-output, " +
1377
+ "or write it to a file in the sandbox and `promigence files get` it\n";
1378
+ }
1379
+ async function stdinHasData(waitMs = 150) {
1380
+ if (process.stdin.isTTY)
1381
+ return false;
1382
+ let st;
1383
+ try {
1384
+ st = fstatSync(0);
1385
+ }
1386
+ catch {
1387
+ return false;
1388
+ }
1389
+ if (st.isFile())
1390
+ return st.size > 0;
1391
+ if (!st.isFIFO() && !st.isSocket())
1392
+ return false;
1393
+ return new Promise((resolve) => {
1394
+ const stdin = process.stdin;
1395
+ const done = (v) => {
1396
+ clearTimeout(timer);
1397
+ stdin.off("data", onData);
1398
+ stdin.off("end", onEnd);
1399
+ stdin.pause();
1400
+ if (!v)
1401
+ stdin.destroy();
1402
+ resolve(v);
1403
+ };
1404
+ const onData = (d) => done(d.length > 0);
1405
+ const onEnd = () => done(false);
1406
+ const timer = setTimeout(() => done(false), waitMs);
1407
+ stdin.on("data", onData);
1408
+ stdin.once("end", onEnd);
1409
+ stdin.resume();
1410
+ });
1411
+ }
1412
+ const exec = async (ctx) => {
1413
+ if (bool(ctx, "--background"))
1414
+ return execBackground(ctx);
1415
+ const sandboxId = str(ctx, "--sandbox");
1416
+ const runId = str(ctx, "--run");
1417
+ if (sandboxId === undefined && runId === undefined) {
1418
+ throw usageError("exec needs --sandbox SID or --run ID --all", "One sandbox: promigence exec --sandbox <sid> -- CMD. Every sandbox of a run: promigence exec --run <run_id> --all -- CMD.");
1419
+ }
1420
+ if (sandboxId !== undefined && runId !== undefined) {
1421
+ throw usageError("exec takes --sandbox or --run, not both", "Pick one target.");
1422
+ }
1423
+ const argv = execArgv(ctx);
1424
+ if (await stdinHasData()) {
1425
+ throw usageError("exec does not pass this command's standard input to the sandbox (it would be dropped)", `Start it with stdin open: ${sh `promigence exec --sandbox ${sandboxId ?? "<sid>"} --background --stdin --`} ${argv.map(shellArg).join(" ")}, then send the input with \`promigence exec stdin <exec_id> --sandbox <sid> --data … --eof\`; or \`promigence files put\` it and read the file in the command.`);
1426
+ }
1427
+ const maxOutput = str(ctx, "--max-output");
1428
+ const timeout = str(ctx, "--timeout");
1429
+ const secrets = list(ctx, "--secret");
1430
+ const idemKey = str(ctx, "--idempotency-key");
1431
+ const execOpts = {
1432
+ argv,
1433
+ ...(secrets.length ? { secrets } : {}),
1434
+ ...(maxOutput === undefined ? {} : { maxOutputBytes: parseMaxOutput(maxOutput) }),
1435
+ ...(timeout === undefined ? {} : { timeoutMs: parseCommandTimeout(timeout, "--timeout") }),
1436
+ ...(idemKey === undefined ? {} : { idempotencyKey: idemKey }),
1437
+ };
1438
+ const c = client(ctx);
1439
+ if (sandboxId !== undefined) {
1440
+ const streaming = wantsStream(ctx) && idemKey === undefined;
1441
+ if (ctx.mode === "text" && !process.stdout.isTTY && execOpts.maxOutputBytes === undefined) {
1442
+ execOpts.maxOutputBytes = MAX_OUTPUT_BYTES_PIPED;
1443
+ }
1444
+ const interrupted = (sig) => {
1445
+ const ending = followKill?.() ?? Promise.resolve();
1446
+ void ending.finally(() => interruptedEnvelope(sig));
1447
+ };
1448
+ const interruptedEnvelope = (sig) => {
1449
+ const err = new PromigenceError({
1450
+ code: "aborted_by_client",
1451
+ message: `interrupted by ${sig}; the command in the sandbox ends with this connection`,
1452
+ fix: "Nothing to fix; billed for the seconds used. The sandbox itself is still running.",
1453
+ exitCode: 130,
1454
+ });
1455
+ const next = nexts([sh `promigence sandbox info ${sandboxId}`, "the sandbox and the execs it has run"]);
1456
+ const env = errorEnvelope(ctx.cmd.name, err, next);
1457
+ env["sandbox_id"] = sandboxId;
1458
+ void emit(ctx.mode, env, [`promigence exec: interrupted by ${sig} (exit 130)`], 130, false);
1459
+ };
1460
+ for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"])
1461
+ process.once(sig, () => interrupted(sig));
1462
+ const execStarted = performance.now();
1463
+ const followed = streaming && ctx.mode === "text" && process.env["PROMIGENCE_EXEC_FOLLOW"] !== "0"
1464
+ ? await followExec(c, sandboxId, execOpts)
1465
+ : undefined;
1466
+ const res = followed ?? (streaming
1467
+ ? await streamExec(ctx, c, sandboxId, execOpts)
1468
+ : asText(await c.sandboxes.exec(sandboxId, { ...execOpts, encoding: "base64" })));
1469
+ const execTotalMs = performance.now() - execStarted;
1470
+ const outcome = execOutcome(res, sandboxId, execOpts.timeoutMs);
1471
+ const passthrough = !bool(ctx, "--no-passthrough");
1472
+ let exit = passthrough ? (outcome.exitCode ?? 4) : (outcome.exitCode === 0 ? 0 : 4);
1473
+ if (ctx.mode === "text") {
1474
+ if (res.truncated && exit === 0)
1475
+ exit = 12;
1476
+ const notice = (res.truncated ? truncationNotice(streaming ? "first" : "last", execOpts.maxOutputBytes) : "")
1477
+ + endNotice(res, execOpts.timeoutMs);
1478
+ const raw = streaming || typeof res.stdout_b64 === "string";
1479
+ const timed = execTimingLine(outcome.exitCode, res.wall_ms, execTotalMs);
1480
+ const all = notice + timed;
1481
+ const gap = all && raw && process.stdout.isTTY && !endsWithNewline(res) ? "\n" : "";
1482
+ if (streaming) {
1483
+ if (all)
1484
+ await write(process.stderr, gap + all);
1485
+ return exitWith(exit);
1486
+ }
1487
+ return emitChild(res, exit, all ? gap + all : undefined);
1488
+ }
1489
+ const next = nexts(...(res.truncated
1490
+ ? [[`${sh `promigence exec --sandbox ${sandboxId} --max-output 32m --`} ${argv.map(shellArg).join(" ")}`, "re-run with a larger --max-output"]]
1491
+ : []), [sh `promigence sandbox kill ${sandboxId}`, "end the sandbox when you are done"]);
1492
+ const env = okEnvelope(ctx.cmd.name, {
1493
+ sandbox_id: sandboxId,
1494
+ exec_id: res.exec_id,
1495
+ argv,
1496
+ ...outcome.fields,
1497
+ stdout: res.stdout ?? "",
1498
+ stderr: res.stderr ?? "",
1499
+ truncated: res.truncated ?? false,
1500
+ ...(res.stdout_b64 === undefined ? {} : { stdout_b64: res.stdout_b64 }),
1501
+ ...(res.stderr_b64 === undefined ? {} : { stderr_b64: res.stderr_b64 }),
1502
+ ...(res["idempotent_replay"] === true ? { idempotent_replay: true } : {}),
1503
+ }, next);
1504
+ if (outcome.error) {
1505
+ env.ok = false;
1506
+ env.error = outcome.error;
1507
+ }
1508
+ return emit(ctx.mode, env, [], exit, false);
1509
+ }
1510
+ const id = runId;
1511
+ if (!bool(ctx, "--all")) {
1512
+ throw usageError("--run needs --all", `Target every ready sandbox: ${sh `promigence exec --run ${id} --all -- CMD`}.`);
1513
+ }
1514
+ let sandboxes = await c.runs.sandboxes(id);
1515
+ if (bool(ctx, "--wait")) {
1516
+ const bound = waitBound(ctx);
1517
+ const waited = await c.runs.waitReady(id, { waitTimeoutMs: bound });
1518
+ if (waited.timedOut)
1519
+ return waitExpired(ctx, id, bound, waited.sandboxes);
1520
+ sandboxes = waited.sandboxes;
1521
+ }
1522
+ const targets = sandboxes.filter((s) => s.state === "ready" || s.state === "running" || s.state === "paused");
1523
+ const skipped = sandboxes
1524
+ .filter((s) => isWaiting(s.state) || s.state === "ended")
1525
+ .map((s) => ({
1526
+ sandbox_id: s.sandbox_id,
1527
+ state: s.state,
1528
+ reason: typeof s.queued_message === "string" && s.queued_message !== ""
1529
+ ? `queued: ${s.queued_message} (pass --wait)`
1530
+ : s.state === "awaiting_capacity"
1531
+ ? "still waiting for capacity being added (pass --wait)"
1532
+ : s.state === "queued" ? "not ready (pass --wait)" : "ended",
1533
+ }));
1534
+ const rows = await Promise.all(targets.map(async (s) => {
1535
+ try {
1536
+ const res = await c.sandboxes.exec(s.sandbox_id, execOpts);
1537
+ const o = execOutcome(res, s.sandbox_id, execOpts.timeoutMs);
1538
+ return {
1539
+ sandbox_id: s.sandbox_id, exec_id: res.exec_id, ...o.fields, exit_code: o.exitCode,
1540
+ stdout: res.stdout ?? "", stderr: res.stderr ?? "", truncated: res.truncated ?? false,
1541
+ ...(o.error ? { error: o.error } : {}),
1542
+ };
1543
+ }
1544
+ catch (err) {
1545
+ const e = toPromigenceError(err);
1546
+ return { sandbox_id: s.sandbox_id, error: e.toJSON(), exit: e.cliExit };
1547
+ }
1548
+ }));
1549
+ const codes = [];
1550
+ for (const r of rows) {
1551
+ if (!("exec_id" in r))
1552
+ codes.push(r.exit);
1553
+ else if (r.exit_code !== 0)
1554
+ codes.push(4);
1555
+ }
1556
+ if (skipped.length)
1557
+ codes.push(12);
1558
+ const exit = pickExit(codes);
1559
+ const next = nexts([sh `promigence run report ${id} --json`, "the validity report for this run"], [sh `promigence run kill ${id}`, "kill every sandbox of this run"]);
1560
+ const env = {
1561
+ ...okEnvelope(ctx.cmd.name, { run_id: id, argv, sandboxes: rows, ...(skipped.length ? { skipped } : {}) }, next),
1562
+ ok: exit === 0,
1563
+ };
1564
+ if (exit !== 0) {
1565
+ const first = exit === 12 && skipped.length ? undefined
1566
+ : rows.find((r) => (!("exec_id" in r) ? r.exit === exit : r.exit_code !== 0 && exit === 4))
1567
+ ?? rows.find((r) => r.error !== undefined);
1568
+ const ended = skipped.filter((k) => k.state === "ended").length;
1569
+ const waiting = skipped.length - ended;
1570
+ const of = `of ${sandboxes.length} sandbox${sandboxes.length === 1 ? "" : "es"}`;
1571
+ env.error = first?.error ?? envError("not_started", [
1572
+ ...(waiting ? [`${waiting} ${of} ${waiting === 1 ? "was" : "were"} not ready yet`] : []),
1573
+ ...(ended ? [`${ended} ${waiting ? "had" : `${of} had`} ended`] : []),
1574
+ ].join(" and ") + " and did not run the command", waiting
1575
+ ? `Pass --wait to wait for the ones not ready: ${sh `promigence exec --run ${id} --all --wait --`} ${argv.map(shellArg).join(" ")}.`
1576
+ : `An ended sandbox runs nothing again; start new ones (${sh `promigence run report ${id}`} says why these ended).`);
1577
+ }
1578
+ if (ctx.mode === "text") {
1579
+ const out = [];
1580
+ const errOut = [];
1581
+ for (const r of rows) {
1582
+ if (!("exec_id" in r))
1583
+ continue;
1584
+ out.push(...tagLines(r.sandbox_id, r.stdout, Boolean(process.stdout.isTTY)));
1585
+ errOut.push(...tagLines(r.sandbox_id, r.stderr, Boolean(process.stderr.isTTY)));
1586
+ }
1587
+ if (out.length)
1588
+ await write(process.stdout, out.join("\n") + "\n");
1589
+ if (errOut.length)
1590
+ await write(process.stderr, errOut.join("\n") + "\n");
1591
+ }
1592
+ const lines = rows.map((r) => (!("exec_id" in r)
1593
+ ? ` ${r.sandbox_id}: ${r.error.code}: ${r.error.message}`
1594
+ : ` ${r.sandbox_id}: exit ${r.exit_code}${r.error && r.error.code !== "nonzero_exit" ? ` (${r.error.message})` : ""}${r.truncated ? " (output cut to the last --max-output bytes; re-run with a larger --max-output)" : ""}`));
1595
+ for (const k of skipped)
1596
+ lines.push(` ${k.sandbox_id}: skipped, ${k.reason}`);
1597
+ return emit(ctx.mode, env, [`${sh `promigence exec --run ${id} --all`}: ${rows.length} sandbox${rows.length === 1 ? "" : "es"}`, ...lines], exit, false, "stderr");
1598
+ };
1599
+ export function tagLines(sandboxId, text, escape = false) {
1600
+ if (!text)
1601
+ return [];
1602
+ const body = text.endsWith("\n") ? text.slice(0, -1) : text;
1603
+ return body.split("\n").map((l) => `${sandboxId}: ${escape ? escapeLine(l) : l}`);
1604
+ }
1605
+ async function runImageSnapshot(ctx, c) {
1606
+ if (!ctx.flags.has("--image"))
1607
+ return undefined;
1608
+ if (ctx.flags.has("--snapshot")) {
1609
+ throw usageError("run takes --snapshot or --image, not both", "An existing snapshot: promigence run --snapshot H --count N -- CMD. An image: promigence run --image REF@sha256:<digest> --count N -- CMD.");
1610
+ }
1611
+ const image = required(ctx, "--image", "Pass --image python:3.12 or registry/name@sha256:<digest>.");
1612
+ checkImageRef(image);
1613
+ const count = parseCount(required(ctx, "--count", "Pass --count N (number of sandboxes to fork)."), "--count");
1614
+ const cap = await spendCap(ctx, c);
1615
+ const most = await buildPrice(c);
1616
+ const price = most.most_usd;
1617
+ const timeout = str(ctx, "--timeout");
1618
+ const quoted = await c.runs.quote({
1619
+ snapshot: DEFAULT_SNAPSHOT, count, ...(timeout === undefined ? {} : { timeoutMs: parseDuration(timeout, "--timeout") }),
1620
+ }).then((q) => q.max_cost).catch(() => undefined);
1621
+ const needed = Math.ceil((price + (quoted ?? 0)) * 100) / 100;
1622
+ if (cap < price + (quoted ?? 0)) {
1623
+ const existing = (quoted ?? 0) <= cap ? await existingImageSnapshot(c, image) : undefined;
1624
+ throw new SpendCapError(`a new image's snapshot build can cost up to ${moneyUsd(price)} on top of the run` +
1625
+ `${quoted === undefined ? "" : ` (about ${moneyUsd(quoted)})`}, over --cap ${moneyUsd(cap)}; nothing was built or started`, (existing === undefined ? "" : `This image already has snapshot ${existing}: run it under this cap with ${sh `promigence run --snapshot ${existing}`} instead of --image (any start command or settings it was made with come with it). Or `) +
1626
+ `${existing === undefined ? "Pass" : "pass"} --cap ${needed.toFixed(2)} or more (a build is billed per second while it runs, only when this image has no snapshot yet, and reused free after), ` +
1627
+ `or build it first, ${sh `promigence snapshot create --image ${image} --wait`}, and run its snapshot with --snapshot.`);
1628
+ }
1629
+ await write(process.stderr, ctx.mode === "json"
1630
+ ? jsonLine(process.stderr, { hint: "image snapshot", image })
1631
+ : `image ${image}: creating or reusing its snapshot (a first build takes minutes)\n`);
1632
+ let charged = 0;
1633
+ let perSecond = false;
1634
+ let buildId;
1635
+ const buildOpts = { image, allowBuild: true };
1636
+ const snapshotId = await imageSnapshot(c, buildOpts, {
1637
+ onBuild: async (b) => {
1638
+ const free = b.deduped === true || b.joined === true || b.idempotent_replay === true || b.state === "verified" || b.state === "built";
1639
+ const said = buildPriceOf(b);
1640
+ perSecond = !free && said !== undefined;
1641
+ charged = free ? 0 : (typeof b.build_price === "number" ? b.build_price : price);
1642
+ buildId = b.build_id;
1643
+ await write(process.stderr, ctx.mode === "json"
1644
+ ? jsonLine(process.stderr, {
1645
+ hint: "build_price", build_price: charged, unit: "USD", image,
1646
+ ...(perSecond && said ? { per_second_usd: said.per_second_usd, max_seconds: said.max_s } : {}),
1647
+ ...(b.build_id ? { build_id: b.build_id } : {}), ...(free ? { charged: false } : {}),
1648
+ })
1649
+ : free
1650
+ ? `build_price: $0.00 - this image's snapshot already exists; reusing it, nothing charged\n`
1651
+ : perSecond && said
1652
+ ? `build_price: ${buildPriceWords(said)} - building ${image}'s snapshot (billed per second while it builds; counted in --cap)\n`
1653
+ : `build_price: $${charged.toFixed(2)} - building ${image}'s snapshot (charged once it verifies; counted in --cap)\n`);
1654
+ },
1655
+ });
1656
+ if (perSecond && buildId !== undefined) {
1657
+ const done = await c.snapshots.status(buildId).catch(() => undefined);
1658
+ if (done !== undefined && typeof done.cost_usd === "number" && !isBuilding(done))
1659
+ charged = done.cost_usd;
1660
+ }
1661
+ await write(process.stderr, ctx.mode === "json"
1662
+ ? jsonLine(process.stderr, { hint: "image snapshot ready", image, snapshot_id: snapshotId })
1663
+ : fieldForTerminal(process.stderr, `image ${image}: snapshot ${snapshotId}`) + "\n");
1664
+ return { snapshotId, charged, ...(buildId === undefined ? {} : { buildId }) };
1665
+ }
1666
+ async function existingImageSnapshot(c, image) {
1667
+ try {
1668
+ const r = await c.images.resolve(image);
1669
+ const digests = new Set([r.digest, r.platform_digest].filter((d) => typeof d === "string" && d !== ""));
1670
+ let cursor;
1671
+ for (let page = 0; page < 5; page++) {
1672
+ const p = await c.snapshots.listPage({ limit: 100, ...(cursor === undefined ? {} : { cursor }) });
1673
+ const hit = p.items.find((s) => s.source?.kind === "image" && typeof s.source.digest === "string" && digests.has(s.source.digest) &&
1674
+ s.source["from_sandbox"] === undefined && !s.unavailable && !(s.setup?.length) && !s.verify?.cmd &&
1675
+ (s.tier === undefined || s.tier === "small"));
1676
+ if (hit)
1677
+ return hit.snapshot_id;
1678
+ if (!p.nextCursor)
1679
+ return undefined;
1680
+ cursor = p.nextCursor;
1681
+ }
1682
+ }
1683
+ catch {
1684
+ }
1685
+ return undefined;
1686
+ }
1687
+ const runComposite = async (ctx) => {
1688
+ const argv = execArgv(ctx);
1689
+ const c = client(ctx);
1690
+ const built = await runImageSnapshot(ctx, c);
1691
+ const opts = await runStartOpts(ctx, c, built?.snapshotId ?? (str(ctx, "--snapshot") === undefined ? DEFAULT_SNAPSHOT : undefined));
1692
+ if (built !== undefined && built.charged > 0 && opts.spendCap !== undefined) {
1693
+ opts.spendCap = Math.max(0, Math.round((opts.spendCap - built.charged) * 1e9) / 1e9);
1694
+ }
1695
+ const keep = bool(ctx, "--keep");
1696
+ const maxOutput = str(ctx, "--max-output");
1697
+ opts.exec = {
1698
+ argv,
1699
+ killOnExit: !keep,
1700
+ ...(maxOutput === undefined ? {} : { maxOutputBytes: parseMaxOutput(maxOutput) }),
1701
+ ...(opts.timeoutMs === undefined ? {} : { timeoutMs: opts.timeoutMs }),
1702
+ };
1703
+ const explicitKey = str(ctx, "--idempotency-key");
1704
+ if (explicitKey !== undefined)
1705
+ opts.idempotencyKey = explicitKey;
1706
+ const failOn = str(ctx, "--fail-on") ?? "task";
1707
+ if (failOn !== "task" && failOn !== "env" && failOn !== "none") {
1708
+ throw usageError(`--fail-on: '${failOn}' is not a mode`, "Pass --fail-on task | env | none (default task).");
1709
+ }
1710
+ const streaming = wantsStream(ctx);
1711
+ const execSpec = opts.exec;
1712
+ if (streaming)
1713
+ delete opts.exec;
1714
+ const started = await c.runs.start(opts);
1715
+ const runId = started.run_id;
1716
+ ctx.startedRunId = runId;
1717
+ const replayed = replayNote(started, true);
1718
+ const waitingWhy = queuedLines(started.sandboxes);
1719
+ await write(process.stderr, ctx.mode === "json"
1720
+ ? jsonLine(process.stderr, { hint: "run started", run_id: runId, sandboxes: started.sandboxes.length, ...replayFields(started) })
1721
+ : `${replayed ? `${fieldForTerminal(process.stderr, replayed)}\n` : ""}`
1722
+ + `${fieldForTerminal(process.stderr, `run ${runId} started: ${started.sandboxes.length} sandbox${started.sandboxes.length === 1 ? "" : "es"} - report: ${sh `promigence run report ${runId}`}`)}\n`
1723
+ + waitingWhy.map((l) => `${fieldForTerminal(process.stderr, l)}\n`).join(""));
1724
+ let interrupted = false;
1725
+ let interruptP;
1726
+ const yieldToSignal = async () => { if (interruptP)
1727
+ await interruptP; };
1728
+ const onSignal = (sig) => {
1729
+ if (interrupted)
1730
+ return;
1731
+ interrupted = true;
1732
+ interruptP = (async () => {
1733
+ let killed = false;
1734
+ if (!keep) {
1735
+ try {
1736
+ await c.runs.kill(runId);
1737
+ killed = true;
1738
+ }
1739
+ catch {
1740
+ killed = false;
1741
+ }
1742
+ }
1743
+ const next = keep || killed
1744
+ ? nexts([sh `promigence run report ${runId}`, "what the sandboxes did"])
1745
+ : nexts([sh `promigence run kill ${runId}`, "the kill did not complete; kill the run yourself"]);
1746
+ const err = new PromigenceError({
1747
+ code: "aborted_by_client",
1748
+ message: `interrupted by ${sig}; ${keep ? "--keep: sandboxes left running" : killed ? "sandboxes killed" : "the kill did not complete"}`,
1749
+ fix: keep || killed ? "Nothing to fix; billed for the seconds used." : `Kill the run yourself: ${sh `promigence run kill ${runId}`}.`,
1750
+ exitCode: 130,
1751
+ });
1752
+ const env = errorEnvelope(ctx.cmd.name, err, next);
1753
+ env["run_id"] = runId;
1754
+ env["killed"] = killed;
1755
+ return emit(ctx.mode, env, [`promigence run: interrupted by ${sig} (exit 130)`, ` killed: ${killed}`, ` next: ${next[0].cmd}`], 130, false);
1756
+ })();
1757
+ };
1758
+ for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"])
1759
+ process.on(sig, () => onSignal(sig));
1760
+ const bound = waitBound(ctx);
1761
+ const waitStarted = Date.now();
1762
+ const status = ctx.mode === "text" ? statusLine() : undefined;
1763
+ const watch = (total) => {
1764
+ let readyCount = 0;
1765
+ let last;
1766
+ const paint = () => {
1767
+ const line = capacityLine(last, readyCount, total);
1768
+ if (line)
1769
+ status?.set(line);
1770
+ else if (readyCount >= total)
1771
+ status?.clear();
1772
+ };
1773
+ status?.set(`capacity requested: ${total} sandbox${total === 1 ? "" : "es"}`);
1774
+ return status
1775
+ ? {
1776
+ onCapacity: (cap) => { last = cap; paint(); },
1777
+ onReady: () => { readyCount += 1; paint(); },
1778
+ }
1779
+ : {};
1780
+ };
1781
+ if (streaming) {
1782
+ const ready = await c.runs.waitReady(runId, {
1783
+ waitTimeoutMs: bound,
1784
+ ...watch(started.sandboxes.length),
1785
+ });
1786
+ status?.clear();
1787
+ await yieldToSignal();
1788
+ if (ready.timedOut)
1789
+ return waitExpired(ctx, runId, bound, ready.sandboxes);
1790
+ const targets = ready.sandboxes.filter((s) => s.state === "ready" || s.state === "running");
1791
+ const many = targets.length > 1;
1792
+ const sink = (sid, fd) => {
1793
+ const dec = new TextDecoder();
1794
+ let partial = "";
1795
+ const put = (text, last) => {
1796
+ if (ctx.mode === "json") {
1797
+ if (text || !last)
1798
+ void write(process.stdout, jsonLine(process.stdout, { stream: fd, sandbox_id: sid, data: text }));
1799
+ return;
1800
+ }
1801
+ const target = fd === "stdout" ? process.stdout : process.stderr;
1802
+ if (!many) {
1803
+ void write(target, text);
1804
+ return;
1805
+ }
1806
+ const line = target.isTTY ? escapeLine : (l) => l;
1807
+ partial += text;
1808
+ let nl;
1809
+ while ((nl = partial.indexOf("\n")) >= 0) {
1810
+ void write(target, `${sid}: ${line(partial.slice(0, nl))}\n`);
1811
+ partial = partial.slice(nl + 1);
1812
+ }
1813
+ if (last && partial) {
1814
+ void write(target, `${sid}: ${line(partial)}\n`);
1815
+ partial = "";
1816
+ }
1817
+ };
1818
+ return {
1819
+ feed: (b) => put(dec.decode(b, { stream: true }), false),
1820
+ end: () => put(dec.decode(), true),
1821
+ };
1822
+ };
1823
+ await Promise.all(targets.map(async (s) => {
1824
+ const out = sink(s.sandbox_id, "stdout");
1825
+ const errOut = sink(s.sandbox_id, "stderr");
1826
+ let failed;
1827
+ try {
1828
+ await c.sandboxes.execStream(s.sandbox_id, {
1829
+ ...execSpec,
1830
+ terminal: true,
1831
+ killOnExit: !keep,
1832
+ }, { stdout: out.feed, stderr: errOut.feed });
1833
+ }
1834
+ catch (err) {
1835
+ failed = toPromigenceError(err);
1836
+ }
1837
+ out.end();
1838
+ errOut.end();
1839
+ if (failed)
1840
+ await write(process.stderr, forTerminal(process.stderr, `${s.sandbox_id}: ${failed.code}: ${failed.message}`) + "\n");
1841
+ }));
1842
+ }
1843
+ if (!streaming) {
1844
+ const ready = await c.runs.waitReady(runId, {
1845
+ waitTimeoutMs: bound,
1846
+ ...watch(started.sandboxes.length),
1847
+ });
1848
+ status?.clear();
1849
+ await yieldToSignal();
1850
+ if (ready.timedOut)
1851
+ return waitExpired(ctx, runId, bound, ready.sandboxes);
1852
+ }
1853
+ const waited = await c.runs.waitDone(runId, { waitTimeoutMs: bound });
1854
+ await yieldToSignal();
1855
+ if (waited.timedOut)
1856
+ return waitExpired(ctx, runId, bound, waited.run.sandboxes ?? []);
1857
+ const { report, settled } = await settledReport(c, runId, keep, Math.max(0, bound - (Date.now() - waitStarted)));
1858
+ await yieldToSignal();
1859
+ const codes = reportExits(report);
1860
+ const filtered = failOn === "none" ? [] : failOn === "env" ? codes.filter((x) => x !== 4) : codes;
1861
+ const exit = pickExit(filtered);
1862
+ const kept = keep ? (report.sandboxes ?? []).filter((s) => s.state !== "ended").map((s) => s.sandbox_id) : [];
1863
+ const capPaused = (report.sandboxes ?? []).find((s) => limitPauseOf(s));
1864
+ const next = nexts(...(capPaused
1865
+ ? [limitPauseOf(capPaused) === "spend_cap"
1866
+ ? [sh `promigence sandbox resume ${capPaused.sandbox_id} --cap USD`, "go on: a cap above the run's (its files and state are kept)"]
1867
+ : ["promigence billing card", `add a card, then ${sh `promigence sandbox resume ${capPaused.sandbox_id}`}`]]
1868
+ : []), [sh `promigence run report ${runId} --json`, "the full validity report (per-sandbox evidence)"], ...((report.totals?.env_failed ?? 0) > 0
1869
+ ? [[sh `promigence run replay ${runId}`, "re-fork the env failures (a replay is a new sandbox, billed like any other)"]]
1870
+ : []), ...(kept.length
1871
+ ? [[sh `promigence run kill ${runId}`, "end the kept sandboxes: they bill until killed or their --timeout"]]
1872
+ : []));
1873
+ const outputs = streaming ? new Map() : await outputTails(c, report.sandboxes ?? []);
1874
+ const firstExec = (report.sandboxes ?? []).find((s) => s.execs?.length);
1875
+ if (firstExec) {
1876
+ const eid = firstExec.execs[firstExec.execs.length - 1].exec_id;
1877
+ next.push(...nexts([sh `promigence exec output ${eid} --sandbox ${firstExec.sandbox_id}`, "one sandbox's full command output"]));
1878
+ }
1879
+ const fields = {
1880
+ run_id: runId,
1881
+ state: report.state,
1882
+ snapshot: opts.snapshot,
1883
+ count: opts.count,
1884
+ argv,
1885
+ fork: forkSummary(report.fork),
1886
+ totals: report.totals,
1887
+ sandboxes: (report.sandboxes ?? []).map((s) => ({
1888
+ sandbox_id: s.sandbox_id,
1889
+ outcome: s.outcome,
1890
+ ...(limitPauseOf(s) ? { state: s.state, pause_reason: limitPauseOf(s) } : {}),
1891
+ ...(s.execs?.length ? { exec: s.execs[s.execs.length - 1] } : {}),
1892
+ ...(outputs.has(s.sandbox_id) ? { output: outputs.get(s.sandbox_id) } : {}),
1893
+ ...(s.failure ? { failure: s.failure } : {}),
1894
+ ...(s.warnings?.length ? { warnings: s.warnings } : {}),
1895
+ ...gradeFields(s),
1896
+ })),
1897
+ exit,
1898
+ };
1899
+ if (report.cost) {
1900
+ fields["cost"] = settled
1901
+ ? { ...report.cost, final: true }
1902
+ : { ...report.cost, final: false, not_final: settledWhy(report, keep) };
1903
+ }
1904
+ if (built !== undefined) {
1905
+ fields["build"] = { snapshot_id: built.snapshotId, charged: built.charged, ...(built.buildId ? { build_id: built.buildId } : {}) };
1906
+ }
1907
+ if (report.grade)
1908
+ fields["grade"] = report.grade;
1909
+ if (keep)
1910
+ fields["kept"] = kept;
1911
+ Object.assign(fields, replayFields(started));
1912
+ await yieldToSignal();
1913
+ const env = { ...okEnvelope(ctx.cmd.name, fields, next), ok: exit === 0 };
1914
+ const failure = runError(report, exit);
1915
+ if (failure)
1916
+ env.error = failure;
1917
+ const keptLine = kept.length
1918
+ ? [` --keep: ${kept.length} sandbox${kept.length === 1 ? "" : "es"} left running (${kept.join(", ")}), billed until killed or --timeout`]
1919
+ : [];
1920
+ const grade = gradeLine(report.grade);
1921
+ const printed = !streaming && ctx.mode === "text" ? commandOutputLines(report, outputs) : [];
1922
+ const lines = [
1923
+ ...printed,
1924
+ settled ? runHeadline(report, opts.count) : runHeadline(report, opts.count).replace(/ · charged (\$[0-9.]+)$/, " · charged $1 so far"),
1925
+ ...(settled ? [] : [` cost not final: ${settledWhy(report, keep)} (${sh `promigence run report ${runId}`} has the final amount)`]),
1926
+ ...(built !== undefined && built.charged > 0 ? [` snapshot build: ${moneyUsd(built.charged)} (${built.buildId ?? built.snapshotId}), within --cap`] : []),
1927
+ ...(bool(ctx, "--verbose") ? [` ${forkLine(report.fork)}`, ` ${totalsLine(report.totals)}`] : []),
1928
+ ...(grade ? [` ${grade}`] : []),
1929
+ ...gradeRowLines(report),
1930
+ ...failureLines(report, outputs, !streaming),
1931
+ ...limitPauseLines(report),
1932
+ ...keptLine,
1933
+ ...(exit === 0 ? [] : [` exit ${exit} (--fail-on ${failOn})`]),
1934
+ ];
1935
+ return emit(ctx.mode, env, lines, exit, ctx.pointer, streaming ? "stderr" : "payload");
1936
+ };
1937
+ function gradeFields(s) {
1938
+ const r = s;
1939
+ const out = {};
1940
+ for (const k of ["graded", "passed", "score", "holdout_passed", "holdout_score", "grade"]) {
1941
+ if (r[k] !== undefined && r[k] !== null)
1942
+ out[k] = r[k];
1943
+ }
1944
+ return out;
1945
+ }
1946
+ export const GRADES_SHOWN = 10;
1947
+ export function gradeRowLines(report) {
1948
+ const graded = (report.sandboxes ?? []).filter((s) => s.graded === true && (s.passed !== undefined || s.holdout_passed !== undefined));
1949
+ const lines = graded.slice(0, GRADES_SHOWN).map((s) => ` ${s.sandbox_id}: grade: ${sandboxGrade(s)}`);
1950
+ if (graded.length > GRADES_SHOWN) {
1951
+ lines.push(` … ${graded.length - GRADES_SHOWN} more graded (${sh `promigence run report ${report.run_id} --sandboxes all`})`);
1952
+ }
1953
+ return lines;
1954
+ }
1955
+ export function commandOutputLines(report, outputs) {
1956
+ const lines = [];
1957
+ let cut = 0;
1958
+ for (const s of report.sandboxes ?? []) {
1959
+ const o = outputs.get(s.sandbox_id);
1960
+ if (!o)
1961
+ continue;
1962
+ if (o.truncated)
1963
+ cut += 1;
1964
+ lines.push(...tagLines(s.sandbox_id, o.stdout));
1965
+ }
1966
+ const ran = (report.sandboxes ?? []).filter((s) => s.execs?.length).length;
1967
+ const unread = ran - outputs.size;
1968
+ if (cut > 0 || unread > 0) {
1969
+ const first = (report.sandboxes ?? []).find((s) => s.execs?.length);
1970
+ const eid = first?.execs?.at(-1)?.exec_id;
1971
+ const how = eid && first ? sh `promigence exec output ${eid} --sandbox ${first.sandbox_id}` : sh `promigence run report ${report.run_id}`;
1972
+ const what = [cut > 0 ? `${cut} cut to the last ${OUTPUT_TAIL_CHARS} characters` : "", unread > 0 ? `${unread} more not shown` : ""].filter(Boolean).join(", ");
1973
+ lines.push(` (output: ${what}; one sandbox's in full: ${how})`);
1974
+ }
1975
+ return lines;
1976
+ }
1977
+ export const SETTLE_WAIT_MS = 15_000;
1978
+ const SETTLE_POLL_MS = 250;
1979
+ const allSettled = (report) => (report.sandboxes ?? []).every((s) => s.state === "ended");
1980
+ const LIMIT_PAUSES = { cap_paused: "spend_cap", credits_paused: "credits_exhausted" };
1981
+ export const limitPauseOf = (s) => s.state === "paused" ? LIMIT_PAUSES[s.failure?.code ?? ""] : undefined;
1982
+ async function settledReport(c, runId, keep, budgetMs) {
1983
+ const until = Date.now() + Math.min(budgetMs, SETTLE_WAIT_MS);
1984
+ for (;;) {
1985
+ const report = await c.runs.report(runId);
1986
+ if (allSettled(report))
1987
+ return { report, settled: true };
1988
+ if (keep)
1989
+ return { report, settled: false };
1990
+ if ((report.sandboxes ?? []).every((s) => s.state === "ended" || limitPauseOf(s)))
1991
+ return { report, settled: false };
1992
+ if (Date.now() + SETTLE_POLL_MS > until)
1993
+ return { report, settled: false };
1994
+ await new Promise((r) => setTimeout(r, SETTLE_POLL_MS));
1995
+ }
1996
+ }
1997
+ function settledWhy(report, keep) {
1998
+ const open = (report.sandboxes ?? []).filter((s) => s.state !== "ended");
1999
+ if (keep)
2000
+ return "kept sandboxes are still running and billing until they are killed or time out";
2001
+ const paused = open.filter((s) => limitPauseOf(s)).length;
2002
+ if (paused && paused === open.length) {
2003
+ return `${paused} sandbox${paused === 1 ? " is" : "es are"} paused at a limit (paused time is not billed); ${paused === 1 ? "its" : "their"} last charge lands when ${paused === 1 ? "it ends" : "they end"}`;
2004
+ }
2005
+ return `${open.length} sandbox${open.length === 1 ? " has" : "es have"} not ended yet, so ${open.length === 1 ? "its" : "their"} last charge is still to land`;
2006
+ }
2007
+ const OUTCOME_EXIT = {
2008
+ env_failed: 3, grader_failed: 3, not_started: 12, cap_killed: 12, expired: 12, stopped_by_support: 12,
2009
+ task_failed: 4, snapshot_failed: 4,
2010
+ };
2011
+ const OUTCOME_CODE = {
2012
+ env_failed: "env_failed", grader_failed: "grader_failed", not_started: "not_started", cap_killed: "cap_killed",
2013
+ expired: "expired", stopped_by_support: "stopped_by_support", task_failed: "nonzero_exit", snapshot_failed: "snapshot_failed",
2014
+ };
2015
+ function failureFix(runId, s) {
2016
+ const f = s.failure;
2017
+ if (f?.fix)
2018
+ return f.fix;
2019
+ const code = f?.code ?? OUTCOME_CODE[s.outcome ?? ""] ?? "";
2020
+ const execId = s.execs?.at(-1)?.exec_id;
2021
+ const filled = (lookupError(code)?.fix ?? "")
2022
+ .replaceAll("`promigence exec output {exec_id}`", execId ? `\`${sh `promigence exec output ${execId} --sandbox ${s.sandbox_id}`}\`` : "{exec_id}")
2023
+ .replaceAll("{run_id}", runId).replaceAll("{sandbox_id}", s.sandbox_id);
2024
+ return filled && !/\{[a-z_]+\}/.test(filled)
2025
+ ? filled
2026
+ : `Read this sandbox's evidence: \`${sh `promigence run report ${runId} --sandbox ${s.sandbox_id}`}\`.`;
2027
+ }
2028
+ export function runError(report, exit) {
2029
+ if (exit === 0)
2030
+ return undefined;
2031
+ const rows = report.sandboxes ?? [];
2032
+ const s = rows.find((x) => OUTCOME_EXIT[x.outcome ?? ""] === exit) ?? rows.find((x) => x.failure);
2033
+ if (!s) {
2034
+ return envError("internal", `run ${report.run_id} exited ${exit} with no sandbox to show for it`, `Read the report: ${sh `promigence run report ${report.run_id}`}.`);
2035
+ }
2036
+ const f = s.failure;
2037
+ const code = f?.code ?? OUTCOME_CODE[s.outcome ?? ""] ?? "internal";
2038
+ const exitCode = s.execs?.at(-1)?.exit_code;
2039
+ const message = f?.message
2040
+ ?? `${s.sandbox_id} ${s.outcome}${f?.code ? ` (${f.code})` : ""}${typeof exitCode === "number" ? `: the command exited ${exitCode}` : ""}`;
2041
+ return envError(code, message, failureFix(report.run_id, s));
2042
+ }
2043
+ export function runHeadline(report, count) {
2044
+ const t = (report.totals ?? {});
2045
+ const others = Object.entries(t)
2046
+ .filter(([k, v]) => !["completed", "env_verified", "evidence_missing"].includes(k) && typeof v === "number" && v > 0)
2047
+ .map(([k, v]) => `${v} ${k === "stopped_by_support" ? "stopped by Promigence support" : k}`);
2048
+ const done = typeof t["completed"] === "number" ? t["completed"] : 0;
2049
+ return `run ${report.run_id}: ${done} of ${count} completed${others.length ? ` · ${others.join(" · ")}` : ""}` +
2050
+ `${report.cost ? ` · charged ${moneyUsd(report.cost.charged)}` : ""}`;
2051
+ }
2052
+ export const FAILURES_SHOWN = 5;
2053
+ export function limitPauseLines(report) {
2054
+ const lines = [];
2055
+ const paused = (report.sandboxes ?? []).filter((x) => limitPauseOf(x));
2056
+ for (const s of paused.slice(0, FAILURES_SHOWN)) {
2057
+ const cap = s.failure?.evidence?.["spend_cap"] ?? report.spend_cap;
2058
+ const usd = typeof cap === "number" ? moneyUsd(cap) : undefined;
2059
+ if (limitPauseOf(s) === "spend_cap") {
2060
+ lines.push(` ${s.sandbox_id}: paused: spend cap reached${usd ? ` (${usd})` : ""}; its files and state are kept`);
2061
+ lines.push(` go on: ${sh `promigence sandbox resume ${s.sandbox_id} --cap USD`}${usd ? ` (above the run's ${usd})` : ""}, or end it: ${sh `promigence sandbox kill ${s.sandbox_id}`}`);
2062
+ }
2063
+ else {
2064
+ lines.push(` ${s.sandbox_id}: paused: the organization's credits ran out; its files and state are kept`);
2065
+ lines.push(` go on: add a card (owners: promigence billing card), then ${sh `promigence sandbox resume ${s.sandbox_id}`}, or end it: ${sh `promigence sandbox kill ${s.sandbox_id}`}`);
2066
+ }
2067
+ }
2068
+ if (paused.length > FAILURES_SHOWN) {
2069
+ lines.push(` … ${paused.length - FAILURES_SHOWN} more paused the same way: ${sh `promigence sandbox list --state paused`}`);
2070
+ }
2071
+ return lines;
2072
+ }
2073
+ export function failureLines(report, outputs, stderrTail) {
2074
+ const failed = (report.sandboxes ?? []).filter((x) => x.state === "ended" && x.outcome !== "completed" && x.outcome !== "superseded");
2075
+ const lines = [];
2076
+ for (const s of failed.slice(0, FAILURES_SHOWN)) {
2077
+ const f = s.failure;
2078
+ const ex = s.execs?.at(-1);
2079
+ const billed = s.billable_seconds === undefined ? "" : ` · ${s.billable_seconds}s ${moneyUsd(s.cost ?? 0)}`;
2080
+ lines.push(` ${s.sandbox_id}: ${s.outcome}${f?.code ? ` (${f.code})` : ""}${typeof ex?.exit_code === "number" ? ` · exit ${ex.exit_code}` : ""}${billed}`);
2081
+ if (f?.message)
2082
+ lines.push(` ${f.message}`);
2083
+ if (stderrTail) {
2084
+ const err = (outputs.get(s.sandbox_id)?.stderr ?? "").replace(/\n+$/, "");
2085
+ if (err)
2086
+ for (const l of err.split("\n").slice(-3))
2087
+ lines.push(` stderr: ${l}`);
2088
+ }
2089
+ lines.push(` fix: ${failureFix(report.run_id, s)}`);
2090
+ if (ex?.exec_id)
2091
+ lines.push(` next: ${sh `promigence exec output ${ex.exec_id} --sandbox ${s.sandbox_id} --stderr`}`);
2092
+ }
2093
+ if (failed.length > FAILURES_SHOWN) {
2094
+ lines.push(` … ${failed.length - FAILURES_SHOWN} more: ${sh `promigence run report ${report.run_id} --sandboxes failed`}`);
2095
+ }
2096
+ return lines;
2097
+ }
2098
+ export const OUTPUT_TAIL_SANDBOXES = 20;
2099
+ export const OUTPUT_TAIL_CHARS = 4096;
2100
+ export async function outputTails(c, sandboxes) {
2101
+ const out = new Map();
2102
+ const ran = sandboxes.filter((s) => s.execs?.length).slice(0, OUTPUT_TAIL_SANDBOXES);
2103
+ await Promise.all(ran.map(async (s) => {
2104
+ const eid = s.execs[s.execs.length - 1].exec_id;
2105
+ try {
2106
+ const rec = await c.sandboxes.get(s.sandbox_id);
2107
+ const o = (rec?.execs ?? []).find((e) => e.exec_id === eid);
2108
+ if (!o)
2109
+ return;
2110
+ const tail = (t) => (t ?? "").slice(-OUTPUT_TAIL_CHARS);
2111
+ out.set(s.sandbox_id, {
2112
+ stdout: tail(o.stdout), stderr: tail(o.stderr),
2113
+ truncated: Boolean(o.truncated) || (o.stdout ?? "").length > OUTPUT_TAIL_CHARS || (o.stderr ?? "").length > OUTPUT_TAIL_CHARS,
2114
+ });
2115
+ }
2116
+ catch {
2117
+ }
2118
+ }));
2119
+ return out;
2120
+ }
2121
+ export function positional(ctx, what, example) {
2122
+ const id = ctx.args[0];
2123
+ if (!id)
2124
+ throw usageError(`${ctx.cmd.name} needs ${what}`, `Example: ${example}.`);
2125
+ return id;
2126
+ }
2127
+ const runReport = async (ctx) => {
2128
+ const runId = positional(ctx, "a run id", "promigence run report run_01 --json");
2129
+ const which = str(ctx, "--sandboxes");
2130
+ if (which !== undefined && which !== "all" && which !== "failed") {
2131
+ throw usageError(`--sandboxes: '${which}' is not a selection`, "Pass --sandboxes all | failed (omit it for the summary plus what needs attention).");
2132
+ }
2133
+ const limitRaw = str(ctx, "--limit");
2134
+ const view = {
2135
+ sandboxes: (which ?? "default"),
2136
+ limit: limitRaw === undefined ? 50 : Number(limitRaw),
2137
+ };
2138
+ if (!Number.isInteger(view.limit) || view.limit < 0) {
2139
+ throw usageError(`--limit: '${limitRaw}' is not a row count`, "Pass --limit N with N >= 0; --limit 0 prints every sandbox.");
2140
+ }
2141
+ const report = await client(ctx).runs.report(runId);
2142
+ let one = str(ctx, "--sandbox");
2143
+ if (one === undefined && bool(ctx, "--repro")) {
2144
+ const all = report.sandboxes ?? [];
2145
+ if (all.length !== 1) {
2146
+ throw usageError(`--repro is the command for one sandbox, and run ${runId} has ${all.length}`, `Name it: ${sh `promigence run report ${runId} --sandbox SID --repro`} (list them: ${sh `promigence run report ${runId} --sandboxes all`}).`);
2147
+ }
2148
+ one = all[0].sandbox_id;
2149
+ }
2150
+ if (one !== undefined) {
2151
+ const s = (report.sandboxes ?? []).find((x) => x.sandbox_id === one);
2152
+ if (!s) {
2153
+ throw usageError(`${one} is not a sandbox of run ${runId}`, `List them: ${sh `promigence run report ${runId} --sandboxes all`}.`);
2154
+ }
2155
+ const row = s;
2156
+ const repro = reproCommand(report, row);
2157
+ const next = nexts([sh `promigence run replay ${runId} --sandbox ${one}`, "run this sandbox again (env failures only)"]);
2158
+ const lines = [`run ${runId} · sandbox ${one}`, ...sandboxLines(row), ...(bool(ctx, "--repro") ? [` repro: ${repro}`] : [])];
2159
+ const env = okEnvelope(ctx.cmd.name, {
2160
+ run_id: runId, sandbox_id: one, sandbox: s, repro,
2161
+ }, next);
2162
+ return emit(ctx.mode, env, lines, 0, ctx.pointer);
2163
+ }
2164
+ const next = nexts([sh `promigence run status ${runId}`, "the run's current state"]);
2165
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, { run_id: runId, report }, next), reportLines(report, view), 0, ctx.pointer);
2166
+ };
2167
+ export function supportStoppedLines(sandboxes) {
2168
+ const stopped = sandboxes.filter((s) => s.outcome === "stopped_by_support");
2169
+ if (!stopped.length)
2170
+ return [];
2171
+ return [` ${stopped.length} stopped by Promigence support; ${sh `promigence sandbox info ${stopped[0].sandbox_id}`} says why`];
2172
+ }
2173
+ const runStatus = async (ctx) => {
2174
+ const runId = positional(ctx, "a run id", "promigence run status run_01 --wait");
2175
+ const c = client(ctx);
2176
+ let run = await c.runs.get(runId);
2177
+ if (bool(ctx, "--wait")) {
2178
+ const bound = waitBound(ctx);
2179
+ const waited = await c.runs.waitDone(runId, { waitTimeoutMs: bound });
2180
+ if (waited.timedOut)
2181
+ return waitExpired(ctx, runId, bound, waited.run.sandboxes ?? []);
2182
+ run = waited.run;
2183
+ }
2184
+ const states = {};
2185
+ for (const s of run.sandboxes ?? [])
2186
+ states[s.state] = (states[s.state] ?? 0) + 1;
2187
+ const next = nexts([sh `promigence run report ${runId} --json`, "the validity report"]);
2188
+ const tail = Object.keys(states).length ? ` - ${Object.entries(states).map(([k, v]) => `${k} ${v}`).join(" - ")}` : "";
2189
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, { run_id: runId, state: run.state, sandbox_states: states, run }, next), [`run ${runId}: ${run.state}${tail}`, ...supportStoppedLines(run.sandboxes ?? []), ...queuedLines(run.sandboxes ?? [])], 0, ctx.pointer);
2190
+ };
2191
+ const runKill = async (ctx) => {
2192
+ const runId = positional(ctx, "a run id", "promigence run kill run_01");
2193
+ await client(ctx).runs.kill(runId);
2194
+ const next = nexts([sh `promigence run report ${runId} --json`, "what the sandboxes did before the kill"]);
2195
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, { run_id: runId, killed: true }, next), [`run ${runId}: killed`], 0, ctx.pointer);
2196
+ };
2197
+ const runCancel = async (ctx) => {
2198
+ const runId = positional(ctx, "a run id", "promigence run cancel run_01");
2199
+ await client(ctx).runs.cancel(runId);
2200
+ const next = nexts([sh `promigence run kill ${runId}`, "end the sandboxes that already started as well"], [sh `promigence run report ${runId} --json`, "the validity report"]);
2201
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, { run_id: runId, cancelled: true, started_keep_running: true }, next), [
2202
+ `run ${runId}: queued starts cancelled`,
2203
+ ` sandboxes that already started keep running (and billing) until they end; ${sh `promigence run kill ${runId}`} ends them too`,
2204
+ ], 0, ctx.pointer);
2205
+ };
2206
+ const sandboxKill = async (ctx) => {
2207
+ const sid = positional(ctx, "a sandbox id", "promigence sandbox kill sbx_01");
2208
+ const c = client(ctx);
2209
+ const res = (await c.sandboxes.kill(sid) ?? {});
2210
+ const runId = await c.sandboxes.get(sid).then((x) => x.run_id).catch(() => undefined);
2211
+ const next = runId
2212
+ ? nexts([sh `promigence run report ${runId} --json`, "the validity report for the sandbox's run"])
2213
+ : nexts([sh `promigence sandbox info ${sid}`, "how it ended"]);
2214
+ const already = res.already_ended === true;
2215
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, {
2216
+ sandbox_id: sid, killed: !already, ...(already ? { already_ended: true } : {}), ...(res.outcome ? { outcome: res.outcome } : {}),
2217
+ }, next), [already ? `sandbox ${sid}: had already ended${res.outcome ? ` (${res.outcome})` : ""}; nothing to kill` : `sandbox ${sid}: killed`], 0, ctx.pointer);
2218
+ };
2219
+ const sandboxNetwork = async (ctx) => {
2220
+ const sid = positional(ctx, "a sandbox id", "promigence sandbox network sbx_01");
2221
+ if (bool(ctx, "--cut")) {
2222
+ const cut = await client(ctx).sandboxes.setNetworkNone(sid);
2223
+ const lines = cut.applied
2224
+ ? [`sandbox ${sid}: egress cut`]
2225
+ : [`sandbox ${sid}: egress was already cut`];
2226
+ lines.push(" nothing it runs from here can reach the network");
2227
+ const next = nexts(["promigence sandbox network " + sid, "what it may reach, and what it was refused"]);
2228
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, cut, next), lines, 0, ctx.pointer);
2229
+ }
2230
+ const v = await client(ctx).sandboxes.network(sid);
2231
+ const mode = typeof v.network === "string" ? v.network : `only ${v.network.allow.join(", ")}`;
2232
+ const lines = [`sandbox ${sid}: ${mode}`];
2233
+ const refused = v.refused_hosts ?? [];
2234
+ for (const r of refused) {
2235
+ lines.push(` refused ${r.host}${r.count ? ` (${r.count} attempts)` : ""}`);
2236
+ }
2237
+ const toAddresses = Number(v["refused_connections"] ?? 0) || 0;
2238
+ if (toAddresses > 0) {
2239
+ lines.push(` refused ${toAddresses} connection${toAddresses === 1 ? "" : "s"} to an address with no name (counted; addresses are not recorded)`);
2240
+ }
2241
+ if (refused.length) {
2242
+ lines.push(`add one with --allow HOST when you start the sandbox`);
2243
+ }
2244
+ else if (typeof v.network !== "string" && toAddresses === 0) {
2245
+ lines.push(" nothing has been refused");
2246
+ }
2247
+ for (const b of v.via_secret ?? []) {
2248
+ lines.push(` via secret ${b.secret}: ${b.hosts.join(", ") || "(no hosts)"}`);
2249
+ }
2250
+ const next = nexts([sh `promigence sandbox info ${sid}`, "the sandbox, its run and the commands it ran"]);
2251
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, v, next), lines, 0, ctx.pointer);
2252
+ };
2253
+ const usd = (n) => `$${n.toFixed(n < 0.01 && n > 0 ? 4 : 2)}`;
2254
+ const rate = (n) => `$${Number(n.toFixed(9))}`;
2255
+ function bytes(n) {
2256
+ if (n < 1024)
2257
+ return `${n} B`;
2258
+ const u = ["KiB", "MiB", "GiB", "TiB"];
2259
+ let v = n / 1024;
2260
+ let i = 0;
2261
+ while (v >= 1024 && i < u.length - 1) {
2262
+ v /= 1024;
2263
+ i++;
2264
+ }
2265
+ return `${v.toFixed(v < 10 ? 1 : 0)} ${u[i]}`;
2266
+ }
2267
+ const runQuote = async (ctx) => {
2268
+ const c = client(ctx);
2269
+ const snapshot = str(ctx, "--snapshot") === undefined ? DEFAULT_SNAPSHOT : await snapshotArg(ctx, c);
2270
+ const count = parseCount(required(ctx, "--count", "Pass --count N (number of sandboxes to fork)."), "--count");
2271
+ const timeout = str(ctx, "--timeout");
2272
+ const tier = str(ctx, "--tier");
2273
+ const cap = str(ctx, "--cap");
2274
+ const q = await c.runs.quote({
2275
+ snapshot,
2276
+ count,
2277
+ ...(timeout ? { timeoutMs: parseDuration(timeout, "--timeout") } : {}),
2278
+ ...(tier ? { tier } : {}),
2279
+ ...(cap ? { spendCap: parseUsd(cap, "--cap") } : {}),
2280
+ });
2281
+ const next = nexts([`${sh `promigence run --snapshot ${snapshot} --count ${count}`}${timeout ? sh ` --timeout ${timeout}` : ""}${tier ? sh ` --tier ${tier}` : ""}${sh ` --cap ${q.max_cost || 1}`} -- CMD`, "run it, capped at the quote"], [`promigence me`, "what you have spent so far"]);
2282
+ const lines = [
2283
+ `quote ${usd(q.max_cost)} — ${q.count} x ${q.seconds_per_sandbox}s x ${rate(q.per_second_usd)}/s (tier ${q.tier}: ${q.vcpus} vCPU, ${q.mem_gib} GiB; ${rate(q.vcpu_second_usd)}/vCPU-s + ${rate(q.gib_second_usd)}/GiB-s)`,
2284
+ ...q.breakdown
2285
+ .filter((b) => q.breakdown.length > 1)
2286
+ .map((b) => ` ${b.sandbox_count} x ${b.seconds_per_sandbox}s x ${rate(b.per_second_usd)}/s = ${usd(b.sandbox_count * b.seconds_per_sandbox * b.per_second_usd)}`),
2287
+ ...q.warnings.map((w) => ` warning: ${w}`),
2288
+ ];
2289
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, { quote: q, max_cost: q.max_cost, per_second_usd: q.per_second_usd, seconds_per_sandbox: q.seconds_per_sandbox, breakdown: q.breakdown, warnings: q.warnings }, next), lines, 0, ctx.pointer);
2290
+ };
2291
+ const runReplay = async (ctx) => {
2292
+ const runId = positional(ctx, "a run id", "promigence run replay run_01 --sandbox sbx_01");
2293
+ const only = list(ctx, "--sandbox");
2294
+ const env = {};
2295
+ for (const kv of list(ctx, "--env")) {
2296
+ const eq = kv.indexOf("=");
2297
+ if (eq <= 0)
2298
+ throw usageError(`--env '${kv}' is not K=V`, "Pass --env NAME=value (repeatable).");
2299
+ env[kv.slice(0, eq)] = kv.slice(eq + 1);
2300
+ }
2301
+ const r = await client(ctx).runs.replay(runId, only, Object.keys(env).length ? { env } : {});
2302
+ const next = r.replayed === 0
2303
+ ? nexts([sh `promigence run report ${runId} --json`, "the report: only env failures (ours) are replayable"])
2304
+ : nexts([sh `promigence run status ${runId} --wait`, "wait for the replayed sandboxes"], [sh `promigence run report ${runId} --json`, "fork.replay carries them; the headline numbers do not"]);
2305
+ const lines = r.replayed === 0
2306
+ ? [`run ${runId}: nothing to replay${r.detail ? ` (${r.detail})` : ""}`]
2307
+ : [
2308
+ `run ${runId}: replayed ${r.replayed} env failure${r.replayed === 1 ? "" : "s"}`,
2309
+ ...r.sandboxes.map((s) => ` ${s.sandbox_id} replays ${s.replay_of}`),
2310
+ ];
2311
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, {
2312
+ run_id: runId, replayed: r.replayed, sandboxes: r.sandboxes,
2313
+ ...(r.replayed === 0 ? { reason: r.detail ?? "no env_failed or preempted sandboxes to replay" } : {}),
2314
+ }, next), lines, 0, ctx.pointer);
2315
+ };
2316
+ const runOpen = async (ctx) => {
2317
+ const runId = positional(ctx, "a run id", "promigence run open run_01");
2318
+ const sandboxId = str(ctx, "--sandbox");
2319
+ const cap = str(ctx, "--cap");
2320
+ const timeout = str(ctx, "--timeout");
2321
+ const r = await client(ctx).runs.open(runId, {
2322
+ ...(sandboxId ? { sandboxId } : {}),
2323
+ ...(cap ? { spendCap: parseUsd(cap, "--cap") } : {}),
2324
+ ...(timeout ? { timeoutMs: parseDuration(timeout, "--timeout") } : {}),
2325
+ });
2326
+ const next = nexts([sh `promigence exec --sandbox ${r.sandbox_id} -- ls -la`, "look at what it left behind"], [sh `promigence sandbox kill ${r.sandbox_id}`, "stop it — it is billing from now"]);
2327
+ const lines = [
2328
+ `sandbox ${r.sandbox_id} ready${r.ready_ms === undefined ? "" : ` ${Math.round(r.ready_ms)} ms`}`,
2329
+ ` a new sandbox, forked from ${r.opened_from} as it was when it failed`,
2330
+ ` failed at ${r.failed_at}, ${r.outcome}${r.code ? ` (${r.code})` : ""}`,
2331
+ ` preserved until ${r.expires_at}`,
2332
+ ];
2333
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, r, next), lines, 0, ctx.pointer);
2334
+ };
2335
+ const runCaptures = async (ctx) => {
2336
+ const runId = positional(ctx, "a run id", "promigence run captures run_01");
2337
+ const doomed = str(ctx, "--delete");
2338
+ if (doomed !== undefined) {
2339
+ const d = await client(ctx).captures.delete(doomed);
2340
+ const line = d.deleted
2341
+ ? `deleted ${d.snapshot_id}: ${bytes(d.bytes)} freed at ${d.deleted_at}`
2342
+ : `${d.snapshot_id} was already gone`;
2343
+ const after = nexts([sh `promigence run captures ${runId}`, "what is still preserved"]);
2344
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, { run_id: runId, ...d }, after), [line], 0, ctx.pointer);
2345
+ }
2346
+ const r = await client(ctx).runs.captures(runId);
2347
+ const openable = r.captures.filter((x) => x.available !== false);
2348
+ const next = openable.length === 0
2349
+ ? nexts([sh `promigence run report ${runId}`, "the run's report"])
2350
+ : nexts([
2351
+ openable.length === 1 ? sh `promigence run open ${runId}` : sh `promigence run open ${runId} --sandbox ${openable[0].sandbox_id}`,
2352
+ "reopen one as a live sandbox",
2353
+ ]);
2354
+ const lines = r.captures.length === 0
2355
+ ? [`run ${runId}: nothing preserved`]
2356
+ : [
2357
+ `run ${runId}: ${r.captures.length} preserved failure${r.captures.length === 1 ? "" : "s"}`,
2358
+ ...r.captures.map((c) => ` ${c.sandbox_id} ${c.outcome}${c.code ? ` (${c.code})` : ""} ${bytes(c.bytes)} ${c.available === false ? "no longer available (cannot be opened)" : `until ${c.expires_at} at the latest`}`),
2359
+ ];
2360
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, { run_id: runId, captures: r.captures }, next), lines, 0, ctx.pointer);
2361
+ };
2362
+ const snapshotVerify = async (ctx) => {
2363
+ const id = positional(ctx, "a snapshot id or alias", "promigence snapshot verify snap_01");
2364
+ const c = client(ctx);
2365
+ const started = await c.snapshots.verify(id);
2366
+ await write(process.stderr, forTerminal(process.stderr, `verify ${started.snapshot_id}: up to ${usd(started.price)} (${started.seconds}s of tier ${started.tier} at ${rate(started.per_second_usd)}/s) — running \`${started.verify}\` in a fresh fork`) + "\n");
2367
+ const bound = waitBound(ctx);
2368
+ const waited = await c.runs.waitDone(started.run_id, { waitTimeoutMs: bound });
2369
+ if (waited.timedOut)
2370
+ return waitExpired(ctx, started.run_id, bound, waited.run.sandboxes ?? []);
2371
+ const report = await c.runs.report(started.run_id);
2372
+ const t = report.totals;
2373
+ const verified = (t?.completed ?? 0) > 0;
2374
+ const exit = verified ? 0 : (t?.env_failed ?? 0) > 0 ? 3 : 6;
2375
+ const next = nexts([sh `promigence run report ${started.run_id} --json`, "the fork's own validity report"], [sh `promigence snapshot inspect ${started.snapshot_id}`, "the recorded verification"]);
2376
+ const lines = [
2377
+ `snapshot ${started.snapshot_id}: ${verified ? "verified" : exit === 3 ? "not run — the environment failed (ours; the report says what was billed)" : "DID NOT VERIFY"}`,
2378
+ ` \`${started.verify}\` in ${started.workspace} · charged ${usd(report.cost?.charged ?? 0)}`,
2379
+ ...reportSummaryLines(report).slice(1),
2380
+ ];
2381
+ const env = okEnvelope(ctx.cmd.name, {
2382
+ snapshot_id: started.snapshot_id, run_id: started.run_id, verified,
2383
+ verify: started.verify, price: started.price, report,
2384
+ }, next);
2385
+ env.ok = exit === 0;
2386
+ return emit(ctx.mode, env, lines, exit, ctx.pointer);
2387
+ };
2388
+ function localTar(args, what) {
2389
+ const r = spawnSync("tar", args, { encoding: "utf8", env: { ...process.env, COPYFILE_DISABLE: "1" } });
2390
+ if (r.error && r.error.code === "ENOENT") {
2391
+ throw usageError("`tar` is not on PATH, and a directory transfer needs one locally", "Install tar (macOS and every Linux ship one), or move the files one at a time without --recursive.");
2392
+ }
2393
+ if (r.status !== 0) {
2394
+ throw usageError(`${what} failed: ${(r.stderr ?? "").trim() || `tar exited ${r.status}`}`, "Check the path and that you can read/write it.");
2395
+ }
2396
+ }
2397
+ function filesArgs(ctx) {
2398
+ const sid = required(ctx, "--sandbox", "Pass --sandbox sbx_<id> (from `promigence sandbox list`).");
2399
+ const [src, dst] = [ctx.args[0], ctx.args[1]];
2400
+ if (!src || !dst) {
2401
+ throw usageError(`${ctx.cmd.name} needs SRC and DST`, `Example: promigence ${ctx.cmd.name} --sandbox ${shellArg(sid)} ${ctx.cmd.name === "files put" ? "./fixture.json /tmp/fixture.json" : "/tmp/trace.json ./trace.json"}.`);
2402
+ }
2403
+ return { sid, src, dst };
2404
+ }
2405
+ const filesPut = async (ctx) => {
2406
+ if (bool(ctx, "--recursive"))
2407
+ return filesPutDir(ctx);
2408
+ const { sid, src, dst } = filesArgs(ctx);
2409
+ if (!dst.startsWith("/"))
2410
+ throw usageError(`DST ${dst} must be absolute inside the sandbox`, "Example: /tmp/fixture.json.");
2411
+ const r = await withLocalFile(src, (source) => client(ctx).sandboxes.files.putFrom(sid, dst, source, source.mode === undefined ? {} : { mode: source.mode }));
2412
+ const next = nexts([sh `promigence exec --sandbox ${sid} -- ls -l ${dst}`, "see it in the sandbox"]);
2413
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, { sandbox_id: sid, src, dst, bytes: r.bytes, chunks: r.chunks, mode: r.mode }, next), [`${src} -> ${sid}:${dst} (${bytes(r.bytes)}${r.chunks > 1 ? `, ${r.chunks} chunks` : ""})`], 0, ctx.pointer);
2414
+ };
2415
+ async function withLocalFile(path, fn) {
2416
+ let fd;
2417
+ let size;
2418
+ let mode;
2419
+ try {
2420
+ fd = openSync(path, "r");
2421
+ const st = fstatSync(fd);
2422
+ if (st.isDirectory()) {
2423
+ closeSync(fd);
2424
+ throw Object.assign(new Error("it is a directory"), { code: "EISDIR" });
2425
+ }
2426
+ size = st.isFile() ? st.size : -1;
2427
+ mode = st.isFile() && process.platform !== "win32" ? st.mode & 0o777 : undefined;
2428
+ }
2429
+ catch (e) {
2430
+ const code = e.code;
2431
+ throw usageError(`cannot read ${path}: ${code === "EISDIR" ? "it is a directory" : e.message}`, code === "EISDIR"
2432
+ ? "Pass --recursive to copy a directory."
2433
+ : "Give a readable local path as SRC; DST is the absolute path inside the sandbox.");
2434
+ }
2435
+ try {
2436
+ if (size < 0) {
2437
+ const all = readFileSync(fd);
2438
+ return await fn({ size: all.length, read: (o, l) => all.subarray(o, o + l) });
2439
+ }
2440
+ const buf = Buffer.alloc(FILE_PUT_CHUNK_BYTES);
2441
+ return await fn({
2442
+ size,
2443
+ mode,
2444
+ read: (offset, len) => {
2445
+ const want = Math.min(len, buf.length);
2446
+ let got = 0;
2447
+ while (got < want) {
2448
+ const n = readSync(fd, buf, got, want - got, offset + got);
2449
+ if (n === 0)
2450
+ break;
2451
+ got += n;
2452
+ }
2453
+ return buf.subarray(0, got);
2454
+ },
2455
+ });
2456
+ }
2457
+ finally {
2458
+ closeSync(fd);
2459
+ }
2460
+ }
2461
+ export function filesPutTarArgs(out, parent, base, noMetadata = localTarNoMetadataArgs()) {
2462
+ return ["-c", "-f", out, ...noMetadata, "-C", parent, base];
2463
+ }
2464
+ const filesPutDir = async (ctx) => {
2465
+ const { sid, src, dst } = filesArgs(ctx);
2466
+ if (!dst.startsWith("/"))
2467
+ throw usageError(`DST ${dst} must be absolute inside the sandbox`, "Example: /tmp/data.");
2468
+ if (!existsSync(src))
2469
+ throw usageError(`cannot read ${src}`, "SRC is a local directory when --recursive is given.");
2470
+ const tmp = join(mkdtempSync(join(tmpdir(), "promigence-put-")), "dir.tar");
2471
+ const parent = dirname(resolve(src));
2472
+ const base = basename(resolve(src));
2473
+ localTar(filesPutTarArgs(tmp, parent, base), `tarring ${src}`);
2474
+ const bytesIn = statSync(tmp).size;
2475
+ const c = client(ctx);
2476
+ const scratch = `/tmp/.promigence-upload/cli-${Date.now().toString(36)}-${randomBytes(6).toString("hex")}.tar`;
2477
+ await withLocalFile(tmp, (source) => c.sandboxes.files.putFrom(sid, scratch, source));
2478
+ let entries;
2479
+ try {
2480
+ ({ entries } = await c.sandboxes.files.extract(sid, scratch, dst, { strip: 1 }));
2481
+ }
2482
+ finally {
2483
+ await c.sandboxes.files.delete(sid, scratch).catch(() => { });
2484
+ rmSync(dirname(tmp), { recursive: true, force: true });
2485
+ }
2486
+ const next = nexts([sh `promigence files ls ${dst} --sandbox ${sid}`, "what landed"]);
2487
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, { sandbox_id: sid, src, dst, bytes: bytesIn, entries, recursive: true }, next), [`${src}/ -> ${sid}:${dst} (${bytes(bytesIn)}${entries === null ? "" : `, ${entries} entries`})`], 0, ctx.pointer);
2488
+ };
2489
+ const filesGet = async (ctx) => {
2490
+ if (bool(ctx, "--recursive"))
2491
+ return filesGetDir(ctx);
2492
+ if (list(ctx, "--exclude").length) {
2493
+ throw usageError("--exclude needs --recursive", "It leaves paths out of a directory; drop it for one file, or add --recursive.");
2494
+ }
2495
+ const { sid, src, dst } = filesArgs(ctx);
2496
+ if (!src.startsWith("/"))
2497
+ throw usageError(`SRC ${src} must be absolute inside the sandbox`, "Example: /tmp/trace.json.");
2498
+ const c = client(ctx);
2499
+ const stat = await c.sandboxes.files.stat(sid, src);
2500
+ if (stat.stat?.is_dir === true) {
2501
+ throw usageError(`${src} is a directory`, `Pass --recursive: ${sh `promigence files get ${src} ${dst} --sandbox ${sid} --recursive`}.`);
2502
+ }
2503
+ let sink;
2504
+ let wrote = dst;
2505
+ if (dst === "-") {
2506
+ sink = {
2507
+ write: (chunk) => new Promise((resolve) => {
2508
+ if (process.stdout.write(Buffer.from(chunk.buffer, chunk.byteOffset, chunk.length)))
2509
+ resolve();
2510
+ else
2511
+ process.stdout.once("drain", () => resolve());
2512
+ }),
2513
+ done: async () => { },
2514
+ abort: () => { },
2515
+ };
2516
+ }
2517
+ else {
2518
+ const d = openDownload(dst, src.split("/").filter(Boolean).pop());
2519
+ wrote = d.path;
2520
+ sink = d;
2521
+ }
2522
+ let got;
2523
+ try {
2524
+ got = await c.sandboxes.files.download(sid, src, (chunk) => sink.write(chunk));
2525
+ await sink.done();
2526
+ }
2527
+ catch (e) {
2528
+ sink.abort();
2529
+ if (stat.stat?.is_symlink !== true)
2530
+ throw e;
2531
+ throw usageError(`${src} is a symbolic link, which files get cannot copy yet`, `Copy the file it points to: ${sh `promigence exec --sandbox ${sid} -- readlink -f ${src}`} names it.`);
2532
+ }
2533
+ if (dst === "-")
2534
+ return exitWith(0);
2535
+ const next = nexts([sh `promigence files ls ${dirname(src)} --sandbox ${sid}`, "what else is beside it"]);
2536
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, { sandbox_id: sid, src, dst: wrote, bytes: got.bytes, stat: stat.stat }, next), [`${sid}:${src} -> ${wrote} (${bytes(got.bytes)})`], 0, ctx.pointer);
2537
+ };
2538
+ function openDownload(dstIn, srcName) {
2539
+ let dst = dstIn;
2540
+ let st;
2541
+ try {
2542
+ st = lstatSync(dst);
2543
+ }
2544
+ catch {
2545
+ }
2546
+ if (st?.isSymbolicLink()) {
2547
+ throw usageError(`${dst} is a symbolic link; files get does not write through links`, "Remove the link, or name another local path as DST.");
2548
+ }
2549
+ if (st?.isDirectory()) {
2550
+ if (srcName === undefined)
2551
+ throw usageError(`${dst} is a directory`, "Name a file path as DST, e.g. ./out/trace.json.");
2552
+ dst = join(dst, srcName);
2553
+ let inner;
2554
+ try {
2555
+ inner = lstatSync(dst);
2556
+ }
2557
+ catch { }
2558
+ if (inner?.isSymbolicLink()) {
2559
+ throw usageError(`${dst} is a symbolic link; files get does not write through links`, "Remove the link, or name another local path as DST.");
2560
+ }
2561
+ if (inner?.isDirectory())
2562
+ throw usageError(`${dst} is a directory`, "Name a file path as DST.");
2563
+ }
2564
+ const local = (e, what) => {
2565
+ throw usageError(`cannot write ${what}: ${e.message}`, "Check that the directory exists and that you can write to it, or name another DST.");
2566
+ };
2567
+ try {
2568
+ mkdirSync(dirname(dst), { recursive: true });
2569
+ }
2570
+ catch (e) {
2571
+ local(e, dirname(dst));
2572
+ }
2573
+ const partial = join(dirname(dst), `.${basename(dst)}.promigence-partial-${randomBytes(4).toString("hex")}`);
2574
+ const NOFOLLOW = fsConstants["O_NOFOLLOW"] ?? 0;
2575
+ let fd = -1;
2576
+ try {
2577
+ fd = openSync(partial, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | NOFOLLOW, 0o644);
2578
+ }
2579
+ catch (e) {
2580
+ local(e, dst);
2581
+ }
2582
+ const close = () => { if (fd >= 0) {
2583
+ try {
2584
+ closeSync(fd);
2585
+ }
2586
+ catch { }
2587
+ fd = -1;
2588
+ } };
2589
+ return {
2590
+ path: dst,
2591
+ write: async (chunk) => {
2592
+ let at = 0;
2593
+ while (at < chunk.length)
2594
+ at += writeSync(fd, chunk, at, chunk.length - at);
2595
+ },
2596
+ done: async () => {
2597
+ close();
2598
+ try {
2599
+ renameSync(partial, dst);
2600
+ }
2601
+ catch (e) {
2602
+ rmSync(partial, { force: true });
2603
+ local(e, dst);
2604
+ }
2605
+ },
2606
+ abort: () => {
2607
+ close();
2608
+ rmSync(partial, { force: true });
2609
+ },
2610
+ };
2611
+ }
2612
+ const filesGetDir = async (ctx) => {
2613
+ const { sid, src, dst } = filesArgs(ctx);
2614
+ if (!src.startsWith("/"))
2615
+ throw usageError(`SRC ${src} must be absolute inside the sandbox`, "Example: /tmp/data.");
2616
+ if (dst === "-")
2617
+ throw usageError("files get --recursive writes a directory, not a stream", "Name a local directory as DST, or drop --recursive for one file.");
2618
+ const c = client(ctx);
2619
+ const exclude = list(ctx, "--exclude");
2620
+ const made = await c.sandboxes.files.archive(sid, src, { ...(exclude.length ? { exclude } : {}) });
2621
+ const tmp = join(mkdtempSync(join(tmpdir(), "promigence-get-")), "dir.tar");
2622
+ let unpacked;
2623
+ try {
2624
+ const tarOut = openDownload(tmp);
2625
+ try {
2626
+ await c.sandboxes.files.download(sid, made.archive_path, (chunk) => tarOut.write(chunk));
2627
+ await tarOut.done();
2628
+ }
2629
+ catch (e) {
2630
+ tarOut.abort();
2631
+ throw e;
2632
+ }
2633
+ try {
2634
+ unpacked = untarSafe(tmp, dst, { strip: 1, keepLinks: bool(ctx, "--keep-links") });
2635
+ }
2636
+ catch (e) {
2637
+ if (!(e instanceof UnsafeArchiveError))
2638
+ throw e;
2639
+ throw usageError(`files get --recursive refused ${src}: ${e.message}; nothing was written`, `Leave them out with --exclude NAME, or remove them in the sandbox. --keep-links recreates links that point outside ${dst} as they are (hard links, devices and fifos are always refused).`);
2640
+ }
2641
+ }
2642
+ finally {
2643
+ await c.sandboxes.files.delete(sid, made.archive_path).catch(() => { });
2644
+ rmSync(dirname(tmp), { recursive: true, force: true });
2645
+ }
2646
+ const next = nexts([sh `promigence files ls ${src} --sandbox ${sid}`, "what is still in the sandbox"]);
2647
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, {
2648
+ sandbox_id: sid, src, dst, bytes: made.size, recursive: true,
2649
+ files: unpacked.files, dirs: unpacked.dirs, links: unpacked.links,
2650
+ }, next), [`${sid}:${src}/ -> ${dst} (${bytes(made.size)})`], 0, ctx.pointer);
2651
+ };
2652
+ export const HANDLERS = {
2653
+ "auth login": authLogin,
2654
+ "snapshot create": snapshotCreate,
2655
+ "snapshot status": snapshotStatus,
2656
+ "snapshot list": snapshotList,
2657
+ "snapshot inspect": snapshotInspect,
2658
+ fork,
2659
+ exec,
2660
+ run: runComposite,
2661
+ "run report": runReport,
2662
+ "run status": runStatus,
2663
+ "run kill": runKill,
2664
+ "run cancel": runCancel,
2665
+ "run quote": runQuote,
2666
+ "run replay": runReplay,
2667
+ "run open": runOpen,
2668
+ "run captures": runCaptures,
2669
+ "snapshot verify": snapshotVerify,
2670
+ "files put": filesPut,
2671
+ "files get": filesGet,
2672
+ "sandbox kill": sandboxKill,
2673
+ "sandbox network": sandboxNetwork,
2674
+ "secrets set": secretsSet,
2675
+ };