@outerlayer/cli 0.2.0 → 0.4.1
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 +132 -0
- package/README.md +229 -60
- 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-7MFH6HNC.js +94 -0
- package/dist/chunk-3TFPDVTI.js +94 -0
- package/dist/{chunk-4TNZMO7V.js → chunk-4C4THABW.js} +5566 -754
- package/dist/{chunk-R5KBGVII.js → chunk-5QRP3MVS.js} +30 -14
- package/dist/chunk-5Y3QQRUH.js +300 -0
- package/dist/chunk-774HEQL6.js +178 -0
- package/dist/{work-pr-cmd-AZQPWH4H.js → chunk-7WERLFVR.js} +9 -26
- 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-DM3VFDS3.js +1461 -0
- package/dist/{chunk-65WXAANR.js → chunk-DVDEBNQJ.js} +17 -2
- 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/{emit-cmd-TWSYYEZI.js → chunk-H5NCNVDA.js} +68 -17
- package/dist/chunk-HZRNMGLF.js +2233 -0
- package/dist/chunk-LF6MLJCY.js +38 -0
- package/dist/{chunk-U32VSRLO.js → chunk-LT5TZHYH.js} +95 -634
- package/dist/chunk-LYCDNMOH.js +83 -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-OGFGZQA3.js +23 -0
- package/dist/chunk-QDQEUVUF.js +9 -0
- package/dist/chunk-QOZKPLVJ.js +226 -0
- package/dist/chunk-QY22NZUW.js +1764 -0
- package/dist/chunk-RA3O54FC.js +30 -0
- package/dist/chunk-RFUPN6KX.js +71 -0
- package/dist/chunk-SY4GK4SP.js +55 -0
- package/dist/chunk-TBT347UY.js +41 -0
- package/dist/chunk-TWNWNS2Q.js +1084 -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-Q6CL5THG.js → chunk-W5FVUZXV.js} +208 -61
- package/dist/chunk-WFJ65NUD.js +383 -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-Y7KJLXYN.js +232 -0
- package/dist/chunk-YFHIIH4B.js +102 -0
- 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-W62AFRSW.js} +616 -878
- package/dist/{paths-D2VGWWFI.js → cli-build-K56DK4DS.js} +1 -1
- package/dist/config-XVJYZ4IQ.js +9 -0
- package/dist/connect-cmd-ZTJONP37.js +16 -0
- package/dist/context-adopt-UMA4O5NY.js +105 -0
- package/dist/context-materialize-G2FUFC72.js +19 -0
- package/dist/dist-44CQVWGT.js +6 -0
- package/dist/docker-NEGDLU6D.js +7 -0
- package/dist/doctor-53F2PYY7.js +43 -0
- package/dist/doctor-ZLGZYMDK.js +426 -0
- package/dist/{emit-artifact-cmd-UQVT3OKW.js → emit-artifact-cmd-AKZP7TMF.js} +87 -23
- package/dist/emit-cmd-DCERAU27.js +9 -0
- package/dist/emit-criteria-cmd-7MXQVCKD.js +166 -0
- package/dist/{emit-finding-cmd-FDOU2OPD.js → emit-finding-cmd-N2FTGCVC.js} +34 -31
- package/dist/{emit-result-cmd-H2T4C2K3.js → emit-result-cmd-4CPDBMN2.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-55Y4LHWF.js +36 -0
- package/dist/init-XXEKWZQP.js +221 -0
- package/dist/init-cmd-WLST2F7G.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-JFIBP4B7.js +43 -0
- package/dist/machine-ZBPT2J3R.js +35 -0
- package/dist/mcp-install-cmd-RLK4SO3N.js +9 -0
- package/dist/{mcp-serve-cmd-57EZZOTL.js → mcp-serve-cmd-N6UD2KBX.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-UJXWZRN5.js} +30 -13
- package/dist/{statusline-fast-3C5OXHDD.js → statusline-fast-SVTMBMD7.js} +3 -2
- package/dist/sync-cmd-4G4A7EVN.js +28 -0
- package/dist/version-BWM6VLDI.js +6 -0
- package/dist/{watch-3DTPJETH.js → watch-7SKJKGAA.js} +19 -8
- package/dist/{work-claim-cmd-5AUNCLS2.js → work-claim-cmd-5F7VZ42N.js} +34 -21
- package/dist/work-cmd-QAUZ7MTD.js +17 -0
- package/dist/work-comment-cmd-BNCSBTLH.js +106 -0
- package/dist/{work-launch-LT663PB3.js → work-launch-YI4CJDAP.js} +1 -1
- package/dist/work-open-pr-cmd-VFYZHIIW.js +156 -0
- package/dist/work-pr-cmd-PV32ZLSI.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 +108 -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,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 };
|