promigence 0.1.0-beta.6 → 0.1.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/account.js 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)
@@ -244,7 +259,7 @@ const billingUsage = async (ctx) => {
244
259
  const trial = b.trial;
245
260
  if (trial?.credits_remaining !== undefined)
246
261
  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)" : ""}`}`);
262
+ lines.push(` card: ${b.card_on_file ? paymentMethodText(b.card) : `none on file${billingOff ? " (card payments are not open yet)" : ""}`}`);
248
263
  if (b.upcoming_invoice?.amount_due !== undefined && b.upcoming_invoice?.amount_due !== null) {
249
264
  lines.push(` next invoice: ${usd(b.upcoming_invoice.amount_due)}`);
250
265
  }
@@ -520,6 +535,12 @@ export function sandboxFactLines(s, facts, nowMs = Date.now()) {
520
535
  }
521
536
  return out;
522
537
  }
538
+ export function supportStopLines(s) {
539
+ if (s.outcome !== "stopped_by_support")
540
+ return [];
541
+ const message = s.failure?.message;
542
+ return [` ${typeof message === "string" && message.trim() ? message : "Stopped by Promigence support"}`];
543
+ }
523
544
  const sandboxInfo = async (ctx) => {
524
545
  const sid = positional(ctx, "a sandbox id", "promigence sandbox info sbx_01");
525
546
  const c = client(ctx);
@@ -539,6 +560,7 @@ const sandboxInfo = async (ctx) => {
539
560
  : why ? [] : [[sh `promigence exec --sandbox ${sid} -- CMD`, "run something in it"]]));
