@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,383 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { ArtifactTombstoneRecordSchema, ArtifactSpoolRecordSchema } from './chunk-4C4THABW.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 };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
-
import { CONTEXT_PIN_SCHEMA_VERSION, CONTEXT_PIN_FILENAME } from './chunk-
|
|
3
|
+
import { CONTEXT_PIN_SCHEMA_VERSION, CONTEXT_PIN_FILENAME } from './chunk-EMY4I27X.js';
|
|
4
4
|
import { readFileSync, existsSync, writeFileSync, mkdirSync, utimesSync, statSync, unlinkSync } from 'fs';
|
|
5
5
|
import { join, dirname } from 'path';
|
|
6
6
|
|
|
@@ -116,5 +116,18 @@ function writeSourceCache(gitCommonDir, cache) {
|
|
|
116
116
|
function isSourceCacheFresh(cache, now) {
|
|
117
117
|
return now - Date.parse(cache.checkedAt) < cache.ttlMs;
|
|
118
118
|
}
|
|
119
|
+
function buildHoldPath(gitCommonDir) {
|
|
120
|
+
return join(stateDir(gitCommonDir), "build-hold.json");
|
|
121
|
+
}
|
|
122
|
+
function readBuildHold(gitCommonDir) {
|
|
123
|
+
const parsed = readJson(buildHoldPath(gitCommonDir));
|
|
124
|
+
if (parsed && typeof parsed.sha === "string" && typeof parsed.repository === "string" && typeof parsed.ref === "string" && typeof parsed.at === "string") {
|
|
125
|
+
return parsed;
|
|
126
|
+
}
|
|
127
|
+
return void 0;
|
|
128
|
+
}
|
|
129
|
+
function writeBuildHold(gitCommonDir, hold) {
|
|
130
|
+
writeJson(buildHoldPath(gitCommonDir), hold);
|
|
131
|
+
}
|
|
119
132
|
|
|
120
|
-
export { SOURCE_CACHE_TTL_RETRY_MS, SOURCE_CACHE_TTL_SUCCESS_MS, clearDegraded, isLastCheckFresh, isSourceCacheFresh, manifestFilesPresent, pinExists, readContextManifest, readDegraded, readPin, readSourceCache, touchLastCheck, writeContextManifest, writeDegraded, writePinIfChanged, writeSourceCache };
|
|
133
|
+
export { SOURCE_CACHE_TTL_RETRY_MS, SOURCE_CACHE_TTL_SUCCESS_MS, clearDegraded, isLastCheckFresh, isSourceCacheFresh, manifestFilesPresent, pinExists, readBuildHold, readContextManifest, readDegraded, readPin, readSourceCache, touchLastCheck, writeBuildHold, writeContextManifest, writeDegraded, writePinIfChanged, writeSourceCache };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { splitRawFile } from './chunk-EMY4I27X.js';
|
|
4
|
+
import { resolvePackageVersion } from './chunk-RA3O54FC.js';
|
|
5
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync, readdirSync } from 'fs';
|
|
6
|
+
import { join, dirname } from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
|
|
9
|
+
createRequire(import.meta.url);
|
|
10
|
+
var TEMPLATE_SKILLS = ["spec", "writing-specs", "build"];
|
|
11
|
+
var MAINTAINED_SKILLS = ["emitting-evidence", "amend", "reporting-findings", "outerlayer"];
|
|
12
|
+
var TEMPLATE_CLASS_LINE = "<!-- yours to edit. Copied once from the default template by outerlayer init; never overwritten. -->";
|
|
13
|
+
function maintainedClassLine(version) {
|
|
14
|
+
return `<!-- maintained by OuterLayer ${version}. Edits are overwritten on the next outerlayer context emit. -->`;
|
|
15
|
+
}
|
|
16
|
+
function resolvePackRoot(entryPath = fileURLToPath(import.meta.url)) {
|
|
17
|
+
return join(dirname(dirname(entryPath)), "skill-pack");
|
|
18
|
+
}
|
|
19
|
+
function skillDir(skill) {
|
|
20
|
+
return `.outerlayer/skills/${skill}`;
|
|
21
|
+
}
|
|
22
|
+
function renderSkillFile(skillClass, content, version) {
|
|
23
|
+
const line = skillClass === "maintained" ? maintainedClassLine(version) : TEMPLATE_CLASS_LINE;
|
|
24
|
+
const { frontmatterInner, body } = splitRawFile(content);
|
|
25
|
+
if (frontmatterInner === null) return `${line}
|
|
26
|
+
${content}`;
|
|
27
|
+
const block = content.slice(0, content.length - body.length);
|
|
28
|
+
return `${block}${block.endsWith("\n") ? "" : "\n"}${line}
|
|
29
|
+
${body}`;
|
|
30
|
+
}
|
|
31
|
+
function filesUnder(dir, base = dir) {
|
|
32
|
+
const out = [];
|
|
33
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
34
|
+
const abs = join(dir, entry.name);
|
|
35
|
+
if (entry.isDirectory()) out.push(...filesUnder(abs, base));
|
|
36
|
+
else if (entry.isFile()) out.push(abs.slice(base.length + 1).split("\\").join("/"));
|
|
37
|
+
}
|
|
38
|
+
return out.sort();
|
|
39
|
+
}
|
|
40
|
+
function orderSkillFiles(relPaths) {
|
|
41
|
+
return [...relPaths.filter((p) => p === "SKILL.md"), ...relPaths.filter((p) => p !== "SKILL.md")];
|
|
42
|
+
}
|
|
43
|
+
function loadClass(packRoot, skillClass, names, version) {
|
|
44
|
+
const files = [];
|
|
45
|
+
for (const skill of names) {
|
|
46
|
+
const dir = join(packRoot, skillClass, skill);
|
|
47
|
+
if (!existsSync(dir)) throw new Error(`skill pack is missing ${skillClass}/${skill} under ${packRoot}`);
|
|
48
|
+
for (const rel of orderSkillFiles(filesUnder(dir))) {
|
|
49
|
+
const raw = readFileSync(join(dir, rel), "utf8");
|
|
50
|
+
files.push({
|
|
51
|
+
skill,
|
|
52
|
+
skillClass,
|
|
53
|
+
path: `${skillDir(skill)}/${rel}`,
|
|
54
|
+
content: rel === "SKILL.md" ? renderSkillFile(skillClass, raw, version) : raw
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return files;
|
|
59
|
+
}
|
|
60
|
+
function loadTemplateFiles(packRoot = resolvePackRoot(), version = resolvePackageVersion()) {
|
|
61
|
+
return loadClass(packRoot, "template", TEMPLATE_SKILLS, version);
|
|
62
|
+
}
|
|
63
|
+
function loadMaintainedFiles(packRoot = resolvePackRoot(), version = resolvePackageVersion()) {
|
|
64
|
+
return loadClass(packRoot, "maintained", MAINTAINED_SKILLS, version);
|
|
65
|
+
}
|
|
66
|
+
function writeMaintainedFiles(repoRoot, files) {
|
|
67
|
+
const created = [];
|
|
68
|
+
const rewritten = [];
|
|
69
|
+
for (const file of files) {
|
|
70
|
+
const abs = join(repoRoot, file.path);
|
|
71
|
+
const exists = existsSync(abs);
|
|
72
|
+
if (exists && readFileSync(abs, "utf8") === file.content) continue;
|
|
73
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
74
|
+
writeFileSync(abs, file.content);
|
|
75
|
+
(exists ? rewritten : created).push(file.path);
|
|
76
|
+
}
|
|
77
|
+
return { created, rewritten };
|
|
78
|
+
}
|
|
79
|
+
function maintainedFilesForExistingSkills(repoRoot, files) {
|
|
80
|
+
return files.filter((file) => existsSync(join(repoRoot, skillDir(file.skill))));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export { MAINTAINED_SKILLS, TEMPLATE_SKILLS, loadMaintainedFiles, loadTemplateFiles, maintainedFilesForExistingSkills, resolvePackRoot, skillDir, writeMaintainedFiles };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import { homeFromEnv } from './chunk-VNVZDWO3.js';
|
|
4
|
-
import {
|
|
4
|
+
import { mkdirSync, chmodSync, writeFileSync, readFileSync, readdirSync, existsSync, unlinkSync } from 'fs';
|
|
5
5
|
import { join, isAbsolute, dirname, basename, resolve } from 'path';
|
|
6
6
|
import { execFileSync } from 'child_process';
|
|
7
7
|
|
|
@@ -74,8 +74,103 @@ function contextExcludesPresent(gitCommonDir) {
|
|
|
74
74
|
if (beginIdx === -1) return false;
|
|
75
75
|
return BLOCK_LINES.every((l, i) => lines[beginIdx + i] === l);
|
|
76
76
|
}
|
|
77
|
-
var
|
|
77
|
+
var HOOKS_DIR_NAME = "git-hooks";
|
|
78
|
+
var CO_AUTHOR_FILE = "co-author";
|
|
79
|
+
var HOOKS = [
|
|
80
|
+
"applypatch-msg",
|
|
81
|
+
"pre-applypatch",
|
|
82
|
+
"post-applypatch",
|
|
83
|
+
"pre-commit",
|
|
84
|
+
"pre-merge-commit",
|
|
85
|
+
"prepare-commit-msg",
|
|
86
|
+
"commit-msg",
|
|
87
|
+
"post-commit",
|
|
88
|
+
"pre-rebase",
|
|
89
|
+
"post-checkout",
|
|
90
|
+
"post-merge",
|
|
91
|
+
"pre-push",
|
|
92
|
+
"pre-receive",
|
|
93
|
+
"update",
|
|
94
|
+
"post-receive",
|
|
95
|
+
"post-update",
|
|
96
|
+
"reference-transaction",
|
|
97
|
+
"pre-auto-gc",
|
|
98
|
+
"post-rewrite",
|
|
99
|
+
"sendemail-validate",
|
|
100
|
+
"post-index-change"
|
|
101
|
+
];
|
|
102
|
+
var READS_STDIN = /* @__PURE__ */ new Set(["pre-push", "pre-receive", "post-receive", "reference-transaction", "post-rewrite"]);
|
|
78
103
|
var PRE_COMMIT_GUARD_MARKER = "# outerlayer materialized-context commit guard";
|
|
104
|
+
var RUN_GUARD = `common=$(git rev-parse --git-common-dir 2>/dev/null)
|
|
105
|
+
guard=$common/hooks/pre-commit
|
|
106
|
+
if [ "$guard" != "$hook" ] && [ -f "$guard" ] && [ -x "$guard" ] &&
|
|
107
|
+
sed -n 2p "$guard" | grep -q '^${PRE_COMMIT_GUARD_MARKER}'; then
|
|
108
|
+
"$guard" "$@" || exit $?
|
|
109
|
+
fi
|
|
110
|
+
`;
|
|
111
|
+
var ADD_TRAILER = `
|
|
112
|
+
# Every commit of the build names the requester once: a trailer already there is left alone.
|
|
113
|
+
file=\${1-}
|
|
114
|
+
if [ -n "$file" ] && [ -f "$file" ]; then
|
|
115
|
+
git interpret-trailers --in-place --if-exists addIfDifferent --trailer "Co-authored-by: $(cat "$here/${CO_AUTHOR_FILE}")" "$file" ||
|
|
116
|
+
echo "outerlayer: could not add the co-author line to the commit message" >&2
|
|
117
|
+
fi
|
|
118
|
+
`;
|
|
119
|
+
var HOOK_WRAPPER_MARKER = "# Written by the OuterLayer runner.";
|
|
120
|
+
function script(name) {
|
|
121
|
+
const adds = name === "prepare-commit-msg";
|
|
122
|
+
return `#!/bin/sh
|
|
123
|
+
${HOOK_WRAPPER_MARKER} Runs the repository's own ${name} hook, then
|
|
124
|
+
# ${adds ? "adds the co-author line to the commit message" : "hands its status on"}.
|
|
125
|
+
here=$(dirname "$0")
|
|
126
|
+
|
|
127
|
+
# The hooks path git would use without this directory: cut the list of configuration
|
|
128
|
+
# entries the runner passes just before its own core.hooksPath entry, then ask git.
|
|
129
|
+
n=\${GIT_CONFIG_COUNT-0}
|
|
130
|
+
case $n in '' | *[!0-9]*) n=0 ;; esac
|
|
131
|
+
cut=$n
|
|
132
|
+
i=$n
|
|
133
|
+
while [ "$i" -gt 0 ]; do
|
|
134
|
+
i=$((i - 1))
|
|
135
|
+
eval "key=\\\${GIT_CONFIG_KEY_$i-}"
|
|
136
|
+
eval "value=\\\${GIT_CONFIG_VALUE_$i-}"
|
|
137
|
+
if [ "$key" = core.hooksPath ] && { [ "$value" = "$here" ] || [ "$value" -ef "$here" ]; }; then
|
|
138
|
+
cut=$i
|
|
139
|
+
break
|
|
140
|
+
fi
|
|
141
|
+
done
|
|
142
|
+
path=$(GIT_CONFIG_COUNT=$cut git config --type=path core.hooksPath 2>/dev/null) || path=
|
|
143
|
+
if [ -z "$path" ]; then
|
|
144
|
+
path=$(git rev-parse --git-common-dir 2>/dev/null)/hooks
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
hook=$path/${name}
|
|
148
|
+
${name === "pre-commit" ? RUN_GUARD : ""}status=0
|
|
149
|
+
if [ "$path" -ef "$here" ] || [ ! -f "$hook" ] || [ ! -x "$hook" ]; then
|
|
150
|
+
${READS_STDIN.has(name) ? " cat >/dev/null\n" : ""} :
|
|
151
|
+
else
|
|
152
|
+
${adds ? ` "$hook" "$@" || status=$?` : ` exec "$hook" "$@"`}
|
|
153
|
+
fi
|
|
154
|
+
${adds ? `[ "$status" -eq 0 ] || exit "$status"
|
|
155
|
+
${ADD_TRAILER}` : ""}exit 0
|
|
156
|
+
`;
|
|
157
|
+
}
|
|
158
|
+
function writeCommitHooks(buildDir, coAuthor) {
|
|
159
|
+
const dir = join(buildDir, HOOKS_DIR_NAME);
|
|
160
|
+
mkdirSync(dir, { recursive: true });
|
|
161
|
+
chmodSync(dir, 493);
|
|
162
|
+
for (const name of HOOKS) {
|
|
163
|
+
const path = join(dir, name);
|
|
164
|
+
writeFileSync(path, script(name));
|
|
165
|
+
chmodSync(path, 493);
|
|
166
|
+
}
|
|
167
|
+
const data = join(dir, CO_AUTHOR_FILE);
|
|
168
|
+
writeFileSync(data, `${coAuthor.name} <${coAuthor.email}>
|
|
169
|
+
`);
|
|
170
|
+
chmodSync(data, 420);
|
|
171
|
+
return dir;
|
|
172
|
+
}
|
|
173
|
+
var JOURNAL_HOOK_MARKER = "# outerlayer post-rewrite journal";
|
|
79
174
|
var PRE_COMMIT_GUARD_VERSION = `${PRE_COMMIT_GUARD_MARKER} v1`;
|
|
80
175
|
var PRE_COMMIT_GUARD_BODY_SIGNATURE = "outerlayer: refusing to commit materialized control-plane context";
|
|
81
176
|
var PRE_COMMIT_GUARD_SCRIPT = `#!/bin/sh
|
|
@@ -114,8 +209,8 @@ exit 1
|
|
|
114
209
|
function managedHookScripts() {
|
|
115
210
|
return [["pre-commit", printableHookScript(PRE_COMMIT_GUARD_SCRIPT)]];
|
|
116
211
|
}
|
|
117
|
-
function printableHookScript(
|
|
118
|
-
const lines =
|
|
212
|
+
function printableHookScript(script2) {
|
|
213
|
+
const lines = script2.split("\n");
|
|
119
214
|
return [lines[0], ...lines.slice(2)].join("\n");
|
|
120
215
|
}
|
|
121
216
|
function git(cwd, args) {
|
|
@@ -141,7 +236,7 @@ function commonHooksDir(cwd) {
|
|
|
141
236
|
if (rel === void 0) return void 0;
|
|
142
237
|
return join(isAbsolute(rel) ? rel : join(cwd, rel), "hooks");
|
|
143
238
|
}
|
|
144
|
-
function installHookFile(dir, name,
|
|
239
|
+
function installHookFile(dir, name, script2, marker) {
|
|
145
240
|
const path = join(dir, name);
|
|
146
241
|
let existing;
|
|
147
242
|
try {
|
|
@@ -151,7 +246,7 @@ function installHookFile(dir, name, script, marker) {
|
|
|
151
246
|
}
|
|
152
247
|
if (existing !== void 0) {
|
|
153
248
|
if (!isOwnRecognitionLine(existing, marker)) return "skipped-foreign";
|
|
154
|
-
if (existing ===
|
|
249
|
+
if (existing === script2) {
|
|
155
250
|
try {
|
|
156
251
|
chmodSync(path, 493);
|
|
157
252
|
} catch {
|
|
@@ -160,7 +255,7 @@ function installHookFile(dir, name, script, marker) {
|
|
|
160
255
|
}
|
|
161
256
|
}
|
|
162
257
|
mkdirSync(dir, { recursive: true });
|
|
163
|
-
writeFileSync(path,
|
|
258
|
+
writeFileSync(path, script2);
|
|
164
259
|
chmodSync(path, 493);
|
|
165
260
|
return existing === void 0 ? "installed" : "updated";
|
|
166
261
|
}
|
|
@@ -237,15 +332,29 @@ function expandHooksPathTilde(hooksPath) {
|
|
|
237
332
|
}
|
|
238
333
|
function ensurePreCommitGuardHook(repoRoot, gitCommonDir) {
|
|
239
334
|
const hooksPath = git(repoRoot, ["config", "core.hooksPath"]) ?? "";
|
|
240
|
-
if (hooksPath !== "") {
|
|
335
|
+
if (hooksPath !== "" && !isRunnerHookWrapper(hooksPath, repoRoot)) {
|
|
241
336
|
const expanded = expandHooksPathTilde(hooksPath);
|
|
242
337
|
const effectivePath = isAbsolute(expanded) ? join(expanded, "pre-commit") : join(repoRoot, expanded, "pre-commit");
|
|
243
338
|
return { status: "skipped-custom-hooks-path", adopted: guardAdoptedAtEffectiveHook(effectivePath) };
|
|
244
339
|
}
|
|
340
|
+
return installPreCommitGuardInGitDir(gitCommonDir);
|
|
341
|
+
}
|
|
342
|
+
function installPreCommitGuardInGitDir(gitCommonDir) {
|
|
245
343
|
const path = join(gitCommonDir, "hooks", "pre-commit");
|
|
246
344
|
const status = installHookFile(join(gitCommonDir, "hooks"), "pre-commit", PRE_COMMIT_GUARD_SCRIPT, PRE_COMMIT_GUARD_MARKER);
|
|
247
345
|
return { status, adopted: status === "skipped-foreign" ? guardBodyPresent(path) : true };
|
|
248
346
|
}
|
|
347
|
+
function isRunnerHookWrapper(hooksPath, repoRoot) {
|
|
348
|
+
const dir = resolve(repoRoot, expandHooksPathTilde(hooksPath));
|
|
349
|
+
if (basename(dir) !== HOOKS_DIR_NAME) return false;
|
|
350
|
+
try {
|
|
351
|
+
const script2 = readFileSync(join(dir, "pre-commit"), "utf8");
|
|
352
|
+
const secondLine = script2.indexOf("\n") + 1;
|
|
353
|
+
return secondLine > 0 && script2.startsWith(HOOK_WRAPPER_MARKER, secondLine);
|
|
354
|
+
} catch {
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
249
358
|
function deadHooksState(cwd) {
|
|
250
359
|
const inRepo = git(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
251
360
|
if (inRepo !== "true") return { dead: false };
|
|
@@ -282,11 +391,11 @@ function preparePackageManagerCommand(repoRoot) {
|
|
|
282
391
|
function huskyPrepareScript(repoRoot) {
|
|
283
392
|
try {
|
|
284
393
|
const pkg = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8"));
|
|
285
|
-
const
|
|
286
|
-
if (typeof
|
|
394
|
+
const script2 = pkg.scripts?.prepare;
|
|
395
|
+
if (typeof script2 === "string" && /husky/i.test(script2)) return script2;
|
|
287
396
|
} catch {
|
|
288
397
|
}
|
|
289
398
|
return null;
|
|
290
399
|
}
|
|
291
400
|
|
|
292
|
-
export { contextExcludesPresent, deadHooksState, ensureContextExcludes, ensurePreCommitGuardHook, huskyPrepareScript, installPreCommitGuardHook, managedHookScripts, preparePackageManagerCommand, removeJournalHooks, removePreCommitGuardHook };
|
|
401
|
+
export { HOOKS_DIR_NAME, contextExcludesPresent, deadHooksState, ensureContextExcludes, ensurePreCommitGuardHook, huskyPrepareScript, installPreCommitGuardHook, installPreCommitGuardInGitDir, managedHookScripts, preparePackageManagerCommand, removeJournalHooks, removePreCommitGuardHook, writeCommitHooks };
|