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.
- package/NOTICE +14 -0
- package/README.md +88 -0
- package/dist/account.js +850 -0
- package/dist/agent.js +374 -0
- package/dist/argv.js +159 -0
- package/dist/bin.js +332 -0
- package/dist/bundle.js +133 -0
- package/dist/capacity.js +69 -0
- package/dist/commands.d.ts +110 -0
- package/dist/commands.js +2675 -0
- package/dist/connectreq.js +125 -0
- package/dist/credfile.js +188 -0
- package/dist/credits.js +197 -0
- package/dist/devcontainer.d.ts +99 -0
- package/dist/devcontainer.js +744 -0
- package/dist/device.js +129 -0
- package/dist/dirsnap.js +378 -0
- package/dist/errfix.js +262 -0
- package/dist/generated.d.ts +35 -0
- package/dist/generated.js +307 -0
- package/dist/github.js +307 -0
- package/dist/gitlab.js +210 -0
- package/dist/guide.d.ts +19 -0
- package/dist/guide.js +366 -0
- package/dist/handlers.js +511 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +7 -0
- package/dist/init.d.ts +40 -0
- package/dist/init.js +352 -0
- package/dist/install.d.ts +23 -0
- package/dist/install.js +127 -0
- package/dist/io.d.ts +37 -0
- package/dist/io.js +318 -0
- package/dist/live.js +469 -0
- package/dist/login.js +1063 -0
- package/dist/mcp.js +889 -0
- package/dist/money.js +5 -0
- package/dist/orgs.js +475 -0
- package/dist/pricing.d.ts +52 -0
- package/dist/pricing.js +181 -0
- package/dist/report.js +132 -0
- package/dist/routes.js +135 -0
- package/dist/runners-github.js +257 -0
- package/dist/runners-gitlab.js +125 -0
- package/dist/runners-spec.js +137 -0
- package/dist/runners.js +1458 -0
- package/dist/sdk/blake3.d.ts +10 -0
- package/dist/sdk/blake3.js +111 -0
- package/dist/sdk/bundle.d.ts +96 -0
- package/dist/sdk/bundle.js +227 -0
- package/dist/sdk/ca-guard.js +166 -0
- package/dist/sdk/caller.d.ts +73 -0
- package/dist/sdk/caller.js +47 -0
- package/dist/sdk/client.d.ts +3015 -0
- package/dist/sdk/client.js +2583 -0
- package/dist/sdk/command-errors.d.ts +41 -0
- package/dist/sdk/command-errors.js +36 -0
- package/dist/sdk/context.d.ts +119 -0
- package/dist/sdk/context.js +588 -0
- package/dist/sdk/core.d.ts +285 -0
- package/dist/sdk/core.js +266 -0
- package/dist/sdk/credstore.d.ts +90 -0
- package/dist/sdk/credstore.js +206 -0
- package/dist/sdk/dispatcher.js +86 -0
- package/dist/sdk/github.d.ts +158 -0
- package/dist/sdk/github.js +78 -0
- package/dist/sdk/gitlab.d.ts +104 -0
- package/dist/sdk/gitlab.js +20 -0
- package/dist/sdk/handles.d.ts +82 -0
- package/dist/sdk/handles.js +135 -0
- package/dist/sdk/hint.js +7 -0
- package/dist/sdk/http2.js +1921 -0
- package/dist/sdk/index.d.ts +34 -0
- package/dist/sdk/index.js +15 -0
- package/dist/sdk/keyed.gen.js +7 -0
- package/dist/sdk/models.gen.d.ts +6162 -0
- package/dist/sdk/models.gen.js +1 -0
- package/dist/sdk/proxy.js +249 -0
- package/dist/sdk/pty.d.ts +171 -0
- package/dist/sdk/pty.js +496 -0
- package/dist/sdk/sandbox.d.ts +537 -0
- package/dist/sdk/sandbox.js +725 -0
- package/dist/sdk/ssh.d.ts +25 -0
- package/dist/sdk/ssh.js +114 -0
- package/dist/sdk/trace.js +80 -0
- package/dist/sdk/transport-worker.js +466 -0
- package/dist/sdk/warm.js +40 -0
- package/dist/ssh.js +267 -0
- package/dist/surface.d.ts +95 -0
- package/dist/surface.js +1452 -0
- package/dist/templates.js +53 -0
- package/dist/term.js +50 -0
- package/dist/timing.js +71 -0
- package/dist/untar.js +324 -0
- package/dist/uploadguard.js +75 -0
- package/llms.txt +209 -0
- package/package.json +52 -0
package/dist/account.js
ADDED
|
@@ -0,0 +1,850 @@
|
|
|
1
|
+
import { bool, client, KEPT_BY_HOLDERS, list, nexts, optimizeForFlag, positional, required, secretListLines, str, } from "./commands.js";
|
|
2
|
+
import { queuedLines } from "./capacity.js";
|
|
3
|
+
import { duration } from "./report.js";
|
|
4
|
+
import { emit, ms, okEnvelope, overMost, parseCount, parseDuration, parseUsd, usageError } from "./io.js";
|
|
5
|
+
import { sh } from "./term.js";
|
|
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
|
+
}
|
|
22
|
+
function limitOf(ctx, fallback, most) {
|
|
23
|
+
const raw = str(ctx, "--limit");
|
|
24
|
+
if (raw === undefined)
|
|
25
|
+
return fallback;
|
|
26
|
+
const n = parseCount(raw, "--limit");
|
|
27
|
+
return most !== undefined && n > most ? overMost("--limit", raw, most, String(most), true) : n;
|
|
28
|
+
}
|
|
29
|
+
const HOLDING = ["queued", "placing", "ready", "running"];
|
|
30
|
+
export async function concurrencyOf(c, me) {
|
|
31
|
+
if (!me)
|
|
32
|
+
return undefined;
|
|
33
|
+
const limit = me.concurrency?.limit ?? me.limits?.max_concurrency;
|
|
34
|
+
if (typeof limit !== "number")
|
|
35
|
+
return undefined;
|
|
36
|
+
if (typeof me.concurrency?.in_use === "number")
|
|
37
|
+
return { in_use: me.concurrency.in_use, limit, source: "api" };
|
|
38
|
+
try {
|
|
39
|
+
const page = await c.sandboxes.listPage({ limit: 500, state: HOLDING });
|
|
40
|
+
return { in_use: page.items.length, limit, source: "counted", ...(page.nextCursor ? { at_least: true } : {}) };
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function concurrencyText(c) {
|
|
47
|
+
return `${c.in_use}${c.at_least ? "+" : ""} of ${c.limit} in use (paused sandboxes are not counted)`;
|
|
48
|
+
}
|
|
49
|
+
export function planName(me, b) {
|
|
50
|
+
if (typeof me?.plan === "string" && me.plan)
|
|
51
|
+
return me.plan;
|
|
52
|
+
const onTrial = (me?.trial?.on_trial ?? b?.trial?.on_trial) === true;
|
|
53
|
+
if (onTrial && (b?.plan === undefined || b.plan === "free"))
|
|
54
|
+
return "hobby";
|
|
55
|
+
return b?.plan;
|
|
56
|
+
}
|
|
57
|
+
function span(seconds) {
|
|
58
|
+
const s = Math.max(0, Math.round(seconds));
|
|
59
|
+
if (s >= 3600 && s % 3600 === 0)
|
|
60
|
+
return `${s / 3600}h`;
|
|
61
|
+
if (s >= 3600)
|
|
62
|
+
return `${Math.floor(s / 3600)}h${Math.round((s % 3600) / 60)}m`;
|
|
63
|
+
if (s >= 60)
|
|
64
|
+
return `${Math.floor(s / 60)}m${s % 60 ? `${s % 60}s` : ""}`;
|
|
65
|
+
return `${s}s`;
|
|
66
|
+
}
|
|
67
|
+
const status = async (ctx) => {
|
|
68
|
+
const c = client(ctx);
|
|
69
|
+
let failure;
|
|
70
|
+
const keep = (e) => { failure ??= e; return undefined; };
|
|
71
|
+
const [svc, health] = await Promise.all([
|
|
72
|
+
c.status().catch(keep),
|
|
73
|
+
c.health().catch(keep),
|
|
74
|
+
]);
|
|
75
|
+
if (svc === undefined && health === undefined)
|
|
76
|
+
throw failure;
|
|
77
|
+
const me = c.authenticated ? await c.me().catch(() => undefined) : undefined;
|
|
78
|
+
const conc = await concurrencyOf(c, me);
|
|
79
|
+
const services = svc?.services ?? [];
|
|
80
|
+
const next = nexts(["promigence me", "your limits, spend and trial credits in full"], ["promigence doctor", "check this client, your key and the API in one pass"]);
|
|
81
|
+
const env = okEnvelope(ctx.cmd.name, {
|
|
82
|
+
ok: health?.ok ?? Boolean(svc),
|
|
83
|
+
...(health?.version ? { api_version: health.version } : {}),
|
|
84
|
+
services,
|
|
85
|
+
...(svc?.checks === undefined ? {} : { checks: svc.checks }),
|
|
86
|
+
...(conc ? { concurrency: conc } : {}),
|
|
87
|
+
...(me?.spend === undefined ? {} : { spend: me.spend }),
|
|
88
|
+
}, next);
|
|
89
|
+
const lines = [`promigence status: ${health?.ok ?? svc ? "up" : "no answer"}${health?.version ? ` (api ${health.version})` : ""}`];
|
|
90
|
+
for (const s of services) {
|
|
91
|
+
lines.push(` ${s.name}: ${s.state ?? "?"}`);
|
|
92
|
+
}
|
|
93
|
+
if (!services.length)
|
|
94
|
+
lines.push(" (no service reported)");
|
|
95
|
+
if (conc)
|
|
96
|
+
lines.push(` your concurrency: ${concurrencyText(conc)}`);
|
|
97
|
+
else if (!c.authenticated)
|
|
98
|
+
lines.push(" your concurrency: sign in to see it (promigence login)");
|
|
99
|
+
else
|
|
100
|
+
lines.push(" your concurrency: could not be read with this key (promigence me says why)");
|
|
101
|
+
return emit(ctx.mode, env, lines, 0, ctx.pointer);
|
|
102
|
+
};
|
|
103
|
+
const me = async (ctx) => {
|
|
104
|
+
const c = client(ctx);
|
|
105
|
+
const [info, outcomes] = await Promise.all([c.me(), c.outcomes().catch(() => undefined)]);
|
|
106
|
+
const conc = await concurrencyOf(c, info);
|
|
107
|
+
const plan = planName(info);
|
|
108
|
+
const trial = info.trial;
|
|
109
|
+
const next = nexts(["promigence billing usage", "this month's spend by tier, and the credit that paid it"], ["promigence status", "is the service up, and how much of your limit is in use"]);
|
|
110
|
+
const org = info.org && info.org !== info.org_id ? `${info.org} (${info.org_id})` : (info.org_id ?? "org");
|
|
111
|
+
const lines = [`promigence me: ${org}${info.key?.key_id ? ` · key ${info.key.key_id}` : ""}`];
|
|
112
|
+
if (info.user && typeof info.user === "object" && info.user.email)
|
|
113
|
+
lines.push(` signed up as ${info.user.email}`);
|
|
114
|
+
if (plan)
|
|
115
|
+
lines.push(` plan: ${plan}${trial?.on_trial ? " (trial)" : ""}`);
|
|
116
|
+
if (conc)
|
|
117
|
+
lines.push(` concurrency: ${concurrencyText(conc)}`);
|
|
118
|
+
if (trial?.on_trial) {
|
|
119
|
+
const left = trial.credits_available ?? trial.credits_remaining;
|
|
120
|
+
lines.push(` trial credit: ${usd(left)} left of ${usd(trial.credits_granted)}` +
|
|
121
|
+
`${trial.credits_reserved ? ` (${usd(trial.credits_reserved)} held for running sandboxes)` : ""}`);
|
|
122
|
+
}
|
|
123
|
+
if (typeof info.spend === "number") {
|
|
124
|
+
const kinds = Object.entries(info.spend_detail?.by_kind ?? {}).filter(([, v]) => v > 0);
|
|
125
|
+
const kindName = { sandbox: "sandboxes", build: "builds", egress: "data out", storage: "storage" };
|
|
126
|
+
lines.push(` spent: ${usd(info.spend)}${kinds.length ? ` (${kinds.map(([k, v]) => `${kindName[k] ?? k} ${usd(v)}`).join(" · ")})` : ""}`);
|
|
127
|
+
}
|
|
128
|
+
if (info.spend === undefined && info.trial === undefined) {
|
|
129
|
+
lines.push(" spend and credit: shown to an owner's key of the default project (or `promigence billing usage --ledger` for this key's project)");
|
|
130
|
+
}
|
|
131
|
+
if (info.card_on_file)
|
|
132
|
+
lines.push(" card: on file");
|
|
133
|
+
const cap = info.default_spend_cap;
|
|
134
|
+
lines.push(` default spend cap: ${typeof cap === "number" && cap > 0 ? usd(cap) : "none; pass --cap USD on each fork or run"}`);
|
|
135
|
+
const lim = info.limits ?? {};
|
|
136
|
+
const shape = [
|
|
137
|
+
typeof lim.max_sandbox_vcpus === "number" ? `sandboxes up to ${lim.max_sandbox_vcpus} vCPU` : "",
|
|
138
|
+
typeof lim.max_session_s === "number" ? `sessions up to ${span(lim.max_session_s)}` : "",
|
|
139
|
+
].filter(Boolean);
|
|
140
|
+
if (shape.length)
|
|
141
|
+
lines.push(` limits: ${shape.join(" · ")}`);
|
|
142
|
+
if (outcomes && outcomes.sandboxes > 0) {
|
|
143
|
+
const [head, ...rest] = outcomeLines(outcomes);
|
|
144
|
+
lines.push(` ${head}:`, ...rest.map((l) => ` ${l}`));
|
|
145
|
+
}
|
|
146
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, {
|
|
147
|
+
me: info,
|
|
148
|
+
...(outcomes ? { outcomes } : {}),
|
|
149
|
+
...(plan ? { plan } : {}),
|
|
150
|
+
...(conc ? { concurrency: { in_use: conc.in_use, limit: conc.limit, source: conc.source, ...(conc.at_least ? { at_least: true } : {}) } } : {}),
|
|
151
|
+
}, next), lines, 0, ctx.pointer);
|
|
152
|
+
};
|
|
153
|
+
export function outcomeLines(o) {
|
|
154
|
+
const usd = (n) => `$${n.toFixed(2)}`;
|
|
155
|
+
const lines = [`this month (since ${o.since.slice(0, 10)})`];
|
|
156
|
+
if (o.sandboxes <= 0) {
|
|
157
|
+
lines.push(" nothing ran yet this month");
|
|
158
|
+
return lines;
|
|
159
|
+
}
|
|
160
|
+
lines.push(` runs ${o.runs.toLocaleString("en-US")}`);
|
|
161
|
+
const ready = o.ready_ms_p50 ?? null;
|
|
162
|
+
lines.push(` sandboxes ${o.sandboxes.toLocaleString("en-US")}${ready === null ? "" : `, ready in ${ms(ready)} (median)`}`);
|
|
163
|
+
if (o.failures_ours > 0)
|
|
164
|
+
lines.push(` failures of ours ${o.failures_ours}, not billed${o.failures_ours_usd > 0 ? ` (${usd(o.failures_ours_usd)} given back)` : ""}`);
|
|
165
|
+
const speedup = o.batch_speedup ?? null;
|
|
166
|
+
const saved = o.batch_saved_s ?? null;
|
|
167
|
+
if (speedup !== null && saved !== null) {
|
|
168
|
+
lines.push(` many at once ${o.batch_runs} runs, ${speedup.toFixed(1)}x faster than one after another, ${duration(saved * 1000)} not spent waiting`);
|
|
169
|
+
}
|
|
170
|
+
if (o.kept_failures > 0)
|
|
171
|
+
lines.push(` kept failures ${o.kept_failures} you can still reopen`);
|
|
172
|
+
const hours = o.paused_hours ?? null;
|
|
173
|
+
const paidNot = o.paused_usd ?? null;
|
|
174
|
+
if (hours !== null)
|
|
175
|
+
lines.push(` paused, not billed ${hours.toFixed(1)} h${paidNot === null ? "" : ` (${usd(paidNot)} you did not pay)`}`);
|
|
176
|
+
return lines;
|
|
177
|
+
}
|
|
178
|
+
const billingUsage = async (ctx) => {
|
|
179
|
+
const limit = limitOf(ctx, 20, 1000);
|
|
180
|
+
const wantLedger = bool(ctx, "--ledger");
|
|
181
|
+
const cursor = str(ctx, "--cursor");
|
|
182
|
+
if (cursor !== undefined && !wantLedger)
|
|
183
|
+
throw usageError("--cursor pages the ledger", "Pass it with --ledger.");
|
|
184
|
+
const c = client(ctx);
|
|
185
|
+
const [b, me] = await Promise.all([
|
|
186
|
+
c.billing.usage().catch((e) => {
|
|
187
|
+
if (wantLedger && e?.code === "forbidden")
|
|
188
|
+
return undefined;
|
|
189
|
+
throw e;
|
|
190
|
+
}),
|
|
191
|
+
c.me().catch(() => undefined),
|
|
192
|
+
]);
|
|
193
|
+
const page = wantLedger ? await c.billing.ledgerPage({ limit, ...(cursor === undefined ? {} : { cursor }) }) : undefined;
|
|
194
|
+
const entries = page?.items ?? [];
|
|
195
|
+
const moreLedger = page?.nextCursor
|
|
196
|
+
? [[sh `promigence billing usage --ledger --limit ${String(limit)} --cursor ${page.nextCursor}`, "the next page of rows"]]
|
|
197
|
+
: [];
|
|
198
|
+
if (b === undefined) {
|
|
199
|
+
const env = okEnvelope(ctx.cmd.name, { org_totals: null, ledger: entries, next_cursor: page?.nextCursor ?? null }, nexts(...moreLedger, ["promigence me", "your limits and concurrency"]));
|
|
200
|
+
const lines = ["promigence billing usage: the organization's totals are shown to an owner's key of the default project; this key's project's metered rows:"];
|
|
201
|
+
for (const e of entries) {
|
|
202
|
+
lines.push(` ${e.created_at ?? ""} ${e.kind}${e.tier ? ` ${e.tier}` : ""} ${usd(e.amount)}${e.seconds ? ` · ${e.seconds}s` : ""}${e.run_id ? ` · ${e.run_id}` : ""}`);
|
|
203
|
+
}
|
|
204
|
+
if (!entries.length)
|
|
205
|
+
lines.push(" (no metered rows in this period)");
|
|
206
|
+
if (page?.nextCursor)
|
|
207
|
+
lines.push(` more: --cursor ${page.nextCursor}`);
|
|
208
|
+
return emit(ctx.mode, env, lines, 0, ctx.pointer);
|
|
209
|
+
}
|
|
210
|
+
const usage = (b.usage ?? {});
|
|
211
|
+
const billingOff = b.billing_enabled === false;
|
|
212
|
+
const next = nexts(...(billingOff
|
|
213
|
+
? [["promigence me", "your limits, concurrency and trial credit"]]
|
|
214
|
+
: b.card_on_file
|
|
215
|
+
? [["promigence billing portal", "the card on file and past invoices"]]
|
|
216
|
+
: [["promigence billing card", "an owner adds a card (nothing is charged now)"]]), ...(moreLedger.length ? moreLedger : [["promigence billing usage --ledger", "this key's project's metered rows"]]));
|
|
217
|
+
const plan = planName(me, b);
|
|
218
|
+
const onTrial = (b.trial?.on_trial ?? me?.trial?.on_trial) === true;
|
|
219
|
+
const env = okEnvelope(ctx.cmd.name, {
|
|
220
|
+
plan: plan ?? null,
|
|
221
|
+
billing_plan: b.plan ?? null,
|
|
222
|
+
period: b.period ?? null,
|
|
223
|
+
usage,
|
|
224
|
+
card_on_file: b.card_on_file ?? false,
|
|
225
|
+
...(b.card ? { card: b.card } : {}),
|
|
226
|
+
...(b.card_note ? { card_note: b.card_note } : {}),
|
|
227
|
+
...(b.trial ? { trial: b.trial } : {}),
|
|
228
|
+
...(b.suspended ? { suspended: true, suspend_reason: b.suspend_reason ?? null } : {}),
|
|
229
|
+
upcoming_invoice: b.upcoming_invoice ?? null,
|
|
230
|
+
...(b.upcoming_invoice_error ? { upcoming_invoice_error: b.upcoming_invoice_error } : {}),
|
|
231
|
+
...(wantLedger ? { ledger: entries, next_cursor: page?.nextCursor ?? null } : {}),
|
|
232
|
+
}, next);
|
|
233
|
+
const period = b.period?.start ? `${b.period.start.slice(0, 10)} → ${(b.period.end ?? "").slice(0, 10)}` : "this month";
|
|
234
|
+
const lines = [`promigence billing usage: ${usd(usage.total)} (${period}, plan ${plan ?? "?"}${onTrial ? ", on the trial" : ""})`];
|
|
235
|
+
const detail = (row) => `${usd(row.amount)}${row.seconds === undefined ? "" : ` · ${row.seconds}s`}${row.count === undefined ? "" : ` · ${row.count}`}` +
|
|
236
|
+
`${row.gib === undefined ? "" : ` · ${row.gib} GiB`}${row.entries === undefined ? "" : ` · ${row.entries} entries`}` +
|
|
237
|
+
`${row.episodes === undefined ? "" : ` · ${row.episodes} episodes`}`;
|
|
238
|
+
for (const [k, v] of Object.entries(usage)) {
|
|
239
|
+
if (k === "total" || v === null || typeof v !== "object")
|
|
240
|
+
continue;
|
|
241
|
+
const row = v;
|
|
242
|
+
if (row.amount === undefined) {
|
|
243
|
+
for (const [name, inner] of Object.entries(row)) {
|
|
244
|
+
if (inner === null || typeof inner !== "object")
|
|
245
|
+
continue;
|
|
246
|
+
const r = inner;
|
|
247
|
+
if (!r.amount && !r.seconds && !r.count && !r.entries && !r.episodes)
|
|
248
|
+
continue;
|
|
249
|
+
lines.push(` ${k} ${name}: ${detail(r)}`);
|
|
250
|
+
}
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (!row.amount && !row.count && !row.gib)
|
|
254
|
+
continue;
|
|
255
|
+
lines.push(` ${k}: ${detail(row)}`);
|
|
256
|
+
}
|
|
257
|
+
if (typeof usage["credits_applied"] === "number" && usage["credits_applied"] > 0) {
|
|
258
|
+
lines.push(` paid by trial credits: ${usd(usage["credits_applied"])} · to the card: ${usd(usage["billable"])}`);
|
|
259
|
+
}
|
|
260
|
+
const trial = b.trial;
|
|
261
|
+
if (trial?.credits_remaining !== undefined)
|
|
262
|
+
lines.push(` trial credits left: ${usd(trial.credits_remaining)}`);
|
|
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}`);
|
|
266
|
+
if (b.upcoming_invoice?.amount_due !== undefined && b.upcoming_invoice?.amount_due !== null) {
|
|
267
|
+
lines.push(` next invoice: ${usd(b.upcoming_invoice.amount_due)}`);
|
|
268
|
+
}
|
|
269
|
+
else if (b.upcoming_invoice_error) {
|
|
270
|
+
lines.push(` next invoice: not available (${b.upcoming_invoice_error})`);
|
|
271
|
+
}
|
|
272
|
+
if (b.suspended)
|
|
273
|
+
lines.push(` SUSPENDED: ${b.suspend_reason ?? "see promigence billing card"}`);
|
|
274
|
+
for (const e of entries) {
|
|
275
|
+
lines.push(` ${e.created_at ?? ""} ${e.kind}${e.tier ? ` ${e.tier}` : ""} ${usd(e.amount)}${e.seconds ? ` · ${e.seconds}s` : ""}${e.run_id ? ` · ${e.run_id}` : ""}`);
|
|
276
|
+
}
|
|
277
|
+
if (wantLedger && !entries.length)
|
|
278
|
+
lines.push(" (no metered rows in this period)");
|
|
279
|
+
if (page?.nextCursor)
|
|
280
|
+
lines.push(` more: --cursor ${page.nextCursor}`);
|
|
281
|
+
return emit(ctx.mode, env, lines, 0, ctx.pointer);
|
|
282
|
+
};
|
|
283
|
+
const billingPortal = async (ctx) => {
|
|
284
|
+
const res = await client(ctx).billing.portal();
|
|
285
|
+
const next = nexts(["promigence billing usage", "this month's spend without leaving the terminal"]);
|
|
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);
|
|
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
|
+
};
|
|
368
|
+
const referrals = async (ctx) => {
|
|
369
|
+
const r = await client(ctx).billing.referrals();
|
|
370
|
+
const next = r.code
|
|
371
|
+
? nexts(["promigence login --ref " + r.code, "what a friend runs to sign up with your code"])
|
|
372
|
+
: nexts(["promigence billing usage", "this month's spend and credits"]);
|
|
373
|
+
const lines = [];
|
|
374
|
+
if (r.program !== "on" || !r.code || !r.offer) {
|
|
375
|
+
lines.push("promigence referrals: no referral offer is running right now");
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
const o = r.offer;
|
|
379
|
+
lines.push(`promigence referrals: ${o.headline}`);
|
|
380
|
+
lines.push(` your code: ${r.code} (a friend signs up with: ${sh `promigence login --ref ${r.code}`})`);
|
|
381
|
+
lines.push(` they get ${usd(o.give_usd)} of compute credit when they pay their first plan invoice` +
|
|
382
|
+
`${o.qualifying_plans.length ? ` on ${o.qualifying_plans.join(" / ")}` : ""}; you get ${usd(o.get_usd)} ` +
|
|
383
|
+
`${o.hold_days > 0 ? `${o.hold_days} day(s) later` : "then"}`);
|
|
384
|
+
if (o.credit_expiry_days !== null)
|
|
385
|
+
lines.push(` credit lasts ${o.credit_expiry_days} day(s) from its grant`);
|
|
386
|
+
if (o.ends_at)
|
|
387
|
+
lines.push(` offer ends ${o.ends_at}`);
|
|
388
|
+
}
|
|
389
|
+
const c = r.referrals;
|
|
390
|
+
lines.push(` your referrals: ${c.pending} pending · ${c.held} on hold · ${c.rewarded} rewarded` +
|
|
391
|
+
`${c.under_review ? ` · ${c.under_review} under review` : ""}${c.other ? ` · ${c.other} other` : ""} · earned ${usd(r.earned_usd)}`);
|
|
392
|
+
if (r.referred_by) {
|
|
393
|
+
lines.push(` you were referred: ${r.referred_by.status} (${usd(r.referred_by.reward_usd)} of credit)`);
|
|
394
|
+
}
|
|
395
|
+
for (const e of r.expiring_credits) {
|
|
396
|
+
lines.push(` credit expiring ${e.expires_at}: ${usd(e.remaining_usd)} left of ${usd(e.granted_usd)}`);
|
|
397
|
+
}
|
|
398
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, { ...r }, next), lines, 0, ctx.pointer);
|
|
399
|
+
};
|
|
400
|
+
const referralsApply = async (ctx) => {
|
|
401
|
+
const code = positional(ctx, "a referral code", "promigence referrals apply K7QM2XPD");
|
|
402
|
+
const r = await client(ctx).billing.applyReferral(code);
|
|
403
|
+
const next = nexts(["promigence referrals", "how the referral stands"]);
|
|
404
|
+
const lines = r.status === "under_review"
|
|
405
|
+
? [`promigence referrals apply: ${r.code} recorded; it is being checked before any credit is granted`]
|
|
406
|
+
: [
|
|
407
|
+
`promigence referrals apply: ${r.code} applied`,
|
|
408
|
+
` you get ${usd(r.reward_usd)} of compute credit when you pay your first plan invoice` +
|
|
409
|
+
`${r.qualifying_plans.length ? ` on ${r.qualifying_plans.join(" / ")}` : ""}`,
|
|
410
|
+
];
|
|
411
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, { ...r }, next), lines, 0, ctx.pointer);
|
|
412
|
+
};
|
|
413
|
+
const snapshotDelete = async (ctx) => {
|
|
414
|
+
const arg = positional(ctx, "a snapshot", "promigence snapshot delete snap_01");
|
|
415
|
+
const res = await client(ctx).snapshots.delete(arg);
|
|
416
|
+
const next = nexts(["promigence snapshot list", "what is left in your catalogue"]);
|
|
417
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, { snapshot_id: res.snapshot_id, alias: res.alias ?? null, deleted: true }, next), [
|
|
418
|
+
`snapshot ${res.snapshot_id}${res.alias ? ` (${res.alias})` : ""}: deleted`,
|
|
419
|
+
" the record is gone; finished runs keep their reports",
|
|
420
|
+
], 0, ctx.pointer);
|
|
421
|
+
};
|
|
422
|
+
export function buildAliasNote(b) {
|
|
423
|
+
if (b.snapshot_deleted === true)
|
|
424
|
+
return " (deleted)";
|
|
425
|
+
return b.alias ? ` (${b.alias})` : "";
|
|
426
|
+
}
|
|
427
|
+
function stateFlag(ctx, states) {
|
|
428
|
+
const wanted = str(ctx, "--state");
|
|
429
|
+
if (wanted !== undefined && !states.includes(wanted)) {
|
|
430
|
+
throw usageError(`--state: '${wanted}' is not a state`, `Pass one of ${states.join(" | ")}.`);
|
|
431
|
+
}
|
|
432
|
+
return wanted;
|
|
433
|
+
}
|
|
434
|
+
const BUILD_STATES = ["queued", "running", "verified", "built", "rejected", "failed"];
|
|
435
|
+
const RUN_STATES = ["running", "completed", "cap_killed", "aborted"];
|
|
436
|
+
const snapshotBuilds = async (ctx) => {
|
|
437
|
+
const wanted = stateFlag(ctx, BUILD_STATES);
|
|
438
|
+
const limit = limitOf(ctx, 20, 500);
|
|
439
|
+
const cursor = str(ctx, "--cursor");
|
|
440
|
+
const page = await client(ctx).snapshots.buildsPage({ limit: wanted === undefined ? limit : 500, ...(cursor === undefined ? {} : { cursor }) });
|
|
441
|
+
const matched = wanted === undefined ? page.items : page.items.filter((b) => b.state === wanted);
|
|
442
|
+
const builds = matched.slice(0, limit);
|
|
443
|
+
const more = builds.length === matched.length ? page.nextCursor : null;
|
|
444
|
+
const next = nexts(...(more ? [[`${sh `promigence snapshot builds --limit ${String(limit)}`}${wanted ? sh ` --state ${wanted}` : ""}${sh ` --cursor ${more}`}`, "the next page"]] : []), ...(builds.length ? [[sh `promigence snapshot status ${builds[0].build_id} --log 40`, "the newest build, with the tail of its log"]] : []));
|
|
445
|
+
const lines = [
|
|
446
|
+
`promigence snapshot builds: ${builds.length}${wanted ? ` in state ${wanted}` : ""}`,
|
|
447
|
+
...builds.map((b) => ` ${b.build_id} ${b.state} ${b.snapshot_id ?? "—"}${buildAliasNote(b)}${b.error?.code ? ` ${b.error.code}` : ""}`),
|
|
448
|
+
...(builds.length ? [] : [" (no builds)"]),
|
|
449
|
+
...(more ? [` more: --cursor ${more}`] : []),
|
|
450
|
+
];
|
|
451
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, { builds, count: builds.length, next_cursor: more }, next), lines, 0, ctx.pointer);
|
|
452
|
+
};
|
|
453
|
+
const snapshotAlias = async (ctx) => {
|
|
454
|
+
const arg = positional(ctx, "a snapshot", "promigence snapshot alias snap_ab12 opencode");
|
|
455
|
+
const clear = bool(ctx, "--clear");
|
|
456
|
+
const name = ctx.args[1];
|
|
457
|
+
if (!clear && !name) {
|
|
458
|
+
const rec = await client(ctx).snapshots.get(arg);
|
|
459
|
+
const alias = rec.alias ?? null;
|
|
460
|
+
const id = rec.snapshot_id ?? arg;
|
|
461
|
+
const next = nexts([sh `promigence snapshot alias ${id} NAME`, alias ? "rename it" : "name it"]);
|
|
462
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, { snapshot_id: id, alias }, next), [`snapshot ${id}: ${alias ? `alias ${alias}` : "no alias"}`], 0, ctx.pointer);
|
|
463
|
+
}
|
|
464
|
+
if (clear && name) {
|
|
465
|
+
throw usageError("--clear takes no name", `Use either ${sh `promigence snapshot alias ${arg} NAME`} or ${sh `promigence snapshot alias ${arg} --clear`}.`);
|
|
466
|
+
}
|
|
467
|
+
const res = await client(ctx).snapshots.alias(arg, clear ? null : name);
|
|
468
|
+
const next = nexts(["promigence snapshot list", "your snapshots and their names"]);
|
|
469
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, { snapshot_id: res.snapshot_id, alias: res.alias ?? null }, next), [`snapshot ${res.snapshot_id}: ${clear ? "alias cleared" : `alias ${res.alias}`}`], 0, ctx.pointer);
|
|
470
|
+
};
|
|
471
|
+
const runList = async (ctx) => {
|
|
472
|
+
const wanted = stateFlag(ctx, RUN_STATES);
|
|
473
|
+
const limit = limitOf(ctx, 20, 500);
|
|
474
|
+
const cursor = str(ctx, "--cursor");
|
|
475
|
+
const page = await client(ctx).runs.listPage({ limit: wanted === undefined ? limit : 500, ...(cursor === undefined ? {} : { cursor }) });
|
|
476
|
+
const matched = wanted === undefined ? page.items : page.items.filter((r) => r.state === wanted);
|
|
477
|
+
const runs = matched.slice(0, limit);
|
|
478
|
+
const more = runs.length === matched.length ? page.nextCursor : null;
|
|
479
|
+
const next = nexts(...(more ? [[`${sh `promigence run list --limit ${String(limit)}`}${wanted ? sh ` --state ${wanted}` : ""}${sh ` --cursor ${more}`}`, "the next page"]] : []), ...(runs.length ? [[sh `promigence run report ${runs[0].run_id}`, "the newest run's validity report"]] : []));
|
|
480
|
+
const lines = [
|
|
481
|
+
`promigence run list: ${runs.length}${wanted ? ` in state ${wanted}` : ""}`,
|
|
482
|
+
...runs.map((r) => ` ${r.run_id} ${r.state} ${r.count ?? "?"}× ${r.snapshot_id || r.snapshot || "—"} ${r.started_at ?? ""}`),
|
|
483
|
+
...(runs.length ? [] : [" (no runs)"]),
|
|
484
|
+
...(more ? [` more: --cursor ${more}`] : []),
|
|
485
|
+
];
|
|
486
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, { runs, count: runs.length, next_cursor: more }, next), lines, 0, ctx.pointer);
|
|
487
|
+
};
|
|
488
|
+
function labelsOf(ctx) {
|
|
489
|
+
const out = {};
|
|
490
|
+
for (const raw of list(ctx, "--label")) {
|
|
491
|
+
const at = raw.indexOf("=");
|
|
492
|
+
if (at <= 0) {
|
|
493
|
+
throw usageError(`--label ${JSON.stringify(raw)} is not KEY=VALUE`, "Write it as --label session=abc, and repeat the flag for a second label.");
|
|
494
|
+
}
|
|
495
|
+
const k = raw.slice(0, at);
|
|
496
|
+
if (k in out) {
|
|
497
|
+
throw usageError(`--label names ${JSON.stringify(k)} twice`, "A sandbox holds one value per key, so the second could only ever match nothing.");
|
|
498
|
+
}
|
|
499
|
+
if (raw.includes(",")) {
|
|
500
|
+
throw usageError(`--label ${JSON.stringify(raw)} contains a comma`, "A label value cannot hold a comma; that is the separator on the wire.");
|
|
501
|
+
}
|
|
502
|
+
out[k] = raw.slice(at + 1);
|
|
503
|
+
}
|
|
504
|
+
return out;
|
|
505
|
+
}
|
|
506
|
+
const HELD_STATES = ["queued", "placing", "ready", "running", "paused"];
|
|
507
|
+
export function pauseReasonText(reason) {
|
|
508
|
+
if (reason === "spend_cap")
|
|
509
|
+
return "spend cap reached";
|
|
510
|
+
if (reason === "credits_exhausted")
|
|
511
|
+
return "out of credits";
|
|
512
|
+
return undefined;
|
|
513
|
+
}
|
|
514
|
+
const sandboxList = async (ctx) => {
|
|
515
|
+
const labels = labelsOf(ctx);
|
|
516
|
+
const states = list(ctx, "--state").flatMap((s) => s.split(",")).map((s) => s.trim()).filter(Boolean);
|
|
517
|
+
const cursor = str(ctx, "--cursor");
|
|
518
|
+
const page = await client(ctx).sandboxes.listPage({
|
|
519
|
+
limit: limitOf(ctx, 50),
|
|
520
|
+
...(Object.keys(labels).length ? { labels } : {}),
|
|
521
|
+
...(states.length ? { state: states } : {}),
|
|
522
|
+
...(cursor === undefined ? {} : { cursor }),
|
|
523
|
+
});
|
|
524
|
+
const sandboxes = page.items;
|
|
525
|
+
let held;
|
|
526
|
+
if (!states.length && cursor === undefined && page.nextCursor) {
|
|
527
|
+
const live = await client(ctx).sandboxes.listPage({
|
|
528
|
+
limit: 200,
|
|
529
|
+
...(Object.keys(labels).length ? { labels } : {}),
|
|
530
|
+
state: [...HELD_STATES],
|
|
531
|
+
}).catch(() => undefined);
|
|
532
|
+
if (live) {
|
|
533
|
+
const onPage = new Set(sandboxes.map((s) => s.sandbox_id));
|
|
534
|
+
const count = live.items.filter((s) => !onPage.has(s.sandbox_id)).length;
|
|
535
|
+
if (count > 0)
|
|
536
|
+
held = { count, atLeast: live.nextCursor !== null };
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
const heldCmd = sh `promigence sandbox list --state ${HELD_STATES.join(",")}`;
|
|
540
|
+
const firstLive = sandboxes.find((s) => s.state !== "ended");
|
|
541
|
+
const next = nexts(...(held ? [[heldCmd, "the sandboxes still held (live or paused) that this page does not show"]] : []), ...(sandboxes.length ? [[sh `promigence sandbox info ${sandboxes[0].sandbox_id}`, "one sandbox: its run, its state and the execs it has run"]] : []), ...(page.nextCursor
|
|
542
|
+
? [[sh `promigence sandbox list --cursor ${page.nextCursor}`, "the next page"]]
|
|
543
|
+
: []), ...(firstLive ? [["promigence sandbox kill SID", "end one: put its id from this list in place of SID"]] : []));
|
|
544
|
+
const shown = Object.entries(labels).map(([k, v]) => `${k}=${v}`).join(" ");
|
|
545
|
+
const lines = [
|
|
546
|
+
`promigence sandbox list: ${sandboxes.length}${states.length ? ` in state ${states.join(",")}` : ""}${shown ? ` with ${shown}` : ""}`,
|
|
547
|
+
...sandboxes.map((s) => {
|
|
548
|
+
const why = pauseReasonText(s.pause_reason);
|
|
549
|
+
return ` ${s.sandbox_id} ${s.state}${why ? ` (${why})` : ""}${s.outcome ? ` (${s.outcome})` : ""} ${s.run_id ?? ""}`;
|
|
550
|
+
}),
|
|
551
|
+
...(sandboxes.length ? [] : [" (no sandboxes)"]),
|
|
552
|
+
...(page.nextCursor ? [` more: --cursor ${page.nextCursor}`] : []),
|
|
553
|
+
...(held ? [` ${held.atLeast ? "at least " : ""}${held.count} live or paused not on this page: ${heldCmd}`] : []),
|
|
554
|
+
];
|
|
555
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, {
|
|
556
|
+
sandboxes, count: sandboxes.length, labels, state: states,
|
|
557
|
+
limit: page.limit, next_cursor: page.nextCursor,
|
|
558
|
+
...(held ? { held_not_shown: held.count, ...(held.atLeast ? { held_not_shown_at_least: true } : {}) } : {}),
|
|
559
|
+
}, next), lines, 0, ctx.pointer);
|
|
560
|
+
};
|
|
561
|
+
const imageResolve = async (ctx) => {
|
|
562
|
+
const ref = positional(ctx, "an image reference", "promigence image resolve python:3.12");
|
|
563
|
+
const secret = str(ctx, "--registry-secret");
|
|
564
|
+
const r = await client(ctx).images.resolve(ref, ...(secret ? [{ registrySecret: secret }] : []));
|
|
565
|
+
const lines = [
|
|
566
|
+
r.already_pinned
|
|
567
|
+
? `${ref} is already pinned`
|
|
568
|
+
: `${ref} -> ${r.image}`,
|
|
569
|
+
` digest ${r.digest}`,
|
|
570
|
+
...(r.platform_digest ? [` ${r.platform} ${r.platform_digest}`] : []),
|
|
571
|
+
...(r.platforms.length ? [` platforms ${r.platforms.join(", ")}`] : []),
|
|
572
|
+
];
|
|
573
|
+
const next = nexts([
|
|
574
|
+
sh `promigence snapshot create --image ${r.image} --tier small`,
|
|
575
|
+
"build a snapshot from the pinned image",
|
|
576
|
+
]);
|
|
577
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, r, next), lines, 0, ctx.pointer);
|
|
578
|
+
};
|
|
579
|
+
function runFacts(run) {
|
|
580
|
+
if (!run)
|
|
581
|
+
return {};
|
|
582
|
+
const q = (run["quote"] ?? {});
|
|
583
|
+
return {
|
|
584
|
+
...(typeof run["tier"] === "string" ? { tier: run["tier"] } : {}),
|
|
585
|
+
...(typeof q.vcpus === "number" ? { vcpus: q.vcpus } : {}),
|
|
586
|
+
...(typeof q.mem_gib === "number" ? { memGib: q.mem_gib } : {}),
|
|
587
|
+
...(typeof q.per_second_usd === "number" ? { perSecond: q.per_second_usd } : {}),
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
export function sandboxFactLines(s, facts, nowMs = Date.now()) {
|
|
591
|
+
const out = [];
|
|
592
|
+
if (facts.tier) {
|
|
593
|
+
const shape = facts.vcpus !== undefined && facts.memGib !== undefined ? ` (${facts.vcpus} vCPU, ${facts.memGib} GiB)` : "";
|
|
594
|
+
out.push(` tier ${facts.tier}${shape}`);
|
|
595
|
+
}
|
|
596
|
+
const handoff = typeof s["t_handoff"] === "string" ? Date.parse(s["t_handoff"]) : NaN;
|
|
597
|
+
const end = typeof s["t_end"] === "string" ? Date.parse(s["t_end"]) : NaN;
|
|
598
|
+
const timing = [];
|
|
599
|
+
if (Number.isFinite(handoff)) {
|
|
600
|
+
timing.push(Number.isFinite(end)
|
|
601
|
+
? `ran ${span((end - handoff) / 1000)}`
|
|
602
|
+
: `started ${span((nowMs - handoff) / 1000)} ago`);
|
|
603
|
+
}
|
|
604
|
+
const timeout = typeof s["timeout_ms"] === "number" ? s["timeout_ms"] : undefined;
|
|
605
|
+
const elapsed = typeof s["elapsed_ms"] === "number" ? s["elapsed_ms"] : undefined;
|
|
606
|
+
if (timeout !== undefined && elapsed !== undefined && s["state"] !== "ended") {
|
|
607
|
+
timing.push(`${span((timeout - elapsed) / 1000)} left of ${span(timeout / 1000)} (paused time does not count)`);
|
|
608
|
+
}
|
|
609
|
+
else if (timeout !== undefined && s["state"] !== "ended") {
|
|
610
|
+
timing.push(`timeout ${span(timeout / 1000)}`);
|
|
611
|
+
}
|
|
612
|
+
if (timing.length)
|
|
613
|
+
out.push(` ${timing.join(" · ")}`);
|
|
614
|
+
if (elapsed !== undefined && facts.perSecond !== undefined) {
|
|
615
|
+
const secs = Math.ceil(elapsed / 1000);
|
|
616
|
+
const rate = `$${facts.perSecond.toFixed(9).replace(/0+$/, "").replace(/\.$/, "")}`;
|
|
617
|
+
out.push(` cost so far: about ${usd(secs * facts.perSecond)} (${secs} s at ${rate}/s)`);
|
|
618
|
+
}
|
|
619
|
+
return out;
|
|
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
|
+
}
|
|
627
|
+
const sandboxInfo = async (ctx) => {
|
|
628
|
+
const sid = positional(ctx, "a sandbox id", "promigence sandbox info sbx_01");
|
|
629
|
+
const c = client(ctx);
|
|
630
|
+
const s = await c.sandboxes.get(sid);
|
|
631
|
+
const run = s.run_id ? await c.runs.get(s.run_id).catch(() => undefined) : undefined;
|
|
632
|
+
const facts = runFacts(run);
|
|
633
|
+
const execs = (s.execs ?? []);
|
|
634
|
+
const ended = s.state === "ended";
|
|
635
|
+
const why = pauseReasonText(s.pause_reason);
|
|
636
|
+
const limitNext = s.pause_reason === "spend_cap"
|
|
637
|
+
? [[sh `promigence sandbox resume ${sid} --cap USD`, "raise its run's cap and go on (paused time is not billed)"]]
|
|
638
|
+
: s.pause_reason === "credits_exhausted"
|
|
639
|
+
? [["promigence billing card", "add a card, then resume it"], [sh `promigence sandbox resume ${sid}`, "go on once credits are added"]]
|
|
640
|
+
: [];
|
|
641
|
+
const next = nexts(...limitNext, ...(execs.length ? [[sh `promigence exec output ${execs[execs.length - 1].exec_id} --sandbox ${sid}`, "what its last command printed"]] : []), ...(ended
|
|
642
|
+
? (s.run_id ? [[sh `promigence run report ${s.run_id} --sandbox ${sid}`, "how it ended, in its run's report"]] : [])
|
|
643
|
+
: why ? [] : [[sh `promigence exec --sandbox ${sid} -- CMD`, "run something in it"]]));
|
|
644
|
+
const lines = [
|
|
645
|
+
`sandbox ${sid}: ${s.state}${why ? ` (${why})` : ""}${s.outcome ? ` (${s.outcome})` : ""}${s.run_id ? ` · run ${s.run_id}` : ""}`,
|
|
646
|
+
...supportStopLines(s),
|
|
647
|
+
...(why ? [" its files and state are kept, and paused time is not billed"] : []),
|
|
648
|
+
...queuedLines([s]),
|
|
649
|
+
...(s.snapshot_id ? [` snapshot ${s.snapshot_id}`] : []),
|
|
650
|
+
...sandboxFactLines(s, facts),
|
|
651
|
+
...(ended && typeof s["cost_usd"] === "number"
|
|
652
|
+
? [` billed ${s["billable_seconds"] ?? 0} s · $${s["cost_usd"]}`]
|
|
653
|
+
: []),
|
|
654
|
+
...(Number(s.auto_pause_s ?? 0) > 0 ? [` pauses itself after ${s.auto_pause_s} s idle`] : []),
|
|
655
|
+
...execs.map((e) => ` ${e.exec_id} exit ${e.exit_code ?? "—"} ${(e.argv ?? []).join(" ")}`),
|
|
656
|
+
...(execs.length ? [] : [" (no execs recorded)"]),
|
|
657
|
+
];
|
|
658
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, {
|
|
659
|
+
sandbox: s,
|
|
660
|
+
...(facts.tier ? { tier: facts.tier } : {}),
|
|
661
|
+
...(facts.perSecond !== undefined && typeof s.elapsed_ms === "number"
|
|
662
|
+
? { cost_so_far_usd: Math.round(Math.ceil(s.elapsed_ms / 1000) * facts.perSecond * 1e6) / 1e6 }
|
|
663
|
+
: {}),
|
|
664
|
+
...(ended && typeof s["cost_usd"] === "number"
|
|
665
|
+
? { cost_usd: s["cost_usd"], billable_seconds: s["billable_seconds"] }
|
|
666
|
+
: {}),
|
|
667
|
+
}, next), lines, 0, ctx.pointer);
|
|
668
|
+
};
|
|
669
|
+
const sandboxPause = async (ctx) => {
|
|
670
|
+
const sid = positional(ctx, "a sandbox id", "promigence sandbox pause sbx_01");
|
|
671
|
+
const res = await client(ctx).sandboxes.pause(sid);
|
|
672
|
+
const next = nexts([sh `promigence sandbox resume ${sid}`, "bring it back and restart the clock"]);
|
|
673
|
+
const bytes = res.bytes;
|
|
674
|
+
const took = res.took_ms;
|
|
675
|
+
const killed = Number(res["background_execs_killed"] ?? 0);
|
|
676
|
+
const lines = [
|
|
677
|
+
`sandbox ${sid}: paused${res.already_paused ? " (already was)" : ""}`,
|
|
678
|
+
" it holds no capacity and bills no seconds while parked",
|
|
679
|
+
...(took === undefined ? [] : [` took ${ms(took)}`]),
|
|
680
|
+
...(bytes?.allocated === undefined ? [] : [` parked image: ${Math.round(bytes.allocated / 1048576)} MiB`]),
|
|
681
|
+
...(killed > 0 ? [` ended ${killed} background process${killed === 1 ? "" : "es"} (started with exec --background)`] : []),
|
|
682
|
+
...(res.warnings ?? []).map((w) => ` warning: ${w}`),
|
|
683
|
+
];
|
|
684
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, { ...res, sandbox_id: sid, state: "paused" }, next), lines, 0, ctx.pointer);
|
|
685
|
+
};
|
|
686
|
+
const sandboxResume = async (ctx) => {
|
|
687
|
+
const sid = positional(ctx, "a sandbox id", "promigence sandbox resume sbx_01");
|
|
688
|
+
const optimizeFor = optimizeForFlag(ctx);
|
|
689
|
+
const cap = str(ctx, "--cap");
|
|
690
|
+
const res = await client(ctx).sandboxes.resume(sid, {
|
|
691
|
+
...(optimizeFor === undefined ? {} : { optimizeFor }),
|
|
692
|
+
...(cap === undefined ? {} : { spendCap: parseUsd(cap, "--cap") }),
|
|
693
|
+
});
|
|
694
|
+
const next = nexts([sh `promigence exec --sandbox ${sid} -- CMD`, "run something in it"]);
|
|
695
|
+
const took = res.took_ms;
|
|
696
|
+
const lines = [
|
|
697
|
+
`sandbox ${sid}: ${res.state ?? "ready"}`,
|
|
698
|
+
...(took === undefined ? [] : [` took ${ms(took)}`]),
|
|
699
|
+
];
|
|
700
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, { ...res, sandbox_id: sid }, next), lines, 0, ctx.pointer);
|
|
701
|
+
};
|
|
702
|
+
const sandboxSnapshot = async (ctx) => {
|
|
703
|
+
const sid = positional(ctx, "a sandbox id", "promigence sandbox snapshot sbx_01");
|
|
704
|
+
const alias = str(ctx, "--alias");
|
|
705
|
+
const force = bool(ctx, "--force");
|
|
706
|
+
const moveAlias = bool(ctx, "--move-alias");
|
|
707
|
+
if (moveAlias && !alias) {
|
|
708
|
+
throw usageError("--move-alias needs --alias", `Name the alias to take: ${sh `promigence sandbox snapshot ${sid} --alias NAME --move-alias`}.`);
|
|
709
|
+
}
|
|
710
|
+
const res = await client(ctx).sandboxes.snapshot(sid, {
|
|
711
|
+
...(alias ? { alias } : {}),
|
|
712
|
+
...(force ? { force: true } : {}),
|
|
713
|
+
...(moveAlias ? { moveAlias: true } : {}),
|
|
714
|
+
});
|
|
715
|
+
const next = nexts([`${sh `promigence run --snapshot ${res.snapshot_id} --count 1 --cap 1`} -- CMD`, "run something on the copy"]);
|
|
716
|
+
const bytes = res.bytes;
|
|
717
|
+
const took = res.took_ms;
|
|
718
|
+
const killed = Number(res.background_execs_killed ?? 0);
|
|
719
|
+
const lines = [
|
|
720
|
+
`snapshot ${res.snapshot_id}${res.alias ? ` (${res.alias})` : ""}`,
|
|
721
|
+
` sandbox ${sid} is still running`,
|
|
722
|
+
...(alias && !res.alias ? [` alias ${alias} was taken by another snapshot meanwhile; this one is unnamed (\`${sh `promigence snapshot alias ${res.snapshot_id}`} NAME\`)`] : []),
|
|
723
|
+
...(bytes?.allocated === undefined ? [] : [` image: ${Math.round(bytes.allocated / 1048576)} MiB`]),
|
|
724
|
+
...(took === undefined ? [] : [` took ${ms(took)}`]),
|
|
725
|
+
...(killed ? [` ended ${killed} background process${killed === 1 ? "" : "es"} to take it`] : []),
|
|
726
|
+
...(res.warnings ?? []).map((w) => ` warning: ${w}`),
|
|
727
|
+
];
|
|
728
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, { ...res, sandbox_id: sid }, next), lines, 0, ctx.pointer);
|
|
729
|
+
};
|
|
730
|
+
const sandboxExtend = async (ctx) => {
|
|
731
|
+
const sid = positional(ctx, "a sandbox id", "promigence sandbox extend sbx_01 --timeout 30m");
|
|
732
|
+
const timeoutMs = parseDuration(required(ctx, "--timeout", "Pass --timeout DUR — the sandbox's new total timeout, e.g. --timeout 30m."), "--timeout");
|
|
733
|
+
const res = await client(ctx).sandboxes.patch(sid, { timeoutMs });
|
|
734
|
+
const cost = res.cost;
|
|
735
|
+
const next = nexts([sh `promigence sandbox info ${sid}`, "the sandbox as it stands now"]);
|
|
736
|
+
const lines = [
|
|
737
|
+
`sandbox ${sid}: timeout now ${duration(timeoutMs)} (${res.committed_seconds ?? "?"} s committed)`,
|
|
738
|
+
...(cost?.added === undefined ? [] : [` this extension commits ${usd(cost.added)}; ${usd(cost.remaining)} left under the cap`]),
|
|
739
|
+
];
|
|
740
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, { ...res, sandbox_id: sid, timeout_ms: timeoutMs }, next), lines, 0, ctx.pointer);
|
|
741
|
+
};
|
|
742
|
+
const sandboxAutopause = async (ctx) => {
|
|
743
|
+
const sid = positional(ctx, "a sandbox id", "promigence sandbox autopause sbx_01 --after 15m");
|
|
744
|
+
const after = str(ctx, "--after");
|
|
745
|
+
const off = bool(ctx, "--off");
|
|
746
|
+
if (after !== undefined && off) {
|
|
747
|
+
throw usageError("--after and --off ask for opposite things", "Pass one: --after DUR sets the idle window, --off removes it, neither reads it.");
|
|
748
|
+
}
|
|
749
|
+
const modeFlag = str(ctx, "--mode");
|
|
750
|
+
if (modeFlag !== undefined && modeFlag !== "requests" && modeFlag !== "activity") {
|
|
751
|
+
throw usageError(`--mode must be requests or activity, got ${JSON.stringify(modeFlag)}`, "Pass --mode activity for an agent that runs inside the sandbox, --mode requests for the default.");
|
|
752
|
+
}
|
|
753
|
+
const mode = modeFlag;
|
|
754
|
+
if (mode !== undefined && off) {
|
|
755
|
+
throw usageError("--mode and --off ask for opposite things", "--off removes the idle rule; --mode says what idle means under one.");
|
|
756
|
+
}
|
|
757
|
+
const rule = (s, m) => s > 0
|
|
758
|
+
? `pauses itself after ${s} s idle${m === "activity" ? " (activity: no CPU, no bytes, no open connection)" : ""}`
|
|
759
|
+
: "never pauses itself on idle";
|
|
760
|
+
const next = nexts([sh `promigence sandbox info ${sid}`, "the sandbox as it stands now"]);
|
|
761
|
+
if (after === undefined && !off && mode === undefined) {
|
|
762
|
+
const s = await client(ctx).sandboxes.get(sid);
|
|
763
|
+
const secs = Number(s.auto_pause_s ?? 0);
|
|
764
|
+
const m = String(s.auto_pause_mode ?? "requests");
|
|
765
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, { sandbox_id: sid, auto_pause_s: secs, auto_pause_mode: m }, next), [`sandbox ${sid}: ${rule(secs, m)}`], 0, ctx.pointer);
|
|
766
|
+
}
|
|
767
|
+
const autoPauseS = after === undefined && !off ? undefined : off ? 0 : Math.round(parseDuration(after, "--after") / 1000);
|
|
768
|
+
const res = await client(ctx).sandboxes.patch(sid, {
|
|
769
|
+
...(autoPauseS !== undefined ? { autoPauseS } : {}),
|
|
770
|
+
...(mode !== undefined ? { autoPauseMode: mode } : {}),
|
|
771
|
+
});
|
|
772
|
+
const secs = Number(res.auto_pause_s ?? autoPauseS ?? 0);
|
|
773
|
+
const m = String(res.auto_pause_mode ?? mode ?? "requests");
|
|
774
|
+
const pausedNow = secs === 0
|
|
775
|
+
? await client(ctx).sandboxes.get(sid).then((x) => x.state === "paused").catch(() => false)
|
|
776
|
+
: false;
|
|
777
|
+
const lines = [
|
|
778
|
+
`sandbox ${sid}: ${rule(secs, m)}`,
|
|
779
|
+
...(secs > 0 ? [
|
|
780
|
+
" a command still running counts as busy, so a long build is safe",
|
|
781
|
+
" a finished background command keeps it awake for a short while after it ends (60 s by default), until its output is read (`promigence exec output X_ID --sandbox SID`) or it is forgotten (`promigence exec kill X_ID --sandbox SID --forget`)",
|
|
782
|
+
] : []),
|
|
783
|
+
...(pausedNow ? [` it is paused now and stays paused until \`${sh `promigence sandbox resume ${sid}`}\`: with the rule off, a request no longer wakes it`] : []),
|
|
784
|
+
];
|
|
785
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, { ...res, sandbox_id: sid, auto_pause_s: secs, auto_pause_mode: m, ...(pausedNow ? { paused: true } : {}) }, pausedNow ? nexts([sh `promigence sandbox resume ${sid}`, "bring it back: with the rule off, a request no longer wakes it"]) : next), lines, 0, ctx.pointer);
|
|
786
|
+
};
|
|
787
|
+
const sandboxProbe = async (ctx) => {
|
|
788
|
+
const sid = positional(ctx, "a sandbox id", "promigence sandbox probe sbx_01");
|
|
789
|
+
const paths = list(ctx, "--path");
|
|
790
|
+
const res = await client(ctx).sandboxes.probe(sid, ...(paths.length ? [{ paths }] : []));
|
|
791
|
+
const ok = res.ok !== false;
|
|
792
|
+
const env = { ...okEnvelope(ctx.cmd.name, { ...res, sandbox_id: sid }, nexts([sh `promigence sandbox info ${sid}`, "the sandbox, its run and the commands it ran"])), ok };
|
|
793
|
+
const lines = [
|
|
794
|
+
`sandbox ${sid}: ${ok ? "matches the verified environment" : "DIFFERS from the verified environment"}`,
|
|
795
|
+
...(res.first_bad_path ? [` first difference at ${res.first_bad_path}`] : []),
|
|
796
|
+
...(res.ms === undefined ? [] : [` checked in ${ms(res.ms)}`]),
|
|
797
|
+
];
|
|
798
|
+
return emit(ctx.mode, env, lines, ok ? 0 : 3, ctx.pointer);
|
|
799
|
+
};
|
|
800
|
+
const filesRm = async (ctx) => {
|
|
801
|
+
const path = positional(ctx, "a path", "promigence files rm /work/out.log --sandbox sbx_01");
|
|
802
|
+
if (!path.startsWith("/")) {
|
|
803
|
+
throw usageError(`'${path}' is not an absolute path`, "Paths inside a sandbox are absolute: promigence files rm /work/out.log --sandbox <sid>.");
|
|
804
|
+
}
|
|
805
|
+
const sid = required(ctx, "--sandbox", "Pass --sandbox sbx_<id> — the sandbox the path is in.");
|
|
806
|
+
const recursive = bool(ctx, "--recursive");
|
|
807
|
+
const res = await client(ctx).sandboxes.files.delete(sid, path, { recursive });
|
|
808
|
+
const next = nexts([sh `promigence files ls --sandbox ${sid}`, "what is left"]);
|
|
809
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, { sandbox_id: sid, path, deleted: res.deleted, existed: res.existed, recursive }, next), [`${sid}:${path}: ${res.deleted ? "deleted" : "not deleted"}${res.existed === false ? " (it was not there)" : ""}`], 0, ctx.pointer);
|
|
810
|
+
};
|
|
811
|
+
const secretsList = async (ctx) => {
|
|
812
|
+
const { secrets } = await client(ctx).secrets.list();
|
|
813
|
+
const next = nexts(["promigence run --snapshot H --count N --cap USD --secret NAME -- CMD", "give one to every sandbox of a run"], ["promigence secrets set NAME --from-env VAR", "store or rotate one"]);
|
|
814
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, { secrets, count: secrets.length }, next), [
|
|
815
|
+
`promigence secrets: ${secrets.length} stored`,
|
|
816
|
+
...secrets.flatMap(secretListLines),
|
|
817
|
+
...(secrets.length ? [] : [" (none)"]),
|
|
818
|
+
], 0, false);
|
|
819
|
+
};
|
|
820
|
+
const secretsDelete = async (ctx) => {
|
|
821
|
+
const name = positional(ctx, "a secret name", "promigence secrets delete OPENAI_API_KEY");
|
|
822
|
+
const res = await client(ctx).secrets.delete(name);
|
|
823
|
+
const next = nexts(["promigence secrets list", "what this org still has stored"]);
|
|
824
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, { name: res.name, deleted: res.deleted, note: KEPT_BY_HOLDERS }, next), [`${sh `promigence secrets delete ${name}`}: removed`, KEPT_BY_HOLDERS], 0, false);
|
|
825
|
+
};
|
|
826
|
+
export const ACCOUNT_HANDLERS = {
|
|
827
|
+
me,
|
|
828
|
+
status,
|
|
829
|
+
"billing usage": billingUsage,
|
|
830
|
+
"billing portal": billingPortal,
|
|
831
|
+
"billing history": billingHistory,
|
|
832
|
+
referrals,
|
|
833
|
+
"referrals apply": referralsApply,
|
|
834
|
+
"snapshot delete": snapshotDelete,
|
|
835
|
+
"snapshot builds": snapshotBuilds,
|
|
836
|
+
"snapshot alias": snapshotAlias,
|
|
837
|
+
"run list": runList,
|
|
838
|
+
"image resolve": imageResolve,
|
|
839
|
+
"sandbox list": sandboxList,
|
|
840
|
+
"sandbox info": sandboxInfo,
|
|
841
|
+
"sandbox pause": sandboxPause,
|
|
842
|
+
"sandbox resume": sandboxResume,
|
|
843
|
+
"sandbox snapshot": sandboxSnapshot,
|
|
844
|
+
"sandbox extend": sandboxExtend,
|
|
845
|
+
"sandbox autopause": sandboxAutopause,
|
|
846
|
+
"sandbox probe": sandboxProbe,
|
|
847
|
+
"files rm": filesRm,
|
|
848
|
+
"secrets list": secretsList,
|
|
849
|
+
"secrets delete": secretsDelete,
|
|
850
|
+
};
|