540
561
  const lines = [
541
562
  `sandbox ${sid}: ${s.state}${why ? ` (${why})` : ""}${s.outcome ? ` (${s.outcome})` : ""}${s.run_id ? ` · run ${s.run_id}` : ""}`,
563
+ ...supportStopLines(s),
542
564
  ...(why ? [" its files and state are kept, and paused time is not billed"] : []),
543
565
  ...queuedLines([s]),
544
566
  ...(s.snapshot_id ? [` snapshot ${s.snapshot_id}`] : []),
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/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." },
@@ -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." },
@@ -233,6 +239,9 @@ export const ERRORS = [
233
239
  { code: "key_expired", category: "other", http: 401, exit: 7, billed: "no",
234
240
  message: "this API key expired at {expired_at}",
235
241
  fix: "Create a new key (`promigence keys create`, or sign in again with `promigence login`) and replace this one wherever it is used." },
242
+ { code: "account_blocked", category: "other", http: 401, exit: 7, billed: "no",
243
+ message: "this account is blocked",
244
+ fix: "Write to support@promigence.ai to have it reviewed." },
236
245
  ];
237
246
  export const ERROR_BY_CODE = Object.fromEntries(ERRORS.map((e) => [e.code, e]));
238
247
  export const CLIENT_ERRORS = [
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
package/dist/guide.js CHANGED
@@ -10,7 +10,7 @@ export const SECTIONS = ["core", "commands", "errors", "cost"];
10
10
  export const BUDGETS = {
11
11
  core: 1200,
12
12
  commands: 4475,
13
- errors: 2225,
13
+ errors: 2325,
14
14
  cost: 600,
15
15
  };
16
16
  export function lookupError(code) {
@@ -222,8 +222,10 @@ function renderCost(prices) {
222
222
  const source = prices === undefined
223
223
  ? "List prices when this CLI was built. Live, and your plan's: `promigence pricing`."
224
224
  : prices.source === "api"
225
- ? `Live from the API (${view?.as_of ?? "now"}): ${view?.scope === "organization" ? "your organization's prices" : "what a new account pays"}. \`promigence pricing\` shows the rest.`
226
- : `${prices.note ?? "list prices at build time; could not reach the API"} (\`promigence pricing\` asks again).`;
225
+ ? `Live from the API (${view?.as_of ?? "now"}): your organization's prices. \`promigence pricing\` shows the rest.`
226
+ : prices.source === "published"
227
+ ? "Published list prices. Your organization's own: `promigence login`, then `promigence pricing`."
228
+ : `${prices.note ?? "list prices at build time; could not reach the API"} (\`promigence pricing\` asks again).`;
227
229
  const limit = view?.limits.max_sandbox_vcpus;
228
230
  const out = [
229
231
  `# Promigence — what it costs (promigence@${VERSION})`,
package/dist/index.d.ts CHANGED
@@ -9,6 +9,6 @@ export { FENCE_BEGIN, FENCE_END, FENCE_MAX_LINES, fenceBody, install, spliceFenc
9
9
  export type { Artifact, InstallResult } from "./install.js";
10
10
  export { detect, gitFacts, normalizeRemote, readConfig, renderConfig, stripRemoteCredentials } from "./init.js";
11
11
  export type { Config, Detection, Ecosystem, GitFacts, ReadConfig } from "./init.js";
12
- export { DEVCONTAINER_NAMES, DEVCONTAINER_SPEC, DevcontainerError, detectOne, discover, load, parseJsonc, planJson, planLines, planSetup, resolveGiven, shellQuote, snapshotEnv, substituteString, translate, workspaceRootFor, wrapCommand, } from "./devcontainer.js";
12
+ export { DEVCONTAINER_NAMES, DEVCONTAINER_SPEC, DevcontainerError, detectOne, discover, load, parseJsonc, planJson, planLines, planSetup, resolveGiven, shellQuote, snapshotEnv, snapshotUsers, substituteString, translate, workspaceRootFor, wrapCommand, } from "./devcontainer.js";
13
13
  export type { LoadedDevcontainer, Plan, PlanSource, LifecycleStep, IgnoredProperty } from "./devcontainer.js";
14
14
  export type { Command, Flag, Group, CommandStatus, ExitCode, Envelope, EnvelopeError, NextHint } from "./surface.js";
package/dist/index.js CHANGED
@@ -4,4 +4,4 @@ export { ALL_ERROR_CODES, BILLED_TEXT, BUDGETS, SECTIONS, estimateTokens, guideJ
4
4
  export { CLIENT_ERRORS, CLIENT_ERROR_BY_CODE, ERRORS, ERROR_BY_CODE, TIERS, BUILD_PRICE, PRICING } from "./generated.js";
5
5
  export { FENCE_BEGIN, FENCE_END, FENCE_MAX_LINES, fenceBody, install, spliceFence } from "./install.js";
6
6
  export { detect, gitFacts, normalizeRemote, readConfig, renderConfig, stripRemoteCredentials } from "./init.js";
7
- export { DEVCONTAINER_NAMES, DEVCONTAINER_SPEC, DevcontainerError, detectOne, discover, load, parseJsonc, planJson, planLines, planSetup, resolveGiven, shellQuote, snapshotEnv, substituteString, translate, workspaceRootFor, wrapCommand, } from "./devcontainer.js";
7
+ export { DEVCONTAINER_NAMES, DEVCONTAINER_SPEC, DevcontainerError, detectOne, discover, load, parseJsonc, planJson, planLines, planSetup, resolveGiven, shellQuote, snapshotEnv, snapshotUsers, substituteString, translate, workspaceRootFor, wrapCommand, } from "./devcontainer.js";
package/dist/login.js CHANGED
@@ -765,6 +765,8 @@ async function signInAndStore(ctx, isSignup) {
765
765
  if (!idp) {
766
766
  throw usageError(`--provider: '${provider}' is not a login method`, `Pass --provider ${Object.keys(cfg.identity_providers).join(" | ")}, or omit it to choose in the browser.`);
767
767
  }
768
+ if (provider === "email")
769
+ idp = undefined;
768
770
  }
769
771
  const keyName = (str(ctx, "--key-name") ?? `cli@${deviceName()}`).slice(0, 64);
770
772
  const ref = str(ctx, "--ref")?.trim();
package/dist/pricing.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { PromigenceClient, type PricingView } from "./sdk/index.js";
2
2
  import { type Ctx, type Handler } from "./commands.js";
3
3
  export declare const COMPILED_NOTE = "list prices at build time; could not reach the API";
4
+ export declare const PUBLISHED_NOTE = "published list prices; your organization's own need a key (promigence login)";
4
5
  export interface PriceTier {
5
6
  name: string;
6
7
  vcpus: number;
@@ -12,7 +13,7 @@ export interface PriceTier {
12
13
  available?: boolean;
13
14
  }
14
15
  export interface Prices {
15
- source: "api" | "compiled";
16
+ source: "api" | "compiled" | "published";
16
17
  note?: string;
17
18
  reason?: string;
18
19
  view?: PricingView;
@@ -24,6 +25,7 @@ export interface Prices {
24
25
  granularity_seconds: number;
25
26
  tiers: PriceTier[];
26
27
  }
28
+ export declare function publishedPrices(): Prices;
27
29
  export declare function compiledPrices(note?: string, reason?: string): Prices;
28
30
  export declare function pricesFrom(view: PricingView): Prices;
29
31
  export declare function livePricing(c: PromigenceClient, opts?: {
package/dist/pricing.js CHANGED
@@ -4,7 +4,11 @@ import { BUILD_PRICE, PRICING, TIERS } from "./generated.js";
4
4
  import { emit, okEnvelope, recordNotice } from "./io.js";
5
5
  import { usd } from "./money.js";
6
6
  export const COMPILED_NOTE = "list prices at build time; could not reach the API";
7
+ export const PUBLISHED_NOTE = "published list prices; your organization's own need a key (promigence login)";
7
8
  const nano = (n) => Math.round(n * 1e9) / 1e9;
9
+ export function publishedPrices() {
10
+ return { ...compiledPrices(PUBLISHED_NOTE), source: "published" };
11
+ }
8
12
  export function compiledPrices(note = COMPILED_NOTE, reason) {
9
13
  const perSecond = (v, g) => (t) => nano(t.vcpus * v + t.mem_gib * g);
10
14
  const od = perSecond(PRICING.vcpu_second_usd, PRICING.gib_second_usd);
@@ -45,15 +49,16 @@ const CALLERS_OWN = new Set(["auth", "key_expired", "forbidden", "invalid_argume
45
49
  const CACHE_MS = 60_000;
46
50
  const cache = new Map();
47
51
  export async function livePricing(c, opts = {}) {
48
- const newAccount = opts.newAccount === true || !c.authenticated;
49
- const key = `${c.apiUrl}|${newAccount ? "" : (c.apiKey ?? "")}`;
52
+ if (opts.newAccount === true || !c.authenticated)
53
+ return publishedPrices();
54
+ const key = `${c.apiUrl}|${c.apiKey ?? ""}`;
50
55
  const hit = cache.get(key);
51
56
  let p;
52
57
  if (hit && Date.now() - hit.at < CACHE_MS) {
53
58
  p = hit.p;
54
59
  }
55
60
  else {
56
- p = c.pricing({ newAccount }).then(pricesFrom);
61
+ p = c.pricing().then(pricesFrom);
57
62
  cache.set(key, { at: Date.now(), p });
58
63
  p.catch(() => cache.delete(key));
59
64
  }
@@ -86,9 +91,7 @@ const hours = (s) => s % 3600 === 0 ? `${s / 3600} h` : s % 60 === 0 ? `${s / 60
86
91
  export function pricingLines(p, opts = {}) {
87
92
  const v = p.view;
88
93
  const head = p.source === "api" && v
89
- ? `promigence pricing — live, ${v.as_of} · ${v.scope === "new_account"
90
- ? "what a new account gets"
91
- : `your organization (${v.org_id ?? "?"}), plan ${v.plan.display_name}${v.plan.version === null ? "" : ` v${v.plan.version}`}`}`
94
+ ? `promigence pricing — live, ${v.as_of} · your organization (${v.org_id ?? "?"}), plan ${v.plan.display_name}${v.plan.version === null ? "" : ` v${v.plan.version}`}`
92
95
  : `promigence pricing — ${p.note ?? COMPILED_NOTE}`;
93
96
  const lines = [
94
97
  head,
@@ -123,7 +126,7 @@ export function pricingJson(p) {
123
126
  if (p.source === "api" && p.view)
124
127
  return { source: "api", note: null, ...p.view };
125
128
  return {
126
- source: "compiled",
129
+ source: p.source === "published" ? "published" : "compiled",
127
130
  note: p.note ?? COMPILED_NOTE,
128
131
  ...(p.reason === undefined ? {} : { reason: p.reason }),
129
132
  schema: "promigence.pricing/v1",
@@ -3,9 +3,10 @@ export const ACTIONS_RUNNER_VERSION = "2.337.0";
3
3
  export const ACTIONS_RUNNER_SHA256 = "70920811a4f8ad4328818682bca5c6469c1c942fab52448868071d0063816613";
4
4
  export const GITLAB_RUNNER_VERSION = "19.4.1";
5
5
  export const GITLAB_RUNNER_SHA256 = "5a159b36c5ff54fda9248e00bb0cdfb62c1112ab3741f5282c04a41e94da1306";
6
+ export const GITLAB_RUNNER_DEB_SHA256 = "47d2fd991e0b17af6f85af8fe3cc0cc429aae782d4f9982724c1f11a4de4a47c";
6
7
  export const GITLAB_HELPER_IMAGE = `registry.gitlab.com/gitlab-org/gitlab-runner/gitlab-runner-helper:x86_64-v${GITLAB_RUNNER_VERSION}`;
7
8
  export const GITLAB_DEFAULT_JOB_IMAGE = "ubuntu:24.04";
8
- export const RUNNER_SPEC_REV = { github: 3, gitlab: 2 };
9
+ export const RUNNER_SPEC_REV = { github: 4, gitlab: 4 };
9
10
  export const RUNNER_TIERS = ["small", "medium", "large"];
10
11
  export const GITHUB_CONFIG_PATH = "/root/.ci-runner-config";
11
12
  export const GITLAB_CONFIG_PATH = "/etc/gitlab-runner/config.toml";
@@ -72,6 +73,9 @@ const b64 = (s) => Buffer.from(s, "utf8").toString("base64");
72
73
  function writeStep(path, content, mode) {
73
74
  return `printf '%s' '${b64(content)}' | base64 -d > ${path}; chmod ${mode} ${path}`;
74
75
  }
76
+ export function runnerBuildSteps(s) {
77
+ return [...s.setup, ...s.platform_setup];
78
+ }
75
79
  export function runnerImageSpec(kind, tier) {
76
80
  if (kind === "github") {
77
81
  const url = `https://github.com/actions/runner/releases/download/v${ACTIONS_RUNNER_VERSION}/actions-runner-linux-x64-${ACTIONS_RUNNER_VERSION}.tar.gz`;
@@ -86,11 +90,6 @@ export function runnerImageSpec(kind, tier) {
86
90
  `echo '${ACTIONS_RUNNER_SHA256} /tmp/actions-runner.tar.gz' | sha256sum -c -; ` +
87
91
  `tar -xzf /tmp/actions-runner.tar.gz -C ${GITHUB_RUNNER_ROOT}; rm -f /tmp/actions-runner.tar.gz`,
88
92
  `set -eu; export DEBIAN_FRONTEND=noninteractive; ${GITHUB_RUNNER_ROOT}/bin/installdependencies.sh; rm -rf /var/lib/apt/lists/*`,
89
- "set -eu; D=/.promigence/docker; test -x \"$D/bin/docker\"; mkdir -p /usr/local/libexec/ci-docker /usr/local/lib/docker/cli-plugins; " +
90
- `cp "$D/bin/docker" ${USER_DOCKER_CLI}; ` +
91
- "for p in docker-buildx docker-compose; do rm -f /usr/local/lib/docker/cli-plugins/$p; cp \"$D/cli-plugins/$p\" /usr/local/lib/docker/cli-plugins/$p; done; " +
92
- `chmod 0755 ${USER_DOCKER_CLI} /usr/local/lib/docker/cli-plugins/docker-buildx /usr/local/lib/docker/cli-plugins/docker-compose; ` +
93
- `rm -f /usr/local/bin/docker; ${writeStep("/usr/local/bin/docker", DOCKER_WRAPPER_SCRIPT, "0755")}`,
94
93
  `set -eu; ${writeStep(GITHUB_LAUNCHER, GITHUB_LAUNCHER_SCRIPT, "0755")}; ` +
95
94
  `${writeStep(`${GITHUB_RUNNER_ROOT}/.env`, GITHUB_RUNNER_ENV, "0644")}; chown -R runner:runner /home/runner`,
96
95
  ];
@@ -98,20 +97,32 @@ export function runnerImageSpec(kind, tier) {
98
97
  "git --version; jq --version; python3 --version; lsb_release -rs; " +
99
98
  "setpriv --reuid=runner --regid=runner --init-groups env HOME=/home/runner sh -c 'docker --version && docker compose version && docker buildx version'; " +
100
99
  `sudo -n -u runner true; id runner; test -x ${GITHUB_LAUNCHER}`;
101
- return { image: RUNNER_BASE_IMAGE, setup, verify, tier, alias: runnerAlias(kind, tier) };
100
+ const platform_setup = [
101
+ "set -eu; D=/.promigence/docker; test -x \"$D/bin/docker\"; mkdir -p /usr/local/libexec/ci-docker /usr/local/lib/docker/cli-plugins; " +
102
+ `cp "$D/bin/docker" ${USER_DOCKER_CLI}; ` +
103
+ "for p in docker-buildx docker-compose; do rm -f /usr/local/lib/docker/cli-plugins/$p; cp \"$D/cli-plugins/$p\" /usr/local/lib/docker/cli-plugins/$p; done; " +
104
+ `chmod 0755 ${USER_DOCKER_CLI} /usr/local/lib/docker/cli-plugins/docker-buildx /usr/local/lib/docker/cli-plugins/docker-compose; ` +
105
+ `rm -f /usr/local/bin/docker; ${writeStep("/usr/local/bin/docker", DOCKER_WRAPPER_SCRIPT, "0755")}`,
106
+ ];
107
+ return { image: RUNNER_BASE_IMAGE, setup, platform_setup, verify, tier, alias: runnerAlias(kind, tier) };
102
108
  }
103
- const url = `https://gitlab-runner-downloads.s3.amazonaws.com/v${GITLAB_RUNNER_VERSION}/binaries/gitlab-runner-linux-amd64`;
109
+ const deb = `https://packages.gitlab.com/runner/gitlab-runner/ubuntu/pool/main/g/gitlab-runner/gitlab-runner_${GITLAB_RUNNER_VERSION}-1_amd64.deb`;
104
110
  const setup = [
105
111
  APT_STEP,
106
- "set -eu; curl -fsSL --retry 3 -o /usr/local/bin/gitlab-runner " + url + "; " +
112
+ "set -eu; curl -fsSL --retry 3 -o /tmp/gitlab-runner.deb " + deb + "; " +
113
+ "sha256sum /tmp/gitlab-runner.deb; " +
114
+ `echo '${GITLAB_RUNNER_DEB_SHA256} /tmp/gitlab-runner.deb' | sha256sum -c -; ` +
115
+ "dpkg-deb -x /tmp/gitlab-runner.deb /tmp/gitlab-runner-deb; " +
116
+ "mv /tmp/gitlab-runner-deb/usr/bin/gitlab-runner /usr/local/bin/gitlab-runner; rm -rf /tmp/gitlab-runner.deb /tmp/gitlab-runner-deb; " +
107
117
  "sha256sum /usr/local/bin/gitlab-runner; " +
108
118
  `echo '${GITLAB_RUNNER_SHA256} /usr/local/bin/gitlab-runner' | sha256sum -c -; ` +
109
119
  "chmod 0755 /usr/local/bin/gitlab-runner; mkdir -p /etc/gitlab-runner; chmod 0700 /etc/gitlab-runner",
110
120
  `set -eu; ${writeStep(GITLAB_LAUNCHER, GITLAB_LAUNCHER_SCRIPT, "0755")}`,
111
- `set -eu; docker pull -q ${GITLAB_HELPER_IMAGE}; docker pull -q ${GITLAB_DEFAULT_JOB_IMAGE}; promigence-dockerd stop`,
121
+ `set -eu; docker pull -q ${GITLAB_HELPER_IMAGE}; docker pull -q ${GITLAB_DEFAULT_JOB_IMAGE}`,
112
122
  ];
123
+ const platform_setup = ["set -eu; promigence-dockerd stop"];
113
124
  const verify = `set -eu; gitlab-runner --version; git --version; test -x ${GITLAB_LAUNCHER}`;
114
- return { image: RUNNER_BASE_IMAGE, setup, verify, tier, alias: runnerAlias(kind, tier) };
125
+ return { image: RUNNER_BASE_IMAGE, setup, platform_setup, verify, tier, alias: runnerAlias(kind, tier) };
115
126
  }
116
127
  export function isRunnerTier(t) {
117
128
  return RUNNER_TIERS.includes(t);
package/dist/runners.js CHANGED
@@ -9,7 +9,7 @@ import { TIERS } from "./generated.js";
9
9
  import { COMPILED_NOTE, livePricing } from "./pricing.js";
10
10
  import { emit, errorEnvelope, errorTextLines, okEnvelope, parseCount, parseDuration, parseUsd, usageError, write } from "./io.js";
11
11
  import { escapeLine, jsonLine, sh } from "./term.js";
12
- import { GITHUB_CONFIG_PATH, GITHUB_LAUNCHER, GITLAB_CONFIG_PATH, GITLAB_LAUNCHER, RUNNER_TIERS, isRunnerTier, runnerAlias, runnerImageSpec, } from "./runners-spec.js";
12
+ import { GITHUB_CONFIG_PATH, GITHUB_LAUNCHER, GITLAB_CONFIG_PATH, GITLAB_LAUNCHER, RUNNER_TIERS, isRunnerTier, runnerAlias, runnerBuildSteps, runnerImageSpec, } from "./runners-spec.js";
13
13
  import * as gh from "./runners-github.js";
14
14
  import * as gl from "./runners-gitlab.js";
15
15
  export const TICK_MS = 2500;
@@ -915,7 +915,7 @@ async function ensureSnapshot(ctx, c, kind, tier, mid) {
915
915
  if (!ok)
916
916
  return undefined;
917
917
  }
918
- const b = await c.snapshots.create({ image: s.image, setup: s.setup, verify: { cmd: s.verify }, tier, alias: s.alias });
918
+ const b = await c.snapshots.create({ image: s.image, setup: runnerBuildSteps(s), verify: { cmd: s.verify }, tier, alias: s.alias });
919
919
  const charged = b.deduped || b.joined ? 0 : typeof b.build_price === "number" ? b.build_price : prices.build_usd;
920
920
  await write(process.stderr, ctx.mode === "json"
921
921
  ? jsonLine(process.stderr, { hint: "build_price", build_price: charged, unit: "USD", tier, alias: s.alias, build_id: b.build_id })