@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,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { join, dirname } from 'path';
|
|
4
|
+
|
|
5
|
+
createRequire(import.meta.url);
|
|
6
|
+
var RUNNER_DIR_MODE = 448;
|
|
7
|
+
var RUNNER_FILE_MODE = 384;
|
|
8
|
+
function runnerDir(configPath) {
|
|
9
|
+
return join(dirname(configPath), "runner");
|
|
10
|
+
}
|
|
11
|
+
function hostKeyPath(configPath) {
|
|
12
|
+
return join(runnerDir(configPath), "host-key.pem");
|
|
13
|
+
}
|
|
14
|
+
function pidFilePath(configPath) {
|
|
15
|
+
return join(runnerDir(configPath), "runner.pid");
|
|
16
|
+
}
|
|
17
|
+
function jobsDir(configPath) {
|
|
18
|
+
return join(runnerDir(configPath), "jobs");
|
|
19
|
+
}
|
|
20
|
+
function jobDir(configPath, queue, item, claimId, attempt) {
|
|
21
|
+
const suffix = attempt === void 0 ? "" : `-${attempt}`;
|
|
22
|
+
return join(jobsDir(configPath), `${queue}-${item}-${claimId}${suffix}`);
|
|
23
|
+
}
|
|
24
|
+
function attemptStamp(at) {
|
|
25
|
+
return at.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
26
|
+
}
|
|
27
|
+
function imagesDir(configPath) {
|
|
28
|
+
return join(runnerDir(configPath), "images");
|
|
29
|
+
}
|
|
30
|
+
function repoCacheDir(configPath, owner, name) {
|
|
31
|
+
return join(runnerDir(configPath), "repos", owner, name);
|
|
32
|
+
}
|
|
33
|
+
function buildsDir(configPath) {
|
|
34
|
+
return join(runnerDir(configPath), "builds");
|
|
35
|
+
}
|
|
36
|
+
function buildDir(configPath, buildId) {
|
|
37
|
+
return join(buildsDir(configPath), buildId);
|
|
38
|
+
}
|
|
39
|
+
function ghConfigDir(buildDirPath) {
|
|
40
|
+
return join(buildDirPath, "gh");
|
|
41
|
+
}
|
|
42
|
+
function mirrorDir(runnerDirPath, owner, name) {
|
|
43
|
+
return join(runnerDirPath, "mirrors", owner, `${name}.git`);
|
|
44
|
+
}
|
|
45
|
+
function spoolDir(runnerDirPath) {
|
|
46
|
+
return join(runnerDirPath, "spool");
|
|
47
|
+
}
|
|
48
|
+
function contextHomeDir(configPath) {
|
|
49
|
+
return join(runnerDir(configPath), "context-home");
|
|
50
|
+
}
|
|
51
|
+
function stagedContextDir(buildDirPath) {
|
|
52
|
+
return join(buildDirPath, "context");
|
|
53
|
+
}
|
|
54
|
+
function runnerDirOfBuild(buildDirPath) {
|
|
55
|
+
return dirname(dirname(buildDirPath));
|
|
56
|
+
}
|
|
57
|
+
function vmDir(runnerDirPath) {
|
|
58
|
+
return join(runnerDirPath, "vm");
|
|
59
|
+
}
|
|
60
|
+
function vmAssetsDir(runnerDirPath) {
|
|
61
|
+
return join(vmDir(runnerDirPath), "assets");
|
|
62
|
+
}
|
|
63
|
+
function rootfsDir(runnerDirPath) {
|
|
64
|
+
return join(vmDir(runnerDirPath), "rootfs");
|
|
65
|
+
}
|
|
66
|
+
function rootfsPathFor(runnerDirPath, imageId) {
|
|
67
|
+
return join(rootfsDir(runnerDirPath), `${imageId.replace(/^sha256:/, "").replace(/[^A-Za-z0-9._-]/g, "_")}.ext4`);
|
|
68
|
+
}
|
|
69
|
+
function diskTemplatesDir(runnerDirPath) {
|
|
70
|
+
return join(vmDir(runnerDirPath), "templates");
|
|
71
|
+
}
|
|
72
|
+
function vsockPath(buildDirPath) {
|
|
73
|
+
return join(buildDirPath, "v.sock");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export { RUNNER_DIR_MODE, RUNNER_FILE_MODE, attemptStamp, buildDir, contextHomeDir, diskTemplatesDir, ghConfigDir, hostKeyPath, imagesDir, jobDir, jobsDir, mirrorDir, pidFilePath, repoCacheDir, rootfsDir, rootfsPathFor, runnerDir, runnerDirOfBuild, spoolDir, stagedContextDir, vmAssetsDir, vsockPath };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { C } from './chunk-YRYMNBGQ.js';
|
|
4
|
+
import { resolvePackRoot, loadTemplateFiles, loadMaintainedFiles, TEMPLATE_SKILLS, skillDir, writeMaintainedFiles, MAINTAINED_SKILLS } from './chunk-WO2BXCTQ.js';
|
|
5
|
+
import { resolvePackageVersion } from './chunk-RA3O54FC.js';
|
|
6
|
+
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
7
|
+
import { join, dirname } from 'path';
|
|
8
|
+
|
|
9
|
+
createRequire(import.meta.url);
|
|
10
|
+
var DEFAULT_TEMPLATE = "default";
|
|
11
|
+
var POLICY_PATH = ".outerlayer/policy.yaml";
|
|
12
|
+
var POLICY_CONTENT = "extends: outerlayer:recommended@v1\nreview: required\n";
|
|
13
|
+
var StarterPackError = class extends Error {
|
|
14
|
+
};
|
|
15
|
+
function assertKnownTemplate(name) {
|
|
16
|
+
if (name !== DEFAULT_TEMPLATE) {
|
|
17
|
+
throw new StarterPackError(`unknown template "${name}" \u2014 the only template is "${DEFAULT_TEMPLATE}"`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function installStarterPack(opts) {
|
|
21
|
+
const packRoot = opts.packRoot ?? resolvePackRoot();
|
|
22
|
+
const version = opts.version ?? resolvePackageVersion();
|
|
23
|
+
const templateFiles = loadTemplateFiles(packRoot, version);
|
|
24
|
+
const maintainedFiles = loadMaintainedFiles(packRoot, version);
|
|
25
|
+
const copied = [];
|
|
26
|
+
const kept = [];
|
|
27
|
+
for (const skill of TEMPLATE_SKILLS) {
|
|
28
|
+
if (existsSync(join(opts.repoRoot, skillDir(skill)))) {
|
|
29
|
+
kept.push(skill);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
for (const file of templateFiles.filter((f) => f.skill === skill)) {
|
|
33
|
+
const abs = join(opts.repoRoot, file.path);
|
|
34
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
35
|
+
writeFileSync(abs, file.content);
|
|
36
|
+
}
|
|
37
|
+
copied.push(skill);
|
|
38
|
+
}
|
|
39
|
+
const written = writeMaintainedFiles(opts.repoRoot, maintainedFiles);
|
|
40
|
+
let policy = "kept";
|
|
41
|
+
const policyAbs = join(opts.repoRoot, POLICY_PATH);
|
|
42
|
+
if (!existsSync(policyAbs)) {
|
|
43
|
+
mkdirSync(dirname(policyAbs), { recursive: true });
|
|
44
|
+
writeFileSync(policyAbs, POLICY_CONTENT);
|
|
45
|
+
policy = "written";
|
|
46
|
+
}
|
|
47
|
+
return { copied, kept, maintained: [...written.created, ...written.rewritten], policy };
|
|
48
|
+
}
|
|
49
|
+
var skillFile = (skill) => `${skillDir(skill)}/SKILL.md`;
|
|
50
|
+
function formatStarterPackOutput(result) {
|
|
51
|
+
const lines = [];
|
|
52
|
+
lines.push(`${C.green}\u2713${C.reset} Installed the starter skills in .outerlayer/skills/`, "");
|
|
53
|
+
lines.push("Yours to edit \u2014 copied once, never overwritten:");
|
|
54
|
+
for (const skill of TEMPLATE_SKILLS) {
|
|
55
|
+
lines.push(
|
|
56
|
+
result.kept.includes(skill) ? ` ${C.dim}kept${C.reset} ${skillDir(skill)}/ ${C.dim}(already exists, left exactly as it is)${C.reset}` : ` ${skillFile(skill)}`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
lines.push("", "Rewritten on every emit \u2014 edits are overwritten by the installed CLI:");
|
|
60
|
+
for (const skill of MAINTAINED_SKILLS) lines.push(` ${skillFile(skill)}`);
|
|
61
|
+
lines.push(
|
|
62
|
+
"",
|
|
63
|
+
result.policy === "written" ? `${C.green}\u2713${C.reset} Wrote ${POLICY_PATH} (extends the recommended checks, review required)` : `${C.dim}${POLICY_PATH} already exists \u2014 left as it is${C.reset}`,
|
|
64
|
+
"",
|
|
65
|
+
`Next: create .outerlayer/config.json naming your agent tools, for example {"targets": ["claude-code"]},`,
|
|
66
|
+
`then run ${C.yellow}outerlayer context emit${C.reset} to compile the skills into their native files.`
|
|
67
|
+
);
|
|
68
|
+
return lines.join("\n") + "\n";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export { StarterPackError, assertKnownTemplate, formatStarterPackOutput, installStarterPack };
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { startTimeOf, claimPidFile, RunnerAlreadyRunningError, releasePidFile } from './chunk-N5FOE5PS.js';
|
|
4
|
+
import { RecipeError, locateRecipe, readRecipe, assertBuildable, writeSanitizedRecipe, classifyProperties, SANITIZED_NAME } from './chunk-QOZKPLVJ.js';
|
|
5
|
+
import { RUNNER_DIR_MODE, repoCacheDir, imagesDir, attemptStamp, runnerDir, rootfsPathFor, RUNNER_FILE_MODE } from './chunk-ZM2IMMYN.js';
|
|
6
|
+
import { mkdirSync, rmSync, existsSync, mkdtempSync, cpSync, readFileSync, writeFileSync, readdirSync, lstatSync, readlinkSync } from 'fs';
|
|
7
|
+
import { tmpdir } from 'os';
|
|
8
|
+
import { dirname, join } from 'path';
|
|
9
|
+
import { execFile } from 'child_process';
|
|
10
|
+
import { createHash } from 'crypto';
|
|
11
|
+
|
|
12
|
+
createRequire(import.meta.url);
|
|
13
|
+
var INHERITED_REPOSITORY_VARIABLES = ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_COMMON_DIR"];
|
|
14
|
+
function gitEnvironment(base) {
|
|
15
|
+
const env = { ...base, GIT_TERMINAL_PROMPT: "0" };
|
|
16
|
+
for (const name of INHERITED_REPOSITORY_VARIABLES) delete env[name];
|
|
17
|
+
return env;
|
|
18
|
+
}
|
|
19
|
+
function tokenHeader(token) {
|
|
20
|
+
return `Authorization: Basic ${Buffer.from(`x-access-token:${token}`).toString("base64")}`;
|
|
21
|
+
}
|
|
22
|
+
function tokenSettings(remote, token) {
|
|
23
|
+
return { GIT_CONFIG_COUNT: "1", GIT_CONFIG_KEY_0: `http.${remote}.extraHeader`, GIT_CONFIG_VALUE_0: tokenHeader(token) };
|
|
24
|
+
}
|
|
25
|
+
function git(cwd, args, timeoutMs, extraEnv = {}) {
|
|
26
|
+
const env = { ...gitEnvironment(process.env), ...extraEnv };
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
execFile(
|
|
29
|
+
"git",
|
|
30
|
+
["-C", cwd, "-c", "core.hooksPath=/dev/null", "-c", "protocol.ext.allow=never", ...args],
|
|
31
|
+
{ env, encoding: "utf8", maxBuffer: 16 * 1024 * 1024, timeout: timeoutMs },
|
|
32
|
+
(error, stdout, stderr) => {
|
|
33
|
+
if (error && error.killed && timeoutMs !== void 0) reject(new Error(`git ${args[0]} failed: no answer within ${timeoutMs / 1e3}s`));
|
|
34
|
+
else if (error) reject(new Error(`git ${args[0]} failed: ${stderr.trim() || error.message}`));
|
|
35
|
+
else resolve(stdout.trim());
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function defaultBranchOf(lsRemote) {
|
|
41
|
+
const match = lsRemote.match(/^ref: refs\/heads\/(\S+)\s+HEAD$/m);
|
|
42
|
+
return match ? match[1] : null;
|
|
43
|
+
}
|
|
44
|
+
async function fetchDefaultBranch(opts) {
|
|
45
|
+
const created = !existsSync(opts.dir);
|
|
46
|
+
mkdirSync(opts.dir, { recursive: true, mode: RUNNER_DIR_MODE });
|
|
47
|
+
const credential = opts.token === void 0 ? {} : tokenSettings(opts.remote, opts.token);
|
|
48
|
+
const over = (args) => git(opts.dir, args, void 0, credential);
|
|
49
|
+
try {
|
|
50
|
+
const defaultBranch = defaultBranchOf(await over(["ls-remote", "--symref", opts.remote, "HEAD"]));
|
|
51
|
+
if (defaultBranch === null) {
|
|
52
|
+
throw new RecipeError(
|
|
53
|
+
"default_branch_unknown",
|
|
54
|
+
`${opts.remote} does not say which branch is its default; the runner will not guess one`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
await git(opts.dir, ["init", "--quiet"]);
|
|
58
|
+
await over(["fetch", "--quiet", "--depth", "1", "--no-tags", "--force", opts.remote, `refs/heads/${defaultBranch}`]);
|
|
59
|
+
const commit = await git(opts.dir, ["rev-parse", "FETCH_HEAD"]);
|
|
60
|
+
await git(opts.dir, ["checkout", "--quiet", "--detach", "--force", commit]);
|
|
61
|
+
await git(opts.dir, ["clean", "-ffdx", "--quiet"]);
|
|
62
|
+
return { dir: opts.dir, defaultBranch, commit };
|
|
63
|
+
} catch (err) {
|
|
64
|
+
if (created) rmSync(opts.dir, { recursive: true, force: true });
|
|
65
|
+
if (err instanceof RecipeError || opts.repository === void 0) throw err;
|
|
66
|
+
throw unreadable(opts.repository, opts.token !== void 0, err);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
var REFUSED_BY_REMOTE = /\b(401|403|404)\b|authentication failed|repository not found|could not read username|terminal prompts disabled/i;
|
|
70
|
+
function unreadable(repository, withClaimToken, err) {
|
|
71
|
+
const message = (err instanceof Error ? err.message : String(err)).replace(/^git [\w-]+ failed: /, "");
|
|
72
|
+
const reason = message.split("\n")[0];
|
|
73
|
+
if (!REFUSED_BY_REMOTE.test(message)) return new RecipeError("repository_unreadable", `cannot read ${repository}: ${reason}`);
|
|
74
|
+
const advice = withClaimToken ? "the claim's read token was used and GitHub refused it, so the App installation may not include this repository" : "`outerlayer runner image` fetches with this host's git login, so a private repository needs one on this host; a container build needs none, because it uses the claim's read token";
|
|
75
|
+
return new RecipeError("repository_unreadable", `cannot read ${repository}: ${reason}; ${advice}`);
|
|
76
|
+
}
|
|
77
|
+
var HEAD_TIMEOUT_MS = 3e4;
|
|
78
|
+
async function readDefaultBranchHead(remote, dir) {
|
|
79
|
+
let out;
|
|
80
|
+
try {
|
|
81
|
+
out = await git(dir, ["ls-remote", "--symref", remote, "HEAD"], HEAD_TIMEOUT_MS);
|
|
82
|
+
} catch {
|
|
83
|
+
return void 0;
|
|
84
|
+
}
|
|
85
|
+
return out.match(/^([0-9a-f]{40}|[0-9a-f]{64})\tHEAD$/m)?.[1];
|
|
86
|
+
}
|
|
87
|
+
var CLONE_TIMEOUT_MS = 6e5;
|
|
88
|
+
async function cloneRepository(remote, dest, timeoutMs = CLONE_TIMEOUT_MS) {
|
|
89
|
+
mkdirSync(dirname(dest), { recursive: true, mode: RUNNER_DIR_MODE });
|
|
90
|
+
try {
|
|
91
|
+
await git(dirname(dest), ["clone", "--quiet", "--filter=blob:none", "--single-branch", remote, dest], timeoutMs);
|
|
92
|
+
return { ok: true };
|
|
93
|
+
} catch (err) {
|
|
94
|
+
rmSync(dest, { recursive: true, force: true });
|
|
95
|
+
const detail = (err instanceof Error ? err.message : String(err)).replace(/^git clone failed: /, "").split("\n")[0];
|
|
96
|
+
return { ok: false, reason: `the clone of ${remote} failed: ${detail}` };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/runner/image/pins.ts
|
|
101
|
+
var CLAUDE_CODE_VERSION = "2.1.284";
|
|
102
|
+
var DEFAULT_BASE_IMAGE = "ubuntu:24.04@sha256:008173c23f95b170204355c12626cb5a965d779a7e1283b09e9cffbb1bf33ca3";
|
|
103
|
+
var NODE_IMAGE = "node:22-bookworm-slim@sha256:43ac6c60b8f89723f746e8a92ce91abd5017e627ce1ddfe4238355d3a30b772c";
|
|
104
|
+
var GH_VERSION = "2.101.0";
|
|
105
|
+
var GH_SHA256 = {
|
|
106
|
+
amd64: "9bca2d1c16825f109907a23307628a2f0698fbf99662b73a5cf0b020293072b8",
|
|
107
|
+
arm64: "b57e8063f18862647c9d22727c32e9da1b963f8bf9db648fe123a6975695640f"
|
|
108
|
+
};
|
|
109
|
+
var TINI_VERSION = "0.19.0";
|
|
110
|
+
var TINI_SHA256 = {
|
|
111
|
+
amd64: "c5b0666b4cb676901f90dfcb37106783c5fe2077b04590973b885950611b30ee",
|
|
112
|
+
arm64: "eae1d3aa50c48fb23b8cbdf4e369d0910dfc538566bfd09df89a774aa84a48b9"
|
|
113
|
+
};
|
|
114
|
+
var MICROVM_ARCHES = ["x86_64"];
|
|
115
|
+
function firecrackerArch(nodeArch) {
|
|
116
|
+
return nodeArch === "x64" ? "x86_64" : nodeArch === "arm64" ? "aarch64" : nodeArch;
|
|
117
|
+
}
|
|
118
|
+
var FIRECRACKER_VERSION = "1.13.1";
|
|
119
|
+
var FIRECRACKER_SHA256 = {
|
|
120
|
+
x86_64: "59450b9171ff2ebdf2f9a25be3a248a7ba79fb6371aec51a9d6d8eefca7b4faf"
|
|
121
|
+
};
|
|
122
|
+
var FIRECRACKER_BINARY_SHA256 = {
|
|
123
|
+
x86_64: "7d3924e0c8761edafbf85cb2a94c268ffad249644a40fd02d4e15cde4c206f6e"
|
|
124
|
+
};
|
|
125
|
+
var GUEST_ASSETS_VERSION = "1";
|
|
126
|
+
var GUEST_ASSETS_BASE_URL = `https://github.com/outerlayerai/outerlayer/releases/download/guest-${GUEST_ASSETS_VERSION}`;
|
|
127
|
+
var GUEST_KERNEL_SHA256 = {
|
|
128
|
+
x86_64: "0000000000000000000000000000000000000000000000000000000000000000"
|
|
129
|
+
};
|
|
130
|
+
var GUEST_HELPER_SHA256 = {
|
|
131
|
+
x86_64: "0000000000000000000000000000000000000000000000000000000000000000"
|
|
132
|
+
};
|
|
133
|
+
function guestAssetsPinned(arch) {
|
|
134
|
+
const zero = "0".repeat(64);
|
|
135
|
+
return GUEST_KERNEL_SHA256[arch] !== zero && GUEST_HELPER_SHA256[arch] !== zero;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/runner/image/dockerfiles.ts
|
|
139
|
+
function plain(name, value, allowed) {
|
|
140
|
+
if (!allowed.test(value)) throw new Error(`${name} holds a character a Dockerfile line cannot carry safely: ${JSON.stringify(value)}`);
|
|
141
|
+
return value;
|
|
142
|
+
}
|
|
143
|
+
function defaultDockerfile(baseImage, nodeImage) {
|
|
144
|
+
return [
|
|
145
|
+
`FROM ${baseImage}`,
|
|
146
|
+
"ENV DEBIAN_FRONTEND=noninteractive",
|
|
147
|
+
"RUN apt-get update \\",
|
|
148
|
+
" && apt-get install -y --no-install-recommends git ca-certificates curl \\",
|
|
149
|
+
" && rm -rf /var/lib/apt/lists/*",
|
|
150
|
+
`COPY --from=${nodeImage} /usr/local/bin/node /usr/local/bin/node`,
|
|
151
|
+
`COPY --from=${nodeImage} /usr/local/lib/node_modules /usr/local/lib/node_modules`,
|
|
152
|
+
"RUN ln -s ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \\",
|
|
153
|
+
" && ln -s ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx \\",
|
|
154
|
+
" && ln -s ../lib/node_modules/corepack/dist/corepack.js /usr/local/bin/corepack",
|
|
155
|
+
""
|
|
156
|
+
].join("\n");
|
|
157
|
+
}
|
|
158
|
+
function toolsDockerfile(parts) {
|
|
159
|
+
const recipeImage = plain("the recipe image", parts.recipeImage, /^[A-Za-z0-9_.:@/-]+$/);
|
|
160
|
+
const nodeImage = plain("the Node image", parts.nodeImage, /^[A-Za-z0-9_.:@/-]+$/);
|
|
161
|
+
const version = plain("the Claude Code version", parts.claudeCodeVersion, /^[A-Za-z0-9.+-]+$/);
|
|
162
|
+
const buildId = plain("the build id", parts.buildId, /^[A-Za-z0-9_.:+-]+$/);
|
|
163
|
+
const tools = JSON.stringify({ outerlayerBuildId: buildId, claudeCodeVersion: version, ghVersion: GH_VERSION });
|
|
164
|
+
const managedSettings = JSON.stringify({
|
|
165
|
+
hooks: Object.fromEntries(
|
|
166
|
+
["SessionStart", "SessionEnd", "Stop", "PostToolUse"].map((event) => [event, [{ hooks: [{ type: "command", command: `outerlayer hook ${event}` }] }]])
|
|
167
|
+
)
|
|
168
|
+
});
|
|
169
|
+
const download = `node -e 'fetch(process.argv[1]).then((r)=>{if(!r.ok)throw new Error(process.argv[1]+" "+r.status);return r.arrayBuffer()}).then((b)=>require("fs").writeFileSync(process.argv[2],Buffer.from(b)))'`;
|
|
170
|
+
return [
|
|
171
|
+
`FROM ${recipeImage} AS recipe`,
|
|
172
|
+
`FROM ${nodeImage} AS stage`,
|
|
173
|
+
"ARG TARGETARCH",
|
|
174
|
+
`RUN npm install --global --prefix /out/opt/outerlayer/claude @anthropic-ai/claude-code@${version}`,
|
|
175
|
+
"COPY dist /out/opt/outerlayer/cli/dist",
|
|
176
|
+
"COPY package.json /out/opt/outerlayer/cli/package.json",
|
|
177
|
+
"RUN mkdir -p /out/opt/outerlayer/bin /out/usr/local/bin /out/etc/claude-code /out/build \\",
|
|
178
|
+
" && cp /usr/local/bin/node /out/opt/outerlayer/bin/node \\",
|
|
179
|
+
` && printf '%s\\n' '#!/bin/sh' 'exec /opt/outerlayer/bin/node /opt/outerlayer/cli/dist/index.js "$@"' > /out/usr/local/bin/outerlayer \\`,
|
|
180
|
+
` && printf '%s\\n' '#!/bin/sh' 'PATH=/opt/outerlayer/bin:$PATH' 'export PATH' 'exec /opt/outerlayer/claude/bin/claude "$@"' > /out/usr/local/bin/claude \\`,
|
|
181
|
+
` && printf '%s\\n' '${tools}' > /out/opt/outerlayer/tools.json \\`,
|
|
182
|
+
` && printf '%s\\n' '${managedSettings}' > /out/etc/claude-code/managed-settings.json \\`,
|
|
183
|
+
" && chmod 0755 /out/usr/local/bin/outerlayer /out/usr/local/bin/claude \\",
|
|
184
|
+
// Every build, whatever user runs it, writes here: the first mount of an empty volume copies this mode.
|
|
185
|
+
" && chmod 1777 /out/build",
|
|
186
|
+
...parts.guestHelper === true ? [
|
|
187
|
+
"COPY guest/outerlayer-guest /out/opt/outerlayer/bin/outerlayer-guest",
|
|
188
|
+
// The guest mounts the build's tmpfs, the kernel's file systems and the run directory over these.
|
|
189
|
+
"RUN chmod 0755 /out/opt/outerlayer/bin/outerlayer-guest \\",
|
|
190
|
+
" && mkdir -p /out/outerlayer/job /out/run /out/proc /out/sys /out/dev"
|
|
191
|
+
] : [],
|
|
192
|
+
`RUN set -eu \\`,
|
|
193
|
+
` && case "$TARGETARCH" in \\`,
|
|
194
|
+
` amd64) gh_sha=${GH_SHA256.amd64}; tini_sha=${TINI_SHA256.amd64} ;; \\`,
|
|
195
|
+
` arm64) gh_sha=${GH_SHA256.arm64}; tini_sha=${TINI_SHA256.arm64} ;; \\`,
|
|
196
|
+
` *) echo "no pinned gh and tini for $TARGETARCH" >&2; exit 1 ;; \\`,
|
|
197
|
+
" esac \\",
|
|
198
|
+
` && ${download} https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini-static-$TARGETARCH /tmp/tini \\`,
|
|
199
|
+
' && echo "$tini_sha /tmp/tini" | sha256sum -c - \\',
|
|
200
|
+
" && install -m 0755 /tmp/tini /out/opt/outerlayer/bin/tini \\",
|
|
201
|
+
` && ${download} https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_$TARGETARCH.tar.gz /tmp/gh.tgz \\`,
|
|
202
|
+
' && echo "$gh_sha /tmp/gh.tgz" | sha256sum -c - \\',
|
|
203
|
+
" && tar -xzf /tmp/gh.tgz -C /tmp \\",
|
|
204
|
+
` && install -m 0755 /tmp/gh_${GH_VERSION}_linux_$TARGETARCH/bin/gh /out/usr/local/bin/gh`,
|
|
205
|
+
"COPY --from=recipe /etc/passwd /tmp/recipe-passwd",
|
|
206
|
+
"COPY --from=recipe /etc/group /tmp/recipe-group",
|
|
207
|
+
`RUN uid=$(awk -F: 'BEGIN{m=1499} $3>=1500 && $3<60000 && $3>m{m=$3} END{print m+1}' /tmp/recipe-passwd) \\`,
|
|
208
|
+
` && gid=$(awk -F: 'BEGIN{m=1499} $3>=1500 && $3<60000 && $3>m{m=$3} END{print m+1}' /tmp/recipe-group) \\`,
|
|
209
|
+
" && { grep -v '^ol-agent:' /tmp/recipe-passwd > /out/etc/passwd || true; } \\",
|
|
210
|
+
" && { grep -v '^ol-agent:' /tmp/recipe-group > /out/etc/group || true; } \\",
|
|
211
|
+
` && printf '%s\\n' "ol-agent:x:$uid:$gid::/build/home:/bin/sh" >> /out/etc/passwd \\`,
|
|
212
|
+
` && printf '%s\\n' "ol-agent:x:$gid:" >> /out/etc/group \\`,
|
|
213
|
+
" && chmod 0644 /out/etc/passwd /out/etc/group \\",
|
|
214
|
+
" && chmod -R a+rX /out",
|
|
215
|
+
`FROM ${recipeImage}`,
|
|
216
|
+
"COPY --from=stage /out/ /",
|
|
217
|
+
"ENV DISABLE_AUTOUPDATER=1",
|
|
218
|
+
"RUN outerlayer --version && claude --version && gh --version",
|
|
219
|
+
""
|
|
220
|
+
].join("\n");
|
|
221
|
+
}
|
|
222
|
+
function sha256(...chunks) {
|
|
223
|
+
const hash = createHash("sha256");
|
|
224
|
+
for (const chunk of chunks) hash.update(chunk);
|
|
225
|
+
return hash.digest("hex");
|
|
226
|
+
}
|
|
227
|
+
function filesUnder(root, dir) {
|
|
228
|
+
const found = [];
|
|
229
|
+
for (const entry of readdirSync(join(root, dir))) {
|
|
230
|
+
const rel = join(dir, entry);
|
|
231
|
+
if (rel === join(".devcontainer", SANITIZED_NAME)) continue;
|
|
232
|
+
const stat = lstatSync(join(root, rel));
|
|
233
|
+
if (stat.isDirectory()) found.push(...filesUnder(root, rel));
|
|
234
|
+
else found.push(rel);
|
|
235
|
+
}
|
|
236
|
+
return found;
|
|
237
|
+
}
|
|
238
|
+
function hashFile(root, rel) {
|
|
239
|
+
const path = join(root, rel);
|
|
240
|
+
const bytes = lstatSync(path).isSymbolicLink() ? Buffer.from(`-> ${readlinkSync(path)}`) : readFileSync(path);
|
|
241
|
+
return [rel, "\0", bytes, "\0"];
|
|
242
|
+
}
|
|
243
|
+
function recipeKey(checkoutDir, location, defaultDockerfile2) {
|
|
244
|
+
if (location.kind === "default") return sha256(defaultDockerfile2);
|
|
245
|
+
const files = [];
|
|
246
|
+
if (existsSync(join(checkoutDir, ".devcontainer"))) files.push(...filesUnder(checkoutDir, ".devcontainer"));
|
|
247
|
+
if (location.path === ".devcontainer.json") {
|
|
248
|
+
files.push(".devcontainer.json");
|
|
249
|
+
if (existsSync(join(checkoutDir, ".devcontainer-lock.json"))) files.push(".devcontainer-lock.json");
|
|
250
|
+
}
|
|
251
|
+
return sha256(...files.sort().flatMap((rel) => hashFile(checkoutDir, rel)));
|
|
252
|
+
}
|
|
253
|
+
function toolsKey(parts) {
|
|
254
|
+
const base = [parts.buildId, "\0", parts.claudeCodeVersion, "\0", parts.nodeImage, "\0", parts.ghVersion, "\0", parts.tiniVersion];
|
|
255
|
+
return sha256(...base, ...parts.guestHelperSha256 === void 0 ? [] : ["\0guest-helper\0", parts.guestHelperSha256]);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/runner/image/prune.ts
|
|
259
|
+
var KIND = "ai.outerlayer.kind";
|
|
260
|
+
var RECIPE_KEY = "ai.outerlayer.recipe-key";
|
|
261
|
+
var newestFirst = (a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt);
|
|
262
|
+
function selectPrunable(opts) {
|
|
263
|
+
const used = new Set(opts.inUse);
|
|
264
|
+
const builds = opts.images.filter((i) => i.labels[KIND] === "build").sort(newestFirst);
|
|
265
|
+
const kept = builds.filter((image, index) => index < opts.keep || used.has(image.id));
|
|
266
|
+
const keptKeys = new Set(kept.map((i) => i.labels[RECIPE_KEY]));
|
|
267
|
+
return {
|
|
268
|
+
builds: builds.filter((i) => !kept.includes(i)),
|
|
269
|
+
recipes: opts.images.filter(
|
|
270
|
+
(i) => i.labels[KIND] === "recipe" && !used.has(i.id) && !keptKeys.has(i.labels[RECIPE_KEY])
|
|
271
|
+
)
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/runner/image/build.ts
|
|
276
|
+
var KIND2 = "ai.outerlayer.kind";
|
|
277
|
+
var REPOSITORY = "ai.outerlayer.repository";
|
|
278
|
+
var RECIPE_KEY2 = "ai.outerlayer.recipe-key";
|
|
279
|
+
var TOOLS_KEY = "ai.outerlayer.tools-key";
|
|
280
|
+
var KEEP_BUILD_IMAGES = 3;
|
|
281
|
+
var TAG_HOST = "outerlayer.invalid";
|
|
282
|
+
var KEY_PREFIX = 12;
|
|
283
|
+
var LOCK_NAME = "build.lock";
|
|
284
|
+
function splitRepository(repository) {
|
|
285
|
+
const full = repository.toLowerCase();
|
|
286
|
+
const parts = full.split("/");
|
|
287
|
+
const segment = /^[A-Za-z0-9_.-]+$/;
|
|
288
|
+
if (parts.length !== 2 || !parts.every((p) => segment.test(p) && p !== "." && p !== "..")) {
|
|
289
|
+
throw new RecipeError("invalid_repository", `${JSON.stringify(repository)} is not an owner/name repository`);
|
|
290
|
+
}
|
|
291
|
+
return { full, owner: parts[0], name: parts[1] };
|
|
292
|
+
}
|
|
293
|
+
function metadataOf(image) {
|
|
294
|
+
try {
|
|
295
|
+
const parsed = JSON.parse(image.labels["devcontainer.metadata"] ?? "[]");
|
|
296
|
+
const entries = Array.isArray(parsed) ? parsed : [parsed];
|
|
297
|
+
return entries.filter((e) => typeof e === "object" && e !== null && !Array.isArray(e));
|
|
298
|
+
} catch {
|
|
299
|
+
return [];
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function readReport(recordDir) {
|
|
303
|
+
try {
|
|
304
|
+
return JSON.parse(readFileSync(join(recordDir, "recipe.json"), "utf8"));
|
|
305
|
+
} catch {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function writeRecord(recordDir, report, builtAt) {
|
|
310
|
+
mkdirSync(recordDir, { recursive: true, mode: RUNNER_DIR_MODE });
|
|
311
|
+
const image = { imageId: report.imageId, tag: report.tag, recipeKey: report.recipeKey, toolsKey: report.toolsKey, builtAt };
|
|
312
|
+
writeFileSync(join(recordDir, "image.json"), `${JSON.stringify(image, null, 2)}
|
|
313
|
+
`, { mode: RUNNER_FILE_MODE });
|
|
314
|
+
writeFileSync(join(recordDir, "recipe.json"), `${JSON.stringify(report, null, 2)}
|
|
315
|
+
`, { mode: RUNNER_FILE_MODE });
|
|
316
|
+
}
|
|
317
|
+
async function buildRepositoryImage(opts, ports) {
|
|
318
|
+
const { host } = ports;
|
|
319
|
+
const { full: repository, owner, name } = splitRepository(opts.repository);
|
|
320
|
+
if (!existsSync(join(opts.cli.distDir, "index.js"))) {
|
|
321
|
+
throw new Error(`${opts.cli.distDir} holds no index.js: the tools layer copies the built CLI, so run this from a build (yarn workspace @outerlayer/cli build)`);
|
|
322
|
+
}
|
|
323
|
+
const now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
324
|
+
const claudeCodeVersion = opts.claudeCodeVersion ?? CLAUDE_CODE_VERSION;
|
|
325
|
+
const nodeImage = opts.nodeImage ?? NODE_IMAGE;
|
|
326
|
+
const baseImage = opts.defaultBaseImage ?? DEFAULT_BASE_IMAGE;
|
|
327
|
+
const scratchRoot = opts.tmpDir ?? tmpdir();
|
|
328
|
+
const cacheDir = repoCacheDir(opts.configPath, owner, name);
|
|
329
|
+
const repoRecords = join(imagesDir(opts.configPath), owner, name);
|
|
330
|
+
const logPath = join(repoRecords, "builds", `${attemptStamp(now())}.log`);
|
|
331
|
+
const lockPath = join(repoRecords, LOCK_NAME);
|
|
332
|
+
const self = { pid: process.pid, start: startTimeOf(process.pid) ?? "" };
|
|
333
|
+
mkdirSync(repoRecords, { recursive: true, mode: RUNNER_DIR_MODE });
|
|
334
|
+
try {
|
|
335
|
+
claimPidFile(lockPath, self);
|
|
336
|
+
} catch (err) {
|
|
337
|
+
if (err instanceof RunnerAlreadyRunningError) {
|
|
338
|
+
throw new RecipeError("build_in_progress", `an image build for ${repository} is already running (pid ${err.pid})`);
|
|
339
|
+
}
|
|
340
|
+
throw err;
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
const checkout = await fetchDefaultBranch({
|
|
344
|
+
remote: opts.remote ?? `https://github.com/${owner}/${name}.git`,
|
|
345
|
+
dir: cacheDir,
|
|
346
|
+
token: opts.readToken,
|
|
347
|
+
repository
|
|
348
|
+
});
|
|
349
|
+
const location = locateRecipe(checkout.dir);
|
|
350
|
+
const recipe = readRecipe(checkout.dir, location);
|
|
351
|
+
assertBuildable(checkout.dir, location, recipe);
|
|
352
|
+
const defaultText = defaultDockerfile(baseImage, nodeImage);
|
|
353
|
+
const rKey = recipeKey(checkout.dir, location, defaultText);
|
|
354
|
+
const tKey = toolsKey({ buildId: opts.cli.buildId, claudeCodeVersion, nodeImage, ghVersion: GH_VERSION, tiniVersion: TINI_VERSION, guestHelperSha256: opts.guestHelper?.sha256 });
|
|
355
|
+
const recordDir = join(repoRecords, `${rKey.slice(0, KEY_PREFIX)}-${tKey.slice(0, KEY_PREFIX)}`);
|
|
356
|
+
const buildTag = `${TAG_HOST}/build/${repository}:${rKey.slice(0, KEY_PREFIX)}-${tKey.slice(0, KEY_PREFIX)}`;
|
|
357
|
+
const recipeTag = `${TAG_HOST}/recipe/${repository}:${rKey.slice(0, KEY_PREFIX)}`;
|
|
358
|
+
const existing = await host.list(repository);
|
|
359
|
+
const labelled = existing.find((i) => i.labels[KIND2] === "recipe" && i.labels[RECIPE_KEY2] === rKey);
|
|
360
|
+
const recipeImageBefore = labelled !== void 0 && (await host.inspect(recipeTag))?.id === labelled.id ? labelled : void 0;
|
|
361
|
+
const buildImageBefore = existing.find(
|
|
362
|
+
(i) => i.labels[KIND2] === "build" && i.labels[RECIPE_KEY2] === rKey && i.labels[TOOLS_KEY] === tKey
|
|
363
|
+
);
|
|
364
|
+
const failed = (what, status) => new RecipeError("build_failed", `${what} failed (exit ${status}); the build log is at ${logPath}`, logPath);
|
|
365
|
+
let recipeImage = recipeImageBefore;
|
|
366
|
+
let recipeBuilt = false;
|
|
367
|
+
if (recipeImage === void 0 && buildImageBefore === void 0) {
|
|
368
|
+
const labels = { [REPOSITORY]: repository, [KIND2]: "recipe", [RECIPE_KEY2]: rKey };
|
|
369
|
+
if (location.kind === "default") {
|
|
370
|
+
const context = mkdtempSync(join(scratchRoot, "ol-image-default-"));
|
|
371
|
+
try {
|
|
372
|
+
const status = await host.buildDockerfile({ dockerfile: defaultText, context, tag: recipeTag, labels, logPath });
|
|
373
|
+
if (status !== 0) throw failed("the docker build of the default image", status);
|
|
374
|
+
} finally {
|
|
375
|
+
rmSync(context, { recursive: true, force: true });
|
|
376
|
+
}
|
|
377
|
+
} else {
|
|
378
|
+
const config = writeSanitizedRecipe(checkout.dir, location, recipe);
|
|
379
|
+
try {
|
|
380
|
+
const status = await host.devcontainerBuild({ workspace: checkout.dir, config, imageName: recipeTag, labels, logPath });
|
|
381
|
+
if (status !== 0) throw failed("devcontainer build", status);
|
|
382
|
+
} finally {
|
|
383
|
+
rmSync(config, { force: true });
|
|
384
|
+
}
|
|
385
|
+
const context = mkdtempSync(join(scratchRoot, "ol-image-label-"));
|
|
386
|
+
try {
|
|
387
|
+
const status = await host.buildDockerfile({ dockerfile: `FROM ${recipeTag}
|
|
388
|
+
`, context, tag: recipeTag, labels, logPath });
|
|
389
|
+
if (status !== 0) throw failed("labelling the recipe image", status);
|
|
390
|
+
} finally {
|
|
391
|
+
rmSync(context, { recursive: true, force: true });
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
recipeImage = await host.inspect(recipeTag) ?? void 0;
|
|
395
|
+
if (recipeImage === void 0) throw failed("finding the recipe image after devcontainer build", 0);
|
|
396
|
+
recipeBuilt = true;
|
|
397
|
+
}
|
|
398
|
+
let buildImage = buildImageBefore;
|
|
399
|
+
if (buildImage === void 0) {
|
|
400
|
+
const context = mkdtempSync(join(scratchRoot, "ol-image-tools-"));
|
|
401
|
+
try {
|
|
402
|
+
cpSync(opts.cli.distDir, join(context, "dist"), { recursive: true });
|
|
403
|
+
cpSync(join(opts.cli.distDir, "..", "package.json"), join(context, "package.json"));
|
|
404
|
+
if (opts.guestHelper !== void 0) {
|
|
405
|
+
mkdirSync(join(context, "guest"));
|
|
406
|
+
cpSync(opts.guestHelper.path, join(context, "guest", "outerlayer-guest"));
|
|
407
|
+
}
|
|
408
|
+
const dockerfile = toolsDockerfile({ recipeImage: recipeTag, nodeImage, claudeCodeVersion, buildId: opts.cli.buildId, guestHelper: opts.guestHelper !== void 0 });
|
|
409
|
+
const labels = { [REPOSITORY]: repository, [KIND2]: "build", [RECIPE_KEY2]: rKey, [TOOLS_KEY]: tKey };
|
|
410
|
+
const status = await host.buildDockerfile({ dockerfile, context, tag: buildTag, labels, logPath });
|
|
411
|
+
if (status !== 0) throw failed("the docker build of the tools layer", status);
|
|
412
|
+
} finally {
|
|
413
|
+
rmSync(context, { recursive: true, force: true });
|
|
414
|
+
}
|
|
415
|
+
buildImage = await host.inspect(buildTag) ?? void 0;
|
|
416
|
+
if (buildImage === void 0) throw failed("finding the image after the tools build", 0);
|
|
417
|
+
}
|
|
418
|
+
const toolsBuilt = buildImage !== buildImageBefore;
|
|
419
|
+
let report = readReport(recordDir);
|
|
420
|
+
if (report === null || report.imageId !== buildImage.id || report.docker === void 0) {
|
|
421
|
+
report = {
|
|
422
|
+
repository,
|
|
423
|
+
sourceCommit: checkout.commit,
|
|
424
|
+
defaultBranch: checkout.defaultBranch,
|
|
425
|
+
recipe: location.kind === "default" ? "default" : location.path,
|
|
426
|
+
recipeKey: rKey,
|
|
427
|
+
toolsKey: tKey,
|
|
428
|
+
imageId: buildImage.id,
|
|
429
|
+
tag: buildTag,
|
|
430
|
+
// A rebuilt record reads the tools image's label, which it inherits from the recipe image.
|
|
431
|
+
...classifyProperties(recipe, metadataOf(recipeImage ?? buildImage))
|
|
432
|
+
};
|
|
433
|
+
writeRecord(recordDir, report, now().toISOString());
|
|
434
|
+
}
|
|
435
|
+
const removed = [];
|
|
436
|
+
if (toolsBuilt) removed.push(...await prune(host, repository, repoRecords, opts.keep ?? KEEP_BUILD_IMAGES, runnerDir(opts.configPath)));
|
|
437
|
+
return {
|
|
438
|
+
reused: !recipeBuilt && !toolsBuilt,
|
|
439
|
+
recipeBuilt,
|
|
440
|
+
toolsBuilt,
|
|
441
|
+
checkedCommit: checkout.commit,
|
|
442
|
+
recordDir,
|
|
443
|
+
report,
|
|
444
|
+
removed
|
|
445
|
+
};
|
|
446
|
+
} finally {
|
|
447
|
+
releasePidFile(lockPath, self);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
async function prune(host, repository, repoRecords, keep, runnerDirPath) {
|
|
451
|
+
const { builds, recipes } = selectPrunable({ images: await host.list(repository), inUse: await host.inUse(), keep });
|
|
452
|
+
const removed = [];
|
|
453
|
+
for (const image of [...builds, ...recipes]) {
|
|
454
|
+
try {
|
|
455
|
+
await host.remove(image.id);
|
|
456
|
+
} catch {
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
removed.push(image.id);
|
|
460
|
+
rmSync(rootfsPathFor(runnerDirPath, image.id), { force: true });
|
|
461
|
+
const recipeKeyLabel = image.labels[RECIPE_KEY2];
|
|
462
|
+
const toolsKeyLabel = image.labels[TOOLS_KEY];
|
|
463
|
+
if (image.labels[KIND2] === "build" && recipeKeyLabel !== void 0 && toolsKeyLabel !== void 0) {
|
|
464
|
+
rmSync(join(repoRecords, `${recipeKeyLabel.slice(0, KEY_PREFIX)}-${toolsKeyLabel.slice(0, KEY_PREFIX)}`), { recursive: true, force: true });
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return removed;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export { CLAUDE_CODE_VERSION, DEFAULT_BASE_IMAGE, FIRECRACKER_BINARY_SHA256, FIRECRACKER_SHA256, FIRECRACKER_VERSION, GUEST_ASSETS_BASE_URL, GUEST_ASSETS_VERSION, GUEST_HELPER_SHA256, GUEST_KERNEL_SHA256, MICROVM_ARCHES, buildRepositoryImage, cloneRepository, firecrackerArch, guestAssetsPinned, readDefaultBranchHead };
|