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
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { PromigenceError } from "./sdk/index.js";
|
|
2
|
+
import { bool, nexts } from "./commands.js";
|
|
3
|
+
import { emit, okEnvelope, write } from "./io.js";
|
|
4
|
+
import { headless, openBrowser } from "./login.js";
|
|
5
|
+
import { jsonLine } from "./term.js";
|
|
6
|
+
const NAMES = { github: "GitHub", gitlab: "GitLab" };
|
|
7
|
+
const CONNECTED = new Set(["connected", "updated", "already_connected"]);
|
|
8
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
9
|
+
export async function consoleTicket(mint) {
|
|
10
|
+
try {
|
|
11
|
+
return await mint();
|
|
12
|
+
}
|
|
13
|
+
catch (e) {
|
|
14
|
+
if (e instanceof PromigenceError && e.code === "not_found")
|
|
15
|
+
return null;
|
|
16
|
+
throw e;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function endLine(host, r) {
|
|
20
|
+
const a = r.account ?? `the ${NAMES[host]} account`;
|
|
21
|
+
switch (r.state) {
|
|
22
|
+
case "connected":
|
|
23
|
+
return `connected: ${a}`;
|
|
24
|
+
case "updated":
|
|
25
|
+
return `already connected: ${a} (repository access updated)`;
|
|
26
|
+
case "already_connected":
|
|
27
|
+
return `already connected: ${a}`;
|
|
28
|
+
default:
|
|
29
|
+
return `${r.state}: ${r.message}`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function fixFor(host, r) {
|
|
33
|
+
const again = `\`promigence ${host} connect\``;
|
|
34
|
+
const n = NAMES[host];
|
|
35
|
+
switch (r.state) {
|
|
36
|
+
case "wrong_user":
|
|
37
|
+
return `The page was opened in a browser signed in to another Promigence account, so nothing was started. Run ${again} again and open the page where you are signed in to the console as yourself.`;
|
|
38
|
+
case "declined":
|
|
39
|
+
return `The connection was declined, so nothing was connected. Run ${again} again to start over.`;
|
|
40
|
+
case "other_org":
|
|
41
|
+
return `That ${n} account is connected to another Promigence organization. Disconnect it there first (\`promigence ${host} disconnect\` with that organization's key), then run ${again} again.`;
|
|
42
|
+
case "needs_admin":
|
|
43
|
+
return `Connect it signed in to ${n} as an owner (admin) of that account, or ask one of its owners to; then run ${again} again.`;
|
|
44
|
+
case "not_installer":
|
|
45
|
+
return `The ${n} user who installed the App on that account can connect it now; an owner of the account can 15 minutes after the install. Then run ${again} again.`;
|
|
46
|
+
default:
|
|
47
|
+
return `Run ${again} again. If it fails the same way, write to support@promigence.ai with the time it happened.`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export async function consoleConnect(ctx, host, o) {
|
|
51
|
+
const n = NAMES[host];
|
|
52
|
+
const url = o.ticket.connect_url;
|
|
53
|
+
const opened = bool(ctx, "--no-open") || headless() ? false : openBrowser(url);
|
|
54
|
+
const noWait = bool(ctx, "--no-wait");
|
|
55
|
+
if (ctx.mode === "json") {
|
|
56
|
+
await write(process.stderr, jsonLine(process.stderr, {
|
|
57
|
+
hint: "connect_url", connect_url: url, request_id: o.ticket.request_id, expires_at: o.ticket.expires_at, opened,
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
await write(process.stderr, [
|
|
62
|
+
opened
|
|
63
|
+
? `Opening the Promigence console to connect ${n}…`
|
|
64
|
+
: `Open this page in a browser where you are signed in to the Promigence console (any device works):`,
|
|
65
|
+
` ${opened ? "If it did not open: " : ""}${url}`,
|
|
66
|
+
host === "github"
|
|
67
|
+
? " It starts the connection on GitHub, where you choose the account, and all repositories or only the ones you pick."
|
|
68
|
+
: " It starts the connection on GitLab, where you approve read-only access.",
|
|
69
|
+
" The page works once, within 5 minutes, and only signed in to the console as you, an owner of this organization: keep it to yourself.",
|
|
70
|
+
...(noWait ? [] : [` Waiting up to ${o.waitText} (Ctrl-C stops waiting; the connection still finishes in the browser).`]),
|
|
71
|
+
].join("\n") + "\n");
|
|
72
|
+
}
|
|
73
|
+
if (noWait) {
|
|
74
|
+
const next = nexts([`promigence ${host} status`, "once it is done: the connection"]);
|
|
75
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, {
|
|
76
|
+
connect_url: url, request_id: o.ticket.request_id, expires_at: o.ticket.expires_at, opened, waited: false, connected: [],
|
|
77
|
+
}, next), [`connect page: ${url}`, ` next: ${next[0].cmd}`], 0, ctx.pointer);
|
|
78
|
+
}
|
|
79
|
+
const t0 = Date.now();
|
|
80
|
+
let said = "";
|
|
81
|
+
for (;;) {
|
|
82
|
+
await sleep(Math.min(o.pollMs, Math.max(0, o.bound - (Date.now() - t0))));
|
|
83
|
+
const r = await o.poll();
|
|
84
|
+
if (r.state !== "pending")
|
|
85
|
+
return finish(ctx, host, r, { url, opened, waitedMs: Date.now() - t0, ...o });
|
|
86
|
+
if (r.message !== said && ctx.mode !== "json") {
|
|
87
|
+
said = r.message;
|
|
88
|
+
await write(process.stderr, ` ${r.message}\n`);
|
|
89
|
+
}
|
|
90
|
+
if (Date.now() - t0 >= o.bound) {
|
|
91
|
+
throw new PromigenceError({
|
|
92
|
+
code: "timeout",
|
|
93
|
+
message: `no ${n} connection finished within ${o.waitText}`,
|
|
94
|
+
fix: `Finish it in the browser if it is still open, then \`promigence ${host} status\`. Otherwise run \`promigence ${host} connect\` again for a new page (each works once, within 5 minutes).`,
|
|
95
|
+
exitCode: 11,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function finish(ctx, host, r, o) {
|
|
101
|
+
const n = NAMES[host];
|
|
102
|
+
if (CONNECTED.has(r.state)) {
|
|
103
|
+
const connected = await o.connections(r);
|
|
104
|
+
const next = nexts(...o.okNext);
|
|
105
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, {
|
|
106
|
+
connect_url: o.url, opened: o.opened, waited: true, waited_ms: o.waitedMs, state: r.state, request: r, connected,
|
|
107
|
+
}, next), [endLine(host, r), ` ${r.message}`, ` next: ${next[0].cmd}`], 0, ctx.pointer);
|
|
108
|
+
}
|
|
109
|
+
if (r.state === "pending_provider_approval") {
|
|
110
|
+
const next = nexts([`promigence ${host} connect`, `once an owner of the account approved it on ${n}`]);
|
|
111
|
+
const line = `waiting for an owner of ${r.account ?? "that account"} to approve it on ${n}; run \`promigence ${host} connect\` again after`;
|
|
112
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, {
|
|
113
|
+
connect_url: o.url, opened: o.opened, waited: true, waited_ms: o.waitedMs, state: r.state, request: r, connected: [],
|
|
114
|
+
}, next), [line, ` ${r.message}`], 0, ctx.pointer);
|
|
115
|
+
}
|
|
116
|
+
if (r.state === "expired") {
|
|
117
|
+
throw new PromigenceError({
|
|
118
|
+
code: "timeout",
|
|
119
|
+
message: r.message,
|
|
120
|
+
fix: `Nothing was connected. Run \`promigence ${host} connect\` again and finish it in the browser: the page works within 5 minutes, the connection within 15.`,
|
|
121
|
+
exitCode: 11,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
throw new PromigenceError({ code: "not_connected", message: `${r.state}: ${r.message}`, fix: fixFor(host, r), exitCode: 7 });
|
|
125
|
+
}
|
package/dist/credfile.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { chmodSync, closeSync, constants, fsyncSync, lstatSync, mkdirSync, openSync, renameSync, statSync, unlinkSync, writeSync, } from "node:fs";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import { DEFAULT_API_URL, PromigenceClient, credentialsPath, storedCredentialsText } from "./sdk/index.js";
|
|
5
|
+
import { encodeCredentials, keychainCacheSet, keychainDelete, keychainRead, keychainStore, keychainWrite, keychainsToClear, storeLabel, } from "./sdk/credstore.js";
|
|
6
|
+
import { recordNotice, usageError } from "./io.js";
|
|
7
|
+
import { sh } from "./term.js";
|
|
8
|
+
function lstatOrUndefined(path) {
|
|
9
|
+
try {
|
|
10
|
+
return lstatSync(path);
|
|
11
|
+
}
|
|
12
|
+
catch (e) {
|
|
13
|
+
if (e.code === "ENOENT")
|
|
14
|
+
return undefined;
|
|
15
|
+
throw e;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function secureConfigDir(dir) {
|
|
19
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
20
|
+
const st = statSync(dir);
|
|
21
|
+
if (!st.isDirectory()) {
|
|
22
|
+
throw usageError(`${dir} is not a directory`, `Move it aside; the CLI keeps its credentials in ${dir}/.`);
|
|
23
|
+
}
|
|
24
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
25
|
+
if (uid === undefined)
|
|
26
|
+
return;
|
|
27
|
+
if (st.uid !== uid) {
|
|
28
|
+
throw usageError(`${dir} belongs to another user (uid ${st.uid})`, `Refusing to store an API key there. Remove it or set XDG_CONFIG_HOME to a directory you own.`);
|
|
29
|
+
}
|
|
30
|
+
if ((st.mode & 0o077) !== 0)
|
|
31
|
+
chmodSync(dir, 0o700);
|
|
32
|
+
}
|
|
33
|
+
export function checkCredentialsPath(path) {
|
|
34
|
+
secureConfigDir(dirname(path));
|
|
35
|
+
const st = lstatOrUndefined(path);
|
|
36
|
+
if (st && !st.isFile()) {
|
|
37
|
+
throw usageError(`${path} is ${st.isSymbolicLink() ? "a symlink" : "not a regular file"}; refusing to write an API key through it`, `Remove it (rm ${path}) and run the command again.`);
|
|
38
|
+
}
|
|
39
|
+
return st;
|
|
40
|
+
}
|
|
41
|
+
export function writeCredentialsFile(path, body) {
|
|
42
|
+
const dir = dirname(path);
|
|
43
|
+
const st = checkCredentialsPath(path);
|
|
44
|
+
if (body === undefined) {
|
|
45
|
+
if (st)
|
|
46
|
+
unlinkSync(path);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const tmp = join(dir, `.${basename(path)}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`);
|
|
50
|
+
const flags = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0);
|
|
51
|
+
const fd = openSync(tmp, flags, 0o600);
|
|
52
|
+
try {
|
|
53
|
+
writeSync(fd, JSON.stringify(body, null, 2) + "\n");
|
|
54
|
+
fsyncSync(fd);
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
closeSync(fd);
|
|
58
|
+
try {
|
|
59
|
+
unlinkSync(tmp);
|
|
60
|
+
}
|
|
61
|
+
catch { }
|
|
62
|
+
throw e;
|
|
63
|
+
}
|
|
64
|
+
closeSync(fd);
|
|
65
|
+
try {
|
|
66
|
+
chmodSync(tmp, 0o600);
|
|
67
|
+
renameSync(tmp, path);
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
try {
|
|
71
|
+
unlinkSync(tmp);
|
|
72
|
+
}
|
|
73
|
+
catch { }
|
|
74
|
+
throw e;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export const MINTED_BY = "promigence login";
|
|
78
|
+
export function loginMintedKey(env = process.env) {
|
|
79
|
+
const stored = storedCredentialsText(env);
|
|
80
|
+
if (stored === undefined)
|
|
81
|
+
return undefined;
|
|
82
|
+
try {
|
|
83
|
+
const j = JSON.parse(stored.text.trim());
|
|
84
|
+
const [apiKey, keyId, apiUrl] = [j["api_key"], j["key_id"], j["api_url"]];
|
|
85
|
+
if (j["minted_by"] !== MINTED_BY || typeof apiKey !== "string" || typeof keyId !== "string" || !apiKey || !keyId)
|
|
86
|
+
return undefined;
|
|
87
|
+
return { apiKey, keyId, apiUrl: typeof apiUrl === "string" ? apiUrl : undefined };
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export function storedWhere(r) {
|
|
94
|
+
return r.store === "file" ? `${r.path} (0600)` : storeLabel(r.store);
|
|
95
|
+
}
|
|
96
|
+
const isCi = (env) => {
|
|
97
|
+
const v = (env["CI"] ?? "").trim().toLowerCase();
|
|
98
|
+
return v !== "" && v !== "0" && v !== "false";
|
|
99
|
+
};
|
|
100
|
+
function message(e) {
|
|
101
|
+
return e.message ?? String(e);
|
|
102
|
+
}
|
|
103
|
+
function clearFix(store, path) {
|
|
104
|
+
return store === "macos-keychain"
|
|
105
|
+
? `unlock the keychain and run \`promigence logout\` again, or remove it: security delete-generic-password -s promigence-cli -a "${path}"`
|
|
106
|
+
: `unlock the keyring and run \`promigence logout\` again, or remove it: secret-tool clear service promigence-cli account "${path}"`;
|
|
107
|
+
}
|
|
108
|
+
export function storeCredentials(body, env = process.env) {
|
|
109
|
+
const path = credentialsPath(env);
|
|
110
|
+
const chosen = keychainStore(env);
|
|
111
|
+
const warnings = [];
|
|
112
|
+
const left = [];
|
|
113
|
+
const clearOthers = (except) => {
|
|
114
|
+
for (const k of keychainsToClear(env)) {
|
|
115
|
+
if (k === except)
|
|
116
|
+
continue;
|
|
117
|
+
const d = keychainDelete(k, path, env);
|
|
118
|
+
if (d.ok) {
|
|
119
|
+
keychainCacheSet(k, path, undefined);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
left.push(k);
|
|
123
|
+
warnings.push(`a sign-in in ${storeLabel(k)} could not be removed (${d.reason}); ${clearFix(k, path)}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
if (body === undefined || !body["api_key"]) {
|
|
128
|
+
writeCredentialsFile(path, body !== undefined && Object.keys(body).length > 0 ? body : undefined);
|
|
129
|
+
clearOthers();
|
|
130
|
+
return { store: "file", path, warnings, left };
|
|
131
|
+
}
|
|
132
|
+
let reason;
|
|
133
|
+
if (chosen !== undefined) {
|
|
134
|
+
const text = encodeCredentials(body);
|
|
135
|
+
const w = keychainWrite(chosen, path, text, env);
|
|
136
|
+
if (w.ok) {
|
|
137
|
+
const back = keychainRead(chosen, path, env);
|
|
138
|
+
if (back.status === "found" && back.text === text) {
|
|
139
|
+
keychainCacheSet(chosen, path, text);
|
|
140
|
+
try {
|
|
141
|
+
writeCredentialsFile(path, undefined);
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
try {
|
|
145
|
+
writeCredentialsFile(path, body);
|
|
146
|
+
}
|
|
147
|
+
catch { }
|
|
148
|
+
warnings.push(`the credentials file ${path} could not be removed (${message(e)}); it holds the same key as ${storeLabel(chosen)}. Remove it: rm ${path}`);
|
|
149
|
+
}
|
|
150
|
+
clearOthers(chosen);
|
|
151
|
+
return { store: chosen, path, warnings, left };
|
|
152
|
+
}
|
|
153
|
+
keychainDelete(chosen, path, env);
|
|
154
|
+
keychainCacheSet(chosen, path, undefined);
|
|
155
|
+
reason = back.status === "found" ? "it gave back something else" : back.status === "absent" ? "it did not keep the item" : back.reason;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
reason = w.reason;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
writeCredentialsFile(path, body);
|
|
162
|
+
clearOthers();
|
|
163
|
+
let note;
|
|
164
|
+
if (chosen !== undefined)
|
|
165
|
+
note = `${storeLabel(chosen)} could not take the key (${reason}), so it is in ${path} (0600)`;
|
|
166
|
+
else if ((env["PROMIGENCE_CREDENTIAL_STORE"] ?? "").trim().toLowerCase() !== "file" && process.stderr.isTTY && !isCi(env)) {
|
|
167
|
+
note = `no OS keychain is available here, so the key is in ${path} (0600)`;
|
|
168
|
+
}
|
|
169
|
+
return note === undefined ? { store: "file", path, warnings, left } : { store: "file", path, note, warnings, left };
|
|
170
|
+
}
|
|
171
|
+
export async function revokeReplacedKey(previous, why, replacement) {
|
|
172
|
+
try {
|
|
173
|
+
await new PromigenceClient({ apiKey: previous.apiKey, apiUrl: previous.apiUrl ?? DEFAULT_API_URL, onNotice: recordNotice })
|
|
174
|
+
.request({
|
|
175
|
+
method: "DELETE", path: `/keys/${encodeURIComponent(previous.keyId)}?reason=replaced`,
|
|
176
|
+
...(replacement ? { headers: { "promigence-replaced-by": replacement } } : {}),
|
|
177
|
+
});
|
|
178
|
+
return { revoked: previous.keyId };
|
|
179
|
+
}
|
|
180
|
+
catch (e) {
|
|
181
|
+
const err = e;
|
|
182
|
+
if (err.code === "auth")
|
|
183
|
+
return {};
|
|
184
|
+
return {
|
|
185
|
+
warning: `the key ${why} (${previous.keyId}) was not revoked (${err.code ?? "error"}: ${err.message}); revoke it with \`${sh `promigence keys revoke ${previous.keyId}`}\``,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
package/dist/credits.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
import { PromigenceClient } from "./sdk/index.js";
|
|
3
|
+
import { bool, clientOpts, nexts } from "./commands.js";
|
|
4
|
+
import { deviceFetch, deviceHeader } from "./device.js";
|
|
5
|
+
import { emit, okEnvelope, recordNotice, write } from "./io.js";
|
|
6
|
+
import { headless, openBrowser } from "./login.js";
|
|
7
|
+
import { usd } from "./money.js";
|
|
8
|
+
export function dollars(n) {
|
|
9
|
+
if (typeof n !== "number" || !Number.isFinite(n))
|
|
10
|
+
return "free credit";
|
|
11
|
+
return Number.isInteger(n) ? `$${n}` : usd(n);
|
|
12
|
+
}
|
|
13
|
+
export function campaignName(c) {
|
|
14
|
+
if (!c)
|
|
15
|
+
return undefined;
|
|
16
|
+
if (typeof c === "string")
|
|
17
|
+
return c;
|
|
18
|
+
return typeof c.name === "string" ? c.name : typeof c.campaign_id === "string" ? c.campaign_id : undefined;
|
|
19
|
+
}
|
|
20
|
+
export function stateWords(o) {
|
|
21
|
+
switch (o.state) {
|
|
22
|
+
case "claimable": return `${dollars(o.credit_usd)} of free credit is waiting for you to claim it`;
|
|
23
|
+
case "claimed": return "your free credit is claimed";
|
|
24
|
+
case "pending_review": return "we are checking your free credit (the claim page says what releases it)";
|
|
25
|
+
case "needs_card": return "free credit for this account comes with a card on file";
|
|
26
|
+
case "not_eligible": return "this account is not eligible for the free credit";
|
|
27
|
+
case "offer_ended": return "the free-credit offer has ended";
|
|
28
|
+
case "none": return "there is no free-credit offer for this account";
|
|
29
|
+
default: return `free credit: ${o.state}`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const say = (o) => (typeof o.message === "string" && o.message.trim() ? o.message.trim() : stateWords(o));
|
|
33
|
+
function offerNext(o) {
|
|
34
|
+
switch (o.state) {
|
|
35
|
+
case "claimable": return nexts(["promigence credits claim", "open the claim page and accept the terms"]);
|
|
36
|
+
case "needs_card": return nexts(["promigence billing card", "add a card; the card bonus comes with it"]);
|
|
37
|
+
case "pending_review": return nexts(["promigence credits", "check again later"]);
|
|
38
|
+
default: return nexts(["promigence me", "your org, limits and credit"]);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function getOffer(c) {
|
|
42
|
+
return c.request({ method: "GET", path: "/credits/offer" });
|
|
43
|
+
}
|
|
44
|
+
export function getClaimLink(c) {
|
|
45
|
+
return c.request({ method: "POST", path: "/credits/claim-link", body: {} });
|
|
46
|
+
}
|
|
47
|
+
export function claimClient(opts) {
|
|
48
|
+
const f = deviceFetch(deviceHeader());
|
|
49
|
+
return new PromigenceClient({ ...opts, ...(f ? { fetch: f } : {}) });
|
|
50
|
+
}
|
|
51
|
+
function claimLines(o, link, opened) {
|
|
52
|
+
const credit = dollars(o.credit_usd);
|
|
53
|
+
return [
|
|
54
|
+
opened
|
|
55
|
+
? `You have ${credit} of free credit waiting. Opening the claim page…`
|
|
56
|
+
: `You have ${credit} of free credit waiting. Open the claim page to accept the terms and claim it:`,
|
|
57
|
+
` ${opened ? "If it did not open: " : ""}${link.url}`,
|
|
58
|
+
` The link works once${link.expires_at ? `, until ${link.expires_at}` : ", for 15 minutes"}; for a new one: promigence credits claim`,
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
function maybeOpen(ctx, url) {
|
|
62
|
+
if (bool(ctx, "--no-open") || bool(ctx, "--no-browser") || headless())
|
|
63
|
+
return false;
|
|
64
|
+
return openBrowser(url);
|
|
65
|
+
}
|
|
66
|
+
export async function offerAfterLogin(ctx, apiUrl, apiKey, consoleHome) {
|
|
67
|
+
const opts = { apiKey, apiUrl, onNotice: recordNotice, requestTimeoutMs: 10_000, maxRetries: 1 };
|
|
68
|
+
let offer;
|
|
69
|
+
try {
|
|
70
|
+
offer = await getOffer(new PromigenceClient(opts));
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
const err = e;
|
|
74
|
+
if (err?.code === "not_found")
|
|
75
|
+
return { field: null, lines: [] };
|
|
76
|
+
return { field: null, lines: [" free credit: could not check just now; see `promigence credits`"] };
|
|
77
|
+
}
|
|
78
|
+
if (!offer || typeof offer.state !== "string")
|
|
79
|
+
return { field: null, lines: [] };
|
|
80
|
+
if (offer.state === "none")
|
|
81
|
+
return { field: { ...offer }, lines: [] };
|
|
82
|
+
if (offer.state !== "claimable")
|
|
83
|
+
return { field: { ...offer }, lines: [` free credit: ${say(offer)}`] };
|
|
84
|
+
if (consoleHome) {
|
|
85
|
+
return {
|
|
86
|
+
field: { ...offer, claim_url: consoleHome, opened: false },
|
|
87
|
+
lines: [` free credit: ${dollars(offer.credit_usd)} is waiting; claim it in the console: ${consoleHome}`],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
let link;
|
|
91
|
+
try {
|
|
92
|
+
link = await getClaimLink(claimClient(opts));
|
|
93
|
+
if (!link || typeof link.url !== "string")
|
|
94
|
+
throw new Error("no url");
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return {
|
|
98
|
+
field: { ...offer },
|
|
99
|
+
lines: [` free credit: ${dollars(offer.credit_usd)} is waiting; claim it with \`promigence credits claim\``],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const opened = maybeOpen(ctx, link.url);
|
|
103
|
+
return {
|
|
104
|
+
field: { ...offer, claim_url: link.url, claim_expires_at: link.expires_at ?? null, opened },
|
|
105
|
+
lines: claimLines(offer, link, opened),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
async function askLine(prompt) {
|
|
109
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
110
|
+
try {
|
|
111
|
+
return await new Promise((resolve) => rl.question(prompt, resolve));
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
rl.close();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export function canAsk(env = process.env) {
|
|
118
|
+
const ci = (env["CI"] ?? "").trim().toLowerCase();
|
|
119
|
+
return Boolean(process.stdin.isTTY && process.stderr.isTTY) && (ci === "" || ci === "0" || ci === "false");
|
|
120
|
+
}
|
|
121
|
+
export async function claimOnRefusal(ctx, io = {}) {
|
|
122
|
+
const opts = { ...clientOpts(ctx), onNotice: recordNotice };
|
|
123
|
+
let o;
|
|
124
|
+
try {
|
|
125
|
+
o = await getOffer(new PromigenceClient(opts));
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
if (!o || o.state !== "claimable")
|
|
131
|
+
return undefined;
|
|
132
|
+
const credit = typeof o.credit_usd === "number" ? `${dollars(o.credit_usd)} of free credit` : "free credit";
|
|
133
|
+
await write(process.stderr, `This account has ${credit} waiting to be claimed; nothing was started or charged.\n`);
|
|
134
|
+
const answer = await (io.ask ?? askLine)("Claim it now? It opens the claim page in your browser, where you accept the terms. [y/N] ");
|
|
135
|
+
if (!/^\s*y(es)?\s*$/i.test(answer))
|
|
136
|
+
return ["Not claimed. When you are ready: promigence credits claim"];
|
|
137
|
+
let link;
|
|
138
|
+
try {
|
|
139
|
+
link = await getClaimLink(claimClient(opts));
|
|
140
|
+
if (!link || typeof link.url !== "string")
|
|
141
|
+
throw new Error("no claim link in the answer");
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
const err = e;
|
|
145
|
+
return [
|
|
146
|
+
`Could not open the claim page: ${err?.message ?? String(e)}`,
|
|
147
|
+
` Try again: promigence credits claim${err?.code === "forbidden" ? " (owners; members ask an owner)" : ""}`,
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
const opened = maybeOpen(ctx, link.url);
|
|
151
|
+
return [...claimLines(o, link, opened), "Once it is claimed, run the command again."];
|
|
152
|
+
}
|
|
153
|
+
async function cardState(c, o) {
|
|
154
|
+
const none = { off: false, onFile: false };
|
|
155
|
+
if (!(typeof o.card_bonus_usd === "number" && o.card_bonus_usd > 0) || o.state === "none")
|
|
156
|
+
return none;
|
|
157
|
+
return c.billing.usage().then((b) => ({ off: b.billing_enabled === false, onFile: b.card_on_file === true })).catch(() => none);
|
|
158
|
+
}
|
|
159
|
+
function offerLines(head, o, st = { off: false, onFile: false }) {
|
|
160
|
+
const lines = [`${head}: ${say(o)}`];
|
|
161
|
+
if (o.state !== "none") {
|
|
162
|
+
if (typeof o.credit_usd === "number")
|
|
163
|
+
lines.push(` credit: ${dollars(o.credit_usd)}`);
|
|
164
|
+
if (typeof o.card_bonus_usd === "number" && o.card_bonus_usd > 0) {
|
|
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)"}`);
|
|
168
|
+
}
|
|
169
|
+
if (o.expires_at)
|
|
170
|
+
lines.push(` expires: ${o.expires_at}`);
|
|
171
|
+
const camp = campaignName(o.campaign);
|
|
172
|
+
if (camp)
|
|
173
|
+
lines.push(` campaign: ${camp}`);
|
|
174
|
+
}
|
|
175
|
+
return lines;
|
|
176
|
+
}
|
|
177
|
+
const credits = async (ctx) => {
|
|
178
|
+
const c = new PromigenceClient({ ...clientOpts(ctx), onNotice: recordNotice });
|
|
179
|
+
const o = await getOffer(c);
|
|
180
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, { ...o }, offerNext(o)), offerLines("promigence credits", o, await cardState(c, o)), 0, ctx.pointer);
|
|
181
|
+
};
|
|
182
|
+
const creditsClaim = async (ctx) => {
|
|
183
|
+
const opts = { ...clientOpts(ctx), onNotice: recordNotice };
|
|
184
|
+
const c = new PromigenceClient(opts);
|
|
185
|
+
const o = await getOffer(c);
|
|
186
|
+
if (o.state !== "claimable") {
|
|
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);
|
|
188
|
+
}
|
|
189
|
+
const link = await getClaimLink(claimClient(opts));
|
|
190
|
+
const opened = maybeOpen(ctx, link.url);
|
|
191
|
+
const next = nexts(["promigence credits", "after the claim page: the credit and its expiry"]);
|
|
192
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, { ...o, claim_url: link.url, claim_expires_at: link.expires_at ?? null, opened }, next), claimLines(o, link, opened), 0, ctx.pointer);
|
|
193
|
+
};
|
|
194
|
+
export const CREDITS_HANDLERS = {
|
|
195
|
+
credits,
|
|
196
|
+
"credits claim": creditsClaim,
|
|
197
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { type TierEntry } from "./generated.js";
|
|
2
|
+
export declare const DEVCONTAINER_SPEC = "containers.dev json_reference + features, read 2026-09-18";
|
|
3
|
+
export declare const DEVCONTAINER_NAMES: readonly [".devcontainer/devcontainer.json", ".devcontainer.json"];
|
|
4
|
+
export declare class DevcontainerError extends Error {
|
|
5
|
+
readonly kind: string;
|
|
6
|
+
readonly fix: string;
|
|
7
|
+
readonly file: string | undefined;
|
|
8
|
+
constructor(kind: string, message: string, fix: string, file?: string);
|
|
9
|
+
}
|
|
10
|
+
export interface JsoncPosition {
|
|
11
|
+
line: number;
|
|
12
|
+
column: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function parseJsonc(text: string, file?: string): unknown;
|
|
15
|
+
export declare function discover(root: string): string[];
|
|
16
|
+
export declare function resolveGiven(pathArg: string): string;
|
|
17
|
+
export declare function detectOne(root: string, asked?: boolean): string | undefined;
|
|
18
|
+
export declare function workspaceRootFor(file: string): string;
|
|
19
|
+
export interface LoadedDevcontainer {
|
|
20
|
+
path: string;
|
|
21
|
+
dir: string;
|
|
22
|
+
config: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
export declare function load(file: string): LoadedDevcontainer;
|
|
25
|
+
export interface SubstitutionContext {
|
|
26
|
+
localWorkspaceFolder: string;
|
|
27
|
+
localWorkspaceFolderBasename: string;
|
|
28
|
+
containerWorkspaceFolder: string;
|
|
29
|
+
containerWorkspaceFolderBasename: string;
|
|
30
|
+
localEnv: Readonly<Record<string, string | undefined>>;
|
|
31
|
+
containerEnv: Readonly<Record<string, string>>;
|
|
32
|
+
devcontainerId: string;
|
|
33
|
+
allowContainerEnv: boolean;
|
|
34
|
+
property: string;
|
|
35
|
+
file?: string | undefined;
|
|
36
|
+
}
|
|
37
|
+
export declare function substituteString(input: string, ctx: SubstitutionContext): string;
|
|
38
|
+
export declare function substituteDeep<T>(value: T, ctx: SubstitutionContext): T;
|
|
39
|
+
export declare function devcontainerId(workspaceRoot: string, file: string): string;
|
|
40
|
+
export type PlanSource = {
|
|
41
|
+
kind: "image";
|
|
42
|
+
image: string;
|
|
43
|
+
} | {
|
|
44
|
+
kind: "dockerfile";
|
|
45
|
+
dockerfile: string;
|
|
46
|
+
context: string;
|
|
47
|
+
};
|
|
48
|
+
export interface LifecycleStep {
|
|
49
|
+
field: string;
|
|
50
|
+
command: string;
|
|
51
|
+
label?: string;
|
|
52
|
+
}
|
|
53
|
+
export interface IgnoredProperty {
|
|
54
|
+
field: string;
|
|
55
|
+
why: string;
|
|
56
|
+
}
|
|
57
|
+
export interface Plan {
|
|
58
|
+
spec: typeof DEVCONTAINER_SPEC;
|
|
59
|
+
file: string;
|
|
60
|
+
name: string | undefined;
|
|
61
|
+
source: PlanSource;
|
|
62
|
+
env: Record<string, string>;
|
|
63
|
+
workspaceFolder: string;
|
|
64
|
+
user: string | undefined;
|
|
65
|
+
remoteUser?: string | undefined;
|
|
66
|
+
lifecycle: LifecycleStep[];
|
|
67
|
+
ports: number[];
|
|
68
|
+
tier: string | undefined;
|
|
69
|
+
tierReason: string | undefined;
|
|
70
|
+
ignored: IgnoredProperty[];
|
|
71
|
+
notes: string[];
|
|
72
|
+
localEnvBaked?: string[];
|
|
73
|
+
}
|
|
74
|
+
export interface TranslateOptions {
|
|
75
|
+
workspaceRoot: string;
|
|
76
|
+
localEnv?: Readonly<Record<string, string | undefined>>;
|
|
77
|
+
tiers?: readonly TierEntry[];
|
|
78
|
+
imageOverride?: string | undefined;
|
|
79
|
+
allowLocalEnv?: boolean;
|
|
80
|
+
}
|
|
81
|
+
export declare function translate(loaded: LoadedDevcontainer, opts: TranslateOptions): Plan;
|
|
82
|
+
export declare const OPT_IN_TIERS: ReadonlySet<string>;
|
|
83
|
+
export declare function sizeGib(v: unknown, field: string, file: string): number;
|
|
84
|
+
export declare function lifecycleSteps(v: unknown, field: string, ctx: SubstitutionContext, file: string): LifecycleStep[];
|
|
85
|
+
export declare function shellQuote(s: string): string;
|
|
86
|
+
export declare function wrapCommand(plan: Plan, command: string): string;
|
|
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;
|
|
92
|
+
export interface SetupOptions {
|
|
93
|
+
repo?: string | undefined;
|
|
94
|
+
commit?: string | undefined;
|
|
95
|
+
userSetup?: readonly string[];
|
|
96
|
+
}
|
|
97
|
+
export declare function planSetup(plan: Plan, opts?: SetupOptions): string[];
|
|
98
|
+
export declare function planJson(plan: Plan, setup: readonly string[]): Record<string, unknown>;
|
|
99
|
+
export declare function planLines(plan: Plan, setup: readonly string[]): string[];
|