@outerlayer/cli 0.2.0 → 0.4.0
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/CHANGELOG.md +124 -0
- package/README.md +226 -59
- package/dist/agent-setup-IFH3FB5X.js +197 -0
- package/dist/build-TZUUTRGH.js +9 -0
- package/dist/build-info.json +1 -1
- package/dist/check-WQP2CZDW.js +94 -0
- package/dist/chunk-2E54Z3P5.js +232 -0
- package/dist/chunk-3TT7RQYB.js +55 -0
- package/dist/{chunk-U32VSRLO.js → chunk-3YPKXE5L.js} +90 -632
- package/dist/{chunk-Q6CL5THG.js → chunk-5DLBAXY5.js} +208 -61
- package/dist/{chunk-R5KBGVII.js → chunk-5QRP3MVS.js} +30 -14
- package/dist/chunk-5Y3QQRUH.js +300 -0
- package/dist/chunk-6Y63SXFO.js +383 -0
- package/dist/chunk-774HEQL6.js +178 -0
- package/dist/chunk-7A6RVXPZ.js +2233 -0
- package/dist/chunk-7YIOOFVJ.js +71 -0
- package/dist/chunk-ABIWDUMS.js +104 -0
- package/dist/chunk-AUFG23AA.js +534 -0
- package/dist/chunk-B2G7JHHB.js +42 -0
- package/dist/chunk-BCJNHMZT.js +34 -0
- package/dist/chunk-BCLJSQCV.js +56 -0
- package/dist/chunk-BFESLKPP.js +61 -0
- package/dist/chunk-BJ3KTMDH.js +63 -0
- package/dist/chunk-BKO6JEZI.js +47 -0
- package/dist/chunk-BOLTI6LR.js +37 -0
- package/dist/chunk-CBPPJSAR.js +89 -0
- package/dist/chunk-CBZB6XY6.js +102 -0
- package/dist/{chunk-65WXAANR.js → chunk-DVDEBNQJ.js} +17 -2
- package/dist/{work-pr-cmd-AZQPWH4H.js → chunk-DVYHCMB2.js} +9 -26
- package/dist/{chunk-TFUIDMOB.js → chunk-EABW6AJQ.js} +12 -1
- package/dist/{chunk-KFYJV2ZG.js → chunk-EMY4I27X.js} +1 -1
- package/dist/{chunk-NTTPJV35.js → chunk-F47JBIAW.js} +3 -1
- package/dist/chunk-F6GFZXZ3.js +60 -0
- package/dist/{chunk-YOOSBOKS.js → chunk-F7CU5ABH.js} +1 -1
- package/dist/chunk-FAJSS2WN.js +1357 -0
- package/dist/chunk-FS2VYGZT.js +1764 -0
- package/dist/chunk-FV2CCGXK.js +9 -0
- package/dist/{chunk-4TNZMO7V.js → chunk-G7FSV7JX.js} +3452 -751
- package/dist/{emit-cmd-TWSYYEZI.js → chunk-H5NCNVDA.js} +68 -17
- package/dist/chunk-HE2D4EY5.js +83 -0
- package/dist/chunk-L2DBRKRA.js +94 -0
- package/dist/chunk-LF6MLJCY.js +38 -0
- package/dist/chunk-M5POMKKW.js +1051 -0
- package/dist/{mcp-install-cmd-FDQH6SEN.js → chunk-MQ3IPHIZ.js} +35 -18
- package/dist/{chunk-D77LS3UI.js → chunk-N5FOE5PS.js} +33 -4
- package/dist/chunk-N6LURUEF.js +64 -0
- package/dist/chunk-NYC54VBD.js +1084 -0
- package/dist/chunk-OGFGZQA3.js +23 -0
- package/dist/chunk-QOZKPLVJ.js +226 -0
- package/dist/chunk-RA3O54FC.js +30 -0
- package/dist/chunk-TBT347UY.js +41 -0
- package/dist/{chunk-NXLLURA4.js → chunk-UFFXNXLQ.js} +115 -71
- package/dist/chunk-USO2DKBD.js +421 -0
- package/dist/chunk-W4SQCOZU.js +206 -0
- package/dist/{chunk-A3WLZX2F.js → chunk-WIOZAJ2W.js} +15 -2
- package/dist/chunk-WO2BXCTQ.js +83 -0
- package/dist/{chunk-I3ETLSNE.js → chunk-XCCLFVXM.js} +120 -11
- package/dist/{context-materialize-6WKT3RBQ.js → chunk-XDDW4FRS.js} +134 -26
- package/dist/chunk-YYYXRJUW.js +101 -0
- package/dist/chunk-ZM2IMMYN.js +76 -0
- package/dist/chunk-ZMYPLFG3.js +71 -0
- package/dist/chunk-ZNA27WEV.js +470 -0
- package/dist/{cli-G7DILYJY.js → cli-NHUXABYH.js} +590 -876
- package/dist/{paths-D2VGWWFI.js → cli-build-K56DK4DS.js} +1 -1
- package/dist/config-XVJYZ4IQ.js +9 -0
- package/dist/connect-cmd-OOG4TPU7.js +16 -0
- package/dist/context-adopt-UMA4O5NY.js +105 -0
- package/dist/context-materialize-G2FUFC72.js +19 -0
- package/dist/dist-RW4UPPC2.js +6 -0
- package/dist/docker-NEGDLU6D.js +7 -0
- package/dist/doctor-53F2PYY7.js +43 -0
- package/dist/doctor-UFVL4PZY.js +426 -0
- package/dist/{emit-artifact-cmd-UQVT3OKW.js → emit-artifact-cmd-KTYW2TN2.js} +24 -20
- package/dist/emit-cmd-DCERAU27.js +9 -0
- package/dist/emit-criteria-cmd-VAUUSAYD.js +166 -0
- package/dist/{emit-finding-cmd-FDOU2OPD.js → emit-finding-cmd-UR4ID45Z.js} +34 -31
- package/dist/{emit-result-cmd-H2T4C2K3.js → emit-result-cmd-FDTUKGKE.js} +34 -62
- package/dist/exec-client-4XGXV3XN.js +8 -0
- package/dist/guest-init-GENRHNP7.js +8 -0
- package/dist/hook-fast-EYENPK6F.js +9 -0
- package/dist/{hook-wrap-fast-KXYNX3AD.js → hook-wrap-fast-NPLHJXFK.js} +2 -2
- package/dist/host-key-KDYZ5ECC.js +7 -0
- package/dist/import-capture-cmd-AVZ4VIVJ.js +68 -0
- package/dist/{import-ruler-cmd-7LH2QOMG.js → import-ruler-cmd-GUHL6IY5.js} +1 -1
- package/dist/index.js +4 -4
- package/dist/init-DXBCOAJK.js +36 -0
- package/dist/init-YEXK35CF.js +221 -0
- package/dist/init-cmd-NHR7PRSQ.js +135 -0
- package/dist/install-cmd-7WXOIFDO.js +55 -0
- package/dist/lima-XN65D7GN.js +55 -0
- package/dist/login-browser-7XGSV7MA.js +10 -0
- package/dist/{config-POF7DEQW.js → logout-cmd-QAUFDJ6J.js} +3 -1
- package/dist/{logs-TRNPQM42.js → logs-7RYUH5A6.js} +1 -1
- package/dist/loop-VZOLL4WQ.js +43 -0
- package/dist/machine-WSG52J75.js +35 -0
- package/dist/mcp-install-cmd-RLK4SO3N.js +9 -0
- package/dist/{mcp-serve-cmd-57EZZOTL.js → mcp-serve-cmd-JPTP5FMS.js} +20 -9
- package/dist/paths-OYKMVYJP.js +6 -0
- package/dist/{pidfile-PTW76F56.js → pidfile-UZRH774M.js} +2 -3
- package/dist/real-deps-SU24ZA2K.js +21 -0
- package/dist/relay-L76HDX72.js +46 -0
- package/dist/settings-J2652U5N.js +7 -0
- package/dist/starter-pack-LIZYMKYQ.js +10 -0
- package/dist/{status-I27IST4L.js → status-2UZKKNB7.js} +30 -13
- package/dist/{statusline-fast-3C5OXHDD.js → statusline-fast-SVTMBMD7.js} +3 -2
- package/dist/sync-cmd-VPPYCNNM.js +28 -0
- package/dist/version-BWM6VLDI.js +6 -0
- package/dist/{watch-3DTPJETH.js → watch-5C4ZBBGO.js} +19 -8
- package/dist/{work-claim-cmd-5AUNCLS2.js → work-claim-cmd-LSO2B2EX.js} +33 -20
- package/dist/work-cmd-UOQO2AD4.js +17 -0
- package/dist/work-comment-cmd-WF3EOLAE.js +106 -0
- package/dist/{work-launch-LT663PB3.js → work-launch-YI4CJDAP.js} +1 -1
- package/dist/work-open-pr-cmd-4SSQZMYG.js +156 -0
- package/dist/work-pr-cmd-6AN6RRQJ.js +16 -0
- package/package.json +13 -3
- package/skill-pack/maintained/amend/SKILL.md +104 -0
- package/skill-pack/maintained/emitting-evidence/SKILL.md +99 -0
- package/skill-pack/maintained/emitting-evidence/references/agents-snippet.md +20 -0
- package/skill-pack/maintained/outerlayer/SKILL.md +55 -0
- package/skill-pack/maintained/reporting-findings/SKILL.md +148 -0
- package/skill-pack/template/build/SKILL.md +193 -0
- package/skill-pack/template/build/references/agent-briefs.md +243 -0
- package/skill-pack/template/build/references/criteria-judge.md +91 -0
- package/skill-pack/template/build/references/evidence.md +42 -0
- package/skill-pack/template/build/references/release.md +74 -0
- package/skill-pack/template/build/references/review-briefs.md +275 -0
- package/skill-pack/template/build/references/review-loop.md +158 -0
- package/skill-pack/template/build/scripts/record-criteria.mjs +235 -0
- package/skill-pack/template/spec/SKILL.md +84 -0
- package/skill-pack/template/writing-specs/SKILL.md +134 -0
- package/dist/chunk-DCNOXRMV.js +0 -589
- package/dist/chunk-JJP7YLMN.js +0 -25
- package/dist/chunk-OZ7C3XUE.js +0 -34
- package/dist/chunk-WQ6VGRGZ.js +0 -150
- package/dist/hook-fast-J5LCDHUJ.js +0 -8
- package/dist/import-capture-cmd-EUMBGIV3.js +0 -176
- package/dist/init-PTBITAUO.js +0 -103
- package/dist/login-cmd-IRX6LZT7.js +0 -62
- package/dist/loop-ASZRX3CZ.js +0 -648
- package/dist/sync-cmd-BBAUZ5JD.js +0 -17
- package/dist/work-cmd-2P4BVX47.js +0 -18
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { parseSize, MAX_ALLOW_HOSTS, allowHostProblem } from './chunk-F6GFZXZ3.js';
|
|
4
|
+
import { isRefusedInConfig, resolveDestinations, isReservedVariable } from './chunk-AUFG23AA.js';
|
|
5
|
+
import { homeFromEnv } from './chunk-VNVZDWO3.js';
|
|
6
|
+
import { existsSync, statSync, accessSync, constants, readFileSync } from 'fs';
|
|
7
|
+
import { hostname } from 'os';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
|
|
10
|
+
createRequire(import.meta.url);
|
|
11
|
+
var BUILTIN_CONTAINER = "builtin:container";
|
|
12
|
+
var BUILTIN_PROCESS = "builtin:process";
|
|
13
|
+
var BUILTIN_VM = "builtin:vm";
|
|
14
|
+
function isBuiltinContainer(hook) {
|
|
15
|
+
return hook === BUILTIN_CONTAINER;
|
|
16
|
+
}
|
|
17
|
+
function isBuiltinProcess(hook) {
|
|
18
|
+
return hook === BUILTIN_PROCESS;
|
|
19
|
+
}
|
|
20
|
+
function isBuiltinVm(hook) {
|
|
21
|
+
return hook === BUILTIN_VM;
|
|
22
|
+
}
|
|
23
|
+
function builtinKind(hook) {
|
|
24
|
+
return isBuiltinContainer(hook) ? "container" : isBuiltinProcess(hook) ? "process" : isBuiltinVm(hook) ? "vm" : "own";
|
|
25
|
+
}
|
|
26
|
+
var RUNNER_QUEUES = ["implement", "amend"];
|
|
27
|
+
var RunnerConfigError = class extends Error {
|
|
28
|
+
};
|
|
29
|
+
var DEFAULT_CONCURRENCY = 1;
|
|
30
|
+
var DEFAULT_POLL_SECONDS = 60;
|
|
31
|
+
var DEFAULT_TIME_LIMIT_MINUTES = 480;
|
|
32
|
+
var DEFAULT_IDLE_LIMIT_MINUTES = 30;
|
|
33
|
+
var DEFAULT_BUILD = { memory: "8g", cpus: 4, pids: 4096, disk: "40g", variables: [] };
|
|
34
|
+
var BUILD_KEYS = ["memory", "cpus", "pids", "disk", "variables", "destinations", "allowHosts"];
|
|
35
|
+
var VARIABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,254}$/;
|
|
36
|
+
var MAX_VARIABLES = 100;
|
|
37
|
+
function defaultRunnerConfigPath(home = homeFromEnv()) {
|
|
38
|
+
return join(home, ".outerlayer", "config.json");
|
|
39
|
+
}
|
|
40
|
+
function fail(configPath, key, reason) {
|
|
41
|
+
throw new RunnerConfigError(`${configPath}: ${key} ${reason}`);
|
|
42
|
+
}
|
|
43
|
+
function readConfigFile(configPath) {
|
|
44
|
+
if (!existsSync(configPath)) return {};
|
|
45
|
+
const text = readFileSync(configPath, "utf8");
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(text);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
throw new RunnerConfigError(
|
|
50
|
+
`${configPath}: is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function hasRunnerBlock(configPath) {
|
|
55
|
+
try {
|
|
56
|
+
const runner = readConfigFile(configPath).runner;
|
|
57
|
+
return typeof runner === "object" && runner !== null;
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function runnerVmOf(configPath) {
|
|
63
|
+
try {
|
|
64
|
+
const vm = readConfigFile(configPath).runnerVm;
|
|
65
|
+
return typeof vm?.instance === "string" && typeof vm.since === "string" ? { instance: vm.instance, since: vm.since } : void 0;
|
|
66
|
+
} catch {
|
|
67
|
+
return void 0;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
var RUNNER_KEYS = [
|
|
71
|
+
"host",
|
|
72
|
+
"queues",
|
|
73
|
+
"concurrency",
|
|
74
|
+
"pollSeconds",
|
|
75
|
+
"timeLimitMinutes",
|
|
76
|
+
"idleLimitMinutes",
|
|
77
|
+
"commands",
|
|
78
|
+
"hooks",
|
|
79
|
+
"build",
|
|
80
|
+
"autoUpdate"
|
|
81
|
+
];
|
|
82
|
+
function runnerNumbers() {
|
|
83
|
+
return [
|
|
84
|
+
{ key: "concurrency", min: 1 },
|
|
85
|
+
// Below five seconds a runner is asking the gateway a question it has
|
|
86
|
+
// just been answered. Above one day the gateway refuses every heartbeat
|
|
87
|
+
// this runner sends (`POST /v1/hosts/heartbeat` caps `pollSeconds` at
|
|
88
|
+
// 86400), so the host would poll and never appear in `GET /v1/hosts`.
|
|
89
|
+
{ key: "pollSeconds", min: 5, max: 86400 },
|
|
90
|
+
// A build's item key expires 24 hours after its lease was created, and
|
|
91
|
+
// the time limit is counted from job start, after the claim and the
|
|
92
|
+
// provision hook. The runner still syncs and releases once the limit
|
|
93
|
+
// fires, so the ceiling leaves two hours of the key's life for the
|
|
94
|
+
// provision hook, the final sync and the release.
|
|
95
|
+
{ key: "timeLimitMinutes", min: 1, max: 1320 },
|
|
96
|
+
{ key: "idleLimitMinutes", min: 1 }
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
function nearestKey(unknown, accepted) {
|
|
100
|
+
const lower = unknown.toLowerCase();
|
|
101
|
+
let best;
|
|
102
|
+
for (const key of accepted) {
|
|
103
|
+
const distance = editDistance(lower, key.toLowerCase());
|
|
104
|
+
if (distance <= Math.max(1, Math.floor(key.length / 4)) && (!best || distance < best.distance)) {
|
|
105
|
+
best = { key, distance };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return best?.key;
|
|
109
|
+
}
|
|
110
|
+
function editDistance(a, b) {
|
|
111
|
+
const rows = Array.from({ length: a.length + 1 }, (_unused, i) => [i, ...Array(b.length).fill(0)]);
|
|
112
|
+
for (let j = 0; j <= b.length; j++) rows[0][j] = j;
|
|
113
|
+
for (let i = 1; i <= a.length; i++) {
|
|
114
|
+
for (let j = 1; j <= b.length; j++) {
|
|
115
|
+
rows[i][j] = Math.min(
|
|
116
|
+
rows[i - 1][j] + 1,
|
|
117
|
+
rows[i][j - 1] + 1,
|
|
118
|
+
rows[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return rows[a.length][b.length];
|
|
123
|
+
}
|
|
124
|
+
function checkHooks(config, env = process.env) {
|
|
125
|
+
const named = [
|
|
126
|
+
["runner.hooks.provision", config.hooks.provision],
|
|
127
|
+
["runner.hooks.cleanup", config.hooks.cleanup],
|
|
128
|
+
["runner.hooks.report", config.hooks.report]
|
|
129
|
+
];
|
|
130
|
+
for (const [key, path] of named) {
|
|
131
|
+
if (path === void 0) continue;
|
|
132
|
+
if (isBuiltinContainer(path) || isBuiltinProcess(path) || isBuiltinVm(path)) continue;
|
|
133
|
+
if (path.includes("$") || path.startsWith("~")) {
|
|
134
|
+
return `${key}: ${path} \u2014 hooks are executable paths, not shell lines; $HOME and ~ are not expanded`;
|
|
135
|
+
}
|
|
136
|
+
if (!existsSync(path)) return `${key}: ${path} does not exist`;
|
|
137
|
+
if (!statSync(path).isFile()) return `${key}: ${path} is not a file`;
|
|
138
|
+
try {
|
|
139
|
+
accessSync(path, constants.X_OK);
|
|
140
|
+
} catch {
|
|
141
|
+
return `${key}: ${path} is not executable`;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return checkDestinationVariables(config, env);
|
|
145
|
+
}
|
|
146
|
+
function checkDestinationVariables(config, env) {
|
|
147
|
+
const destinations = config.build?.destinations ?? [];
|
|
148
|
+
for (const [index, destination] of destinations.entries()) {
|
|
149
|
+
for (const name of destination.header.variables) {
|
|
150
|
+
if (env[name] === void 0 || env[name] === "") {
|
|
151
|
+
return `runner.build.destinations[${index}].header: ${name} is not set in the runner's environment`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return void 0;
|
|
156
|
+
}
|
|
157
|
+
function loadRunnerConfig(configPath) {
|
|
158
|
+
const config = readConfigFile(configPath);
|
|
159
|
+
if (!config.url || !config.apiKey || !config.appId) {
|
|
160
|
+
fail(configPath, "url/apiKey/appId", "missing \u2014 pipe a factory key into `outerlayer login` for this config file first; a runner uses a factory key, never a person's login");
|
|
161
|
+
}
|
|
162
|
+
const runner = config.runner;
|
|
163
|
+
if (!runner) {
|
|
164
|
+
fail(configPath, "runner", "block is missing");
|
|
165
|
+
}
|
|
166
|
+
for (const key of Object.keys(runner)) {
|
|
167
|
+
if (RUNNER_KEYS.includes(key)) continue;
|
|
168
|
+
const near = nearestKey(key, RUNNER_KEYS);
|
|
169
|
+
fail(configPath, `runner.${key}`, near ? `is not a key this block accepts; did you mean ${near}?` : "is not a key this block accepts");
|
|
170
|
+
}
|
|
171
|
+
for (const { key, min, max = Number.POSITIVE_INFINITY } of runnerNumbers()) {
|
|
172
|
+
const value = runner[key];
|
|
173
|
+
if (value === void 0) continue;
|
|
174
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < min) {
|
|
175
|
+
fail(configPath, `runner.${key}`, `must be a whole number of at least ${min}`);
|
|
176
|
+
}
|
|
177
|
+
if (value > max) {
|
|
178
|
+
fail(configPath, `runner.${key}`, `must be at most ${max}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (runner.autoUpdate !== void 0 && typeof runner.autoUpdate !== "boolean") {
|
|
182
|
+
fail(configPath, "runner.autoUpdate", "must be true or false");
|
|
183
|
+
}
|
|
184
|
+
const queues = runner.queues && runner.queues.length > 0 ? runner.queues : [...RUNNER_QUEUES];
|
|
185
|
+
for (const queue of queues) {
|
|
186
|
+
if (!RUNNER_QUEUES.includes(queue)) {
|
|
187
|
+
fail(configPath, "runner.queues", `names an unknown queue "${queue}" \u2014 must be implement or amend`);
|
|
188
|
+
}
|
|
189
|
+
if (!runner.commands?.[queue]) {
|
|
190
|
+
fail(configPath, `runner.commands.${queue}`, "is required for an enabled queue");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (!runner.hooks?.provision) fail(configPath, "runner.hooks.provision", "is required");
|
|
194
|
+
if (!runner.hooks?.cleanup) fail(configPath, "runner.hooks.cleanup", "is required");
|
|
195
|
+
checkBuiltinHooks(configPath, runner.hooks);
|
|
196
|
+
const build = resolveBuild(configPath, runner.build);
|
|
197
|
+
if (config.scrub?.literals !== void 0 && !Array.isArray(config.scrub.literals)) {
|
|
198
|
+
fail(configPath, "scrub.literals", "must be a list of strings");
|
|
199
|
+
}
|
|
200
|
+
if (config.scrub?.patterns !== void 0 && !Array.isArray(config.scrub.patterns)) {
|
|
201
|
+
fail(configPath, "scrub.patterns", "must be a list of {label, pattern} entries");
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
configPath,
|
|
205
|
+
url: config.url,
|
|
206
|
+
apiKey: config.apiKey,
|
|
207
|
+
appId: config.appId,
|
|
208
|
+
repos: config.repos,
|
|
209
|
+
scrub: config.scrub,
|
|
210
|
+
host: runner.host ?? hostname(),
|
|
211
|
+
queues,
|
|
212
|
+
concurrency: runner.concurrency ?? DEFAULT_CONCURRENCY,
|
|
213
|
+
pollSeconds: runner.pollSeconds ?? DEFAULT_POLL_SECONDS,
|
|
214
|
+
timeLimitMinutes: runner.timeLimitMinutes ?? DEFAULT_TIME_LIMIT_MINUTES,
|
|
215
|
+
idleLimitMinutes: runner.idleLimitMinutes ?? DEFAULT_IDLE_LIMIT_MINUTES,
|
|
216
|
+
commands: runner.commands ?? {},
|
|
217
|
+
hooks: {
|
|
218
|
+
provision: runner.hooks.provision,
|
|
219
|
+
cleanup: runner.hooks.cleanup,
|
|
220
|
+
report: runner.hooks.report
|
|
221
|
+
},
|
|
222
|
+
build,
|
|
223
|
+
// A host with executables of its own for provision and cleanup chose
|
|
224
|
+
// its own environment, and an update could change what its hooks run
|
|
225
|
+
// under; it opts in. The built-in hooks are the runner's own. Provision
|
|
226
|
+
// and cleanup are built-in together or not at all (checkBuiltinHooks), so
|
|
227
|
+
// provision alone tells which.
|
|
228
|
+
autoUpdate: runner.autoUpdate ?? builtinKind(runner.hooks.provision) !== "own"
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function checkBuiltinHooks(configPath, hooks) {
|
|
232
|
+
for (const key of ["provision", "cleanup", "report"]) {
|
|
233
|
+
const value = hooks[key];
|
|
234
|
+
if (value === void 0 || !value.startsWith("builtin:")) continue;
|
|
235
|
+
if (key === "report") fail(configPath, "runner.hooks.report", `cannot be ${value}: the report hook stays an executable`);
|
|
236
|
+
if (!isBuiltinContainer(value) && !isBuiltinProcess(value) && !isBuiltinVm(value)) {
|
|
237
|
+
fail(configPath, `runner.hooks.${key}`, `names an unknown built-in hook "${value}"; the ones that exist are ${BUILTIN_CONTAINER}, ${BUILTIN_PROCESS} and ${BUILTIN_VM}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const provision = hooks.provision;
|
|
241
|
+
const cleanup = hooks.cleanup;
|
|
242
|
+
if (isBuiltinContainer(provision) !== isBuiltinContainer(cleanup) || isBuiltinProcess(provision) !== isBuiltinProcess(cleanup) || isBuiltinVm(provision) !== isBuiltinVm(cleanup)) {
|
|
243
|
+
fail(configPath, "runner.hooks", `provision and cleanup must both be ${BUILTIN_CONTAINER}, both be ${BUILTIN_PROCESS}, both be ${BUILTIN_VM}, or neither`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function resolveBuild(configPath, block) {
|
|
247
|
+
if (block === void 0) return { ...DEFAULT_BUILD, variables: [] };
|
|
248
|
+
if (typeof block !== "object" || block === null || Array.isArray(block)) fail(configPath, "runner.build", "must be an object");
|
|
249
|
+
const given = block;
|
|
250
|
+
for (const key of Object.keys(given)) {
|
|
251
|
+
if (BUILD_KEYS.includes(key)) continue;
|
|
252
|
+
const near = nearestKey(key, BUILD_KEYS);
|
|
253
|
+
fail(configPath, `runner.build.${key}`, near ? `is not a key this block accepts; did you mean ${near}?` : "is not a key this block accepts");
|
|
254
|
+
}
|
|
255
|
+
for (const key of ["memory", "disk"]) {
|
|
256
|
+
const value = given[key];
|
|
257
|
+
if (value === void 0) continue;
|
|
258
|
+
if (typeof value !== "string" || parseSize(value) === void 0) fail(configPath, `runner.build.${key}`, 'must be a size such as "8g" or "512m"');
|
|
259
|
+
}
|
|
260
|
+
if (given.cpus !== void 0 && (typeof given.cpus !== "number" || !Number.isFinite(given.cpus) || given.cpus <= 0)) {
|
|
261
|
+
fail(configPath, "runner.build.cpus", "must be a number above 0");
|
|
262
|
+
}
|
|
263
|
+
if (given.pids !== void 0 && (typeof given.pids !== "number" || !Number.isInteger(given.pids) || given.pids < 1)) {
|
|
264
|
+
fail(configPath, "runner.build.pids", "must be a whole number of at least 1");
|
|
265
|
+
}
|
|
266
|
+
const variables = [];
|
|
267
|
+
if (given.variables !== void 0) {
|
|
268
|
+
if (!Array.isArray(given.variables)) fail(configPath, "runner.build.variables", "must be a list of variable names");
|
|
269
|
+
if (given.variables.length > MAX_VARIABLES) fail(configPath, "runner.build.variables", `names ${given.variables.length} variables; a release records at most ${MAX_VARIABLES}`);
|
|
270
|
+
for (const name of given.variables) {
|
|
271
|
+
if (typeof name !== "string" || !VARIABLE_NAME.test(name)) fail(configPath, "runner.build.variables", `names ${JSON.stringify(name)}, which is not a variable name`);
|
|
272
|
+
if (isRefusedInConfig(name)) fail(configPath, "runner.build.variables", `names ${name}, which the runner sets itself`);
|
|
273
|
+
if (!variables.includes(name)) variables.push(name);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const destinations = given.destinations === void 0 ? [] : resolveDestinations(given.destinations, variables, isReservedVariable, (key, reason) => fail(configPath, key, reason));
|
|
277
|
+
let allowHosts;
|
|
278
|
+
if (given.allowHosts !== void 0) {
|
|
279
|
+
if (!Array.isArray(given.allowHosts)) fail(configPath, "runner.build.allowHosts", "must be a list of host names");
|
|
280
|
+
if (given.allowHosts.length > MAX_ALLOW_HOSTS) fail(configPath, "runner.build.allowHosts", `names ${given.allowHosts.length} hosts; at most ${MAX_ALLOW_HOSTS}`);
|
|
281
|
+
allowHosts = [];
|
|
282
|
+
for (const entry of given.allowHosts) {
|
|
283
|
+
const problem = allowHostProblem(entry);
|
|
284
|
+
if (problem !== void 0) fail(configPath, "runner.build.allowHosts", `names ${JSON.stringify(entry)}, which ${problem}`);
|
|
285
|
+
const host = entry.toLowerCase();
|
|
286
|
+
if (!allowHosts.includes(host)) allowHosts.push(host);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
memory: given.memory ?? DEFAULT_BUILD.memory,
|
|
291
|
+
cpus: given.cpus ?? DEFAULT_BUILD.cpus,
|
|
292
|
+
pids: given.pids ?? DEFAULT_BUILD.pids,
|
|
293
|
+
disk: given.disk ?? DEFAULT_BUILD.disk,
|
|
294
|
+
variables,
|
|
295
|
+
...destinations.length === 0 ? {} : { destinations },
|
|
296
|
+
...allowHosts === void 0 ? {} : { allowHosts }
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export { RunnerConfigError, builtinKind, checkHooks, defaultRunnerConfigPath, hasRunnerBlock, isBuiltinContainer, isBuiltinProcess, isBuiltinVm, loadRunnerConfig, resolveBuild, runnerVmOf };
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { ArtifactTombstoneRecordSchema, ArtifactSpoolRecordSchema } from './chunk-G7FSV7JX.js';
|
|
4
|
+
import { homeFromEnv } from './chunk-VNVZDWO3.js';
|
|
5
|
+
import { mkdirSync, appendFileSync, writeFileSync, renameSync, existsSync, unlinkSync, readdirSync, readFileSync } from 'fs';
|
|
6
|
+
import { join, dirname } from 'path';
|
|
7
|
+
|
|
8
|
+
createRequire(import.meta.url);
|
|
9
|
+
function artifactsSpoolPath(home = homeFromEnv()) {
|
|
10
|
+
return join(home, ".outerlayer", "spool", "artifacts.jsonl");
|
|
11
|
+
}
|
|
12
|
+
function artifactBlobsDir(home = homeFromEnv()) {
|
|
13
|
+
return join(home, ".outerlayer", "spool", "artifact-blobs");
|
|
14
|
+
}
|
|
15
|
+
function artifactsStatePath(home = homeFromEnv()) {
|
|
16
|
+
return join(home, ".outerlayer", "spool", "artifacts.state.json");
|
|
17
|
+
}
|
|
18
|
+
var ARTIFACT_RETRY_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1e3;
|
|
19
|
+
function appendArtifactRecord(record, home = homeFromEnv()) {
|
|
20
|
+
const path = artifactsSpoolPath(home);
|
|
21
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
22
|
+
appendFileSync(path, JSON.stringify(record) + "\n");
|
|
23
|
+
}
|
|
24
|
+
function appendArtifactTombstone(artifactId, home = homeFromEnv(), now = () => /* @__PURE__ */ new Date()) {
|
|
25
|
+
const path = artifactsSpoolPath(home);
|
|
26
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
27
|
+
const record = { rec: "artifact-tombstone", artifactId, t: now().toISOString() };
|
|
28
|
+
appendFileSync(path, JSON.stringify(record) + "\n");
|
|
29
|
+
}
|
|
30
|
+
function walkArtifactSpoolLines(home, visit) {
|
|
31
|
+
let buf;
|
|
32
|
+
try {
|
|
33
|
+
buf = readFileSync(artifactsSpoolPath(home));
|
|
34
|
+
} catch {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
let pos = 0;
|
|
38
|
+
while (pos < buf.length) {
|
|
39
|
+
const nl = buf.indexOf(10, pos);
|
|
40
|
+
const textEnd = nl === -1 ? buf.length : nl;
|
|
41
|
+
const text = buf.subarray(pos, textEnd).toString("utf8").trim();
|
|
42
|
+
if (text) visit(text, pos);
|
|
43
|
+
if (nl === -1) break;
|
|
44
|
+
pos = nl + 1;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function isArtifactTombstoned(artifactId, home = homeFromEnv()) {
|
|
48
|
+
let found = false;
|
|
49
|
+
walkArtifactSpoolLines(home, (text) => {
|
|
50
|
+
if (found) return;
|
|
51
|
+
try {
|
|
52
|
+
const parsed = ArtifactTombstoneRecordSchema.safeParse(JSON.parse(text));
|
|
53
|
+
if (parsed.success && parsed.data.artifactId === artifactId) found = true;
|
|
54
|
+
} catch {
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
return found;
|
|
58
|
+
}
|
|
59
|
+
var KNOWN_SPOOL_RECORD_TYPES = /* @__PURE__ */ new Set(["artifact", "artifact-tombstone"]);
|
|
60
|
+
var SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
61
|
+
var MAX_DISPLAYED_REC_TYPE_LENGTH = 100;
|
|
62
|
+
var CONTROL_CHAR_PATTERN = new RegExp(
|
|
63
|
+
`[${String.fromCharCode(0)}-${String.fromCharCode(31)}${String.fromCharCode(127)}-${String.fromCharCode(159)}]`,
|
|
64
|
+
"g"
|
|
65
|
+
);
|
|
66
|
+
function sanitizeTerminal(text) {
|
|
67
|
+
return text.replace(CONTROL_CHAR_PATTERN, "");
|
|
68
|
+
}
|
|
69
|
+
function sanitizeRecType(rec) {
|
|
70
|
+
const stripped = sanitizeTerminal(rec);
|
|
71
|
+
return stripped.length > MAX_DISPLAYED_REC_TYPE_LENGTH ? `${stripped.slice(0, MAX_DISPLAYED_REC_TYPE_LENGTH)}\u2026` : stripped;
|
|
72
|
+
}
|
|
73
|
+
function readUnknownRecordEnvelope(json) {
|
|
74
|
+
if (typeof json !== "object" || json === null) return void 0;
|
|
75
|
+
const rec = json.rec;
|
|
76
|
+
const t = json.t;
|
|
77
|
+
if (typeof rec !== "string" || typeof t !== "string") return void 0;
|
|
78
|
+
const shaRaw = json.sha256;
|
|
79
|
+
const sha256 = typeof shaRaw === "string" && SHA256_HEX.test(shaRaw) ? shaRaw : void 0;
|
|
80
|
+
return { rec, t, sha256 };
|
|
81
|
+
}
|
|
82
|
+
function classifySpoolLine(text) {
|
|
83
|
+
let json;
|
|
84
|
+
try {
|
|
85
|
+
json = JSON.parse(text);
|
|
86
|
+
} catch {
|
|
87
|
+
return { kind: "skipped", reason: "not valid JSON" };
|
|
88
|
+
}
|
|
89
|
+
const tombstone = ArtifactTombstoneRecordSchema.safeParse(json);
|
|
90
|
+
if (tombstone.success) return { kind: "tombstone", artifactId: tombstone.data.artifactId };
|
|
91
|
+
const parsed = ArtifactSpoolRecordSchema.safeParse(json);
|
|
92
|
+
if (parsed.success) return { kind: "record", record: parsed.data };
|
|
93
|
+
const envelope = readUnknownRecordEnvelope(json);
|
|
94
|
+
if (envelope !== void 0 && !KNOWN_SPOOL_RECORD_TYPES.has(envelope.rec)) {
|
|
95
|
+
const sanitizedRecType = sanitizeRecType(envelope.rec);
|
|
96
|
+
const recType = sanitizedRecType === "" ? "(empty)" : `"${sanitizedRecType}"`;
|
|
97
|
+
return {
|
|
98
|
+
kind: "held",
|
|
99
|
+
reason: `unrecognized record type ${recType} \u2014 this build's schemas do not recognize it`,
|
|
100
|
+
...envelope.sha256 !== void 0 ? { sha256: envelope.sha256 } : {}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const issue = parsed.error.issues[0];
|
|
104
|
+
const issueDetail = issue ? ` (${issue.path.length > 0 ? `${issue.path.join(".")}: ` : ""}${sanitizeTerminal(issue.message)})` : "";
|
|
105
|
+
return { kind: "skipped", reason: `does not match the artifact spool record shape${issueDetail}` };
|
|
106
|
+
}
|
|
107
|
+
function legacySidecarPaths(home) {
|
|
108
|
+
const dir = join(home, ".outerlayer", "spool");
|
|
109
|
+
return [
|
|
110
|
+
join(dir, "artifacts.watermark"),
|
|
111
|
+
join(dir, "artifacts.uploaded.json"),
|
|
112
|
+
join(dir, "artifacts.dropped.json"),
|
|
113
|
+
join(dir, "artifacts.held-offsets.json"),
|
|
114
|
+
join(dir, "artifacts.pending-first-seen.json")
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
function readLegacySidecars(home) {
|
|
118
|
+
const dir = join(home, ".outerlayer", "spool");
|
|
119
|
+
let watermark = 0;
|
|
120
|
+
try {
|
|
121
|
+
const n = parseInt(readFileSync(join(dir, "artifacts.watermark"), "utf8").trim(), 10);
|
|
122
|
+
if (Number.isFinite(n) && n >= 0) watermark = n;
|
|
123
|
+
} catch {
|
|
124
|
+
}
|
|
125
|
+
const readIdSet = (name) => {
|
|
126
|
+
try {
|
|
127
|
+
const raw = JSON.parse(readFileSync(join(dir, name), "utf8"));
|
|
128
|
+
return new Set(Array.isArray(raw) ? raw.filter((v) => typeof v === "string") : []);
|
|
129
|
+
} catch {
|
|
130
|
+
return /* @__PURE__ */ new Set();
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
let pendingFirstSeen = {};
|
|
134
|
+
try {
|
|
135
|
+
const raw = JSON.parse(readFileSync(join(dir, "artifacts.pending-first-seen.json"), "utf8"));
|
|
136
|
+
if (raw !== null && typeof raw === "object" && !Array.isArray(raw)) {
|
|
137
|
+
const out = {};
|
|
138
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
139
|
+
if (typeof value === "string") out[key] = value;
|
|
140
|
+
}
|
|
141
|
+
pendingFirstSeen = out;
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
watermark,
|
|
147
|
+
uploadedIds: readIdSet("artifacts.uploaded.json"),
|
|
148
|
+
droppedIds: readIdSet("artifacts.dropped.json"),
|
|
149
|
+
pendingFirstSeen
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function computeMigratedState(home) {
|
|
153
|
+
const legacy = readLegacySidecars(home);
|
|
154
|
+
const state = /* @__PURE__ */ new Map();
|
|
155
|
+
const tombstoned = /* @__PURE__ */ new Set();
|
|
156
|
+
const rawRecords = [];
|
|
157
|
+
walkArtifactSpoolLines(home, (text, offset) => {
|
|
158
|
+
const classified = classifySpoolLine(text);
|
|
159
|
+
if (classified.kind === "tombstone") {
|
|
160
|
+
tombstoned.add(classified.artifactId);
|
|
161
|
+
} else if (classified.kind === "record") {
|
|
162
|
+
rawRecords.push({
|
|
163
|
+
artifactId: classified.record.artifactId,
|
|
164
|
+
offset,
|
|
165
|
+
sha256: classified.record.sha256,
|
|
166
|
+
t: classified.record.t
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
171
|
+
for (const { artifactId, offset, sha256, t } of rawRecords) {
|
|
172
|
+
if (offset >= legacy.watermark) continue;
|
|
173
|
+
if (legacy.uploadedIds.has(artifactId)) {
|
|
174
|
+
state.set(artifactId, { state: "uploaded", attempts: 1, firstSeen: t || nowIso, sha256, reachedServer: true });
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (legacy.droppedIds.has(artifactId)) {
|
|
178
|
+
state.set(artifactId, {
|
|
179
|
+
state: "dropped",
|
|
180
|
+
attempts: 1,
|
|
181
|
+
firstSeen: legacy.pendingFirstSeen[artifactId] ?? t ?? nowIso,
|
|
182
|
+
sha256,
|
|
183
|
+
reachedServer: false
|
|
184
|
+
});
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (tombstoned.has(artifactId)) continue;
|
|
188
|
+
state.set(artifactId, { state: "dropped", attempts: 1, firstSeen: t || nowIso, sha256, reachedServer: true });
|
|
189
|
+
}
|
|
190
|
+
return state;
|
|
191
|
+
}
|
|
192
|
+
function readPersistedArtifactSpoolState(home) {
|
|
193
|
+
let raw;
|
|
194
|
+
try {
|
|
195
|
+
raw = readFileSync(artifactsStatePath(home), "utf8");
|
|
196
|
+
} catch {
|
|
197
|
+
return { state: /* @__PURE__ */ new Map(), corrupt: false };
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
const parsed = JSON.parse(raw);
|
|
201
|
+
const records = parsed !== null && typeof parsed === "object" && Array.isArray(parsed.records) ? parsed.records : void 0;
|
|
202
|
+
if (records === void 0) return { state: /* @__PURE__ */ new Map(), corrupt: true };
|
|
203
|
+
const state = /* @__PURE__ */ new Map();
|
|
204
|
+
for (const entry of records) {
|
|
205
|
+
if (entry === null || typeof entry !== "object") continue;
|
|
206
|
+
const e = entry;
|
|
207
|
+
if (typeof e.artifactId !== "string") continue;
|
|
208
|
+
if (e.state !== "pending" && e.state !== "uploaded" && e.state !== "held" && e.state !== "dropped") continue;
|
|
209
|
+
state.set(e.artifactId, {
|
|
210
|
+
state: e.state,
|
|
211
|
+
attempts: typeof e.attempts === "number" && Number.isFinite(e.attempts) ? e.attempts : 0,
|
|
212
|
+
firstSeen: typeof e.firstSeen === "string" ? e.firstSeen : (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
213
|
+
sha256: typeof e.sha256 === "string" ? e.sha256 : "",
|
|
214
|
+
reachedServer: e.reachedServer === true
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
return { state, corrupt: false };
|
|
218
|
+
} catch {
|
|
219
|
+
return { state: /* @__PURE__ */ new Map(), corrupt: true };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
var ARTIFACT_STATE_RESOLVED_CAP = 5e3;
|
|
223
|
+
function writeArtifactSpoolState(state, home = homeFromEnv()) {
|
|
224
|
+
try {
|
|
225
|
+
const path = artifactsStatePath(home);
|
|
226
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
227
|
+
const tmp = `${path}.tmp`;
|
|
228
|
+
const pending = [];
|
|
229
|
+
const resolved = [];
|
|
230
|
+
for (const pair of state.entries()) {
|
|
231
|
+
(pair[1].state === "uploaded" || pair[1].state === "dropped" ? resolved : pending).push(pair);
|
|
232
|
+
}
|
|
233
|
+
const cappedResolved = resolved.length > ARTIFACT_STATE_RESOLVED_CAP ? resolved.slice(-ARTIFACT_STATE_RESOLVED_CAP) : resolved;
|
|
234
|
+
const records = [...pending, ...cappedResolved].map(([artifactId, entry]) => ({ artifactId, ...entry }));
|
|
235
|
+
writeFileSync(tmp, JSON.stringify({ version: 1, records }));
|
|
236
|
+
renameSync(tmp, path);
|
|
237
|
+
return true;
|
|
238
|
+
} catch {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function migrateLegacyArtifactSidecars(home = homeFromEnv()) {
|
|
243
|
+
if (existsSync(artifactsStatePath(home))) return;
|
|
244
|
+
const paths = legacySidecarPaths(home);
|
|
245
|
+
if (!paths.some((p) => existsSync(p))) return;
|
|
246
|
+
writeArtifactSpoolState(computeMigratedState(home), home);
|
|
247
|
+
for (const p of paths) {
|
|
248
|
+
try {
|
|
249
|
+
unlinkSync(p);
|
|
250
|
+
} catch {
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function readEffectiveArtifactSpoolState(home = homeFromEnv()) {
|
|
255
|
+
const { state: persisted, corrupt } = existsSync(artifactsStatePath(home)) ? readPersistedArtifactSpoolState(home) : { state: computeMigratedState(home), corrupt: false };
|
|
256
|
+
const state = new Map(persisted);
|
|
257
|
+
const tombstonedIds = /* @__PURE__ */ new Set();
|
|
258
|
+
walkArtifactSpoolLines(home, (text) => {
|
|
259
|
+
const classified = classifySpoolLine(text);
|
|
260
|
+
if (classified.kind === "tombstone") tombstonedIds.add(classified.artifactId);
|
|
261
|
+
});
|
|
262
|
+
if (tombstonedIds.size > 0) {
|
|
263
|
+
walkArtifactSpoolLines(home, (text) => {
|
|
264
|
+
const classified = classifySpoolLine(text);
|
|
265
|
+
if (classified.kind !== "record") return;
|
|
266
|
+
const { record } = classified;
|
|
267
|
+
if (!tombstonedIds.has(record.artifactId)) return;
|
|
268
|
+
const existing = state.get(record.artifactId);
|
|
269
|
+
if (existing === void 0 || existing.state === "held" || existing.state === "pending") {
|
|
270
|
+
state.set(record.artifactId, {
|
|
271
|
+
state: "dropped",
|
|
272
|
+
attempts: existing?.attempts ?? 0,
|
|
273
|
+
firstSeen: existing?.firstSeen ?? record.t,
|
|
274
|
+
sha256: record.sha256,
|
|
275
|
+
reachedServer: false
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
return { state, corrupt };
|
|
281
|
+
}
|
|
282
|
+
function planArtifactUpload(opts = {}) {
|
|
283
|
+
const home = opts.home ?? homeFromEnv();
|
|
284
|
+
const { state: effective, corrupt } = readEffectiveArtifactSpoolState(home);
|
|
285
|
+
const state = new Map(effective);
|
|
286
|
+
const candidates = [];
|
|
287
|
+
const skipped = [];
|
|
288
|
+
const held = [];
|
|
289
|
+
const tombstonedIds = /* @__PURE__ */ new Set();
|
|
290
|
+
walkArtifactSpoolLines(home, (text) => {
|
|
291
|
+
const classified = classifySpoolLine(text);
|
|
292
|
+
if (classified.kind === "tombstone") tombstonedIds.add(classified.artifactId);
|
|
293
|
+
});
|
|
294
|
+
walkArtifactSpoolLines(home, (text, offset) => {
|
|
295
|
+
const classified = classifySpoolLine(text);
|
|
296
|
+
if (classified.kind === "tombstone") return;
|
|
297
|
+
if (classified.kind === "skipped") {
|
|
298
|
+
skipped.push({ offset, reason: classified.reason });
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (classified.kind === "held") {
|
|
302
|
+
held.push({ offset, reason: classified.reason, ...classified.sha256 !== void 0 ? { sha256: classified.sha256 } : {} });
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const { record } = classified;
|
|
306
|
+
if (tombstonedIds.has(record.artifactId)) return;
|
|
307
|
+
const existing = state.get(record.artifactId);
|
|
308
|
+
if (existing === void 0 || existing.state === "held" || existing.state === "pending") {
|
|
309
|
+
candidates.push({ record });
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
return { candidates, state, skipped, held, corrupt };
|
|
313
|
+
}
|
|
314
|
+
function neededArtifactShas(home, finalState) {
|
|
315
|
+
const needed = /* @__PURE__ */ new Set();
|
|
316
|
+
walkArtifactSpoolLines(home, (text) => {
|
|
317
|
+
const classified = classifySpoolLine(text);
|
|
318
|
+
if (classified.kind === "held") {
|
|
319
|
+
if (classified.sha256 !== void 0) needed.add(classified.sha256);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (classified.kind !== "record") return;
|
|
323
|
+
const entry = finalState.get(classified.record.artifactId);
|
|
324
|
+
if (entry === void 0 || entry.state === "held" || entry.state === "pending") needed.add(classified.record.sha256);
|
|
325
|
+
});
|
|
326
|
+
return needed;
|
|
327
|
+
}
|
|
328
|
+
function reclaimUnneededArtifactBlobs(home, neededShas) {
|
|
329
|
+
let names;
|
|
330
|
+
try {
|
|
331
|
+
names = readdirSync(artifactBlobsDir(home));
|
|
332
|
+
} catch {
|
|
333
|
+
return 0;
|
|
334
|
+
}
|
|
335
|
+
let reclaimed = 0;
|
|
336
|
+
for (const name of names) {
|
|
337
|
+
if (!SHA256_HEX.test(name) || neededShas.has(name)) continue;
|
|
338
|
+
try {
|
|
339
|
+
unlinkSync(join(artifactBlobsDir(home), name));
|
|
340
|
+
reclaimed += 1;
|
|
341
|
+
} catch {
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return reclaimed;
|
|
345
|
+
}
|
|
346
|
+
function localArtifactIds(home = homeFromEnv()) {
|
|
347
|
+
const ids = /* @__PURE__ */ new Set();
|
|
348
|
+
walkArtifactSpoolLines(home, (text) => {
|
|
349
|
+
const classified = classifySpoolLine(text);
|
|
350
|
+
if (classified.kind === "record") ids.add(classified.record.artifactId);
|
|
351
|
+
});
|
|
352
|
+
return ids;
|
|
353
|
+
}
|
|
354
|
+
function findPendingArtifactRecord(artifactId, home = homeFromEnv()) {
|
|
355
|
+
let found;
|
|
356
|
+
walkArtifactSpoolLines(home, (text) => {
|
|
357
|
+
if (found !== void 0) return;
|
|
358
|
+
const classified = classifySpoolLine(text);
|
|
359
|
+
if (classified.kind === "record" && classified.record.artifactId === artifactId) found = classified.record;
|
|
360
|
+
});
|
|
361
|
+
if (found === void 0) return void 0;
|
|
362
|
+
const { state } = readEffectiveArtifactSpoolState(home);
|
|
363
|
+
const entry = state.get(artifactId);
|
|
364
|
+
if (entry !== void 0 && entry.state !== "held" && entry.state !== "pending") return void 0;
|
|
365
|
+
return { record: found };
|
|
366
|
+
}
|
|
367
|
+
function resolveArtifactTurnIndex(turns, filename) {
|
|
368
|
+
let found;
|
|
369
|
+
for (const turn of turns) {
|
|
370
|
+
for (const call of turn.toolCalls ?? []) {
|
|
371
|
+
const haystack = [
|
|
372
|
+
call.name,
|
|
373
|
+
typeof call.input === "string" ? call.input : call.input === void 0 ? "" : JSON.stringify(call.input),
|
|
374
|
+
typeof call.output === "string" ? call.output : call.output === void 0 ? "" : JSON.stringify(call.output),
|
|
375
|
+
call.file ?? ""
|
|
376
|
+
].join("\n");
|
|
377
|
+
if (haystack.includes("emit artifact") && haystack.includes(filename)) found = turn.index;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return found;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export { ARTIFACT_RETRY_MAX_AGE_MS, appendArtifactRecord, appendArtifactTombstone, artifactBlobsDir, artifactsSpoolPath, artifactsStatePath, findPendingArtifactRecord, isArtifactTombstoned, localArtifactIds, migrateLegacyArtifactSidecars, neededArtifactShas, planArtifactUpload, readEffectiveArtifactSpoolState, reclaimUnneededArtifactBlobs, resolveArtifactTurnIndex, sanitizeTerminal, writeArtifactSpoolState };
|