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/device.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
+
import { chmodSync, closeSync, constants, lstatSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeSync, } from "node:fs";
|
|
4
|
+
import { basename, dirname, join, win32 } from "node:path";
|
|
5
|
+
import { configDir } from "./sdk/index.js";
|
|
6
|
+
export const DEVICE_HEADER = "promigence-device";
|
|
7
|
+
const HEX64 = /^[0-9a-f]{64}$/;
|
|
8
|
+
export function parseIoregUuid(out) {
|
|
9
|
+
return /"IOPlatformUUID"\s*=\s*"([^"\r\n]+)"/.exec(out)?.[1];
|
|
10
|
+
}
|
|
11
|
+
export function parseRegMachineGuid(out) {
|
|
12
|
+
return /MachineGuid\s+REG_SZ\s+(\S+)/i.exec(out)?.[1];
|
|
13
|
+
}
|
|
14
|
+
const realIo = {
|
|
15
|
+
run: (cmd, args) => execFileSync(cmd, args, {
|
|
16
|
+
encoding: "utf8", timeout: 2_000, windowsHide: true, stdio: ["ignore", "pipe", "ignore"], maxBuffer: 1 << 20,
|
|
17
|
+
}),
|
|
18
|
+
read: (path) => readFileSync(path, "utf8"),
|
|
19
|
+
env: process.env,
|
|
20
|
+
};
|
|
21
|
+
function system32(name, env) {
|
|
22
|
+
const root = env["SystemRoot"] ?? env["SYSTEMROOT"];
|
|
23
|
+
return win32.join(root && /^[A-Za-z]:[\\/]/.test(root) ? root : "C:\\Windows", "System32", name);
|
|
24
|
+
}
|
|
25
|
+
export function rawMachineId(platform = process.platform, io = realIo) {
|
|
26
|
+
const tryRead = (f) => {
|
|
27
|
+
try {
|
|
28
|
+
const v = f()?.trim();
|
|
29
|
+
return v ? v : undefined;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
if (platform === "darwin")
|
|
36
|
+
return tryRead(() => parseIoregUuid(io.run("/usr/sbin/ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"])));
|
|
37
|
+
if (platform === "win32") {
|
|
38
|
+
return tryRead(() => parseRegMachineGuid(io.run(system32("reg.exe", io.env), [
|
|
39
|
+
"query", "HKLM\\SOFTWARE\\Microsoft\\Cryptography", "/v", "MachineGuid",
|
|
40
|
+
])));
|
|
41
|
+
}
|
|
42
|
+
return tryRead(() => io.read("/etc/machine-id")) ?? tryRead(() => io.read("/var/lib/dbus/machine-id"));
|
|
43
|
+
}
|
|
44
|
+
export function machineHash(raw) {
|
|
45
|
+
if (!raw)
|
|
46
|
+
return undefined;
|
|
47
|
+
return createHash("sha256").update(raw.trim().toLowerCase()).digest("hex");
|
|
48
|
+
}
|
|
49
|
+
export function installIdPath(env = process.env) {
|
|
50
|
+
return join(configDir(env), "install-id");
|
|
51
|
+
}
|
|
52
|
+
function readInstallId(path) {
|
|
53
|
+
const st = lstatSync(path, { throwIfNoEntry: false });
|
|
54
|
+
if (!st || !st.isFile())
|
|
55
|
+
return undefined;
|
|
56
|
+
const v = readFileSync(path, "utf8").trim();
|
|
57
|
+
return HEX64.test(v) ? v : undefined;
|
|
58
|
+
}
|
|
59
|
+
export function installId(env = process.env) {
|
|
60
|
+
const path = installIdPath(env);
|
|
61
|
+
try {
|
|
62
|
+
const have = readInstallId(path);
|
|
63
|
+
if (have)
|
|
64
|
+
return have;
|
|
65
|
+
if (lstatSync(path, { throwIfNoEntry: false })?.isSymbolicLink())
|
|
66
|
+
return undefined;
|
|
67
|
+
const dir = dirname(path);
|
|
68
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
69
|
+
const dst = statSync(dir);
|
|
70
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
71
|
+
if (!dst.isDirectory() || (uid !== undefined && dst.uid !== uid))
|
|
72
|
+
return undefined;
|
|
73
|
+
const id = randomBytes(32).toString("hex");
|
|
74
|
+
const tmp = join(dir, `.${basename(path)}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`);
|
|
75
|
+
const fd = openSync(tmp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 0o600);
|
|
76
|
+
try {
|
|
77
|
+
writeSync(fd, id + "\n");
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
closeSync(fd);
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
chmodSync(tmp, 0o600);
|
|
84
|
+
renameSync(tmp, path);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
try {
|
|
88
|
+
unlinkSync(tmp);
|
|
89
|
+
}
|
|
90
|
+
catch { }
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
return readInstallId(path) ?? id;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export function deviceHeaderValue(machine, install) {
|
|
100
|
+
const parts = [];
|
|
101
|
+
if (machine && HEX64.test(machine))
|
|
102
|
+
parts.push(`m=${machine}`);
|
|
103
|
+
if (install && HEX64.test(install))
|
|
104
|
+
parts.push(`i=${install}`);
|
|
105
|
+
return parts.length ? parts.join("; ") : undefined;
|
|
106
|
+
}
|
|
107
|
+
let cached;
|
|
108
|
+
export function deviceHeader() {
|
|
109
|
+
if (!cached) {
|
|
110
|
+
let v;
|
|
111
|
+
try {
|
|
112
|
+
v = deviceHeaderValue(machineHash(rawMachineId()), installId());
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
v = undefined;
|
|
116
|
+
}
|
|
117
|
+
cached = { v };
|
|
118
|
+
}
|
|
119
|
+
return cached.v;
|
|
120
|
+
}
|
|
121
|
+
export function deviceFetch(value, base = globalThis.fetch) {
|
|
122
|
+
if (!value)
|
|
123
|
+
return undefined;
|
|
124
|
+
return ((input, init) => {
|
|
125
|
+
const headers = new Headers(init?.headers);
|
|
126
|
+
headers.set(DEVICE_HEADER, value);
|
|
127
|
+
return base(input, { ...init, headers });
|
|
128
|
+
});
|
|
129
|
+
}
|
package/dist/dirsnap.js
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { closeSync, fstatSync, lstatSync, mkdtempSync, openSync, readSync, realpathSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { basename, join, resolve, sep } from "node:path";
|
|
6
|
+
import { DEFAULT_SNAPSHOT, FILE_PUT_CHUNK_BYTES, PromigenceError, secretLike } from "./sdk/index.js";
|
|
7
|
+
import { bool, client, list, nexts, str, waitBound } from "./commands.js";
|
|
8
|
+
import { emit, okEnvelope, parseDuration, usageError, write } from "./io.js";
|
|
9
|
+
import { jsonLine, sh } from "./term.js";
|
|
10
|
+
import { CREDENTIAL_REMEDY, confirmOverride, credentialRefusal, emitUploadList, localTarNoMetadataArgs } from "./uploadguard.js";
|
|
11
|
+
const BASE_WORKSPACE = "/work";
|
|
12
|
+
export const DIR_SANDBOX_TIMEOUT_DEFAULT = "30m";
|
|
13
|
+
const STEP_TIMEOUT_MS = 30 * 60_000;
|
|
14
|
+
const STEP_OUTPUT_BYTES = 256 * 1024;
|
|
15
|
+
const FAILED_TAIL_LINES = 20;
|
|
16
|
+
const PROGRESS_FROM_BYTES = 8 * 1024 * 1024;
|
|
17
|
+
const INTERRUPT_START_WAIT_MS = 15_000;
|
|
18
|
+
const INTERRUPTED = {
|
|
19
|
+
ended: () => "The build sandbox was ended.",
|
|
20
|
+
kept: (id) => `The build sandbox was kept (--keep-sandbox); end it with ${sh `promigence run kill ${id ?? "<run_id>"}`}.`,
|
|
21
|
+
none: () => "No build sandbox was started.",
|
|
22
|
+
unknown: (id) => id
|
|
23
|
+
? `The build sandbox may still be running; end it with ${sh `promigence run kill ${id}`}.`
|
|
24
|
+
: "A build sandbox may still be starting; `promigence run list` shows it and `promigence run kill <run_id>` ends it.",
|
|
25
|
+
};
|
|
26
|
+
const NOT_WITH_DIR = [
|
|
27
|
+
"--repo", "--commit", "--ref", "--track", "--also-repo", "--image", "--dockerfile", "--context",
|
|
28
|
+
"--devcontainer", "--start", "--grade", "--holdout", "--grade-score", "--grade-timeout",
|
|
29
|
+
"--grade-output", "--tier",
|
|
30
|
+
];
|
|
31
|
+
export { secretLike };
|
|
32
|
+
function git(dir, args) {
|
|
33
|
+
const r = spawnSync("git", ["-C", dir, ...args], { encoding: "buffer", maxBuffer: 512 * 1024 * 1024 });
|
|
34
|
+
if (r.error && r.error.code === "ENOENT")
|
|
35
|
+
return { ok: false, out: "", err: "", missing: true };
|
|
36
|
+
return { ok: r.status === 0, out: r.stdout?.toString("utf8") ?? "", err: r.stderr?.toString("utf8") ?? "" };
|
|
37
|
+
}
|
|
38
|
+
export function listTreeFiles(dir, opts = {}) {
|
|
39
|
+
const inside = git(dir, ["rev-parse", "--is-inside-work-tree"]);
|
|
40
|
+
if (inside.missing) {
|
|
41
|
+
throw usageError("`git` is not on PATH, and --dir lists what it uploads with git", "Install git, or upload a directory into a sandbox with `promigence files put DIR DST --recursive`.");
|
|
42
|
+
}
|
|
43
|
+
if (!inside.ok || inside.out.trim() !== "true") {
|
|
44
|
+
throw usageError(`${dir} is not inside a git work tree: --dir uploads the files git tracks there`, "Run `git init && git add -A` in it first (no commit needed), or point --dir at your repository. --include-untracked does not help here: it adds the untracked files of a work tree.");
|
|
45
|
+
}
|
|
46
|
+
const tracked = git(dir, ["ls-files", "-z", "--cached", "--recurse-submodules"]);
|
|
47
|
+
if (!tracked.ok)
|
|
48
|
+
throw usageError(`git ls-files failed in ${dir}: ${tracked.err.trim()}`, "Check the repository with `git status`.");
|
|
49
|
+
const seen = new Set();
|
|
50
|
+
const files = [];
|
|
51
|
+
const outside = [];
|
|
52
|
+
let missing = 0;
|
|
53
|
+
let bytes = 0;
|
|
54
|
+
const realRoot = realpathSync(dir);
|
|
55
|
+
const linkedDirs = new Map();
|
|
56
|
+
const throughLink = (p) => {
|
|
57
|
+
const parts = p.split("/");
|
|
58
|
+
for (let i = 1; i < parts.length; i++) {
|
|
59
|
+
const d = parts.slice(0, i).join("/");
|
|
60
|
+
let linked = linkedDirs.get(d);
|
|
61
|
+
if (linked === undefined) {
|
|
62
|
+
try {
|
|
63
|
+
linked = !lstatSync(join(dir, d)).isDirectory();
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
linked = false;
|
|
67
|
+
}
|
|
68
|
+
linkedDirs.set(d, linked);
|
|
69
|
+
}
|
|
70
|
+
if (linked)
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
};
|
|
75
|
+
const take = (p) => {
|
|
76
|
+
if (!p || seen.has(p))
|
|
77
|
+
return false;
|
|
78
|
+
seen.add(p);
|
|
79
|
+
if (throughLink(p)) {
|
|
80
|
+
outside.push(p);
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
let st;
|
|
84
|
+
try {
|
|
85
|
+
st = lstatSync(join(dir, p));
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
missing += 1;
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
if (st.isDirectory())
|
|
92
|
+
return false;
|
|
93
|
+
if (!st.isSymbolicLink()) {
|
|
94
|
+
let real;
|
|
95
|
+
try {
|
|
96
|
+
real = realpathSync(join(dir, p));
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
missing += 1;
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
if (real !== realRoot && !real.startsWith(realRoot + sep)) {
|
|
103
|
+
outside.push(p);
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
files.push(p);
|
|
108
|
+
if (st.isFile())
|
|
109
|
+
bytes += st.size;
|
|
110
|
+
return true;
|
|
111
|
+
};
|
|
112
|
+
for (const p of tracked.out.split("\0"))
|
|
113
|
+
take(p);
|
|
114
|
+
let untracked = 0;
|
|
115
|
+
if (opts.includeUntracked) {
|
|
116
|
+
const others = git(dir, ["ls-files", "-z", "--others", "--exclude-standard"]);
|
|
117
|
+
if (!others.ok)
|
|
118
|
+
throw usageError(`git ls-files failed in ${dir}: ${others.err.trim()}`, "Check the repository with `git status`.");
|
|
119
|
+
for (const p of others.out.split("\0"))
|
|
120
|
+
if (take(p))
|
|
121
|
+
untracked += 1;
|
|
122
|
+
}
|
|
123
|
+
files.sort();
|
|
124
|
+
outside.sort();
|
|
125
|
+
return { files, missing, untracked, bytes, outside };
|
|
126
|
+
}
|
|
127
|
+
export function refuseSecrets(files, allow) {
|
|
128
|
+
const hits = files.filter(secretLike);
|
|
129
|
+
if (hits.length && !allow)
|
|
130
|
+
throw credentialRefusal(hits, CREDENTIAL_REMEDY.dir);
|
|
131
|
+
return hits;
|
|
132
|
+
}
|
|
133
|
+
export function human(n) {
|
|
134
|
+
if (n < 1024)
|
|
135
|
+
return `${n} B`;
|
|
136
|
+
const u = ["KiB", "MiB", "GiB", "TiB"];
|
|
137
|
+
let v = n / 1024;
|
|
138
|
+
let i = 0;
|
|
139
|
+
while (v >= 1024 && i < u.length - 1) {
|
|
140
|
+
v /= 1024;
|
|
141
|
+
i++;
|
|
142
|
+
}
|
|
143
|
+
return `${v.toFixed(v < 10 ? 1 : 0)} ${u[i]}`;
|
|
144
|
+
}
|
|
145
|
+
export function packFiles(dir, files, out) {
|
|
146
|
+
const listPath = `${out}.list`;
|
|
147
|
+
writeFileSync(listPath, files.join("\0") + (files.length ? "\0" : ""));
|
|
148
|
+
try {
|
|
149
|
+
const r = spawnSync("tar", ["-c", "-f", out, ...localTarNoMetadataArgs(), "-C", dir, "--no-recursion", "--null", "-T", listPath], {
|
|
150
|
+
encoding: "utf8", env: { ...process.env, COPYFILE_DISABLE: "1" },
|
|
151
|
+
});
|
|
152
|
+
if (r.error && r.error.code === "ENOENT") {
|
|
153
|
+
throw usageError("`tar` is not on PATH, and --dir packs the tree with it", "Install tar (macOS and every Linux ship one).");
|
|
154
|
+
}
|
|
155
|
+
if (r.status !== 0) {
|
|
156
|
+
throw usageError(`packing ${dir} failed: ${(r.stderr ?? "").trim() || `tar exited ${r.status}`}`, "Check that every listed file is readable (`git status`).");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
rmSync(listPath, { force: true });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function tail(r) {
|
|
164
|
+
const lines = `${r.stdout ?? ""}${r.stderr ? `\n${r.stderr}` : ""}`.split("\n").filter((l) => l.trim() !== "");
|
|
165
|
+
return lines.slice(-FAILED_TAIL_LINES);
|
|
166
|
+
}
|
|
167
|
+
const exitOf = (r) => r.timed_out ? null : typeof r.exit_code === "number" ? r.exit_code : 1;
|
|
168
|
+
export async function dirSnapshotCreate(ctx) {
|
|
169
|
+
for (const f of NOT_WITH_DIR) {
|
|
170
|
+
if (bool(ctx, f)) {
|
|
171
|
+
throw usageError(`${f} is not taken with --dir`, f === "--tier"
|
|
172
|
+
? "The snapshot has the size of the snapshot the upload starts from: pass --from a snapshot built at that tier."
|
|
173
|
+
: `--dir uploads a local tree into a sandbox of --from (default ${DEFAULT_SNAPSHOT}); drop ${f}, or build that form without --dir.`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const dirArg = str(ctx, "--dir");
|
|
177
|
+
if (dirArg === undefined)
|
|
178
|
+
throw usageError("--dir needs a directory", "Example: promigence snapshot create --dir . --alias app --wait.");
|
|
179
|
+
const dir = resolve(dirArg);
|
|
180
|
+
try {
|
|
181
|
+
if (!statSync(dir).isDirectory())
|
|
182
|
+
throw new Error("not a directory");
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
throw usageError(`--dir ${dirArg} is not a directory`, "Pass the path of your working tree: --dir .");
|
|
186
|
+
}
|
|
187
|
+
const listing = listTreeFiles(dir, { includeUntracked: bool(ctx, "--include-untracked") });
|
|
188
|
+
if (!listing.files.length) {
|
|
189
|
+
throw usageError(`nothing to upload: git tracks no file in ${dirArg}`, "`git add` the files first, or pass --include-untracked for files git does not ignore.");
|
|
190
|
+
}
|
|
191
|
+
if (bool(ctx, "--list-files")) {
|
|
192
|
+
return emitUploadList(ctx, {
|
|
193
|
+
kind: "dir", root: dirArg, paths: listing.files, files: listing.files.length, bytes: listing.bytes,
|
|
194
|
+
secrets: listing.files.filter(secretLike),
|
|
195
|
+
notes: listing.outside.length
|
|
196
|
+
? [`Left out ${listing.outside.length} path${listing.outside.length === 1 ? "" : "s"} under a directory that is now a link or outside ${dirArg}: ${listing.outside.slice(0, 10).join(", ")}${listing.outside.length > 10 ? ` and ${listing.outside.length - 10} more` : ""}.`]
|
|
197
|
+
: [],
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
const allowed = await confirmOverride(ctx, {
|
|
201
|
+
flag: "--allow-secrets", what: "files that look like credentials",
|
|
202
|
+
items: refuseSecrets(listing.files, bool(ctx, "--allow-secrets")),
|
|
203
|
+
});
|
|
204
|
+
const from = str(ctx, "--from") ?? DEFAULT_SNAPSHOT;
|
|
205
|
+
const alias = str(ctx, "--alias");
|
|
206
|
+
const setup = list(ctx, "--setup");
|
|
207
|
+
const verify = str(ctx, "--verify");
|
|
208
|
+
const keep = bool(ctx, "--keep-sandbox");
|
|
209
|
+
const timeoutMs = parseDuration(str(ctx, "--timeout") ?? DIR_SANDBOX_TIMEOUT_DEFAULT, "--timeout");
|
|
210
|
+
if (timeoutMs <= 0)
|
|
211
|
+
throw usageError("--timeout must be more than 0", `Omit it for ${DIR_SANDBOX_TIMEOUT_DEFAULT}.`);
|
|
212
|
+
const c = client(ctx);
|
|
213
|
+
const q = await c.runs.quote({ snapshot: from, count: 1, timeoutMs });
|
|
214
|
+
const perMin = q.per_second_usd * 60;
|
|
215
|
+
const most = q.quote?.max_cost ?? q.max_cost;
|
|
216
|
+
const priceFields = {
|
|
217
|
+
per_second_usd: q.per_second_usd, per_minute_usd: Number(perMin.toFixed(6)), tier: q.tier,
|
|
218
|
+
max_cost: q.max_cost, sandbox_timeout_s: Math.ceil(timeoutMs / 1000), snapshot_charge: 0,
|
|
219
|
+
};
|
|
220
|
+
await write(process.stderr, ctx.mode === "json"
|
|
221
|
+
? jsonLine(process.stderr, { hint: "price", ...priceFields, unit: "USD" })
|
|
222
|
+
: `price: the sandbox is billed per second while the upload and setup run (~$${perMin.toFixed(4)}/min at tier ${q.tier}, at most $${q.max_cost.toFixed(2)} for its ${Math.round(timeoutMs / 60_000)} min limit); the snapshot itself is not charged\n` +
|
|
223
|
+
` uploading ${listing.files.length} file${listing.files.length === 1 ? "" : "s"} (${human(listing.bytes)}) from ${dirArg}${listing.untracked ? `, ${listing.untracked} untracked` : ""}${listing.missing ? `; ${listing.missing} deleted in the working tree, skipped` : ""}${allowed.length ? `; ${allowed.length} secret-looking, allowed by --allow-secrets` : ""}\n` +
|
|
224
|
+
(listing.outside.length ? ` skipped ${listing.outside.length} under a directory that is now a link or outside ${dirArg}: ${listing.outside.slice(0, 10).join(", ")}${listing.outside.length > 10 ? " ..." : ""}\n` : ""));
|
|
225
|
+
const scratchDir = mkdtempSync(join(tmpdir(), "promigence-dir-"));
|
|
226
|
+
const tarPath = join(scratchDir, "tree.tar");
|
|
227
|
+
let runId;
|
|
228
|
+
let starting;
|
|
229
|
+
let ended = false;
|
|
230
|
+
const t0 = Date.now();
|
|
231
|
+
const end = async () => {
|
|
232
|
+
if (runId === undefined && starting !== undefined) {
|
|
233
|
+
const got = await Promise.race([
|
|
234
|
+
starting.then((id) => id, () => null),
|
|
235
|
+
new Promise((r) => setTimeout(() => r(undefined), INTERRUPT_START_WAIT_MS).unref()),
|
|
236
|
+
]);
|
|
237
|
+
if (got === undefined)
|
|
238
|
+
return "unknown";
|
|
239
|
+
if (got !== null)
|
|
240
|
+
runId = got;
|
|
241
|
+
}
|
|
242
|
+
if (runId === undefined)
|
|
243
|
+
return "none";
|
|
244
|
+
if (keep)
|
|
245
|
+
return "kept";
|
|
246
|
+
if (ended)
|
|
247
|
+
return "ended";
|
|
248
|
+
ended = true;
|
|
249
|
+
return c.runs.kill(runId).then(() => "ended", () => "unknown");
|
|
250
|
+
};
|
|
251
|
+
const onSigint = () => {
|
|
252
|
+
void end().then((what) => {
|
|
253
|
+
rmSync(scratchDir, { recursive: true, force: true });
|
|
254
|
+
process.stderr.write(`\npromigence: interrupted; nothing was snapshotted. ${INTERRUPTED[what](runId)}\n`);
|
|
255
|
+
process.exit(130);
|
|
256
|
+
});
|
|
257
|
+
};
|
|
258
|
+
process.once("SIGINT", onSigint);
|
|
259
|
+
let result;
|
|
260
|
+
try {
|
|
261
|
+
packFiles(dir, listing.files, tarPath);
|
|
262
|
+
const packed = statSync(tarPath).size;
|
|
263
|
+
const spendCap = Math.ceil((most + 0.01) * 100) / 100;
|
|
264
|
+
starting = c.runs.start({ snapshot: from, count: 1, timeoutMs, spendCap }).then((s) => s.run_id);
|
|
265
|
+
runId = await starting;
|
|
266
|
+
if (keep)
|
|
267
|
+
ctx.startedRunId = runId;
|
|
268
|
+
const waited = await c.runs.waitReady(runId, { waitTimeoutMs: waitBound(ctx) });
|
|
269
|
+
const sb = waited.sandboxes.find((s) => s.state === "ready" || s.state === "running");
|
|
270
|
+
if (waited.timedOut || !sb) {
|
|
271
|
+
throw new PromigenceError({
|
|
272
|
+
code: waited.timedOut ? "timeout" : "fork_failed",
|
|
273
|
+
message: waited.timedOut
|
|
274
|
+
? `the sandbox did not start within the wait (--wait-timeout); it was ended and nothing was uploaded`
|
|
275
|
+
: `the sandbox ended before it started (${waited.sandboxes.map((s) => s.state).join(", ")}); nothing was uploaded`,
|
|
276
|
+
fix: "Run the command again; `promigence status` shows whether capacity is short.",
|
|
277
|
+
exitCode: waited.timedOut ? 11 : 3,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
const sid = sb.sandbox_id;
|
|
281
|
+
let workspace = BASE_WORKSPACE;
|
|
282
|
+
if (from !== DEFAULT_SNAPSHOT) {
|
|
283
|
+
const where = await c.sandboxes.exec(sid, { sh: "pwd", timeoutMs: 60_000 });
|
|
284
|
+
const w = where.stdout.trim().split("\n")[0] ?? "";
|
|
285
|
+
if (exitOf(where) === 0 && w.startsWith("/") && w !== "/")
|
|
286
|
+
workspace = w;
|
|
287
|
+
}
|
|
288
|
+
const scratch = `/tmp/.promigence-upload/dir-${Date.now().toString(36)}-${randomBytes(6).toString("hex")}.tar`;
|
|
289
|
+
const fd = openSync(tarPath, "r");
|
|
290
|
+
try {
|
|
291
|
+
const size = fstatSync(fd).size;
|
|
292
|
+
const buf = Buffer.alloc(FILE_PUT_CHUNK_BYTES);
|
|
293
|
+
let lastSaid = Date.now();
|
|
294
|
+
const show = size > PROGRESS_FROM_BYTES && ctx.mode !== "json";
|
|
295
|
+
await c.sandboxes.files.putFrom(sid, scratch, {
|
|
296
|
+
size,
|
|
297
|
+
read: (offset, len) => {
|
|
298
|
+
const want = Math.min(len, buf.length);
|
|
299
|
+
let got = 0;
|
|
300
|
+
while (got < want) {
|
|
301
|
+
const n = readSync(fd, buf, got, want - got, offset + got);
|
|
302
|
+
if (n === 0)
|
|
303
|
+
break;
|
|
304
|
+
got += n;
|
|
305
|
+
}
|
|
306
|
+
if (show && Date.now() - lastSaid >= 2000) {
|
|
307
|
+
lastSaid = Date.now();
|
|
308
|
+
process.stderr.write(` uploading ${human(offset)} / ${human(size)}\n`);
|
|
309
|
+
}
|
|
310
|
+
return buf.subarray(0, got);
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
if (ctx.mode === "json" && size > PROGRESS_FROM_BYTES) {
|
|
314
|
+
await write(process.stderr, jsonLine(process.stderr, { hint: "uploaded", bytes: size }));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
finally {
|
|
318
|
+
closeSync(fd);
|
|
319
|
+
}
|
|
320
|
+
try {
|
|
321
|
+
await c.sandboxes.files.extract(sid, scratch, workspace);
|
|
322
|
+
}
|
|
323
|
+
finally {
|
|
324
|
+
await c.sandboxes.files.delete(sid, scratch).catch(() => { });
|
|
325
|
+
}
|
|
326
|
+
const steps = [
|
|
327
|
+
...setup.map((s, i) => [`setup step ${i + 1}`, s]),
|
|
328
|
+
...(verify === undefined ? [] : [["verify", verify]]),
|
|
329
|
+
];
|
|
330
|
+
for (const [what, cmd] of steps) {
|
|
331
|
+
if (ctx.mode !== "json")
|
|
332
|
+
await write(process.stderr, ` $ ${cmd}\n`);
|
|
333
|
+
const r = await c.sandboxes.exec(sid, { sh: cmd, cwd: workspace, timeoutMs: STEP_TIMEOUT_MS, maxOutputBytes: STEP_OUTPUT_BYTES });
|
|
334
|
+
const code = exitOf(r);
|
|
335
|
+
if (code === 0)
|
|
336
|
+
continue;
|
|
337
|
+
const lines = tail(r);
|
|
338
|
+
await write(process.stderr, ctx.mode === "json"
|
|
339
|
+
? jsonLine(process.stderr, { hint: "step_failed", step: what, cmd, exit_code: code, timed_out: code === null, tail: lines })
|
|
340
|
+
: lines.map((l) => ` | ${l}`).join("\n") + (lines.length ? "\n" : ""));
|
|
341
|
+
throw new PromigenceError({
|
|
342
|
+
code: "nonzero_exit",
|
|
343
|
+
message: `${what} (\`${cmd}\`) ${code === null ? "did not finish within 30 min" : `exited ${code}`}; nothing was snapshotted and the sandbox was ended`,
|
|
344
|
+
fix: "Fix the step (its last lines are above) and run the command again; to look around first, add --keep-sandbox and `promigence exec --sandbox <id> -- sh`.",
|
|
345
|
+
exitCode: 4,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
const snap = await c.sandboxes.snapshot(sid, { ...(alias === undefined ? {} : { alias, moveAlias: true }) });
|
|
349
|
+
result = { snapshot_id: snap.snapshot_id, alias: snap.alias ?? alias ?? null, sandbox_id: sid, workspace, packed };
|
|
350
|
+
}
|
|
351
|
+
finally {
|
|
352
|
+
process.removeListener("SIGINT", onSigint);
|
|
353
|
+
await end();
|
|
354
|
+
rmSync(scratchDir, { recursive: true, force: true });
|
|
355
|
+
}
|
|
356
|
+
const seconds = Math.round((Date.now() - t0) / 100) / 10;
|
|
357
|
+
const name = result.alias ?? result.snapshot_id;
|
|
358
|
+
const next = nexts([sh `promigence run --snapshot ${name} --count 1 --cap 1 -- ls`, "start it and run a command where the tree is"], [sh `promigence snapshot inspect ${result.snapshot_id}`, "what was recorded"]);
|
|
359
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, {
|
|
360
|
+
snapshot_id: result.snapshot_id,
|
|
361
|
+
alias: result.alias,
|
|
362
|
+
source: { kind: "dir", dir: dirArg, files: listing.files.length, untracked: listing.untracked, bytes: listing.bytes, packed_bytes: result.packed },
|
|
363
|
+
...(allowed.length ? { secret_files_allowed: allowed } : {}),
|
|
364
|
+
...(listing.outside.length ? { skipped_outside: listing.outside } : {}),
|
|
365
|
+
from,
|
|
366
|
+
workspace: result.workspace,
|
|
367
|
+
setup,
|
|
368
|
+
...(verify === undefined ? {} : { verify }),
|
|
369
|
+
sandbox_id: result.sandbox_id,
|
|
370
|
+
sandbox_ended: !keep,
|
|
371
|
+
sandbox_seconds: seconds,
|
|
372
|
+
price: priceFields,
|
|
373
|
+
}, next), [
|
|
374
|
+
`snapshot ${result.snapshot_id}${result.alias ? ` (${result.alias})` : ""}: ${listing.files.length} files from ${basename(dir)} in ${result.workspace}${setup.length ? `, ${setup.length} setup step${setup.length === 1 ? "" : "s"}` : ""}${verify ? ", verified" : ""}`,
|
|
375
|
+
` sandbox ${result.sandbox_id}: ${seconds}s, ${keep ? `kept (--keep-sandbox): kill it with ${sh `promigence sandbox kill ${result.sandbox_id}`}` : "ended"}`,
|
|
376
|
+
` next: ${next[0].cmd}`,
|
|
377
|
+
], 0, ctx.pointer);
|
|
378
|
+
}
|