@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
|
@@ -1,17 +1,55 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
-
import { readPin, pinExists, writeDegraded, writeSourceCache, SOURCE_CACHE_TTL_RETRY_MS, SOURCE_CACHE_TTL_SUCCESS_MS,
|
|
4
|
-
import {
|
|
3
|
+
import { readPin, pinExists, readBuildHold, readContextManifest, manifestFilesPresent, writeDegraded, writeSourceCache, SOURCE_CACHE_TTL_RETRY_MS, SOURCE_CACHE_TTL_SUCCESS_MS, isLastCheckFresh, readDegraded, touchLastCheck, clearDegraded, writeContextManifest, writePinIfChanged, readSourceCache, isSourceCacheFresh } from './chunk-WIOZAJ2W.js';
|
|
4
|
+
import { loadMaintainedFiles, writeMaintainedFiles } from './chunk-WO2BXCTQ.js';
|
|
5
|
+
import { parseGoverns, normalizeRepoName, CONTEXT_SOURCE_CONFIG_PATH, parseOuterlayerConfig, classifyTree, emitTree } from './chunk-EMY4I27X.js';
|
|
5
6
|
import { appendContextProvenance } from './chunk-ZJSNLVCE.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
7
|
+
import { defaultFetch } from './chunk-BCLJSQCV.js';
|
|
8
|
+
import { resolveGatewayCredentials } from './chunk-BOLTI6LR.js';
|
|
9
|
+
import { ensureContextExcludes, ensurePreCommitGuardHook } from './chunk-XCCLFVXM.js';
|
|
8
10
|
import { homeFromEnv } from './chunk-VNVZDWO3.js';
|
|
9
|
-
import './chunk-Z5GEFMRW.js';
|
|
10
11
|
import { execFileSync } from 'child_process';
|
|
11
|
-
import { existsSync, mkdirSync, writeFileSync, chmodSync, unlinkSync, readdirSync, rmSync,
|
|
12
|
-
import {
|
|
12
|
+
import { lstatSync, existsSync, mkdirSync, writeFileSync, chmodSync, unlinkSync, readdirSync, rmSync, readFileSync, copyFileSync } from 'fs';
|
|
13
|
+
import { join, isAbsolute, dirname, relative } from 'path';
|
|
13
14
|
|
|
14
15
|
createRequire(import.meta.url);
|
|
16
|
+
var CONFIG_PATH = ".outerlayer/config.json";
|
|
17
|
+
var CONTENT_KINDS = /* @__PURE__ */ new Set(["skill", "skill-reference"]);
|
|
18
|
+
function overlayMaintainedSkills(repoRoot, unsafe) {
|
|
19
|
+
const sources = loadMaintainedFiles().filter((file) => !unsafe(file.path));
|
|
20
|
+
writeMaintainedFiles(repoRoot, sources);
|
|
21
|
+
const paths = sources.map((file) => file.path);
|
|
22
|
+
const configAbs = join(repoRoot, CONFIG_PATH);
|
|
23
|
+
if (!existsSync(configAbs)) return paths;
|
|
24
|
+
const config = parseOuterlayerConfig(readFileSync(configAbs, "utf8"));
|
|
25
|
+
if (!config.ok || !config.config) return paths;
|
|
26
|
+
const { entries } = classifyTree(sources.map((file) => file.path));
|
|
27
|
+
const input = {
|
|
28
|
+
entries,
|
|
29
|
+
contents: new Map(entries.filter((e) => CONTENT_KINDS.has(e.kind)).map((e) => [e.path, sources.find((f) => f.path === e.path).content])),
|
|
30
|
+
assetPaths: [],
|
|
31
|
+
targets: config.config.targets
|
|
32
|
+
};
|
|
33
|
+
const compiled = emitTree(input);
|
|
34
|
+
if (compiled.errors.length > 0) return paths;
|
|
35
|
+
for (const file of compiled.files) {
|
|
36
|
+
if (unsafe(file.path)) continue;
|
|
37
|
+
const abs = join(repoRoot, file.path);
|
|
38
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
39
|
+
if (!existsSync(abs) || readFileSync(abs, "utf8") !== file.content) writeFileSync(abs, file.content);
|
|
40
|
+
paths.push(file.path);
|
|
41
|
+
}
|
|
42
|
+
for (const copy of compiled.copies) {
|
|
43
|
+
if (unsafe(copy.toPath)) continue;
|
|
44
|
+
const abs = join(repoRoot, copy.toPath);
|
|
45
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
46
|
+
copyFileSync(join(repoRoot, copy.fromSourcePath), abs);
|
|
47
|
+
paths.push(copy.toPath);
|
|
48
|
+
}
|
|
49
|
+
return paths;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/context-materialize.ts
|
|
15
53
|
var defaultGitRunner = (args, opts) => {
|
|
16
54
|
try {
|
|
17
55
|
const stdout = execFileSync("git", args, {
|
|
@@ -54,8 +92,11 @@ function gitText(git, cwd, args, timeoutMs) {
|
|
|
54
92
|
var NOT_GOVERNED_MESSAGE = "this repository's control plane no longer governs it";
|
|
55
93
|
var ADOPTION_MESSAGE = "committed context files shadow the control plane \u2014 remove them in a commit";
|
|
56
94
|
var CANNOT_IDENTIFY_MESSAGE = "this repository is governed, but the control plane cannot be identified \u2014 check OuterLayer connectivity/config";
|
|
57
|
-
|
|
95
|
+
function designationAuthRejectedMessage(credential) {
|
|
96
|
+
return credential.startsWith("olu_") ? "the OuterLayer login was rejected (401/403) \u2014 run `outerlayer login` to sign in again" : "the OuterLayer API key was rejected (401/403) \u2014 check apiKey in ~/.outerlayer/config.json";
|
|
97
|
+
}
|
|
58
98
|
var PARTIAL_WRITE_MESSAGE = "control-plane content could not be fully written \u2014 running on previous context";
|
|
99
|
+
var OVERLAY_FAILED_MESSAGE = "control-plane content was written but the maintained skills could not be refreshed \u2014 retrying next session";
|
|
59
100
|
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
101
|
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
102
|
function guardNotInstalledMessage(guard) {
|
|
@@ -100,7 +141,7 @@ async function materializeContext(deps) {
|
|
|
100
141
|
const home = deps.home ?? homeFromEnv();
|
|
101
142
|
const now = deps.now ?? Date.now;
|
|
102
143
|
const git = deps.git ?? defaultGitRunner;
|
|
103
|
-
const fetchImpl = deps.fetchImpl ??
|
|
144
|
+
const fetchImpl = deps.fetchImpl ?? defaultFetch;
|
|
104
145
|
const repoRoot = gitText(git, deps.cwd, ["rev-parse", "--show-toplevel"], 2e3);
|
|
105
146
|
if (repoRoot === void 0) {
|
|
106
147
|
return { status: "not-a-repo", sha: null, verification: "none" };
|
|
@@ -125,34 +166,53 @@ async function materializeContext(deps) {
|
|
|
125
166
|
clearDegraded(gitCommonDir);
|
|
126
167
|
return { status: "self", sha: null, verification: "none" };
|
|
127
168
|
};
|
|
169
|
+
const hold = readBuildHold(gitCommonDir);
|
|
170
|
+
if (hold !== void 0) {
|
|
171
|
+
const heldManifest = readContextManifest(gitCommonDir);
|
|
172
|
+
if (heldManifest !== void 0 && heldManifest.sha === hold.sha && pin?.sha === hold.sha && manifestFilesPresent(repoRoot, heldManifest)) {
|
|
173
|
+
ensureContextExcludes(gitCommonDir);
|
|
174
|
+
recordMaintainedOverlay(repoRoot, gitCommonDir, heldManifest);
|
|
175
|
+
const guardMessage2 = guardNotInstalledMessage(ensurePreCommitGuardHook(repoRoot, gitCommonDir));
|
|
176
|
+
record(hold.sha, "verified", hold.repository, hold.ref);
|
|
177
|
+
return {
|
|
178
|
+
status: "debounced",
|
|
179
|
+
sha: hold.sha,
|
|
180
|
+
verification: "verified",
|
|
181
|
+
...guardMessage2 ? { message: guardMessage2 } : {}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
128
185
|
const ownRepoName = deriveOwnRepoName(git, repoRoot);
|
|
129
186
|
if (ownRepoName === void 0) {
|
|
130
187
|
return selfOutcome();
|
|
131
188
|
}
|
|
132
|
-
const designation = await resolveDesignation({ git, fetchImpl, home, now, gitCommonDir, ownRepoName });
|
|
189
|
+
const designation = await resolveDesignation({ git, fetchImpl, home, cloud: deps.cloud, now, gitCommonDir, ownRepoName, repoRoot });
|
|
190
|
+
const answered = { designation: "answered" };
|
|
133
191
|
if (designation.status === "self") {
|
|
134
|
-
return selfOutcome();
|
|
192
|
+
return { ...selfOutcome(), ...answered };
|
|
135
193
|
}
|
|
136
194
|
if (designation.status === "unknown") {
|
|
137
|
-
const message = designation.authRejected
|
|
195
|
+
const message = designation.authRejected ?? CANNOT_IDENTIFY_MESSAGE;
|
|
138
196
|
if (hasPin) {
|
|
139
197
|
writeDegraded(gitCommonDir, { at: new Date(now()).toISOString(), kind: "designation-unverified", message });
|
|
140
198
|
record(null, "none");
|
|
141
|
-
return { status: "designation-unverified", message, sha: null, verification: "none" };
|
|
199
|
+
return { status: "designation-unverified", message, sha: null, verification: "none", designation: "unknown" };
|
|
142
200
|
}
|
|
143
201
|
if (designation.authRejected) {
|
|
144
202
|
writeDegraded(gitCommonDir, { at: new Date(now()).toISOString(), kind: "designation-unverified", message });
|
|
145
203
|
}
|
|
146
204
|
writeSourceCache(gitCommonDir, { checkedAt: new Date(now()).toISOString(), status: "self", ttlMs: SOURCE_CACHE_TTL_RETRY_MS });
|
|
147
|
-
return { status: "self", sha: null, verification: "none" };
|
|
205
|
+
return { status: "self", sha: null, verification: "none", designation: "unknown", ...designation.authRejected ? { authRejected: true } : {} };
|
|
148
206
|
}
|
|
149
207
|
let repository;
|
|
150
208
|
let ref;
|
|
151
209
|
let unverifiedIdentity;
|
|
152
210
|
let preFetched;
|
|
211
|
+
let unreadableCandidates = [];
|
|
153
212
|
if (designation.status === "candidates") {
|
|
154
213
|
const sortedCandidates = [...designation.candidates].sort((a, b) => a.repository.localeCompare(b.repository));
|
|
155
214
|
const claimants = [];
|
|
215
|
+
const unfetched = [];
|
|
156
216
|
for (const candidate of sortedCandidates) {
|
|
157
217
|
const candidateUrl = deps.resolveControlPlaneUrl ? deps.resolveControlPlaneUrl(candidate.repository, candidate.ref, repoRoot) : buildControlPlaneUrl(git, repoRoot, candidate.repository);
|
|
158
218
|
const candidateHost = parseRemoteHost(candidateUrl).host || "unknown-host";
|
|
@@ -162,9 +222,15 @@ async function materializeContext(deps) {
|
|
|
162
222
|
cwd: candidateCacheDir,
|
|
163
223
|
timeoutMs: 1e4
|
|
164
224
|
});
|
|
165
|
-
if (!candidateFetch.ok)
|
|
225
|
+
if (!candidateFetch.ok) {
|
|
226
|
+
unfetched.push(candidate.repository);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
166
229
|
const candidateSha = gitText(git, candidateCacheDir, ["rev-parse", candidateLocalRef], 2e3);
|
|
167
|
-
if (candidateSha === void 0)
|
|
230
|
+
if (candidateSha === void 0) {
|
|
231
|
+
unfetched.push(candidate.repository);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
168
234
|
const candidateConfig = readBlobAtPath(git, candidateCacheDir, candidateSha, CONTEXT_SOURCE_CONFIG_PATH);
|
|
169
235
|
const candidateGoverns = candidateConfig === void 0 ? { governs: [] } : parseGoverns(candidateConfig);
|
|
170
236
|
const candidateGovernsList = "error" in candidateGoverns ? [] : candidateGoverns.governs;
|
|
@@ -174,7 +240,7 @@ async function materializeContext(deps) {
|
|
|
174
240
|
}
|
|
175
241
|
if (claimants.length === 0) {
|
|
176
242
|
writeSourceCache(gitCommonDir, { checkedAt: new Date(now()).toISOString(), status: "self", ttlMs: SOURCE_CACHE_TTL_SUCCESS_MS });
|
|
177
|
-
return selfOutcome();
|
|
243
|
+
return { ...selfOutcome(), ...answered, ...unfetched.length > 0 ? { unfetched } : {} };
|
|
178
244
|
}
|
|
179
245
|
if (claimants.length > 1) {
|
|
180
246
|
const names = claimants.map((c) => c.repository).sort();
|
|
@@ -186,6 +252,7 @@ async function materializeContext(deps) {
|
|
|
186
252
|
record(lastGood, verification);
|
|
187
253
|
return { status: "conflict", message, sha: lastGood, verification };
|
|
188
254
|
}
|
|
255
|
+
unreadableCandidates = unfetched;
|
|
189
256
|
const winner = claimants[0];
|
|
190
257
|
writeSourceCache(gitCommonDir, {
|
|
191
258
|
checkedAt: new Date(now()).toISOString(),
|
|
@@ -227,6 +294,7 @@ async function materializeContext(deps) {
|
|
|
227
294
|
const degraded = readDegraded(gitCommonDir);
|
|
228
295
|
const verification = degraded ? "unverified" : "verified";
|
|
229
296
|
ensureContextExcludes(gitCommonDir);
|
|
297
|
+
recordMaintainedOverlay(repoRoot, gitCommonDir, manifest);
|
|
230
298
|
const guardMessage2 = guardNotInstalledMessage(ensurePreCommitGuardHook(repoRoot, gitCommonDir));
|
|
231
299
|
record(manifest.sha, verification, repository, ref);
|
|
232
300
|
return {
|
|
@@ -255,6 +323,7 @@ async function materializeContext(deps) {
|
|
|
255
323
|
touchLastCheck(gitCommonDir, now());
|
|
256
324
|
clearOrFlagIdentity();
|
|
257
325
|
ensureContextExcludes(gitCommonDir);
|
|
326
|
+
recordMaintainedOverlay(repoRoot, gitCommonDir, manifest);
|
|
258
327
|
const guardMessage2 = guardNotInstalledMessage(ensurePreCommitGuardHook(repoRoot, gitCommonDir));
|
|
259
328
|
writeSourceCache(gitCommonDir, { checkedAt: new Date(now()).toISOString(), status: "governed", repository, ref, ttlMs: SOURCE_CACHE_TTL_SUCCESS_MS });
|
|
260
329
|
record(remoteSha, "verified", repository, ref);
|
|
@@ -335,14 +404,36 @@ async function materializeContext(deps) {
|
|
|
335
404
|
if (!writeResult.ok) {
|
|
336
405
|
return partialWrite();
|
|
337
406
|
}
|
|
338
|
-
|
|
407
|
+
let overlayPaths = [];
|
|
408
|
+
let overlayFailed = false;
|
|
409
|
+
try {
|
|
410
|
+
overlayPaths = overlayMaintainedSkills(repoRoot, (rel) => hasSymlinkComponent(repoRoot, rel));
|
|
411
|
+
} catch {
|
|
412
|
+
overlayFailed = true;
|
|
413
|
+
}
|
|
414
|
+
const writtenPaths = [.../* @__PURE__ */ new Set([...writeResult.writtenPaths, ...overlayPaths])];
|
|
415
|
+
const deleteResult = manifest ? deleteRemovedFiles(repoRoot, manifest.files, writtenPaths) : { skipped: [] };
|
|
416
|
+
if (overlayFailed && deleteResult.skipped.length === 0) {
|
|
417
|
+
writeContextManifest(gitCommonDir, { schemaVersion: 1, sha, files: writtenPaths, materializedAt: new Date(now()).toISOString() });
|
|
418
|
+
writeDegraded(gitCommonDir, {
|
|
419
|
+
at: new Date(now()).toISOString(),
|
|
420
|
+
kind: "unreachable",
|
|
421
|
+
message: OVERLAY_FAILED_MESSAGE,
|
|
422
|
+
repository,
|
|
423
|
+
ref,
|
|
424
|
+
lastGoodSha: sha
|
|
425
|
+
});
|
|
426
|
+
if (!evidenced) return { status: "unreachable", sha: null, verification: "none" };
|
|
427
|
+
record(sha, "unverified", repository, ref);
|
|
428
|
+
return { status: "unreachable", message: OVERLAY_FAILED_MESSAGE, sha, verification: "unverified" };
|
|
429
|
+
}
|
|
339
430
|
if (deleteResult.skipped.length > 0) {
|
|
340
431
|
return partialWrite();
|
|
341
432
|
}
|
|
342
433
|
const newManifest = {
|
|
343
434
|
schemaVersion: 1,
|
|
344
435
|
sha,
|
|
345
|
-
files:
|
|
436
|
+
files: writtenPaths,
|
|
346
437
|
materializedAt: new Date(now()).toISOString()
|
|
347
438
|
};
|
|
348
439
|
writeContextManifest(gitCommonDir, newManifest);
|
|
@@ -357,23 +448,40 @@ async function materializeContext(deps) {
|
|
|
357
448
|
status: "materialized",
|
|
358
449
|
sha,
|
|
359
450
|
verification: "verified",
|
|
360
|
-
...guardMessage ? { message: guardMessage } : {}
|
|
451
|
+
...guardMessage ? { message: guardMessage } : {},
|
|
452
|
+
...unreadableCandidates.length > 0 ? { unfetched: unreadableCandidates } : {}
|
|
361
453
|
};
|
|
362
454
|
}
|
|
455
|
+
function recordMaintainedOverlay(repoRoot, gitCommonDir, manifest) {
|
|
456
|
+
let overlayPaths;
|
|
457
|
+
try {
|
|
458
|
+
overlayPaths = overlayMaintainedSkills(repoRoot, (rel) => hasSymlinkComponent(repoRoot, rel));
|
|
459
|
+
} catch {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
const known = new Set(manifest.files);
|
|
463
|
+
const added = overlayPaths.filter((path) => !known.has(path));
|
|
464
|
+
if (added.length > 0) writeContextManifest(gitCommonDir, { ...manifest, files: [...manifest.files, ...added] });
|
|
465
|
+
}
|
|
363
466
|
async function resolveDesignation(ctx) {
|
|
364
467
|
const cache = readSourceCache(ctx.gitCommonDir);
|
|
365
468
|
if (cache && isSourceCacheFresh(cache, ctx.now())) {
|
|
366
469
|
return cache.status === "governed" && cache.repository && cache.ref ? { status: "governed", repository: cache.repository, ref: cache.ref } : { status: "self" };
|
|
367
470
|
}
|
|
368
|
-
let authRejected
|
|
369
|
-
const
|
|
370
|
-
|
|
471
|
+
let authRejected;
|
|
472
|
+
const destination = resolveGatewayCredentials({
|
|
473
|
+
home: ctx.home,
|
|
474
|
+
env: {},
|
|
475
|
+
cwd: ctx.repoRoot,
|
|
476
|
+
...ctx.cloud === void 0 ? {} : { url: ctx.cloud.url, apiKey: ctx.cloud.apiKey, appId: ctx.cloud.appId }
|
|
477
|
+
});
|
|
478
|
+
if (destination.ok) {
|
|
371
479
|
const sources = await fetchContextSource(
|
|
372
|
-
|
|
480
|
+
{ url: destination.url, apiKey: destination.credential, appId: destination.appId },
|
|
373
481
|
ctx.ownRepoName,
|
|
374
482
|
ctx.fetchImpl,
|
|
375
483
|
() => {
|
|
376
|
-
authRejected =
|
|
484
|
+
authRejected = designationAuthRejectedMessage(destination.credential);
|
|
377
485
|
}
|
|
378
486
|
);
|
|
379
487
|
if (sources !== void 0) {
|
|
@@ -586,4 +694,4 @@ function pruneEmptyDirs(dir, stopAt) {
|
|
|
586
694
|
}
|
|
587
695
|
}
|
|
588
696
|
|
|
589
|
-
export { defaultGitRunner, materializeContext, parseOwnerRepo, parseRemoteHost };
|
|
697
|
+
export { defaultGitRunner, hasSymlinkComponent, materializeContext, parseOwnerRepo, parseRemoteHost, resolveGitCommonDir };
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { dashboardBase, isRepositoryLinked, workPageUrl, notLinkedText } from './chunk-SY4GK4SP.js';
|
|
4
|
+
import { sanitizeTerminal } from './chunk-WFJ65NUD.js';
|
|
5
|
+
import { remoteLookupFailed } from './chunk-4C4THABW.js';
|
|
6
|
+
import { resolveGatewayCredentials } from './chunk-BOLTI6LR.js';
|
|
7
|
+
import { findGitDirs, readRepoConnection, writeRepoConnection } from './chunk-BJ3KTMDH.js';
|
|
8
|
+
import { createInterface } from 'readline/promises';
|
|
9
|
+
|
|
10
|
+
createRequire(import.meta.url);
|
|
11
|
+
var REQUEST_TIMEOUT_MS = 15e3;
|
|
12
|
+
var EXIT_NOT_LINKED = 2;
|
|
13
|
+
var REASON_TEXT = {
|
|
14
|
+
flag: "you passed --factory",
|
|
15
|
+
saved: "already connected",
|
|
16
|
+
linked: "the repository is linked to it",
|
|
17
|
+
only: "the only factory you can use",
|
|
18
|
+
prompt: "you chose it"
|
|
19
|
+
};
|
|
20
|
+
var slug = (f) => `${f.organization.name}/${f.name}`;
|
|
21
|
+
function result(code, outcome, reason, text) {
|
|
22
|
+
return { code, outcome, reason, text: text.endsWith("\n") ? text : `${text}
|
|
23
|
+
` };
|
|
24
|
+
}
|
|
25
|
+
function realConnectDeps() {
|
|
26
|
+
return { fetchImpl: fetch, env: process.env, prompt: promptOnTerminal, now: () => /* @__PURE__ */ new Date() };
|
|
27
|
+
}
|
|
28
|
+
async function promptOnTerminal(question, io = { input: process.stdin, output: process.stderr }) {
|
|
29
|
+
const rl = createInterface(io);
|
|
30
|
+
try {
|
|
31
|
+
let text = `
|
|
32
|
+
${question.message}
|
|
33
|
+
`;
|
|
34
|
+
question.options.forEach((o, i) => {
|
|
35
|
+
text += ` ${i + 1}) ${o.label}
|
|
36
|
+
`;
|
|
37
|
+
});
|
|
38
|
+
for (; ; ) {
|
|
39
|
+
const answer = (await rl.question(`${text}Choose 1-${question.options.length}: `)).trim();
|
|
40
|
+
if (answer === "") return void 0;
|
|
41
|
+
const picked = question.options[Number(answer) - 1];
|
|
42
|
+
if (picked !== void 0) return picked.value;
|
|
43
|
+
text = "";
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
return void 0;
|
|
47
|
+
} finally {
|
|
48
|
+
rl.close();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function parseFactories(body) {
|
|
52
|
+
const rows = body?.data;
|
|
53
|
+
if (!Array.isArray(rows)) return void 0;
|
|
54
|
+
const out = [];
|
|
55
|
+
for (const row of rows) {
|
|
56
|
+
const org = row?.organization;
|
|
57
|
+
if (typeof row?.id !== "string" || typeof row.name !== "string" || typeof org?.id !== "string" || typeof org.name !== "string") return void 0;
|
|
58
|
+
out.push({
|
|
59
|
+
id: row.id,
|
|
60
|
+
name: sanitizeTerminal(row.name),
|
|
61
|
+
displayName: sanitizeTerminal(typeof row.displayName === "string" ? row.displayName : row.name),
|
|
62
|
+
organization: {
|
|
63
|
+
id: org.id,
|
|
64
|
+
name: sanitizeTerminal(org.name),
|
|
65
|
+
displayName: sanitizeTerminal(typeof org.displayName === "string" ? org.displayName : org.name)
|
|
66
|
+
},
|
|
67
|
+
linked: typeof row.linked === "boolean" ? row.linked : null
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
function listText(factories, repository) {
|
|
73
|
+
return factories.map((f) => ` ${slug(f)} ${f.displayName}${f.linked === true ? ` (linked to ${repository})` : ""}`).join("\n");
|
|
74
|
+
}
|
|
75
|
+
async function fetchFactories(deps, url, token, repository) {
|
|
76
|
+
let res;
|
|
77
|
+
try {
|
|
78
|
+
res = await deps.fetchImpl(`${url.replace(/\/+$/, "")}/v1/me/factories?${new URLSearchParams({ repository }).toString()}`, {
|
|
79
|
+
method: "GET",
|
|
80
|
+
headers: { authorization: `Bearer ${token}` },
|
|
81
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
82
|
+
});
|
|
83
|
+
} catch (err) {
|
|
84
|
+
const message = `could not reach ${url}: ${err instanceof Error ? err.message : String(err)}`;
|
|
85
|
+
return { ok: false, result: result(1, "failed", message, message) };
|
|
86
|
+
}
|
|
87
|
+
if (res.status === 401) {
|
|
88
|
+
const message = "the gateway refused the login. Run `outerlayer login` to sign in again.";
|
|
89
|
+
return { ok: false, result: result(1, "failed", message, message) };
|
|
90
|
+
}
|
|
91
|
+
if (!res.ok) {
|
|
92
|
+
const message = `${url} answered HTTP ${res.status} when listing your factories`;
|
|
93
|
+
return { ok: false, result: result(1, "failed", message, message) };
|
|
94
|
+
}
|
|
95
|
+
const factories = parseFactories(await res.json().catch(() => null));
|
|
96
|
+
if (factories === void 0) {
|
|
97
|
+
const message = `${url} answered, but the reply is not a list of factories. Check the gateway address with \`outerlayer doctor\`, then run \`outerlayer connect\` again.`;
|
|
98
|
+
return { ok: false, result: result(1, "failed", `${url} did not answer with a factory list`, message) };
|
|
99
|
+
}
|
|
100
|
+
return { ok: true, factories };
|
|
101
|
+
}
|
|
102
|
+
async function choose(flags, deps, factories, saved, repository) {
|
|
103
|
+
if (flags.factory !== void 0) {
|
|
104
|
+
const wanted = flags.factory.toLowerCase();
|
|
105
|
+
const named = factories.find((f) => f.id.toLowerCase() === wanted || slug(f).toLowerCase() === wanted);
|
|
106
|
+
if (named) return { factory: named, reason: "flag" };
|
|
107
|
+
const message = `no factory you can use is named "${flags.factory}". Choose one with --factory:
|
|
108
|
+
${listText(factories, repository)}`;
|
|
109
|
+
return result(1, "failed", `--factory ${flags.factory} names no factory you can use`, message);
|
|
110
|
+
}
|
|
111
|
+
const remembered = saved && factories.find((f) => f.id === saved.appId);
|
|
112
|
+
if (remembered) return { factory: remembered, reason: "saved" };
|
|
113
|
+
const linked = factories.filter((f) => f.linked === true);
|
|
114
|
+
if (linked.length === 1) return { factory: linked[0], reason: "linked" };
|
|
115
|
+
if (factories.length === 1) return { factory: factories[0], reason: "only" };
|
|
116
|
+
if (!deps.isTerminal) {
|
|
117
|
+
const why = linked.length > 1 ? `${repository} is linked to more than one factory you can use` : `you can use more than one factory, and none is the only one ${repository} is linked to`;
|
|
118
|
+
const message = `${why}, and there is no terminal to ask on.
|
|
119
|
+
Run \`outerlayer connect --factory <org>/<factory>\` with one of:
|
|
120
|
+
${listText(factories, repository)}`;
|
|
121
|
+
return result(1, "failed", "more than one factory fits; pass --factory", message);
|
|
122
|
+
}
|
|
123
|
+
let pool = factories;
|
|
124
|
+
const orgs = new Map(factories.map((f) => [f.organization.id, f.organization]));
|
|
125
|
+
if (orgs.size > 1) {
|
|
126
|
+
const org = await deps.prompt({
|
|
127
|
+
message: "Which organization?",
|
|
128
|
+
options: [...orgs.values()].map((o) => ({ label: `${o.displayName} (${o.name})`, value: o.id }))
|
|
129
|
+
});
|
|
130
|
+
pool = factories.filter((f) => f.organization.id === org);
|
|
131
|
+
}
|
|
132
|
+
const picked = pool.length === 0 ? void 0 : await deps.prompt({
|
|
133
|
+
message: `Which factory should ${repository} use?`,
|
|
134
|
+
options: pool.map((f) => ({
|
|
135
|
+
label: `${f.displayName} (${slug(f)})${f.linked === true ? ` - linked to ${repository}` : ""}`,
|
|
136
|
+
value: f.id
|
|
137
|
+
}))
|
|
138
|
+
});
|
|
139
|
+
const factory = pool.find((f) => f.id === picked);
|
|
140
|
+
if (!factory) return result(1, "failed", "no factory was chosen", "no factory was chosen. Run `outerlayer connect` again, or pass --factory <org>/<factory>.");
|
|
141
|
+
return { factory, reason: "prompt" };
|
|
142
|
+
}
|
|
143
|
+
async function runConnect(flags, deps) {
|
|
144
|
+
const creds = resolveGatewayCredentials({ home: deps.home, env: deps.env, cwd: deps.cwd });
|
|
145
|
+
if (creds.credential === void 0) {
|
|
146
|
+
const message = "no saved credential. Run `outerlayer login` to sign in, then run `outerlayer connect` again.";
|
|
147
|
+
return { ...result(1, "skipped", "no credential; run `outerlayer login` first", message), next: "Run `outerlayer login`, then `outerlayer init` again." };
|
|
148
|
+
}
|
|
149
|
+
if (creds.kind === "key") {
|
|
150
|
+
if (flags.factory !== void 0 && flags.factory !== creds.appId) {
|
|
151
|
+
const message2 = `a factory key decides the factory${creds.appId ? ` (${creds.appId})` : ""}, so --factory ${flags.factory} cannot apply. Remove --factory, or sign in with \`outerlayer login\` and unset the key.`;
|
|
152
|
+
return result(1, "failed", "--factory names a factory other than the key's", message2);
|
|
153
|
+
}
|
|
154
|
+
const message = `Your factory key decides the factory${creds.appId ? ` (${creds.appId})` : ""}, so there is nothing to connect. No file was changed.`;
|
|
155
|
+
return result(0, "done", "the factory key decides the factory", message);
|
|
156
|
+
}
|
|
157
|
+
if (findGitDirs(deps.cwd) === void 0) {
|
|
158
|
+
return result(1, "skipped", "not in a git repository", "connect chooses a factory for a repository, and this directory is not in a git repository. Run it inside one.");
|
|
159
|
+
}
|
|
160
|
+
const repository = deps.resolveRepository(deps.cwd);
|
|
161
|
+
if (repository === void 0 && remoteLookupFailed(deps.cwd)) {
|
|
162
|
+
const message = "git did not answer when asked for this repository's remote, so connect cannot tell which repository it is. Run `git remote -v` here, then run `outerlayer connect` again.";
|
|
163
|
+
return result(1, "failed", "git could not read this repository's remote", message);
|
|
164
|
+
}
|
|
165
|
+
if (repository === void 0) {
|
|
166
|
+
return result(1, "skipped", "this repository has no git remote", "this repository has no git remote, so connect cannot tell which repository it is. Add one (`git remote add origin <url>`), then run `outerlayer connect` again.");
|
|
167
|
+
}
|
|
168
|
+
if (creds.url === void 0) {
|
|
169
|
+
const message = "no gateway address is saved. Run `outerlayer login` to sign in.";
|
|
170
|
+
return result(1, "failed", message, message);
|
|
171
|
+
}
|
|
172
|
+
const listed = await fetchFactories(deps, creds.url, creds.credential, repository);
|
|
173
|
+
if (!listed.ok) return listed.result;
|
|
174
|
+
const { factories } = listed;
|
|
175
|
+
if (factories.length === 0) {
|
|
176
|
+
const message = `you can use no factory yet. Create one in the dashboard: ${dashboardBase(deps.home)}/orgs`;
|
|
177
|
+
return result(1, "failed", "no factory to choose from; create one in the dashboard", message);
|
|
178
|
+
}
|
|
179
|
+
const dirs = findGitDirs(deps.cwd);
|
|
180
|
+
const saved = readRepoConnection(deps.cwd);
|
|
181
|
+
const choice = await choose(flags, deps, factories, saved, repository);
|
|
182
|
+
if ("code" in choice) return choice;
|
|
183
|
+
const { factory, reason } = choice;
|
|
184
|
+
if (saved?.appId !== factory.id) {
|
|
185
|
+
try {
|
|
186
|
+
writeRepoConnection(dirs.gitCommonDir, {
|
|
187
|
+
appId: factory.id,
|
|
188
|
+
organization: { name: factory.organization.name, displayName: factory.organization.displayName },
|
|
189
|
+
factory: { name: factory.name, displayName: factory.displayName },
|
|
190
|
+
savedAt: deps.now().toISOString()
|
|
191
|
+
});
|
|
192
|
+
} catch (err) {
|
|
193
|
+
const message = `could not save the connection under ${dirs.gitCommonDir}: ${err instanceof Error ? err.message : String(err)}`;
|
|
194
|
+
return result(1, "failed", "could not save the connection", message);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const connected = `Connected ${repository} to factory "${factory.displayName}" in ${factory.organization.displayName} (${REASON_TEXT[reason]}).
|
|
198
|
+
`;
|
|
199
|
+
const why = `connected to ${slug(factory)}: ${REASON_TEXT[reason]}`;
|
|
200
|
+
const linked = await isRepositoryLinked(deps.fetchImpl, creds.url, creds.credential, factory.id, repository);
|
|
201
|
+
const chosen = { repository, factory: slug(factory) };
|
|
202
|
+
if (linked === false) {
|
|
203
|
+
const linkUrl = workPageUrl(deps.home, factory.organization.name, factory.name);
|
|
204
|
+
return { ...result(EXIT_NOT_LINKED, "done", why, connected + notLinkedText(factory.displayName, repository, linkUrl)), ...chosen, linked, linkUrl };
|
|
205
|
+
}
|
|
206
|
+
const changeIt = "Run `outerlayer connect --factory <org>/<factory>` to change it.";
|
|
207
|
+
const text = linked === null ? `${connected}Could not check whether ${sanitizeTerminal(repository)} is linked to ${factory.displayName}.
|
|
208
|
+
${changeIt}` : connected + changeIt;
|
|
209
|
+
return { ...result(0, "done", why, text), ...chosen, linked };
|
|
210
|
+
}
|
|
211
|
+
function connectDocument(r) {
|
|
212
|
+
return {
|
|
213
|
+
status: r.outcome,
|
|
214
|
+
reason: r.reason,
|
|
215
|
+
...r.repository === void 0 ? {} : { repository: r.repository, factory: r.factory },
|
|
216
|
+
...r.linked === void 0 ? {} : { linked: r.linked },
|
|
217
|
+
...r.linkUrl === void 0 ? {} : { linkUrl: r.linkUrl }
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
async function runConnectCommand(flags, deps, out) {
|
|
221
|
+
const r = await runConnect(flags, deps);
|
|
222
|
+
if (flags.json) {
|
|
223
|
+
out.stdout.write(`${JSON.stringify(connectDocument(r), null, 2)}
|
|
224
|
+
`);
|
|
225
|
+
out.stderr.write(r.outcome === "done" ? r.text : `\u2717 ${r.text}`);
|
|
226
|
+
return r.code;
|
|
227
|
+
}
|
|
228
|
+
(r.code === 0 ? out.stdout : out.stderr).write(r.outcome === "done" ? r.text : `\u2717 ${r.text}`);
|
|
229
|
+
return r.code;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export { promptOnTerminal, realConnectDeps, runConnect, runConnectCommand };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { detectMachine, supportsContainerBuilds, reachDockerStep, describeMachineDocker, supportsMicroVmBuilds } from './chunk-3TFPDVTI.js';
|
|
4
|
+
import { MIN_ENGINE_MAJOR } from './chunk-HZRNMGLF.js';
|
|
5
|
+
import { RUNNER_DIR_MODE, runnerDir, RUNNER_FILE_MODE } from './chunk-ZM2IMMYN.js';
|
|
6
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
|
|
7
|
+
import { dirname } from 'path';
|
|
8
|
+
|
|
9
|
+
createRequire(import.meta.url);
|
|
10
|
+
var RunnerInitError = class extends Error {
|
|
11
|
+
};
|
|
12
|
+
var UNMARKED_PLACEHOLDER_COMMAND = "echo 'edit runner.commands in this config file: it must start your agent on item $OUTERLAYER_WORK' >&2; exit 1";
|
|
13
|
+
var PLACEHOLDER_MARKER = "outerlayer-placeholder";
|
|
14
|
+
var PLACEHOLDER_COMMAND = `${UNMARKED_PLACEHOLDER_COMMAND} # ${PLACEHOLDER_MARKER}`;
|
|
15
|
+
function isolationNote(machine) {
|
|
16
|
+
const has = describeMachineDocker(machine);
|
|
17
|
+
const reach = reachDockerStep(machine);
|
|
18
|
+
const needs = reach !== void 0 ? `Isolated builds need Docker Engine ${MIN_ENGINE_MAJOR} or later: ${reach}, then set both hooks to builtin:container.` : machine.platform === "windows" ? `Isolated builds need Docker Engine ${MIN_ENGINE_MAJOR} or later inside WSL2: install it there, run the runner from a WSL2 shell, and set both hooks to builtin:container.` : `Isolated builds need Docker Engine ${MIN_ENGINE_MAJOR} or later on Linux or WSL2. Once it is installed, set both hooks to builtin:container.`;
|
|
19
|
+
return `This machine has ${has}, so builds run as processes under this user (isolation: shared-user), through the tunnel. ${needs}`;
|
|
20
|
+
}
|
|
21
|
+
function processRefusal(machine) {
|
|
22
|
+
const reach = reachDockerStep(machine);
|
|
23
|
+
const needs = machine.platform === "darwin" ? "Isolated builds on a Mac run in a Linux VM: run this again with --vm." : reach !== void 0 ? `Isolated builds need Docker Engine ${MIN_ENGINE_MAJOR} or later: ${reach}, then run this again.` : machine.platform === "windows" ? `Isolated builds need Docker Engine ${MIN_ENGINE_MAJOR} or later inside WSL2: install it there, then run this again from a WSL2 shell.` : `Isolated builds need Docker Engine ${MIN_ENGINE_MAJOR} or later on Linux or WSL2: install it, then run this again.`;
|
|
24
|
+
return `This machine has ${describeMachineDocker(machine)}, so it cannot isolate a build. ${needs} To run builds as processes under this user instead, where a build can read this user's files and the runner's Claude credential, run this again with --allow-process-builds.`;
|
|
25
|
+
}
|
|
26
|
+
function isPlaceholderCommand(command) {
|
|
27
|
+
return command === UNMARKED_PLACEHOLDER_COMMAND || command.includes(PLACEHOLDER_MARKER);
|
|
28
|
+
}
|
|
29
|
+
function readConfigObject(configPath) {
|
|
30
|
+
if (!existsSync(configPath)) {
|
|
31
|
+
throw new RunnerInitError(
|
|
32
|
+
`${configPath}: no config file. Run \`outerlayer login\` first, then this command.`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(readFileSync(configPath, "utf8"));
|
|
37
|
+
} catch (err) {
|
|
38
|
+
throw new RunnerInitError(`${configPath}: not valid JSON (${err instanceof Error ? err.message : String(err)})`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function parseStdinConfig(configPath, text) {
|
|
42
|
+
if (existsSync(configPath)) {
|
|
43
|
+
throw new RunnerInitError(
|
|
44
|
+
`${configPath}: a config file is already here, so the runner key in it is not replaced. Remove it, or delete the VM and start again.`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
let config;
|
|
48
|
+
try {
|
|
49
|
+
config = JSON.parse(text);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
throw new RunnerInitError(`the config on stdin is not valid JSON (${err instanceof Error ? err.message : String(err)})`);
|
|
52
|
+
}
|
|
53
|
+
if (typeof config.url !== "string" || typeof config.apiKey !== "string" || typeof config.appId !== "string") {
|
|
54
|
+
throw new RunnerInitError("the config on stdin needs url, apiKey and appId.");
|
|
55
|
+
}
|
|
56
|
+
const carried = config.runner;
|
|
57
|
+
delete config.runner;
|
|
58
|
+
return { config, carried };
|
|
59
|
+
}
|
|
60
|
+
function runRunnerInit(configPath, opts = {}) {
|
|
61
|
+
let config;
|
|
62
|
+
let carried;
|
|
63
|
+
if (opts.configFromStdin === void 0) {
|
|
64
|
+
config = readConfigObject(configPath);
|
|
65
|
+
if (config.runner !== void 0) {
|
|
66
|
+
throw new RunnerInitError(`${configPath}: a runner block is already here. Edit it, or remove it and run this again.`);
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
({ config, carried } = parseStdinConfig(configPath, opts.configFromStdin));
|
|
70
|
+
}
|
|
71
|
+
const machine = opts.machine ?? detectMachine();
|
|
72
|
+
if (opts.requireContainer === true && !supportsContainerBuilds(machine)) {
|
|
73
|
+
const fix = reachDockerStep(machine) ?? `install Docker Engine ${MIN_ENGINE_MAJOR} or later in the VM`;
|
|
74
|
+
throw new RunnerInitError(`the VM has ${describeMachineDocker(machine)}, but the runner in it needs container builds: ${fix}, then run this again.`);
|
|
75
|
+
}
|
|
76
|
+
const hooks = opts.requireContainer !== true && supportsMicroVmBuilds(machine) ? "builtin:vm" : supportsContainerBuilds(machine) ? "builtin:container" : "builtin:process";
|
|
77
|
+
if (hooks === "builtin:process" && opts.allowProcess !== true) throw new RunnerInitError(processRefusal(machine));
|
|
78
|
+
if (opts.configFromStdin !== void 0) mkdirSync(dirname(configPath), { recursive: true, mode: RUNNER_DIR_MODE });
|
|
79
|
+
mkdirSync(runnerDir(configPath), { recursive: true, mode: RUNNER_DIR_MODE });
|
|
80
|
+
config.runner = {
|
|
81
|
+
queues: ["implement", "amend"],
|
|
82
|
+
concurrency: 1,
|
|
83
|
+
pollSeconds: 60,
|
|
84
|
+
timeLimitMinutes: 480,
|
|
85
|
+
idleLimitMinutes: 30,
|
|
86
|
+
// A placeholder that fails loudly. A command line that quietly did
|
|
87
|
+
// nothing would claim an item, run, exit zero, and record `ok` —
|
|
88
|
+
// every attempt reading as a success that produced nothing.
|
|
89
|
+
commands: {
|
|
90
|
+
implement: PLACEHOLDER_COMMAND,
|
|
91
|
+
amend: PLACEHOLDER_COMMAND
|
|
92
|
+
},
|
|
93
|
+
...carried,
|
|
94
|
+
hooks: { provision: hooks, cleanup: hooks }
|
|
95
|
+
};
|
|
96
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
97
|
+
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
98
|
+
`, { mode: RUNNER_FILE_MODE });
|
|
99
|
+
return { configPath, hooks, ...hooks === "builtin:process" ? { note: isolationNote(machine) } : {} };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export { PLACEHOLDER_MARKER, RunnerInitError, isPlaceholderCommand, readConfigObject, runRunnerInit };
|