promigence 0.1.0-beta.6 → 0.1.0-beta.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -21,15 +21,13 @@ own (`npm config set prefix ~/.npm-global` and add `~/.npm-global/bin` to your `
21
21
 
22
22
  ## Create your account
23
23
 
24
- Promigence is in private beta: you need an invite code.
25
-
26
24
  ```sh
27
25
  promigence signup
28
26
  ```
29
27
 
30
- It asks for your invite code, then opens your browser to create the account. Your API key is saved
31
- in your system keychain, or in `~/.config/promigence/credentials` (mode 0600) where there is none. On
32
- another machine, use `promigence login`: it signs you in with a code we email you, or your password. In CI or on an
28
+ It opens your browser to create the account. Your API key is saved in your system keychain, or in
29
+ `~/.config/promigence/credentials` (mode 0600) where there is none.
30
+ On another machine, use `promigence login`: it signs you in with a code we email you. In CI or on an
33
31
  agent's machine, set `PROMIGENCE_API_KEY` instead. `promigence doctor` checks the setup.
34
32
 
35
33
  ## First commands
@@ -57,7 +55,7 @@ promigence fork --snapshot py --count 5 --cap 2
57
55
 
58
56
  ## For AI agents
59
57
 
60
- Run `promigence agent-guide` (about 1.2k tokens) before driving the CLI: it covers the model, the
58
+ Run `promigence agent-guide` (about 1.4k tokens) before driving the CLI: it covers the model, the
61
59
  JSON output contract, exit codes and errors. `promigence agent-guide --install` writes the same
62
60
  guidance into your repo for coding agents, and `promigence mcp` serves the CLI as an MCP server.
63
61
  Every command takes `--json`.
package/dist/account.js CHANGED
@@ -4,6 +4,21 @@ import { duration } from "./report.js";
4
4
  import { emit, ms, okEnvelope, overMost, parseCount, parseDuration, parseUsd, usageError } from "./io.js";
5
5
  import { sh } from "./term.js";
6
6
  const usd = (n) => n === undefined ? "—" : `$${n.toFixed(n < 0.01 && n > 0 ? 4 : 2)}`;
7
+ export const PAYMENT_METHOD_NAMES = {
8
+ apple_pay: "Apple Pay",
9
+ google_pay: "Google Pay",
10
+ samsung_pay: "Samsung Pay",
11
+ link: "Link",
12
+ cashapp: "Cash App Pay",
13
+ amazon_pay: "Amazon Pay",
14
+ };
15
+ export function paymentMethodText(card) {
16
+ const key = card?.brand ?? "";
17
+ const named = Object.prototype.hasOwnProperty.call(PAYMENT_METHOD_NAMES, key) ? PAYMENT_METHOD_NAMES[key] : undefined;
18
+ if (named)
19
+ return card?.last4 ? `${named} •••• ${card.last4}` : named;
20
+ return `${card?.brand ?? "card"} ••${card?.last4 ?? "??"}`;
21
+ }
7
22
  function limitOf(ctx, fallback, most) {
8
23
  const raw = str(ctx, "--limit");
9
24
  if (raw === undefined)
@@ -208,6 +223,7 @@ const billingUsage = async (ctx) => {
208
223
  usage,
209
224
  card_on_file: b.card_on_file ?? false,
210
225
  ...(b.card ? { card: b.card } : {}),
226
+ ...(b.card_note ? { card_note: b.card_note } : {}),
211
227
  ...(b.trial ? { trial: b.trial } : {}),
212
228
  ...(b.suspended ? { suspended: true, suspend_reason: b.suspend_reason ?? null } : {}),
213
229
  upcoming_invoice: b.upcoming_invoice ?? null,
@@ -244,7 +260,9 @@ const billingUsage = async (ctx) => {
244
260
  const trial = b.trial;
245
261
  if (trial?.credits_remaining !== undefined)
246
262
  lines.push(` trial credits left: ${usd(trial.credits_remaining)}`);
247
- lines.push(` card: ${b.card_on_file ? `${b.card?.brand ?? "card"} ••${b.card?.last4 ?? "??"}` : `none on file${billingOff ? " (card payments are not open yet)" : ""}`}`);
263
+ lines.push(` card: ${b.card_on_file ? paymentMethodText(b.card) : `none on file${billingOff ? " (card payments are not open yet)" : ""}`}`);
264
+ if (b.card_note)
265
+ lines.push(` ${b.card_note}`);
248
266
  if (b.upcoming_invoice?.amount_due !== undefined && b.upcoming_invoice?.amount_due !== null) {
249
267
  lines.push(` next invoice: ${usd(b.upcoming_invoice.amount_due)}`);
250
268
  }
@@ -267,6 +285,86 @@ const billingPortal = async (ctx) => {
267
285
  const next = nexts(["promigence billing usage", "this month's spend without leaving the terminal"]);
268
286
  return emit(ctx.mode, okEnvelope(ctx.cmd.name, { url: res.url }, next), ["promigence billing portal: open this page to manage the card and past invoices:", ` ${res.url}`], 0, ctx.pointer);
269
287
  };
288
+ const HISTORY_KINDS = ["all", "credits", "payments"];
289
+ const CREDIT_WHAT = {
290
+ free_credit: "Free credit",
291
+ card_bonus: "Card bonus",
292
+ referral: "Referral reward",
293
+ plan: "Included with your plan",
294
+ from_promigence: "Credit from Promigence",
295
+ service_restart: "Credit for a service restart",
296
+ confirmation_credit: "Credit for the $1 confirmation",
297
+ };
298
+ const CREDIT_ENDED = { used: "used", expired: "expired", taken_back: "taken back" };
299
+ const PAYMENT_WHAT = {
300
+ invoice_paid: "Invoice paid",
301
+ payment_failed: "Payment failed",
302
+ refund: "Refund",
303
+ };
304
+ const PAYMENT_STATUS = {
305
+ paid: "paid",
306
+ retrying: "will be retried",
307
+ failed: "not paid",
308
+ refunded: "refunded",
309
+ confirmed: "confirmed",
310
+ };
311
+ const wordFor = (m, k) => k && Object.prototype.hasOwnProperty.call(m, k) ? m[k] : undefined;
312
+ const unpaid = (status) => status === "retrying" || status === "failed";
313
+ export function historyLines(entries) {
314
+ return entries.map((e) => {
315
+ const day = (e.at ?? "").slice(0, 10);
316
+ const c = e.credit;
317
+ if (c) {
318
+ const state = wordFor(CREDIT_ENDED, c.status) ??
319
+ `${usd(c.remaining_usd)} left, ${c.expires_at ? `expires ${c.expires_at.slice(0, 10)}` : "no expiry"}`;
320
+ return ` ${day} ${wordFor(CREDIT_WHAT, c.kind) ?? "Credit"} ${usd(c.granted_usd)} ${state}`;
321
+ }
322
+ const p = e.payment;
323
+ if (!p)
324
+ return ` ${day} (a row this version cannot show: see --json)`;
325
+ const check = p.kind === "method_check";
326
+ const what = check
327
+ ? `$${Number.isInteger(p.amount_usd) ? String(p.amount_usd) : p.amount_usd.toFixed(2)} to confirm ${p.method || "your payment method"}`
328
+ : (wordFor(PAYMENT_WHAT, p.kind) ?? "Payment");
329
+ const detail = [p.plan ? `${p.plan} plan` : null, p.invoice_number ? `invoice ${p.invoice_number}` : null]
330
+ .filter(Boolean)
331
+ .join(", ");
332
+ const status = check && p.status === "failed" ? "failed" : (wordFor(PAYMENT_STATUS, p.status) ?? p.status);
333
+ const url = typeof p.invoice_url === "string" && p.invoice_url.startsWith("https://") ? p.invoice_url : null;
334
+ const link = url && !check ? ` · ${unpaid(p.status) ? "pay at" : "receipt"} ${url}` : "";
335
+ return ` ${day} ${what}${detail ? ` (${detail})` : ""} ${usd(p.amount_usd)} ${status}${link}`;
336
+ });
337
+ }
338
+ const billingHistory = async (ctx) => {
339
+ const kind = str(ctx, "--kind") ?? "all";
340
+ if (!HISTORY_KINDS.includes(kind)) {
341
+ throw usageError(`--kind: '${kind}' is not a kind`, `Pass one of ${HISTORY_KINDS.join(" | ")}.`);
342
+ }
343
+ const limit = limitOf(ctx, 50, 100);
344
+ const cursor = str(ctx, "--cursor");
345
+ const page = await client(ctx).billing.historyPage({
346
+ kind: kind,
347
+ limit,
348
+ ...(cursor === undefined ? {} : { cursor }),
349
+ });
350
+ const entries = page.items;
351
+ const more = page.nextCursor;
352
+ const owed = entries.some((e) => e.payment && e.payment.kind !== "method_check" && unpaid(e.payment.status));
353
+ const next = nexts(...(more
354
+ ? [[`${sh `promigence billing history --limit ${String(limit)}`}${kind === "all" ? "" : sh ` --kind ${kind}`}${sh ` --cursor ${more}`}`, "the next page"]]
355
+ : []), ...(owed
356
+ ? [["promigence billing portal", "pay the open invoice, or update the card on file"]]
357
+ : [["promigence billing usage", "this month's spend and credit"]]));
358
+ const empty = { all: "no credits or payments yet", credits: "no credit yet", payments: "no payments yet" };
359
+ const noun = kind === "all" ? "" : ` ${entries.length === 1 ? kind.slice(0, -1) : kind}`;
360
+ const lines = [
361
+ `promigence billing history: ${entries.length}${noun}, newest first`,
362
+ ...historyLines(entries),
363
+ ...(entries.length ? [] : [` (${empty[kind]})`]),
364
+ ...(more ? [` more: --cursor ${more}`] : []),
365
+ ];
366
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, { kind, entries, count: entries.length, next_cursor: more }, next), lines, 0, ctx.pointer);
367
+ };
270
368
  const referrals = async (ctx) => {
271
369
  const r = await client(ctx).billing.referrals();
272
370
  const next = r.code
@@ -520,6 +618,12 @@ export function sandboxFactLines(s, facts, nowMs = Date.now()) {
520
618
  }
521
619
  return out;
522
620
  }
621
+ export function supportStopLines(s) {
622
+ if (s.outcome !== "stopped_by_support")
623
+ return [];
624
+ const message = s.failure?.message;
625
+ return [` ${typeof message === "string" && message.trim() ? message : "Stopped by Promigence support"}`];
626
+ }
523
627
  const sandboxInfo = async (ctx) => {
524
628
  const sid = positional(ctx, "a sandbox id", "promigence sandbox info sbx_01");
525
629
  const c = client(ctx);
@@ -539,6 +643,7 @@ const sandboxInfo = async (ctx) => {
539
643
  : why ? [] : [[sh `promigence exec --sandbox ${sid} -- CMD`, "run something in it"]]));
540
644
  const lines = [
541
645
  `sandbox ${sid}: ${s.state}${why ? ` (${why})` : ""}${s.outcome ? ` (${s.outcome})` : ""}${s.run_id ? ` · run ${s.run_id}` : ""}`,
646
+ ...supportStopLines(s),
542
647
  ...(why ? [" its files and state are kept, and paused time is not billed"] : []),
543
648
  ...queuedLines([s]),
544
649
  ...(s.snapshot_id ? [` snapshot ${s.snapshot_id}`] : []),
@@ -723,6 +828,7 @@ export const ACCOUNT_HANDLERS = {
723
828
  status,
724
829
  "billing usage": billingUsage,
725
830
  "billing portal": billingPortal,
831
+ "billing history": billingHistory,
726
832
  referrals,
727
833
  "referrals apply": referralsApply,
728
834
  "snapshot delete": snapshotDelete,
package/dist/agent.js CHANGED
@@ -42,9 +42,17 @@ export const SETUP_SCRIPT = [
42
42
  'if [ -n "$3" ] && [ ! -d "$4/.git" ]; then',
43
43
  ' mkdir -p "$(dirname "$4")"',
44
44
  ' git clone --quiet -- "$3" "$4" >&2',
45
+ ' if [ -n "${5:-}" ]; then chown -R -- "$5" "$4"; fi',
45
46
  "fi",
46
47
  '"$1" --version',
47
48
  ].join("\n");
49
+ async function snapshotUser(c, snapshot) {
50
+ if (snapshot === DEFAULT_SNAPSHOT)
51
+ return undefined;
52
+ const rec = await c.snapshots.get(snapshot).catch(() => undefined);
53
+ const u = rec?.user;
54
+ return typeof u === "string" && u !== "" && u !== "root" && u !== "0" ? u : undefined;
55
+ }
48
56
  export function repoDir(url) {
49
57
  const last = url.replace(/[/]+$/, "").split(/[/:]/).pop() ?? "";
50
58
  const name = last.replace(/\.git$/, "").replace(/[^A-Za-z0-9._-]/g, "") || "repo";
@@ -269,8 +277,10 @@ const agentStart = async (ctx) => {
269
277
  throw e;
270
278
  }
271
279
  };
280
+ const runAs = await snapshotUser(c, snapshot);
272
281
  const setup = await endOnThrow(c.sandboxes.exec(sid, {
273
- argv: ["sh", "-c", SETUP_SCRIPT, "agent-setup", agent.bin, agent.pkg, repo ?? "", cwd ?? ""],
282
+ argv: ["sh", "-c", SETUP_SCRIPT, "agent-setup", agent.bin, agent.pkg, repo ?? "", cwd ?? "", runAs ?? ""],
283
+ user: "root",
274
284
  timeoutMs: 10 * 60_000,
275
285
  }));
276
286
  if (setup.exit_code !== 0) {
@@ -99,6 +99,10 @@ export declare function outputTails(c: PromigenceClient, sandboxes: ReadonlyArra
99
99
  }>;
100
100
  }>): Promise<Map<string, OutputTail>>;
101
101
  export declare function positional(ctx: Ctx, what: string, example: string): string;
102
+ export declare function supportStoppedLines(sandboxes: ReadonlyArray<{
103
+ sandbox_id: string;
104
+ outcome?: string | null;
105
+ }>): string[];
102
106
  export declare function filesPutTarArgs(out: string, parent: string, base: string, noMetadata?: string[]): string[];
103
107
  export declare const HANDLERS: Readonly<Record<string, Handler>>;
104
108
  export {};
package/dist/commands.js CHANGED
@@ -6,7 +6,7 @@ import { basename, dirname, join, resolve } from "node:path";
6
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
7
  import { fmtMs, recordTiming } from "./timing.js";
8
8
  import { capacityLine, queuedClause, queuedLines, statusLine } from "./capacity.js";
9
- import { DevcontainerError, detectOne, load, planJson, planLines, planSetup, resolveGiven, snapshotEnv, translate, workspaceRootFor, wrapCommand, } from "./devcontainer.js";
9
+ import { DevcontainerError, detectOne, load, planJson, planLines, planSetup, resolveGiven, snapshotEnv, snapshotUsers, translate, workspaceRootFor, wrapCommand, } from "./devcontainer.js";
10
10
  import { detect, ecosystemOfCommands, imageCreateCommand, readConfig } from "./init.js";
11
11
  import { CREDENTIAL_REMEDY, confirmOverride, credentialRefusal, emitUploadList, localTarNoMetadataArgs } from "./uploadguard.js";
12
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";
@@ -212,7 +212,7 @@ function reportExits(report) {
212
212
  return codes;
213
213
  if (t.env_failed > 0 || (t.grader_failed ?? 0) > 0)
214
214
  codes.push(3);
215
- if (t.not_started > 0 || t.cap_killed > 0 || t.expired > 0)
215
+ if (t.not_started > 0 || t.cap_killed > 0 || t.expired > 0 || (t.stopped_by_support ?? 0) > 0)
216
216
  codes.push(12);
217
217
  if (t.task_failed > 0 || t.snapshot_failed > 0)
218
218
  codes.push(4);
@@ -385,7 +385,7 @@ function boundUseLines(res) {
385
385
  " requests the sandbox sends to this address are authenticated with it:",
386
386
  ...urls.map((u) => ` ${urlLine(u, res.env)}`),
387
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 === false ? [] : [" the host's answers never show the value: an echo service shows asterisks instead"]),
388
+ ...(res.scan_responses === true ? [" the host's answers never show the value: an echo service shows asterisks instead"] : []),
389
389
  ];
390
390
  return lines;
391
391
  }
@@ -723,7 +723,10 @@ async function devcontainerCreate(ctx, plan) {
723
723
  }
724
724
  const setup = planSetup(plan, { repo, commit, userSetup: list(ctx, "--setup") });
725
725
  const env = snapshotEnv(plan);
726
- const opts = { ...source, ...(setup.length ? { setup } : {}), ...(env ? { env } : {}) };
726
+ const users = snapshotUsers(plan);
727
+ const opts = {
728
+ ...source, ...(setup.length ? { setup } : {}), ...(env ? { env } : {}), ...(users ?? {}),
729
+ };
727
730
  const verify = str(ctx, "--verify");
728
731
  if (verify !== undefined)
729
732
  opts.verify = { cmd: wrapCommand(plan, verify) };
@@ -762,7 +765,6 @@ const snapshotCreate = async (ctx) => {
762
765
  const plan = gh ? undefined : devcontainerPlan(ctx, form);
763
766
  const setup = list(ctx, "--setup");
764
767
  const alias = str(ctx, "--alias");
765
- const cpuClass = str(ctx, "--cpu-class");
766
768
  let opts;
767
769
  let label;
768
770
  let submitted;
@@ -833,8 +835,6 @@ const snapshotCreate = async (ctx) => {
833
835
  const c = client(ctx);
834
836
  if (alias !== undefined)
835
837
  opts.alias = alias;
836
- if (cpuClass !== undefined)
837
- opts.cpuClass = cpuClass;
838
838
  const grade = gradeOpts(ctx);
839
839
  if (grade)
840
840
  opts.grade = grade;
@@ -1472,8 +1472,6 @@ const exec = async (ctx) => {
1472
1472
  stdout: res.stdout ?? "",
1473
1473
  stderr: res.stderr ?? "",
1474
1474
  truncated: res.truncated ?? false,
1475
- ...(res.stdout_ref === undefined ? {} : { stdout_ref: res.stdout_ref }),
1476
- ...(res.stderr_ref === undefined ? {} : { stderr_ref: res.stderr_ref }),
1477
1475
  ...(res.stdout_b64 === undefined ? {} : { stdout_b64: res.stdout_b64 }),
1478
1476
  ...(res.stderr_b64 === undefined ? {} : { stderr_b64: res.stderr_b64 }),
1479
1477
  ...(res["idempotent_replay"] === true ? { idempotent_replay: true } : {}),
@@ -1598,7 +1596,7 @@ async function runImageSnapshot(ctx, c) {
1598
1596
  if (cap < price + (quoted ?? 0)) {
1599
1597
  const existing = (quoted ?? 0) <= cap ? await existingImageSnapshot(c, image) : undefined;
1600
1598
  throw new SpendCapError(`a new image's snapshot build costs ${moneyUsd(price)} on top of the run` +
1601
- `${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. Or `) +
1599
+ `${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 `) +
1602
1600
  `${existing === undefined ? "Pass" : "pass"} --cap ${needed.toFixed(2)} or more (a build is charged once, only when this image has no snapshot yet, and reused free after), ` +
