@outerlayer/cli 0.1.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 +7 -0
- package/LICENSE +202 -0
- package/README.md +320 -0
- package/dist/build-info.json +1 -0
- package/dist/chunk-4H56Y7O6.js +589 -0
- package/dist/chunk-A3WLZX2F.js +120 -0
- package/dist/chunk-A6NNKRJU.js +650 -0
- package/dist/chunk-D77LS3UI.js +42 -0
- package/dist/chunk-DEDV5V4U.js +87324 -0
- package/dist/chunk-ECSYRPCC.js +334 -0
- package/dist/chunk-IWIUYLDR.js +472 -0
- package/dist/chunk-JJP7YLMN.js +25 -0
- package/dist/chunk-KFYJV2ZG.js +7978 -0
- package/dist/chunk-OZ7C3XUE.js +34 -0
- package/dist/chunk-RCQXYLMO.js +96 -0
- package/dist/chunk-TFUIDMOB.js +34 -0
- package/dist/chunk-VNVZDWO3.js +10 -0
- package/dist/chunk-VU34RWDU.js +339 -0
- package/dist/chunk-WQ6VGRGZ.js +150 -0
- package/dist/chunk-XN2XZFTK.js +165 -0
- package/dist/chunk-YMTXR7PJ.js +200 -0
- package/dist/chunk-YRYMNBGQ.js +54 -0
- package/dist/chunk-Z5GEFMRW.js +45 -0
- package/dist/chunk-ZJSNLVCE.js +58 -0
- package/dist/cli-3JYGL2OB.js +5311 -0
- package/dist/config-POF7DEQW.js +7 -0
- package/dist/context-materialize-J6CRQ7O7.js +589 -0
- package/dist/emit-artifact-cmd-23F4YKQ3.js +339 -0
- package/dist/emit-cmd-TWSYYEZI.js +208 -0
- package/dist/emit-commit-credit-cmd-2GX5M2BR.js +121 -0
- package/dist/emit-finding-cmd-6K7ERPKG.js +235 -0
- package/dist/emit-result-cmd-NDFHMJX7.js +225 -0
- package/dist/hook-fast-CBW4ZETX.js +8 -0
- package/dist/hook-wrap-fast-36WGE6EX.js +8 -0
- package/dist/import-capture-cmd-EUMBGIV3.js +176 -0
- package/dist/import-ruler-cmd-7LH2QOMG.js +173 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +25 -0
- package/dist/init-PTBITAUO.js +103 -0
- package/dist/login-cmd-IRX6LZT7.js +62 -0
- package/dist/logs-TRNPQM42.js +63 -0
- package/dist/loop-NFSOBUUJ.js +646 -0
- package/dist/mcp-install-cmd-FDQH6SEN.js +75 -0
- package/dist/mcp-serve-cmd-57EZZOTL.js +123 -0
- package/dist/paths-D2VGWWFI.js +6 -0
- package/dist/pidfile-PTW76F56.js +8 -0
- package/dist/status-WGOJTXZF.js +94 -0
- package/dist/statusline-fast-Z5U5CEC6.js +8 -0
- package/dist/sync-cmd-C5UGI4SF.js +15 -0
- package/dist/watch-V3K4PESQ.js +70 -0
- package/dist/work-claim-cmd-FQDWQVT2.js +98 -0
- package/dist/work-cmd-CW26EZZO.js +16 -0
- package/dist/work-launch-YNNCKIH3.js +6 -0
- package/dist/work-pr-cmd-W2QNMWXP.js +115 -0
- package/package.json +58 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
export { RunnerConfigError, checkHooks, defaultRunnerConfigPath, loadRunnerConfig } from './chunk-WQ6VGRGZ.js';
|
|
4
|
+
import './chunk-VNVZDWO3.js';
|
|
5
|
+
import './chunk-Z5GEFMRW.js';
|
|
6
|
+
|
|
7
|
+
createRequire(import.meta.url);
|
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { readPin, pinExists, writeDegraded, writeSourceCache, SOURCE_CACHE_TTL_RETRY_MS, SOURCE_CACHE_TTL_SUCCESS_MS, readContextManifest, isLastCheckFresh, manifestFilesPresent, readDegraded, touchLastCheck, clearDegraded, writeContextManifest, writePinIfChanged, readSourceCache, isSourceCacheFresh } from './chunk-A3WLZX2F.js';
|
|
4
|
+
import { parseGoverns, normalizeRepoName, CONTEXT_SOURCE_CONFIG_PATH } from './chunk-KFYJV2ZG.js';
|
|
5
|
+
import { appendContextProvenance } from './chunk-ZJSNLVCE.js';
|
|
6
|
+
import { ensureContextExcludes, ensurePreCommitGuardHook } from './chunk-IWIUYLDR.js';
|
|
7
|
+
import { readCloudConfig } from './chunk-TFUIDMOB.js';
|
|
8
|
+
import { homeFromEnv } from './chunk-VNVZDWO3.js';
|
|
9
|
+
import './chunk-Z5GEFMRW.js';
|
|
10
|
+
import { execFileSync } from 'child_process';
|
|
11
|
+
import { existsSync, mkdirSync, writeFileSync, chmodSync, unlinkSync, readdirSync, rmSync, lstatSync } from 'fs';
|
|
12
|
+
import { isAbsolute, join, dirname, relative } from 'path';
|
|
13
|
+
|
|
14
|
+
createRequire(import.meta.url);
|
|
15
|
+
var defaultGitRunner = (args, opts) => {
|
|
16
|
+
try {
|
|
17
|
+
const stdout = execFileSync("git", args, {
|
|
18
|
+
cwd: opts.cwd,
|
|
19
|
+
timeout: opts.timeoutMs,
|
|
20
|
+
input: opts.input,
|
|
21
|
+
// Control-plane trees are small config/doc surfaces (AGENTS.md,
|
|
22
|
+
// .claude/**, .outerlayer/**) — generous but bounded so a runaway
|
|
23
|
+
// cat-file stream can't exhaust memory.
|
|
24
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
25
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
26
|
+
env: {
|
|
27
|
+
...process.env,
|
|
28
|
+
// classifyGitFailure greps English stderr substrings
|
|
29
|
+
// ("Authentication failed", "Permission denied", …) — a git running
|
|
30
|
+
// under a non-English locale would translate those and silently
|
|
31
|
+
// misclassify every failure as "unreachable" instead of "no-access".
|
|
32
|
+
LC_ALL: "C",
|
|
33
|
+
// A bare https:// remote with no cached credential answer otherwise
|
|
34
|
+
// blocks on an interactive username/password prompt with no
|
|
35
|
+
// terminal to read from here, hanging until OUR OWN timeout instead
|
|
36
|
+
// of failing fast with a classifiable stderr message.
|
|
37
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
return { ok: true, stdout: Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout), stderr: "" };
|
|
41
|
+
} catch (err) {
|
|
42
|
+
const e = err;
|
|
43
|
+
return {
|
|
44
|
+
ok: false,
|
|
45
|
+
stdout: e.stdout ? Buffer.from(e.stdout) : Buffer.alloc(0),
|
|
46
|
+
stderr: e.stderr ? Buffer.from(e.stderr).toString("utf8") : e.message ?? String(err)
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
function gitText(git, cwd, args, timeoutMs) {
|
|
51
|
+
const result = git(args, { cwd, timeoutMs });
|
|
52
|
+
return result.ok ? result.stdout.toString("utf8").trim() : void 0;
|
|
53
|
+
}
|
|
54
|
+
var NOT_GOVERNED_MESSAGE = "this repository's control plane no longer governs it";
|
|
55
|
+
var ADOPTION_MESSAGE = "committed context files shadow the control plane \u2014 remove them in a commit";
|
|
56
|
+
var CANNOT_IDENTIFY_MESSAGE = "this repository is governed, but the control plane cannot be identified \u2014 check OuterLayer connectivity/config";
|
|
57
|
+
var DESIGNATION_AUTH_REJECTED_MESSAGE = "the OuterLayer API key was rejected (401/403) \u2014 check apiKey in ~/.outerlayer/config.json";
|
|
58
|
+
var PARTIAL_WRITE_MESSAGE = "control-plane content could not be fully written \u2014 running on previous context";
|
|
59
|
+
var COMMIT_GUARD_FOREIGN_NOT_INSTALLED_MESSAGE = "the materialized-context commit guard is NOT installed \u2014 a pre-commit hook this clone did not write occupies that slot \u2014 resolve the conflict or run `outerlayer hooks install-git`";
|
|
60
|
+
var COMMIT_GUARD_HOOKSPATH_NOT_INSTALLED_MESSAGE = "the materialized-context commit guard is NOT installed \u2014 hooks are managed via core.hooksPath, which this clone never writes to automatically \u2014 adopt it yourself: `outerlayer hooks install-git --print`";
|
|
61
|
+
function guardNotInstalledMessage(guard) {
|
|
62
|
+
if (guard.adopted) return void 0;
|
|
63
|
+
if (guard.status === "skipped-foreign") return COMMIT_GUARD_FOREIGN_NOT_INSTALLED_MESSAGE;
|
|
64
|
+
if (guard.status === "skipped-custom-hooks-path") return COMMIT_GUARD_HOOKSPATH_NOT_INSTALLED_MESSAGE;
|
|
65
|
+
return void 0;
|
|
66
|
+
}
|
|
67
|
+
var FAILURE_DEBOUNCE_MS = 2 * 60 * 1e3;
|
|
68
|
+
function hasGovernedEvidence(hasPin, manifest) {
|
|
69
|
+
return hasPin || manifest !== void 0;
|
|
70
|
+
}
|
|
71
|
+
function hasSymlinkComponent(root, relPath) {
|
|
72
|
+
let current = root;
|
|
73
|
+
for (const segment of relPath.split("/")) {
|
|
74
|
+
current = join(current, segment);
|
|
75
|
+
let st;
|
|
76
|
+
try {
|
|
77
|
+
st = lstatSync(current);
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
if (st.isSymbolicLink()) return true;
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
function unreachableMessage(lastGoodSha) {
|
|
86
|
+
return lastGoodSha ? `could not verify the control plane \u2014 running on previously materialized context at \`${lastGoodSha.slice(0, 7)}\` (it may have moved)` : "this repository is governed but its context has never been materialized and the control plane is unreachable \u2014 running without governed context";
|
|
87
|
+
}
|
|
88
|
+
function noAccessMessage(lastGoodSha) {
|
|
89
|
+
const base = "you do not have access to the control plane";
|
|
90
|
+
const remedy = " \u2014 check your git credentials (SSH key or token) and org/SSO access, then retry";
|
|
91
|
+
return lastGoodSha ? `${base}; running on previously materialized context at \`${lastGoodSha.slice(0, 7)}\`${remedy}` : `${base}; running with repository defaults${remedy}`;
|
|
92
|
+
}
|
|
93
|
+
function classifyGitFailure(stderr) {
|
|
94
|
+
if (/authentication failed|permission denied \(publickey\)|\b403\b|saml sso|access denied/i.test(stderr)) {
|
|
95
|
+
return "no-access";
|
|
96
|
+
}
|
|
97
|
+
return "unreachable";
|
|
98
|
+
}
|
|
99
|
+
async function materializeContext(deps) {
|
|
100
|
+
const home = deps.home ?? homeFromEnv();
|
|
101
|
+
const now = deps.now ?? Date.now;
|
|
102
|
+
const git = deps.git ?? defaultGitRunner;
|
|
103
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
104
|
+
const repoRoot = gitText(git, deps.cwd, ["rev-parse", "--show-toplevel"], 2e3);
|
|
105
|
+
if (repoRoot === void 0) {
|
|
106
|
+
return { status: "not-a-repo", sha: null, verification: "none" };
|
|
107
|
+
}
|
|
108
|
+
const gitCommonDir = resolveGitCommonDir(git, repoRoot);
|
|
109
|
+
if (gitCommonDir === void 0) {
|
|
110
|
+
return { status: "not-a-repo", sha: null, verification: "none" };
|
|
111
|
+
}
|
|
112
|
+
const pin = readPin(repoRoot);
|
|
113
|
+
const hasPin = pin !== void 0 || pinExists(repoRoot);
|
|
114
|
+
const record = (sha2, verification, repository2, ref2) => {
|
|
115
|
+
appendContextProvenance(
|
|
116
|
+
{ t: new Date(now()).toISOString(), sessionId: deps.sessionId, repoRoot, repository: repository2, ref: ref2, sha: sha2, verification },
|
|
117
|
+
home
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
const selfOutcome = () => {
|
|
121
|
+
if (hasPin) {
|
|
122
|
+
writeDegraded(gitCommonDir, { at: new Date(now()).toISOString(), kind: "not-governed", message: NOT_GOVERNED_MESSAGE });
|
|
123
|
+
return { status: "not-governed", message: NOT_GOVERNED_MESSAGE, sha: null, verification: "none" };
|
|
124
|
+
}
|
|
125
|
+
clearDegraded(gitCommonDir);
|
|
126
|
+
return { status: "self", sha: null, verification: "none" };
|
|
127
|
+
};
|
|
128
|
+
const ownRepoName = deriveOwnRepoName(git, repoRoot);
|
|
129
|
+
if (ownRepoName === void 0) {
|
|
130
|
+
return selfOutcome();
|
|
131
|
+
}
|
|
132
|
+
const designation = await resolveDesignation({ git, fetchImpl, home, now, gitCommonDir, ownRepoName });
|
|
133
|
+
if (designation.status === "self") {
|
|
134
|
+
return selfOutcome();
|
|
135
|
+
}
|
|
136
|
+
if (designation.status === "unknown") {
|
|
137
|
+
const message = designation.authRejected ? DESIGNATION_AUTH_REJECTED_MESSAGE : CANNOT_IDENTIFY_MESSAGE;
|
|
138
|
+
if (hasPin) {
|
|
139
|
+
writeDegraded(gitCommonDir, { at: new Date(now()).toISOString(), kind: "designation-unverified", message });
|
|
140
|
+
record(null, "none");
|
|
141
|
+
return { status: "designation-unverified", message, sha: null, verification: "none" };
|
|
142
|
+
}
|
|
143
|
+
if (designation.authRejected) {
|
|
144
|
+
writeDegraded(gitCommonDir, { at: new Date(now()).toISOString(), kind: "designation-unverified", message });
|
|
145
|
+
}
|
|
146
|
+
writeSourceCache(gitCommonDir, { checkedAt: new Date(now()).toISOString(), status: "self", ttlMs: SOURCE_CACHE_TTL_RETRY_MS });
|
|
147
|
+
return { status: "self", sha: null, verification: "none" };
|
|
148
|
+
}
|
|
149
|
+
let repository;
|
|
150
|
+
let ref;
|
|
151
|
+
let unverifiedIdentity;
|
|
152
|
+
let preFetched;
|
|
153
|
+
if (designation.status === "candidates") {
|
|
154
|
+
const sortedCandidates = [...designation.candidates].sort((a, b) => a.repository.localeCompare(b.repository));
|
|
155
|
+
const claimants = [];
|
|
156
|
+
for (const candidate of sortedCandidates) {
|
|
157
|
+
const candidateUrl = deps.resolveControlPlaneUrl ? deps.resolveControlPlaneUrl(candidate.repository, candidate.ref, repoRoot) : buildControlPlaneUrl(git, repoRoot, candidate.repository);
|
|
158
|
+
const candidateHost = parseRemoteHost(candidateUrl).host || "unknown-host";
|
|
159
|
+
const candidateCacheDir = ensureBareCache(git, home, candidate.repository, candidateHost);
|
|
160
|
+
const candidateLocalRef = `refs/outerlayer/${sanitizeRefName(candidate.ref)}`;
|
|
161
|
+
const candidateFetch = git(["fetch", "--depth", "1", candidateUrl, `+${candidate.ref}:${candidateLocalRef}`], {
|
|
162
|
+
cwd: candidateCacheDir,
|
|
163
|
+
timeoutMs: 1e4
|
|
164
|
+
});
|
|
165
|
+
if (!candidateFetch.ok) continue;
|
|
166
|
+
const candidateSha = gitText(git, candidateCacheDir, ["rev-parse", candidateLocalRef], 2e3);
|
|
167
|
+
if (candidateSha === void 0) continue;
|
|
168
|
+
const candidateConfig = readBlobAtPath(git, candidateCacheDir, candidateSha, CONTEXT_SOURCE_CONFIG_PATH);
|
|
169
|
+
const candidateGoverns = candidateConfig === void 0 ? { governs: [] } : parseGoverns(candidateConfig);
|
|
170
|
+
const candidateGovernsList = "error" in candidateGoverns ? [] : candidateGoverns.governs;
|
|
171
|
+
if (candidateGovernsList.some((r) => normalizeRepoName(r) === normalizeRepoName(ownRepoName))) {
|
|
172
|
+
claimants.push({ repository: candidate.repository, ref: candidate.ref, sha: candidateSha, cacheRepoDir: candidateCacheDir });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (claimants.length === 0) {
|
|
176
|
+
writeSourceCache(gitCommonDir, { checkedAt: new Date(now()).toISOString(), status: "self", ttlMs: SOURCE_CACHE_TTL_SUCCESS_MS });
|
|
177
|
+
return selfOutcome();
|
|
178
|
+
}
|
|
179
|
+
if (claimants.length > 1) {
|
|
180
|
+
const names = claimants.map((c) => c.repository).sort();
|
|
181
|
+
const message = `${names.join(" and ")} both claim to govern this repository \u2014 resolve the designation before context can materialize`;
|
|
182
|
+
writeDegraded(gitCommonDir, { at: new Date(now()).toISOString(), kind: "conflict", message });
|
|
183
|
+
const manifestForConflict = readContextManifest(gitCommonDir);
|
|
184
|
+
const lastGood = manifestForConflict?.sha ?? null;
|
|
185
|
+
const verification = manifestForConflict ? "unverified" : "none";
|
|
186
|
+
record(lastGood, verification);
|
|
187
|
+
return { status: "conflict", message, sha: lastGood, verification };
|
|
188
|
+
}
|
|
189
|
+
const winner = claimants[0];
|
|
190
|
+
writeSourceCache(gitCommonDir, {
|
|
191
|
+
checkedAt: new Date(now()).toISOString(),
|
|
192
|
+
status: "governed",
|
|
193
|
+
repository: winner.repository,
|
|
194
|
+
ref: winner.ref,
|
|
195
|
+
ttlMs: SOURCE_CACHE_TTL_SUCCESS_MS
|
|
196
|
+
});
|
|
197
|
+
repository = winner.repository;
|
|
198
|
+
ref = winner.ref;
|
|
199
|
+
preFetched = { sha: winner.sha, cacheRepoDir: winner.cacheRepoDir };
|
|
200
|
+
} else {
|
|
201
|
+
repository = designation.repository;
|
|
202
|
+
ref = designation.ref;
|
|
203
|
+
unverifiedIdentity = designation.unverifiedIdentity;
|
|
204
|
+
}
|
|
205
|
+
const manifest = readContextManifest(gitCommonDir);
|
|
206
|
+
const evidenced = hasGovernedEvidence(hasPin, manifest);
|
|
207
|
+
const clearOrFlagIdentity = () => {
|
|
208
|
+
if (unverifiedIdentity) {
|
|
209
|
+
writeDegraded(gitCommonDir, {
|
|
210
|
+
at: new Date(now()).toISOString(),
|
|
211
|
+
kind: "designation-unverified",
|
|
212
|
+
message: CANNOT_IDENTIFY_MESSAGE,
|
|
213
|
+
repository,
|
|
214
|
+
ref
|
|
215
|
+
});
|
|
216
|
+
} else {
|
|
217
|
+
clearDegraded(gitCommonDir);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
let sha;
|
|
221
|
+
let cacheRepoDir;
|
|
222
|
+
if (preFetched) {
|
|
223
|
+
sha = preFetched.sha;
|
|
224
|
+
cacheRepoDir = preFetched.cacheRepoDir;
|
|
225
|
+
} else {
|
|
226
|
+
if (isLastCheckFresh(gitCommonDir, now()) && manifest !== void 0 && pin?.sha === manifest.sha && manifestFilesPresent(repoRoot, manifest)) {
|
|
227
|
+
const degraded = readDegraded(gitCommonDir);
|
|
228
|
+
const verification = degraded ? "unverified" : "verified";
|
|
229
|
+
ensureContextExcludes(gitCommonDir);
|
|
230
|
+
const guardMessage2 = guardNotInstalledMessage(ensurePreCommitGuardHook(repoRoot, gitCommonDir));
|
|
231
|
+
record(manifest.sha, verification, repository, ref);
|
|
232
|
+
return {
|
|
233
|
+
status: "debounced",
|
|
234
|
+
sha: manifest.sha,
|
|
235
|
+
verification,
|
|
236
|
+
...guardMessage2 ? { message: guardMessage2 } : {}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
const recentFailure = readDegraded(gitCommonDir);
|
|
240
|
+
if (recentFailure && (recentFailure.kind === "unreachable" || recentFailure.kind === "no-access") && recentFailure.repository === repository && recentFailure.ref === ref && now() - Date.parse(recentFailure.at) < FAILURE_DEBOUNCE_MS) {
|
|
241
|
+
const lastGood = manifest?.sha ?? null;
|
|
242
|
+
const verification = lastGood ? "unverified" : "none";
|
|
243
|
+
if (evidenced) {
|
|
244
|
+
record(lastGood, verification, repository, ref);
|
|
245
|
+
return { status: recentFailure.kind, message: recentFailure.message, sha: lastGood, verification };
|
|
246
|
+
}
|
|
247
|
+
return { status: recentFailure.kind, sha: null, verification: "none" };
|
|
248
|
+
}
|
|
249
|
+
const remoteUrl = deps.resolveControlPlaneUrl ? deps.resolveControlPlaneUrl(repository, ref, repoRoot) : buildControlPlaneUrl(git, repoRoot, repository);
|
|
250
|
+
const cacheHost = parseRemoteHost(remoteUrl).host || "unknown-host";
|
|
251
|
+
const lsRemote = git(["ls-remote", remoteUrl, ref], { cwd: repoRoot, timeoutMs: 2e3 });
|
|
252
|
+
if (lsRemote.ok) {
|
|
253
|
+
const remoteSha = parseLsRemoteSha(lsRemote.stdout.toString("utf8"));
|
|
254
|
+
if (remoteSha && manifest !== void 0 && manifest.sha === remoteSha && pin?.sha === remoteSha && manifestFilesPresent(repoRoot, manifest)) {
|
|
255
|
+
touchLastCheck(gitCommonDir, now());
|
|
256
|
+
clearOrFlagIdentity();
|
|
257
|
+
ensureContextExcludes(gitCommonDir);
|
|
258
|
+
const guardMessage2 = guardNotInstalledMessage(ensurePreCommitGuardHook(repoRoot, gitCommonDir));
|
|
259
|
+
writeSourceCache(gitCommonDir, { checkedAt: new Date(now()).toISOString(), status: "governed", repository, ref, ttlMs: SOURCE_CACHE_TTL_SUCCESS_MS });
|
|
260
|
+
record(remoteSha, "verified", repository, ref);
|
|
261
|
+
return {
|
|
262
|
+
status: "debounced",
|
|
263
|
+
sha: remoteSha,
|
|
264
|
+
verification: "verified",
|
|
265
|
+
...guardMessage2 ? { message: guardMessage2 } : {}
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
cacheRepoDir = ensureBareCache(git, home, repository, cacheHost);
|
|
270
|
+
const localRef = `refs/outerlayer/${sanitizeRefName(ref)}`;
|
|
271
|
+
const fetchResult = git(["fetch", "--depth", "1", remoteUrl, `+${ref}:${localRef}`], { cwd: cacheRepoDir, timeoutMs: 1e4 });
|
|
272
|
+
if (!fetchResult.ok) {
|
|
273
|
+
const kind = classifyGitFailure(fetchResult.stderr);
|
|
274
|
+
const lastGood = manifest?.sha ?? null;
|
|
275
|
+
const verification = lastGood ? "unverified" : "none";
|
|
276
|
+
const doctorMessage = kind === "no-access" ? noAccessMessage(lastGood) : unreachableMessage(lastGood);
|
|
277
|
+
writeDegraded(gitCommonDir, { at: new Date(now()).toISOString(), kind, message: doctorMessage, repository, ref, lastGoodSha: lastGood ?? void 0 });
|
|
278
|
+
if (evidenced) {
|
|
279
|
+
record(lastGood, verification, repository, ref);
|
|
280
|
+
return { status: kind, message: doctorMessage, sha: lastGood, verification };
|
|
281
|
+
}
|
|
282
|
+
return { status: kind, sha: null, verification: "none" };
|
|
283
|
+
}
|
|
284
|
+
const fetchedSha = gitText(git, cacheRepoDir, ["rev-parse", localRef], 2e3);
|
|
285
|
+
if (fetchedSha === void 0) {
|
|
286
|
+
const lastGood = manifest?.sha ?? null;
|
|
287
|
+
const verification = lastGood ? "unverified" : "none";
|
|
288
|
+
const doctorMessage = unreachableMessage(lastGood);
|
|
289
|
+
writeDegraded(gitCommonDir, { at: new Date(now()).toISOString(), kind: "unreachable", message: doctorMessage, repository, ref, lastGoodSha: lastGood ?? void 0 });
|
|
290
|
+
if (evidenced) {
|
|
291
|
+
record(lastGood, verification, repository, ref);
|
|
292
|
+
return { status: "unreachable", message: doctorMessage, sha: lastGood, verification };
|
|
293
|
+
}
|
|
294
|
+
return { status: "unreachable", sha: null, verification: "none" };
|
|
295
|
+
}
|
|
296
|
+
sha = fetchedSha;
|
|
297
|
+
const configContent = readBlobAtPath(git, cacheRepoDir, sha, CONTEXT_SOURCE_CONFIG_PATH);
|
|
298
|
+
const parsedGoverns = configContent === void 0 ? { governs: [] } : parseGoverns(configContent);
|
|
299
|
+
const governsList = "error" in parsedGoverns ? [] : parsedGoverns.governs;
|
|
300
|
+
const governed = governsList.some((r) => normalizeRepoName(r) === normalizeRepoName(ownRepoName));
|
|
301
|
+
if (!governed) {
|
|
302
|
+
writeSourceCache(gitCommonDir, { checkedAt: new Date(now()).toISOString(), status: "self", ttlMs: SOURCE_CACHE_TTL_SUCCESS_MS });
|
|
303
|
+
if (hasPin) {
|
|
304
|
+
writeDegraded(gitCommonDir, { at: new Date(now()).toISOString(), kind: "not-governed", message: NOT_GOVERNED_MESSAGE });
|
|
305
|
+
return { status: "not-governed", message: NOT_GOVERNED_MESSAGE, sha: pin?.sha ?? null, verification: "none" };
|
|
306
|
+
}
|
|
307
|
+
clearDegraded(gitCommonDir);
|
|
308
|
+
return { status: "self", sha: null, verification: "none" };
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const tracked = git(["ls-files", "-z", "--", "AGENTS.md", ".claude", ".outerlayer"], { cwd: repoRoot, timeoutMs: 2e3 });
|
|
312
|
+
if (tracked.ok && tracked.stdout.length > 0) {
|
|
313
|
+
writeDegraded(gitCommonDir, { at: new Date(now()).toISOString(), kind: "adoption-conflict", message: ADOPTION_MESSAGE, repository, ref });
|
|
314
|
+
const lastGood = manifest?.sha ?? null;
|
|
315
|
+
record(lastGood, lastGood ? "unverified" : "none", repository, ref);
|
|
316
|
+
return { status: "adoption-conflict", message: ADOPTION_MESSAGE, sha: lastGood, verification: lastGood ? "unverified" : "none" };
|
|
317
|
+
}
|
|
318
|
+
const files = listMaterializedFiles(git, cacheRepoDir, sha);
|
|
319
|
+
const writeResult = writeFilesVerbatim(git, cacheRepoDir, files, repoRoot);
|
|
320
|
+
const partialWrite = () => {
|
|
321
|
+
const lastGood = manifest?.sha ?? null;
|
|
322
|
+
const verification = lastGood ? "unverified" : "none";
|
|
323
|
+
writeDegraded(gitCommonDir, {
|
|
324
|
+
at: new Date(now()).toISOString(),
|
|
325
|
+
kind: "unreachable",
|
|
326
|
+
message: PARTIAL_WRITE_MESSAGE,
|
|
327
|
+
repository,
|
|
328
|
+
ref,
|
|
329
|
+
lastGoodSha: lastGood ?? void 0
|
|
330
|
+
});
|
|
331
|
+
if (!evidenced) return { status: "unreachable", sha: null, verification: "none" };
|
|
332
|
+
record(lastGood, verification, repository, ref);
|
|
333
|
+
return { status: "unreachable", message: PARTIAL_WRITE_MESSAGE, sha: lastGood, verification };
|
|
334
|
+
};
|
|
335
|
+
if (!writeResult.ok) {
|
|
336
|
+
return partialWrite();
|
|
337
|
+
}
|
|
338
|
+
const deleteResult = manifest ? deleteRemovedFiles(repoRoot, manifest.files, writeResult.writtenPaths) : { skipped: [] };
|
|
339
|
+
if (deleteResult.skipped.length > 0) {
|
|
340
|
+
return partialWrite();
|
|
341
|
+
}
|
|
342
|
+
const newManifest = {
|
|
343
|
+
schemaVersion: 1,
|
|
344
|
+
sha,
|
|
345
|
+
files: writeResult.writtenPaths,
|
|
346
|
+
materializedAt: new Date(now()).toISOString()
|
|
347
|
+
};
|
|
348
|
+
writeContextManifest(gitCommonDir, newManifest);
|
|
349
|
+
ensureContextExcludes(gitCommonDir);
|
|
350
|
+
const guardMessage = guardNotInstalledMessage(ensurePreCommitGuardHook(repoRoot, gitCommonDir));
|
|
351
|
+
writePinIfChanged(repoRoot, sha);
|
|
352
|
+
touchLastCheck(gitCommonDir, now());
|
|
353
|
+
clearOrFlagIdentity();
|
|
354
|
+
writeSourceCache(gitCommonDir, { checkedAt: new Date(now()).toISOString(), status: "governed", repository, ref, ttlMs: SOURCE_CACHE_TTL_SUCCESS_MS });
|
|
355
|
+
record(sha, "verified", repository, ref);
|
|
356
|
+
return {
|
|
357
|
+
status: "materialized",
|
|
358
|
+
sha,
|
|
359
|
+
verification: "verified",
|
|
360
|
+
...guardMessage ? { message: guardMessage } : {}
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
async function resolveDesignation(ctx) {
|
|
364
|
+
const cache = readSourceCache(ctx.gitCommonDir);
|
|
365
|
+
if (cache && isSourceCacheFresh(cache, ctx.now())) {
|
|
366
|
+
return cache.status === "governed" && cache.repository && cache.ref ? { status: "governed", repository: cache.repository, ref: cache.ref } : { status: "self" };
|
|
367
|
+
}
|
|
368
|
+
let authRejected = false;
|
|
369
|
+
const config = readCloudConfig(ctx.home);
|
|
370
|
+
if (config.url && config.apiKey && config.appId) {
|
|
371
|
+
const sources = await fetchContextSource(
|
|
372
|
+
config,
|
|
373
|
+
ctx.ownRepoName,
|
|
374
|
+
ctx.fetchImpl,
|
|
375
|
+
() => {
|
|
376
|
+
authRejected = true;
|
|
377
|
+
}
|
|
378
|
+
);
|
|
379
|
+
if (sources !== void 0) {
|
|
380
|
+
if (sources.length === 0) {
|
|
381
|
+
writeSourceCache(ctx.gitCommonDir, {
|
|
382
|
+
checkedAt: new Date(ctx.now()).toISOString(),
|
|
383
|
+
status: "self",
|
|
384
|
+
ttlMs: SOURCE_CACHE_TTL_SUCCESS_MS
|
|
385
|
+
});
|
|
386
|
+
return { status: "self" };
|
|
387
|
+
}
|
|
388
|
+
return { status: "candidates", candidates: sources };
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (cache) {
|
|
392
|
+
return cache.status === "governed" && cache.repository && cache.ref ? { status: "governed", repository: cache.repository, ref: cache.ref, unverifiedIdentity: true } : { status: "self" };
|
|
393
|
+
}
|
|
394
|
+
return { status: "unknown", authRejected };
|
|
395
|
+
}
|
|
396
|
+
async function fetchContextSource(config, ownRepoName, fetchImpl, onAuthRejected) {
|
|
397
|
+
const controller = new AbortController();
|
|
398
|
+
const timer = setTimeout(() => controller.abort(), 2e3);
|
|
399
|
+
try {
|
|
400
|
+
const url = `${config.url.replace(/\/$/, "")}/v1/context/source?repo=${encodeURIComponent(ownRepoName)}`;
|
|
401
|
+
const res = await fetchImpl(url, {
|
|
402
|
+
headers: { Authorization: `Bearer ${config.apiKey}`, "x-outerlayer-app-id": config.appId },
|
|
403
|
+
signal: controller.signal
|
|
404
|
+
});
|
|
405
|
+
if (res.status === 401 || res.status === 403) {
|
|
406
|
+
onAuthRejected();
|
|
407
|
+
return void 0;
|
|
408
|
+
}
|
|
409
|
+
if (!res.ok) return void 0;
|
|
410
|
+
const body = await res.json();
|
|
411
|
+
return body.data?.sources;
|
|
412
|
+
} catch {
|
|
413
|
+
return void 0;
|
|
414
|
+
} finally {
|
|
415
|
+
clearTimeout(timer);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function resolveGitCommonDir(git, repoRoot) {
|
|
419
|
+
const abs = gitText(git, repoRoot, ["rev-parse", "--path-format=absolute", "--git-common-dir"], 2e3);
|
|
420
|
+
if (abs !== void 0) return abs;
|
|
421
|
+
const rel = gitText(git, repoRoot, ["rev-parse", "--git-common-dir"], 2e3);
|
|
422
|
+
if (rel === void 0) return void 0;
|
|
423
|
+
return isAbsolute(rel) ? rel : join(repoRoot, rel);
|
|
424
|
+
}
|
|
425
|
+
function deriveOwnRepoName(git, repoRoot) {
|
|
426
|
+
const url = gitText(git, repoRoot, ["config", "--get", "remote.origin.url"], 2e3);
|
|
427
|
+
if (url === void 0) return void 0;
|
|
428
|
+
return parseOwnerRepo(url);
|
|
429
|
+
}
|
|
430
|
+
function parseOwnerRepo(url) {
|
|
431
|
+
const stripped = url.replace(/\.git$/, "");
|
|
432
|
+
const scpMatch = stripped.match(/^[\w.-]+@[\w.-]+:(.+)$/);
|
|
433
|
+
if (scpMatch) return scpMatch[1];
|
|
434
|
+
try {
|
|
435
|
+
const parsed = new URL(stripped);
|
|
436
|
+
return parsed.pathname.replace(/^\//, "");
|
|
437
|
+
} catch {
|
|
438
|
+
return void 0;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
function parseRemoteHost(url) {
|
|
442
|
+
const scpMatch = url.match(/^([\w.-]+)@([\w.-]+):/);
|
|
443
|
+
if (scpMatch) return { scheme: "ssh", host: scpMatch[2] };
|
|
444
|
+
try {
|
|
445
|
+
const parsed = new URL(url);
|
|
446
|
+
return { scheme: parsed.protocol === "https:" ? "https" : "ssh", host: parsed.host };
|
|
447
|
+
} catch {
|
|
448
|
+
return { scheme: "https", host: "github.com" };
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
function buildControlPlaneUrl(git, repoRoot, repository) {
|
|
452
|
+
const ownUrl = gitText(git, repoRoot, ["config", "--get", "remote.origin.url"], 2e3);
|
|
453
|
+
const { scheme, host } = ownUrl ? parseRemoteHost(ownUrl) : { scheme: "https", host: "github.com" };
|
|
454
|
+
return scheme === "ssh" ? `git@${host}:${repository}.git` : `https://${host}/${repository}.git`;
|
|
455
|
+
}
|
|
456
|
+
function cacheDirName(repository, host) {
|
|
457
|
+
const parts = repository.split("/");
|
|
458
|
+
return [host, ...parts].join("__").replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
459
|
+
}
|
|
460
|
+
function sanitizeRefName(ref) {
|
|
461
|
+
return ref.replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
462
|
+
}
|
|
463
|
+
function ensureBareCache(git, home, repository, host) {
|
|
464
|
+
const dir = join(home, ".outerlayer", "control-planes", `${cacheDirName(repository, host)}.git`);
|
|
465
|
+
if (!existsSync(join(dir, "HEAD"))) {
|
|
466
|
+
mkdirSync(dirname(dir), { recursive: true });
|
|
467
|
+
git(["init", "--bare", "-q", dir], { cwd: dirname(dir), timeoutMs: 5e3 });
|
|
468
|
+
}
|
|
469
|
+
return dir;
|
|
470
|
+
}
|
|
471
|
+
function parseLsRemoteSha(output) {
|
|
472
|
+
const lines = output.split("\n").filter((l) => l.trim().length > 0);
|
|
473
|
+
const peeled = lines.find((l) => l.split(/\s+/)[1]?.endsWith("^{}"));
|
|
474
|
+
const line = peeled ?? lines[0];
|
|
475
|
+
return line?.split(/\s+/)[0];
|
|
476
|
+
}
|
|
477
|
+
var MATERIALIZED_ROOTS = ["AGENTS.md", ".claude", ".outerlayer"];
|
|
478
|
+
function listMaterializedFiles(git, cacheRepoDir, sha) {
|
|
479
|
+
const result = git(["ls-tree", "-r", "-z", sha, "--", ...MATERIALIZED_ROOTS], { cwd: cacheRepoDir, timeoutMs: 1e4 });
|
|
480
|
+
if (!result.ok) return [];
|
|
481
|
+
const entries = [];
|
|
482
|
+
for (const raw of result.stdout.toString("utf8").split("\0")) {
|
|
483
|
+
if (!raw) continue;
|
|
484
|
+
const tabIdx = raw.indexOf(" ");
|
|
485
|
+
if (tabIdx === -1) continue;
|
|
486
|
+
const meta = raw.slice(0, tabIdx).split(" ");
|
|
487
|
+
const [mode, type, objSha] = meta;
|
|
488
|
+
const path = raw.slice(tabIdx + 1);
|
|
489
|
+
if (mode === void 0 || objSha === void 0 || path === void 0) continue;
|
|
490
|
+
if (mode === "120000") continue;
|
|
491
|
+
if (type !== "blob") continue;
|
|
492
|
+
if (path.includes("..") || isAbsolute(path)) continue;
|
|
493
|
+
entries.push({ mode, type: "blob", sha: objSha, path });
|
|
494
|
+
}
|
|
495
|
+
return entries;
|
|
496
|
+
}
|
|
497
|
+
function readBlobAtPath(git, cacheRepoDir, sha, path) {
|
|
498
|
+
const result = git(["show", `${sha}:${path}`], { cwd: cacheRepoDir, timeoutMs: 5e3 });
|
|
499
|
+
return result.ok ? result.stdout.toString("utf8") : void 0;
|
|
500
|
+
}
|
|
501
|
+
function parseBatchReply(files, stdout) {
|
|
502
|
+
const parsed = [];
|
|
503
|
+
let pos = 0;
|
|
504
|
+
for (const file of files) {
|
|
505
|
+
const headerEnd = stdout.indexOf(10, pos);
|
|
506
|
+
if (headerEnd === -1) return void 0;
|
|
507
|
+
const header = stdout.subarray(pos, headerEnd).toString("utf8");
|
|
508
|
+
const parts = header.split(" ");
|
|
509
|
+
if (parts[0] !== file.sha) return void 0;
|
|
510
|
+
if (parts[1] === "missing") return void 0;
|
|
511
|
+
const size = parts[1] === "blob" && parts[2] !== void 0 ? Number(parts[2]) : NaN;
|
|
512
|
+
if (!Number.isFinite(size) || stdout.length < headerEnd + 1 + size + 1) return void 0;
|
|
513
|
+
const contentStart = headerEnd + 1;
|
|
514
|
+
parsed.push({ path: file.path, content: stdout.subarray(contentStart, contentStart + size), mode: file.mode });
|
|
515
|
+
pos = contentStart + size + 1;
|
|
516
|
+
}
|
|
517
|
+
return parsed;
|
|
518
|
+
}
|
|
519
|
+
function writeFilesVerbatim(git, cacheRepoDir, files, workpieceRoot) {
|
|
520
|
+
if (files.length === 0) return { writtenPaths: [], ok: true };
|
|
521
|
+
const input = files.map((f) => f.sha).join("\n") + "\n";
|
|
522
|
+
const result = git(["cat-file", "--batch"], { cwd: cacheRepoDir, timeoutMs: 15e3, input });
|
|
523
|
+
if (!result.ok) return { writtenPaths: [], ok: false };
|
|
524
|
+
const parsed = parseBatchReply(files, result.stdout);
|
|
525
|
+
if (parsed === void 0) return { writtenPaths: [], ok: false };
|
|
526
|
+
const writtenPaths = [];
|
|
527
|
+
let anySkipped = false;
|
|
528
|
+
for (const file of parsed) {
|
|
529
|
+
if (hasSymlinkComponent(workpieceRoot, file.path)) {
|
|
530
|
+
anySkipped = true;
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
const dest = join(workpieceRoot, file.path);
|
|
534
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
535
|
+
const mode = file.mode === "100755" ? 493 : 420;
|
|
536
|
+
writeFileSync(dest, file.content, { mode });
|
|
537
|
+
chmodSync(dest, mode);
|
|
538
|
+
writtenPaths.push(file.path);
|
|
539
|
+
}
|
|
540
|
+
return { writtenPaths, ok: !anySkipped };
|
|
541
|
+
}
|
|
542
|
+
function isDeletableManifestPath(rel) {
|
|
543
|
+
if (isAbsolute(rel)) return false;
|
|
544
|
+
if (rel.split("/").some((segment) => segment === "..")) return false;
|
|
545
|
+
return rel === "AGENTS.md" || rel.startsWith(".claude/") || rel.startsWith(".outerlayer/");
|
|
546
|
+
}
|
|
547
|
+
function deleteRemovedFiles(workpieceRoot, oldFiles, newFiles) {
|
|
548
|
+
const keep = new Set(newFiles);
|
|
549
|
+
const dirsToPrune = /* @__PURE__ */ new Set();
|
|
550
|
+
const skipped = [];
|
|
551
|
+
for (const rel of oldFiles) {
|
|
552
|
+
if (keep.has(rel)) continue;
|
|
553
|
+
if (!isDeletableManifestPath(rel)) continue;
|
|
554
|
+
if (hasSymlinkComponent(workpieceRoot, rel)) {
|
|
555
|
+
skipped.push(rel);
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
const abs = join(workpieceRoot, rel);
|
|
559
|
+
try {
|
|
560
|
+
unlinkSync(abs);
|
|
561
|
+
dirsToPrune.add(dirname(abs));
|
|
562
|
+
} catch {
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
for (const dir of dirsToPrune) {
|
|
566
|
+
pruneEmptyDirs(dir, workpieceRoot);
|
|
567
|
+
}
|
|
568
|
+
return { skipped };
|
|
569
|
+
}
|
|
570
|
+
function pruneEmptyDirs(dir, stopAt) {
|
|
571
|
+
let current = dir;
|
|
572
|
+
while (current !== stopAt && current.startsWith(stopAt) && relative(stopAt, current) !== "") {
|
|
573
|
+
let entries;
|
|
574
|
+
try {
|
|
575
|
+
entries = readdirSync(current);
|
|
576
|
+
} catch {
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
if (entries.length > 0) return;
|
|
580
|
+
try {
|
|
581
|
+
rmSync(current, { recursive: false });
|
|
582
|
+
} catch {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
current = dirname(current);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export { defaultGitRunner, materializeContext, parseOwnerRepo, parseRemoteHost };
|