1603
1601
  `or build it first, ${sh `promigence snapshot create --image ${image} --wait`}, and run its snapshot with --snapshot.`);
1604
1602
  }
@@ -1633,7 +1631,7 @@ async function existingImageSnapshot(c, image) {
1633
1631
  for (let page = 0; page < 5; page++) {
1634
1632
  const p = await c.snapshots.listPage({ limit: 100, ...(cursor === undefined ? {} : { cursor }) });
1635
1633
  const hit = p.items.find((s) => s.source?.kind === "image" && typeof s.source.digest === "string" && digests.has(s.source.digest) &&
1636
- s.source["from_sandbox"] === undefined && !s.unavailable && !(s.setup?.length) && !s.start && !s.verify?.cmd &&
1634
+ s.source["from_sandbox"] === undefined && !s.unavailable && !(s.setup?.length) && !s.verify?.cmd &&
1637
1635
  (s.tier === undefined || s.tier === "small"));
1638
1636
  if (hit)
1639
1637
  return hit.snapshot_id;
@@ -1967,11 +1965,12 @@ function settledWhy(report, keep) {
1967
1965
  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`;
1968
1966
  }
1969
1967
  const OUTCOME_EXIT = {
1970
- env_failed: 3, grader_failed: 3, not_started: 12, cap_killed: 12, expired: 12, task_failed: 4, snapshot_failed: 4,
1968
+ env_failed: 3, grader_failed: 3, not_started: 12, cap_killed: 12, expired: 12, stopped_by_support: 12,
1969
+ task_failed: 4, snapshot_failed: 4,
1971
1970
  };
1972
1971
  const OUTCOME_CODE = {
1973
1972
  env_failed: "env_failed", grader_failed: "grader_failed", not_started: "not_started", cap_killed: "cap_killed",
1974
- expired: "expired", task_failed: "nonzero_exit", snapshot_failed: "snapshot_failed",
1973
+ expired: "expired", stopped_by_support: "stopped_by_support", task_failed: "nonzero_exit", snapshot_failed: "snapshot_failed",
1975
1974
  };
1976
1975
  function failureFix(runId, s) {
1977
1976
  const f = s.failure;
@@ -2005,7 +2004,7 @@ export function runHeadline(report, count) {
2005
2004
  const t = (report.totals ?? {});
2006
2005
  const others = Object.entries(t)
2007
2006
  .filter(([k, v]) => !["completed", "env_verified", "evidence_missing"].includes(k) && typeof v === "number" && v > 0)
2008
- .map(([k, v]) => `${v} ${k}`);
2007
+ .map(([k, v]) => `${v} ${k === "stopped_by_support" ? "stopped by Promigence support" : k}`);
2009
2008
  const done = typeof t["completed"] === "number" ? t["completed"] : 0;
2010
2009
  return `run ${report.run_id}: ${done} of ${count} completed${others.length ? ` · ${others.join(" · ")}` : ""}` +
2011
2010
  `${report.cost ? ` · charged ${moneyUsd(report.cost.charged)}` : ""}`;
@@ -2125,6 +2124,12 @@ const runReport = async (ctx) => {
2125
2124
  const next = nexts([sh `promigence run status ${runId}`, "the run's current state"]);
2126
2125
  return emit(ctx.mode, okEnvelope(ctx.cmd.name, { run_id: runId, report }, next), reportLines(report, view), 0, ctx.pointer);
2127
2126
  };
2127
+ export function supportStoppedLines(sandboxes) {
2128
+ const stopped = sandboxes.filter((s) => s.outcome === "stopped_by_support");
2129
+ if (!stopped.length)
2130
+ return [];
2131
+ return [` ${stopped.length} stopped by Promigence support; ${sh `promigence sandbox info ${stopped[0].sandbox_id}`} says why`];
2132
+ }
2128
2133
  const runStatus = async (ctx) => {
2129
2134
  const runId = positional(ctx, "a run id", "promigence run status run_01 --wait");
2130
2135
  const c = client(ctx);
@@ -2141,7 +2146,7 @@ const runStatus = async (ctx) => {
2141
2146
  states[s.state] = (states[s.state] ?? 0) + 1;
2142
2147
  const next = nexts([sh `promigence run report ${runId} --json`, "the validity report"]);
2143
2148
  const tail = Object.keys(states).length ? ` - ${Object.entries(states).map(([k, v]) => `${k} ${v}`).join(" - ")}` : "";
2144
- return emit(ctx.mode, okEnvelope(ctx.cmd.name, { run_id: runId, state: run.state, sandbox_states: states, run }, next), [`run ${runId}: ${run.state}${tail}`, ...queuedLines(run.sandboxes ?? [])], 0, ctx.pointer);
2149
+ 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);
2145
2150
  };
2146
2151
  const runKill = async (ctx) => {
2147
2152
  const runId = positional(ctx, "a run id", "promigence run kill run_01");
package/dist/credits.js CHANGED
@@ -150,18 +150,21 @@ export async function claimOnRefusal(ctx, io = {}) {
150
150
  const opened = maybeOpen(ctx, link.url);
151
151
  return [...claimLines(o, link, opened), "Once it is claimed, run the command again."];
152
152
  }
153
- async function cardsOff(c, o) {
153
+ async function cardState(c, o) {
154
+ const none = { off: false, onFile: false };
154
155
  if (!(typeof o.card_bonus_usd === "number" && o.card_bonus_usd > 0) || o.state === "none")
155
- return false;
156
- return c.billing.usage().then((b) => b.billing_enabled === false).catch(() => false);
156
+ return none;
157
+ return c.billing.usage().then((b) => ({ off: b.billing_enabled === false, onFile: b.card_on_file === true })).catch(() => none);
157
158
  }
158
- function offerLines(head, o, off = false) {
159
+ function offerLines(head, o, st = { off: false, onFile: false }) {
159
160
  const lines = [`${head}: ${say(o)}`];
160
161
  if (o.state !== "none") {
161
162
  if (typeof o.credit_usd === "number")
162
163
  lines.push(` credit: ${dollars(o.credit_usd)}`);
163
164
  if (typeof o.card_bonus_usd === "number" && o.card_bonus_usd > 0) {
164
- lines.push(` card bonus: ${dollars(o.card_bonus_usd)} more when you add a card${off ? " (card payments are not open yet)" : " (promigence billing card)"}`);
165
+ lines.push(st.onFile
166
+ ? ` card bonus: ${dollars(o.card_bonus_usd)} more with your first paid invoice`
167
+ : ` card bonus: ${dollars(o.card_bonus_usd)} more when you add a card${st.off ? " (card payments are not open yet)" : " (promigence billing card)"}`);
165
168
  }
166
169
  if (o.expires_at)
167
170
  lines.push(` expires: ${o.expires_at}`);
@@ -174,14 +177,14 @@ function offerLines(head, o, off = false) {
174
177
  const credits = async (ctx) => {
175
178
  const c = new PromigenceClient({ ...clientOpts(ctx), onNotice: recordNotice });
176
179
  const o = await getOffer(c);
177
- return emit(ctx.mode, okEnvelope(ctx.cmd.name, { ...o }, offerNext(o)), offerLines("promigence credits", o, await cardsOff(c, o)), 0, ctx.pointer);
180
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, { ...o }, offerNext(o)), offerLines("promigence credits", o, await cardState(c, o)), 0, ctx.pointer);
178
181
  };
179
182
  const creditsClaim = async (ctx) => {
180
183
  const opts = { ...clientOpts(ctx), onNotice: recordNotice };
181
184
  const c = new PromigenceClient(opts);
182
185
  const o = await getOffer(c);
183
186
  if (o.state !== "claimable") {
184
- return emit(ctx.mode, okEnvelope(ctx.cmd.name, { ...o, claim_url: null }, offerNext(o)), offerLines("promigence credits claim", o, await cardsOff(c, o)), 0, ctx.pointer);
187
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, { ...o, claim_url: null }, offerNext(o)), offerLines("promigence credits claim", o, await cardState(c, o)), 0, ctx.pointer);
185
188
  }
186
189
  const link = await getClaimLink(claimClient(opts));
187
190
  const opened = maybeOpen(ctx, link.url);
@@ -62,6 +62,7 @@ export interface Plan {
62
62
  env: Record<string, string>;
63
63
  workspaceFolder: string;
64
64
  user: string | undefined;
65
+ remoteUser?: string | undefined;
65
66
  lifecycle: LifecycleStep[];
66
67
  ports: number[];
67
68
  tier: string | undefined;
@@ -84,6 +85,10 @@ export declare function lifecycleSteps(v: unknown, field: string, ctx: Substitut
84
85
  export declare function shellQuote(s: string): string;
85
86
  export declare function wrapCommand(plan: Plan, command: string): string;
86
87
  export declare function snapshotEnv(plan: Plan): Record<string, string> | undefined;
88
+ export declare function snapshotUsers(plan: Plan): {
89
+ user: string;
90
+ setupUser: string;
91
+ } | undefined;
87
92
  export interface SetupOptions {
88
93
  repo?: string | undefined;
89
94
  commit?: string | undefined;
@@ -462,6 +462,7 @@ export function translate(loaded, opts) {
462
462
  env,
463
463
  workspaceFolder,
464
464
  user,
465
+ ...(remoteUser === undefined ? {} : { remoteUser }),
465
466
  lifecycle,
466
467
  ports,
467
468
  tier,
@@ -682,6 +683,10 @@ export function wrapCommand(plan, command) {
682
683
  export function snapshotEnv(plan) {
683
684
  return Object.keys(plan.env).length ? { ...plan.env } : undefined;
684
685
  }
686
+ export function snapshotUsers(plan) {
687
+ const user = plan.remoteUser ?? plan.user;
688
+ return user === undefined ? undefined : { user, setupUser: "root" };
689
+ }
685
690
  export function planSetup(plan, opts = {}) {
686
691
  const steps = [];
687
692
  if (opts.repo && opts.commit) {
package/dist/dirsnap.js CHANGED
@@ -25,7 +25,7 @@ const INTERRUPTED = {
25
25
  };
26
26
  const NOT_WITH_DIR = [
27
27
  "--repo", "--commit", "--ref", "--track", "--also-repo", "--image", "--dockerfile", "--context",
28
- "--devcontainer", "--start", "--cpu-class", "--grade", "--holdout", "--grade-score", "--grade-timeout",
28
+ "--devcontainer", "--start", "--grade", "--holdout", "--grade-score", "--grade-timeout",
29
29
  "--grade-output", "--tier",
30
30
  ];
31
31
  export { secretLike };
package/dist/errfix.js CHANGED
@@ -106,6 +106,7 @@ function notFoundFor(kind, ctx, apiFix) {
106
106
  const BILLING_CMD = /`promigence billing (?:card|portal)`/;
107
107
  export function withoutBilling(text) {
108
108
  return text
109
+ .replace(/Owners: update the card with `promigence billing portal`; the hold lifts once the invoice is paid\.(?: For a disputed payment, write to support@promigence\.ai\.)?/, `To lift it, email ${SUPPORT}.`)
109
110
  .replace(/,? or move to a plan that allows it: `promigence billing (?:card|portal)`\.?/, `, or email ${SUPPORT} for a plan that allows it.`)
110
111
  .replace(/or add a card to lift it: `promigence billing (?:card|portal)`/, `or email ${SUPPORT} to lift it`)
111
112
  .replace(/adding a card raises it \(owners: `promigence billing (?:card|portal)`\)\.?/, `to raise it, email ${SUPPORT}.`)
package/dist/generated.js CHANGED
@@ -83,6 +83,9 @@ export const ERRORS = [
83
83
  { code: "aborted_by_client", category: "other", http: 200, exit: 130, billed: "yes",
84
84
  message: "sandbox killed by the client",
85
85
  fix: "Nothing to fix; billed for the seconds used." },
86
+ { code: "stopped_by_support", category: "other", http: 200, exit: 12, billed: "unless_ours",
87
+ message: "Stopped by Promigence support: {reason}",
88
+ fix: "Billed for the seconds it ran, as a kill is, unless support found the cause was on our side; then that time is credited back. Questions: tell support@promigence.ai the sandbox id." },
86
89
  { code: "expired", category: "other", http: 200, exit: 12, billed: "yes",
87
90
  message: "sandbox timeout reached (counted from hand-off)",
88
91
  fix: "Raise `--timeout` if the task needs longer." },
@@ -103,7 +106,7 @@ export const ERRORS = [
103
106
  fix: "Set PROMIGENCE_API_KEY or run `promigence auth login --from-env VAR`. No key yet: `promigence login` (sign in) or https://promigence.ai." },
104
107
  { code: "forbidden", category: "other", http: 403, exit: 7, billed: "no",
105
108
  message: "{detail}",
106
- fix: "Do what the message names: sign in (`promigence login`), use a key from that org, or ask an owner." },
109
+ fix: "Do what the message names: sign in (`promigence login`), use a full-access key (`promigence keys create`) or one from that org, or ask an owner." },
107
110
  { code: "not_found", category: "other", http: 404, exit: 8, billed: "no",
108
111
  message: "{kind} `{id}` not found",
109
112
  fix: "Check the id; aliases resolve in your org first, then public snapshots (`promigence examples`)." },
@@ -176,6 +179,9 @@ export const ERRORS = [
176
179
  { code: "credit_unclaimed", category: "other", http: 402, exit: 5, billed: "no",
177
180
  message: "this account has free credit waiting to be claimed",
178
181
  fix: "Claim it in your browser: `promigence credits claim` (owners; members ask an owner). Or add a card: `promigence billing card`. Nothing was started or charged." },
182
+ { code: "setup_required", category: "other", http: 403, exit: 7, billed: "no",
183
+ message: "finish setting up your account before starting new work",
184
+ fix: "Open {setup_url} and answer the setup questions (under a minute), then run this again. Nothing was started or charged." },
179
185
  { code: "card_required", category: "other", http: 402, exit: 5, billed: "no",
180
186
  message: "a card is required for new work since {effective_at}",
181
187
  fix: "Add a card (owners: `promigence billing card`, or POST /v1/billing/checkout; members ask an owner). Your trial credits are kept and used first; reads and results stay available." },
@@ -185,7 +191,7 @@ export const ERRORS = [
185
191
  { code: "concurrency_limit", category: "other", http: 409, exit: 5, billed: "no",
186
192
  message: "{live} live + {requested} requested sandboxes is over your limit of {limit}",
187
193
  fix: "Wait for sandboxes to end or lower `--count` (`--allow-partial` covers the spend cap only); a retry of the same request fails the same way. Trial orgs get {trial_limit}; to raise it, email support@promigence.ai." },
188
- { code: "egress_cap_reached", category: "other", http: 403, exit: 10, billed: "no",
194
+ { code: "egress_cap_reached", category: "other", http: 403, exit: 5, billed: "no",
189
195
  message: "monthly data-out cap reached: {used_gb} of {cap_gb} GB",
190
196
  fix: "Use `--network none` (still allowed) or wait for the 1st (UTC). For more data out a month, email support@promigence.ai." },
191
197
  { code: "project_cap_reached", category: "cap", http: 403, exit: 5, billed: "no",
@@ -193,7 +199,10 @@ export const ERRORS = [
193
199
  fix: "Ask for less (`--count`, `--cap`, a shorter extension), wait for the 1st (UTC), or have an owner raise it: `promigence project cap {project} <usd>`." },
194
200
  { code: "spend_limit_reached", category: "cap", http: 402, exit: 5, billed: "no",
195
201
  message: "today's spend limit {limit} has {remaining} left; this needs {needed}",
196
- fix: "Running sandboxes finish. The limit resets at 00:00 UTC and grows with each paid invoice; ask for less, or email support@promigence.ai. After a failed payment, new work waits until it is paid (owners: `promigence billing portal`)." },
202
+ fix: "Running sandboxes finish. The limit resets at 00:00 UTC and grows with each paid invoice; ask for less, or email support@promigence.ai." },
203
+ { code: "billing_hold", category: "other", http: 402, exit: 5, billed: "no",
204
+ message: "new work is on hold: {reason}",
205
+ fix: "Running sandboxes finish. Owners: update the card with `promigence billing portal`; the hold lifts once the invoice is paid. For a disputed payment, write to support@promigence.ai." },
197
206
  { code: "tier_over_plan", category: "other", http: 403, exit: 2, billed: "no",
198
207
  message: "the {tier} tier ({vcpus} vCPU) is over the {limit} vCPU per-sandbox limit of your {plan} plan",
199
208
  fix: "Use a tier of at most {limit} vCPU (`promigence snapshot create … --tier {max_tier}` is the largest; a fork runs on its snapshot's), or move to a plan that allows it: `promigence billing portal`." },
@@ -233,6 +242,9 @@ export const ERRORS = [
233
242
  { code: "key_expired", category: "other", http: 401, exit: 7, billed: "no",
234
243
  message: "this API key expired at {expired_at}",
235
244
  fix: "Create a new key (`promigence keys create`, or sign in again with `promigence login`) and replace this one wherever it is used." },
245
+ { code: "account_blocked", category: "other", http: 401, exit: 7, billed: "no",
246
+ message: "this account is blocked",
247
+ fix: "Write to support@promigence.ai to have it reviewed." },
236
248
  ];
237
249
  export const ERROR_BY_CODE = Object.fromEntries(ERRORS.map((e) => [e.code, e]));
238
250
  export const CLIENT_ERRORS = [
@@ -257,6 +269,9 @@ export const CLIENT_ERRORS = [
257
269
  { code: "build_failed", category: "env", http: 0, exit: 3, billed: "no",
258
270
  message: "the build could not finish; nothing was stored",
259
271
  fix: "Not charged. Build again; if it repeats, email support@promigence.ai with the build id." },
272
+ { code: "service_restart", category: "env", http: 0, exit: 3, billed: "no",
273
+ message: "a service restart stopped the command while its output was streaming",
274
+ fix: "Not billed: the time the command ran is credited back. The sandbox kept running; the command may have done part of its work before it stopped: run it again if that is safe." },
260
275
  { code: "not_connected", category: "other", http: 0, exit: 7, billed: "no",
261
276
  message: "the connection request ended without connecting anything",
262
277
  fix: "Nothing was connected. Do what the message names, then run the connect command again; `promigence github status` (or `gitlab status`) lists what is connected." },
package/dist/github.js CHANGED
@@ -14,7 +14,7 @@ function pollMs() {
14
14
  }
15
15
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
16
16
  function notConfigured(s) {
17
- return usageError(s.message ?? "GitHub connection is not configured on this deployment", "Bring the code in another way: `promigence snapshot create --dir .` uploads the files git tracks here, or clone a public repository in a --setup step.");
17
+ return usageError(s.message ?? "Connecting GitHub isn't available here yet", "Bring the code in another way: `promigence snapshot create --dir .` uploads the files git tracks here, or clone a public repository in a --setup step.");
18
18
  }
19
19
  function connectionLine(c) {
20
20
  return ` ${c.account} ${c.account_type} · ${c.repository_selection} repositories · installation ${c.installation_id}${c.suspended ? " SUSPENDED on GitHub" : ""}`;
@@ -107,7 +107,7 @@ const githubStatus = async (ctx) => {
107
107
  const s = await client(ctx).github.status();
108
108
  if (!s.configured) {
109
109
  const next = nexts(["promigence snapshot create --dir . --alias app --wait", "bring the files git tracks here instead"]);
110
- return emit(ctx.mode, okEnvelope(ctx.cmd.name, { ...s }, next), [`github: ${s.message ?? "GitHub connection is not configured on this deployment"}`], 0, ctx.pointer);
110
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, { ...s }, next), [`github: ${s.message ?? "Connecting GitHub isn't available here yet"}`], 0, ctx.pointer);
111
111
  }
112
112
  const lines = [
113
113
  s.connections.length
@@ -215,7 +215,6 @@ const NOT_WITH_GITHUB = [
215
215
  ["--devcontainer", "it is not read for a connected repository; use --setup steps"],
216
216
  ["--start", "a background process is part of an image snapshot"],
217
217
  ["--also-repo", "clone further repositories in a --setup step"],
218
- ["--cpu-class", "the build goes wherever there is room"],
219
218
  ["--dir", "--dir uploads a local tree instead; pass one of them"],
220
219
  ];
221
220
  const GITHUB_ONLY = ["--ref", "--from", "--track"];
package/dist/gitlab.js CHANGED
@@ -12,7 +12,7 @@ function pollMs() {
12
12
  }
13
13
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
14
14
  function notConfigured(s) {
15
- return usageError(s.message ?? "GitLab connection is not configured on this deployment", "Bring the code in another way: `promigence snapshot create --dir .` uploads the files git tracks here, or clone in a --setup step.");
15
+ return usageError(s.message ?? "Connecting GitLab isn't available here yet", "Bring the code in another way: `promigence snapshot create --dir .` uploads the files git tracks here, or clone in a --setup step.");
16
16
  }
17
17
  function connectionLine(c) {
18
18
  return ` ${c.username} on ${c.gitlab_url} · ${c.connection_id}${c.broken ? ` BROKEN: ${c.broken_reason ?? "connect again"}` : ""}`;
@@ -108,7 +108,7 @@ const gitlabStatus = async (ctx) => {
108
108
  const s = await client(ctx).gitlab.status();
109
109
  if (!s.configured) {
110
110
  const next = nexts(["promigence snapshot create --dir . --alias app --wait", "bring the files git tracks here instead"]);
111
- return emit(ctx.mode, okEnvelope(ctx.cmd.name, { ...s }, next), [`gitlab: ${s.message ?? "GitLab connection is not configured on this deployment"}`], 0, ctx.pointer);
111
+ return emit(ctx.mode, okEnvelope(ctx.cmd.name, { ...s }, next), [`gitlab: ${s.message ?? "Connecting GitLab isn't available here yet"}`], 0, ctx.pointer);
112
112
  }
113
113
  const lines = [
114
114
  s.connections.length