@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,21 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
-
import {
|
|
4
|
-
import './chunk-
|
|
5
|
-
import
|
|
6
|
-
import
|
|
3
|
+
import { gatherDoctorChecks, doctorExitCode, summarizeChecks, renderDoctorText, recordWrapped, recordUnwrapped, recordRemoval, readManifest, recordInstall, recordStatusline } from './chunk-TWNWNS2Q.js';
|
|
4
|
+
import './chunk-SY4GK4SP.js';
|
|
5
|
+
import './chunk-F47JBIAW.js';
|
|
6
|
+
import './chunk-BKO6JEZI.js';
|
|
7
|
+
import './chunk-W5FVUZXV.js';
|
|
8
|
+
import './chunk-BFESLKPP.js';
|
|
9
|
+
import './chunk-WFJ65NUD.js';
|
|
10
|
+
import './chunk-4C4THABW.js';
|
|
11
|
+
import './chunk-USO2DKBD.js';
|
|
12
|
+
import './chunk-N6LURUEF.js';
|
|
7
13
|
import { C, NO_COLOR_FLAG, warnDeprecated, stripNoColor, API_KEY_FLAG_DEPRECATION } from './chunk-YRYMNBGQ.js';
|
|
8
14
|
import { parseTrackerNumber, trackerNumberError } from './chunk-WZBHBH3J.js';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
import
|
|
15
|
+
import { settingsPath, readSettings, SettingsParseError, listHookCandidates, wrapHooks, writeSettings, unwrapHooks, REGISTERED_EVENTS, cliCommand, removeHooks, removeStatusline, cliBinResolvable, mergeHooks, repointWrappedHooks, AUTO_WRAP_EVENTS, mergeStatusline } from './chunk-5QRP3MVS.js';
|
|
16
|
+
import { pinExists } from './chunk-WIOZAJ2W.js';
|
|
17
|
+
import './chunk-EMY4I27X.js';
|
|
18
|
+
import './chunk-BCLJSQCV.js';
|
|
19
|
+
import './chunk-BOLTI6LR.js';
|
|
20
|
+
import './chunk-EABW6AJQ.js';
|
|
21
|
+
import { resolveVersion } from './chunk-RA3O54FC.js';
|
|
22
|
+
import './chunk-M5POMKKW.js';
|
|
23
|
+
import './chunk-ZM2IMMYN.js';
|
|
24
|
+
import { hookFiredMarkerPath, installedCliDistDir } from './chunk-UFFXNXLQ.js';
|
|
25
|
+
import { managedHookScripts, removeJournalHooks, installPreCommitGuardHook, removePreCommitGuardHook } from './chunk-XCCLFVXM.js';
|
|
26
|
+
import './chunk-BJ3KTMDH.js';
|
|
13
27
|
import { homeFromEnv } from './chunk-VNVZDWO3.js';
|
|
14
28
|
import './chunk-Z5GEFMRW.js';
|
|
15
29
|
import { EventEmitter } from 'events';
|
|
16
30
|
import childProcess, { execFileSync } from 'child_process';
|
|
17
|
-
import path, { dirname, join
|
|
18
|
-
import fs, { existsSync, unlinkSync,
|
|
31
|
+
import path, { isAbsolute, dirname, join } from 'path';
|
|
32
|
+
import fs, { existsSync, unlinkSync, mkdirSync, cpSync, readdirSync, renameSync, statSync, rmSync, readFileSync, writeFileSync, realpathSync } from 'fs';
|
|
19
33
|
import process2 from 'process';
|
|
20
34
|
import { stripVTControlCharacters } from 'util';
|
|
21
35
|
import { fileURLToPath } from 'url';
|
|
@@ -3394,6 +3408,19 @@ function samePath(a, b) {
|
|
|
3394
3408
|
return false;
|
|
3395
3409
|
}
|
|
3396
3410
|
}
|
|
3411
|
+
function sameContent(a, b) {
|
|
3412
|
+
try {
|
|
3413
|
+
const sa = statSync(a);
|
|
3414
|
+
const sb = statSync(b);
|
|
3415
|
+
if (sa.isDirectory() !== sb.isDirectory()) return false;
|
|
3416
|
+
if (!sa.isDirectory()) return sa.size === sb.size && readFileSync(a).equals(readFileSync(b));
|
|
3417
|
+
const na = readdirSync(a).sort();
|
|
3418
|
+
const nb = readdirSync(b).sort();
|
|
3419
|
+
return na.length === nb.length && na.every((n, i) => n === nb[i] && sameContent(join(a, n), join(b, n)));
|
|
3420
|
+
} catch {
|
|
3421
|
+
return false;
|
|
3422
|
+
}
|
|
3423
|
+
}
|
|
3397
3424
|
function installCliCopy(opts) {
|
|
3398
3425
|
const rename = opts.rename ?? renameSync;
|
|
3399
3426
|
const now = opts.now ?? Date.now;
|
|
@@ -3408,12 +3435,28 @@ function installCliCopy(opts) {
|
|
|
3408
3435
|
cpSync(sourceDist, join(staged, "dist"), { recursive: true });
|
|
3409
3436
|
const sourcePkg = join(dirname(sourceDist), "package.json");
|
|
3410
3437
|
if (existsSync(sourcePkg)) cpSync(sourcePkg, join(staged, "package.json"));
|
|
3438
|
+
const sourcePack = join(dirname(sourceDist), "skill-pack");
|
|
3439
|
+
if (existsSync(sourcePack)) cpSync(sourcePack, join(staged, "skill-pack"), { recursive: true });
|
|
3411
3440
|
const incoming = readdirSync(join(staged, "dist"));
|
|
3412
3441
|
for (const name of incoming) {
|
|
3413
|
-
if (name !== "index.js"
|
|
3442
|
+
if (name !== "index.js" && !sameContent(join(staged, "dist", name), join(targetDist, name))) {
|
|
3443
|
+
rename(join(staged, "dist", name), join(targetDist, name));
|
|
3444
|
+
}
|
|
3414
3445
|
}
|
|
3415
|
-
if (existsSync(join(staged, "package.json"))) rename(join(staged, "package.json"), join(targetRoot, "package.json"));
|
|
3416
|
-
|
|
3446
|
+
if (existsSync(join(staged, "package.json")) && !sameContent(join(staged, "package.json"), join(targetRoot, "package.json"))) rename(join(staged, "package.json"), join(targetRoot, "package.json"));
|
|
3447
|
+
if (existsSync(join(staged, "skill-pack")) && !sameContent(join(staged, "skill-pack"), join(targetRoot, "skill-pack"))) {
|
|
3448
|
+
const livePack = join(targetRoot, "skill-pack");
|
|
3449
|
+
const asidePack = join(staged, "previous-skill-pack");
|
|
3450
|
+
const hadPack = existsSync(livePack);
|
|
3451
|
+
if (hadPack) rename(livePack, asidePack);
|
|
3452
|
+
try {
|
|
3453
|
+
rename(join(staged, "skill-pack"), livePack);
|
|
3454
|
+
} catch (err) {
|
|
3455
|
+
if (hadPack) rename(asidePack, livePack);
|
|
3456
|
+
throw err;
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
if (!sameContent(join(staged, "dist", "index.js"), bin)) rename(join(staged, "dist", "index.js"), bin);
|
|
3417
3460
|
const shipped = new Set(incoming);
|
|
3418
3461
|
for (const name of readdirSync(targetDist)) {
|
|
3419
3462
|
const path2 = join(targetDist, name);
|
|
@@ -3424,96 +3467,6 @@ function installCliCopy(opts) {
|
|
|
3424
3467
|
}
|
|
3425
3468
|
return { bin, copied: true };
|
|
3426
3469
|
}
|
|
3427
|
-
function manifestPath(home = homeFromEnv()) {
|
|
3428
|
-
return join(home, ".outerlayer", "hooks-manifest.json");
|
|
3429
|
-
}
|
|
3430
|
-
function readManifestFile(home) {
|
|
3431
|
-
try {
|
|
3432
|
-
const parsed = JSON.parse(readFileSync(manifestPath(home), "utf8"));
|
|
3433
|
-
if (parsed?.version !== 1) {
|
|
3434
|
-
return { manifest: null, newerVersion: typeof parsed?.version === "number" && parsed.version > 1 };
|
|
3435
|
-
}
|
|
3436
|
-
return {
|
|
3437
|
-
manifest: {
|
|
3438
|
-
version: 1,
|
|
3439
|
-
installs: parsed.installs ?? {},
|
|
3440
|
-
wrapped: parsed.wrapped ?? {},
|
|
3441
|
-
statusline: parsed.statusline ?? {}
|
|
3442
|
-
},
|
|
3443
|
-
newerVersion: false
|
|
3444
|
-
};
|
|
3445
|
-
} catch {
|
|
3446
|
-
return { manifest: null, newerVersion: false };
|
|
3447
|
-
}
|
|
3448
|
-
}
|
|
3449
|
-
function readManifest(home = homeFromEnv()) {
|
|
3450
|
-
return readManifestFile(home).manifest;
|
|
3451
|
-
}
|
|
3452
|
-
function writeManifest(home, manifest) {
|
|
3453
|
-
const path2 = manifestPath(home);
|
|
3454
|
-
try {
|
|
3455
|
-
mkdirSync(dirname(path2), { recursive: true });
|
|
3456
|
-
const tmp = `${path2}.tmp`;
|
|
3457
|
-
writeFileSync(tmp, JSON.stringify(manifest, null, 2) + "\n", { mode: 384 });
|
|
3458
|
-
renameSync(tmp, path2);
|
|
3459
|
-
} catch {
|
|
3460
|
-
}
|
|
3461
|
-
}
|
|
3462
|
-
function emptyManifest() {
|
|
3463
|
-
return { version: 1, installs: {}, wrapped: {}, statusline: {} };
|
|
3464
|
-
}
|
|
3465
|
-
function updateManifest(home, opts, mutate) {
|
|
3466
|
-
const { manifest, newerVersion } = readManifestFile(home);
|
|
3467
|
-
if (newerVersion) return;
|
|
3468
|
-
if (!manifest && !opts.createIfMissing) return;
|
|
3469
|
-
const next = manifest ?? emptyManifest();
|
|
3470
|
-
mutate(next);
|
|
3471
|
-
writeManifest(home, next);
|
|
3472
|
-
}
|
|
3473
|
-
function recordInstall(home, settingsPath2, record, now = Date.now) {
|
|
3474
|
-
updateManifest(home, { createIfMissing: true }, (manifest) => {
|
|
3475
|
-
manifest.installs[settingsPath2] = {
|
|
3476
|
-
cliBin: record.cliBin,
|
|
3477
|
-
events: [...record.events],
|
|
3478
|
-
updatedAt: new Date(now()).toISOString()
|
|
3479
|
-
};
|
|
3480
|
-
});
|
|
3481
|
-
}
|
|
3482
|
-
function recordRemoval(home, settingsPath2) {
|
|
3483
|
-
updateManifest(home, { createIfMissing: false }, (manifest) => {
|
|
3484
|
-
delete manifest.installs[settingsPath2];
|
|
3485
|
-
delete manifest.wrapped[settingsPath2];
|
|
3486
|
-
delete manifest.statusline[settingsPath2];
|
|
3487
|
-
});
|
|
3488
|
-
}
|
|
3489
|
-
function recordWrapped(home, settingsPath2, wrapped, now = Date.now) {
|
|
3490
|
-
if (wrapped.length === 0) return;
|
|
3491
|
-
updateManifest(home, { createIfMissing: true }, (manifest) => {
|
|
3492
|
-
const existing = manifest.wrapped[settingsPath2] ?? [];
|
|
3493
|
-
const at = new Date(now()).toISOString();
|
|
3494
|
-
for (const w of wrapped) {
|
|
3495
|
-
if (existing.some((e) => e.event === w.event && e.original === w.original)) continue;
|
|
3496
|
-
existing.push({ event: w.event, original: w.original, updatedAt: at });
|
|
3497
|
-
}
|
|
3498
|
-
manifest.wrapped[settingsPath2] = existing;
|
|
3499
|
-
});
|
|
3500
|
-
}
|
|
3501
|
-
function recordUnwrapped(home, settingsPath2) {
|
|
3502
|
-
updateManifest(home, { createIfMissing: false }, (manifest) => {
|
|
3503
|
-
delete manifest.wrapped[settingsPath2];
|
|
3504
|
-
});
|
|
3505
|
-
}
|
|
3506
|
-
function recordStatusline(home, settingsPath2, outcome, original, now = Date.now) {
|
|
3507
|
-
updateManifest(home, { createIfMissing: true }, (manifest) => {
|
|
3508
|
-
manifest.statusline[settingsPath2] = {
|
|
3509
|
-
outcome,
|
|
3510
|
-
...original !== void 0 ? { original } : {},
|
|
3511
|
-
updatedAt: new Date(now()).toISOString()
|
|
3512
|
-
};
|
|
3513
|
-
});
|
|
3514
|
-
}
|
|
3515
|
-
|
|
3516
|
-
// src/init.ts
|
|
3517
3470
|
function runInit(opts) {
|
|
3518
3471
|
const scope = opts.scope ?? "user";
|
|
3519
3472
|
const path2 = settingsPath(scope, { home: opts.home, cwd: opts.cwd });
|
|
@@ -3633,7 +3586,7 @@ function ensureGitignore(cwd) {
|
|
|
3633
3586
|
function orgRolloutSnippet(cliBin) {
|
|
3634
3587
|
const hooks = {};
|
|
3635
3588
|
for (const event of REGISTERED_EVENTS) {
|
|
3636
|
-
hooks[event] = [{ hooks: [{ type: "command", command: `${cliBin} hook ${event}` }] }];
|
|
3589
|
+
hooks[event] = [{ hooks: [{ type: "command", command: `${cliCommand(cliBin)} hook ${event}` }] }];
|
|
3637
3590
|
}
|
|
3638
3591
|
const managed = JSON.stringify({ hooks }, null, 2);
|
|
3639
3592
|
return [
|
|
@@ -3653,577 +3606,6 @@ function orgRolloutSnippet(cliBin) {
|
|
|
3653
3606
|
"Each developer still runs `outerlayer login` once to attribute sessions."
|
|
3654
3607
|
].join("\n");
|
|
3655
3608
|
}
|
|
3656
|
-
function runDoctor(env = {}) {
|
|
3657
|
-
const home = env.home ?? homeFromEnv();
|
|
3658
|
-
const now = env.now ?? Date.now;
|
|
3659
|
-
const transcripts = scanTranscripts(home);
|
|
3660
|
-
return [
|
|
3661
|
-
checkClaudeDir(home),
|
|
3662
|
-
checkTranscripts(transcripts, now),
|
|
3663
|
-
...checkHooks(home, env.cwd, transcripts, now),
|
|
3664
|
-
...checkDeadHooksState(env.cwd),
|
|
3665
|
-
checkSpoolWritable(home),
|
|
3666
|
-
checkLastSync(home, now),
|
|
3667
|
-
checkDaemon(home),
|
|
3668
|
-
checkCleanupPeriod(home),
|
|
3669
|
-
checkDiskHeadroom(home),
|
|
3670
|
-
checkClaudeVersion(env),
|
|
3671
|
-
checkSettingsValid(home, env.cwd),
|
|
3672
|
-
...checkStatusline(home, now, env.cwd),
|
|
3673
|
-
...checkInstalls(env),
|
|
3674
|
-
...checkContextSource(home, env.cwd),
|
|
3675
|
-
...checkInstalledCli(home, env.cwd ?? process.cwd())
|
|
3676
|
-
];
|
|
3677
|
-
}
|
|
3678
|
-
function checkInstalledCli(home, cwd) {
|
|
3679
|
-
const name = "Installed CLI";
|
|
3680
|
-
const result = checkInstalledCliStaleness({ home, cwd });
|
|
3681
|
-
if (result === null) return [];
|
|
3682
|
-
const installedLabel = result.installedId !== null ? sanitizeForContext(result.installedId) : "unknown";
|
|
3683
|
-
const workspaceLabel = result.workspaceId !== null ? sanitizeForContext(result.workspaceId) : null;
|
|
3684
|
-
const at = result.workspaceDistDir !== null ? ` at ${sanitizeForContext(workspaceRootOf(result.workspaceDistDir))}` : "";
|
|
3685
|
-
if (result.stale) {
|
|
3686
|
-
return [
|
|
3687
|
-
{
|
|
3688
|
-
name,
|
|
3689
|
-
status: "warn",
|
|
3690
|
-
detail: `installed copy (${installedLabel}) is behind the workspace build${at} (${workspaceLabel})`,
|
|
3691
|
-
fix: installedCliFix(result.workspaceDistDir, result.workspaceRootTrusted)
|
|
3692
|
-
}
|
|
3693
|
-
];
|
|
3694
|
-
}
|
|
3695
|
-
if (workspaceLabel === null) {
|
|
3696
|
-
const detail = result.ambiguousWorkspace ? `installed (${installedLabel}) \u2014 more than one candidate workspace build found nearby; refusing to guess which one to compare against` : `installed (${installedLabel}) \u2014 no workspace build to compare (comparison needs a nearby checkout with a stamped build)`;
|
|
3697
|
-
return [{ name, status: "pass", detail }];
|
|
3698
|
-
}
|
|
3699
|
-
if (result.unorderable) {
|
|
3700
|
-
return [
|
|
3701
|
-
{
|
|
3702
|
-
name,
|
|
3703
|
-
status: "pass",
|
|
3704
|
-
detail: `installed (${installedLabel}) differs from the workspace build${at} (${workspaceLabel}) \u2014 build order could not be established`
|
|
3705
|
-
}
|
|
3706
|
-
];
|
|
3707
|
-
}
|
|
3708
|
-
if (result.installedAhead) {
|
|
3709
|
-
return [{ name, status: "pass", detail: `installed (${installedLabel}) is ahead of the workspace build${at} (${workspaceLabel})` }];
|
|
3710
|
-
}
|
|
3711
|
-
if (result.installedId !== result.workspaceId) {
|
|
3712
|
-
return [{ name, status: "pass", detail: `installed (${installedLabel}) differs from the workspace build${at} (${workspaceLabel})` }];
|
|
3713
|
-
}
|
|
3714
|
-
return [{ name, status: "pass", detail: `matches the workspace build${at} (${installedLabel})` }];
|
|
3715
|
-
}
|
|
3716
|
-
function installedCliFix(workspaceDistDir, trusted) {
|
|
3717
|
-
const command = refreshCommand(workspaceDistDir, trusted);
|
|
3718
|
-
if (command) return `Refresh it: ${command}`;
|
|
3719
|
-
const root = sanitizeForContext(workspaceRootOf(workspaceDistDir));
|
|
3720
|
-
return `Workspace build found at ${root}. Rebuild it (yarn workspace @outerlayer/cli build) and copy packages/cli/dist to ~/.outerlayer/cli/dist to refresh the installed copy.`;
|
|
3721
|
-
}
|
|
3722
|
-
function checkStatusline(home, now, cwd) {
|
|
3723
|
-
const name = "Status line";
|
|
3724
|
-
let installed = false;
|
|
3725
|
-
let wrapping;
|
|
3726
|
-
let foreign;
|
|
3727
|
-
for (const scope of ["user", "project"]) {
|
|
3728
|
-
let settings;
|
|
3729
|
-
try {
|
|
3730
|
-
settings = readSettings(settingsPath(scope, { home, cwd }));
|
|
3731
|
-
} catch {
|
|
3732
|
-
continue;
|
|
3733
|
-
}
|
|
3734
|
-
const slot = settings?.statusLine;
|
|
3735
|
-
if (!slot || typeof slot !== "object") continue;
|
|
3736
|
-
if (isOurStatuslineSlot(slot)) {
|
|
3737
|
-
installed = true;
|
|
3738
|
-
const original = statuslineWrappedOriginal(slot);
|
|
3739
|
-
if (original !== null) wrapping = original;
|
|
3740
|
-
break;
|
|
3741
|
-
}
|
|
3742
|
-
if (typeof slot.command === "string") foreign = slot.command;
|
|
3743
|
-
}
|
|
3744
|
-
if (!installed) {
|
|
3745
|
-
const manifest = readManifest(home);
|
|
3746
|
-
const record = ["user", "project"].map((scope) => manifest?.statusline[settingsPath(scope, { home, cwd })]).find((r) => r !== void 0);
|
|
3747
|
-
if (record) {
|
|
3748
|
-
return [{
|
|
3749
|
-
name,
|
|
3750
|
-
status: "warn",
|
|
3751
|
-
detail: `the OuterLayer segment was installed (recorded ${record.updatedAt}) but the slot no longer holds it \u2014 settings were rewritten or reset${record.original !== void 0 ? `; it wrapped: ${record.original}` : ""}`,
|
|
3752
|
-
fix: "Run `outerlayer init` to reinstall the segment."
|
|
3753
|
-
}];
|
|
3754
|
-
}
|
|
3755
|
-
return [
|
|
3756
|
-
foreign !== void 0 ? { name, status: "warn", detail: `slot occupied by: ${foreign}`, fix: "Run `outerlayer init` to add the OuterLayer segment (wraps the existing command, never replaces it)." } : { name, status: "warn", detail: "not installed", fix: "Run `outerlayer init`." }
|
|
3757
|
-
];
|
|
3758
|
-
}
|
|
3759
|
-
const checks = [
|
|
3760
|
-
{ name, status: "pass", detail: wrapping ? `installed, wrapping: ${wrapping}` : "installed" }
|
|
3761
|
-
];
|
|
3762
|
-
const stateName = "Status-line state";
|
|
3763
|
-
const state = readStatuslineState(home);
|
|
3764
|
-
if (!state) {
|
|
3765
|
-
checks.push({ name: stateName, status: "warn", detail: "no state file \u2014 line runs degraded (session cost only)", fix: "Start `outerlayer daemon` (it maintains this file)." });
|
|
3766
|
-
return checks;
|
|
3767
|
-
}
|
|
3768
|
-
const age = now() - Date.parse(state.generatedAt);
|
|
3769
|
-
if (!Number.isFinite(age) || age > STATE_FRESH_MS) {
|
|
3770
|
-
checks.push({ name: stateName, status: "warn", detail: `state ${formatAgo(age)} \u2014 line runs degraded`, fix: "Restart `outerlayer daemon`." });
|
|
3771
|
-
return checks;
|
|
3772
|
-
}
|
|
3773
|
-
checks.push({ name: stateName, status: "pass", detail: `fresh (${formatAgo(age)})` });
|
|
3774
|
-
return checks;
|
|
3775
|
-
}
|
|
3776
|
-
function checkClaudeDir(home) {
|
|
3777
|
-
const dir = join(home, ".claude");
|
|
3778
|
-
return existsSync(dir) ? { name: "Claude Code home", status: "pass", detail: dir } : { name: "Claude Code home", status: "fail", detail: `${dir} not found`, fix: "Install Claude Code, or pass --root to point at your transcripts." };
|
|
3779
|
-
}
|
|
3780
|
-
function scanTranscripts(home) {
|
|
3781
|
-
const root = join(home, ".claude", "projects");
|
|
3782
|
-
let count = 0;
|
|
3783
|
-
let newest = 0;
|
|
3784
|
-
if (!existsSync(root)) return { exists: false, count, newest };
|
|
3785
|
-
const walk = (dir) => {
|
|
3786
|
-
let entries;
|
|
3787
|
-
try {
|
|
3788
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
3789
|
-
} catch {
|
|
3790
|
-
return;
|
|
3791
|
-
}
|
|
3792
|
-
for (const entry of entries) {
|
|
3793
|
-
const p = join(dir, entry.name);
|
|
3794
|
-
if (!entry.isDirectory() && !entry.isSymbolicLink() && !entry.name.endsWith(".jsonl")) continue;
|
|
3795
|
-
let st;
|
|
3796
|
-
try {
|
|
3797
|
-
st = statSync(p);
|
|
3798
|
-
} catch {
|
|
3799
|
-
continue;
|
|
3800
|
-
}
|
|
3801
|
-
if (st.isDirectory()) walk(p);
|
|
3802
|
-
else if (entry.name.endsWith(".jsonl") && st.size > 0) {
|
|
3803
|
-
count += 1;
|
|
3804
|
-
if (st.mtimeMs > newest) newest = st.mtimeMs;
|
|
3805
|
-
}
|
|
3806
|
-
}
|
|
3807
|
-
};
|
|
3808
|
-
walk(root);
|
|
3809
|
-
return { exists: true, count, newest };
|
|
3810
|
-
}
|
|
3811
|
-
function checkTranscripts(transcripts, now) {
|
|
3812
|
-
if (!transcripts.exists) {
|
|
3813
|
-
return { name: "Transcripts", status: "warn", detail: "no projects directory yet", fix: "Run a Claude Code session, then re-check." };
|
|
3814
|
-
}
|
|
3815
|
-
if (transcripts.count === 0) {
|
|
3816
|
-
return { name: "Transcripts", status: "warn", detail: "0 transcripts found", fix: "Run a Claude Code session, then run `outerlayer doctor` again." };
|
|
3817
|
-
}
|
|
3818
|
-
const age = formatAgeH(now() - transcripts.newest);
|
|
3819
|
-
return { name: "Transcripts", status: "pass", detail: `${transcripts.count} transcripts, newest ${age}` };
|
|
3820
|
-
}
|
|
3821
|
-
function formatAgeH(ms) {
|
|
3822
|
-
const ageH = ms / 36e5;
|
|
3823
|
-
return ageH < 1 ? "<1h ago" : ageH < 48 ? `${Math.round(ageH)}h ago` : `${Math.round(ageH / 24)}d ago`;
|
|
3824
|
-
}
|
|
3825
|
-
var HOOK_FIRING_GAP_MS = 24 * 60 * 60 * 1e3;
|
|
3826
|
-
function newestHookFiring(home) {
|
|
3827
|
-
let newest = null;
|
|
3828
|
-
for (const event of REGISTERED_EVENTS) {
|
|
3829
|
-
try {
|
|
3830
|
-
const ms = statSync(hookFiredMarkerPath(event, home)).mtimeMs;
|
|
3831
|
-
if (newest === null || ms > newest) newest = ms;
|
|
3832
|
-
} catch {
|
|
3833
|
-
}
|
|
3834
|
-
}
|
|
3835
|
-
return newest;
|
|
3836
|
-
}
|
|
3837
|
-
function checkHooks(home, cwd, transcripts, now) {
|
|
3838
|
-
const lastFired = newestHookFiring(home);
|
|
3839
|
-
const firingFresh = lastFired !== null && transcripts.count > 0 && transcripts.newest - lastFired <= HOOK_FIRING_GAP_MS;
|
|
3840
|
-
const checks = [checkHooksInstalled(home, cwd, firingFresh)];
|
|
3841
|
-
const firing = checkHooksFiring(transcripts, lastFired, now);
|
|
3842
|
-
if (firing) checks.push(firing);
|
|
3843
|
-
const wrapped = checkWrappedHooks(home, cwd);
|
|
3844
|
-
if (wrapped) checks.push(wrapped);
|
|
3845
|
-
return checks;
|
|
3846
|
-
}
|
|
3847
|
-
function checkHooksInstalled(home, cwd, firingFresh) {
|
|
3848
|
-
const name = "Hooks installed";
|
|
3849
|
-
const readablePaths = [];
|
|
3850
|
-
for (const scope of ["user", "project"]) {
|
|
3851
|
-
const path2 = settingsPath(scope, { home, cwd });
|
|
3852
|
-
let settings;
|
|
3853
|
-
try {
|
|
3854
|
-
settings = readSettings(path2);
|
|
3855
|
-
} catch {
|
|
3856
|
-
continue;
|
|
3857
|
-
}
|
|
3858
|
-
readablePaths.push(path2);
|
|
3859
|
-
if (!settings?.hooks) continue;
|
|
3860
|
-
const installed = REGISTERED_EVENTS.filter(
|
|
3861
|
-
(event) => (settings.hooks?.[event] ?? []).some((b) => (b.hooks ?? []).some(isOurHookCommand))
|
|
3862
|
-
);
|
|
3863
|
-
if (installed.length === REGISTERED_EVENTS.length) {
|
|
3864
|
-
const missing = ourCommandBins(settings).filter((bin) => !cliBinResolvable(bin));
|
|
3865
|
-
if (missing.length > 0) {
|
|
3866
|
-
return {
|
|
3867
|
-
name,
|
|
3868
|
-
status: "fail",
|
|
3869
|
-
detail: `hooks in ${path2} run ${missing.join(", ")}, which no longer exists \u2014 no session is being captured`,
|
|
3870
|
-
fix: "Run `npx @outerlayer/cli init` to reinstall."
|
|
3871
|
-
};
|
|
3872
|
-
}
|
|
3873
|
-
return { name, status: "pass", detail: `${installed.join(", ")} (${scope})` };
|
|
3874
|
-
}
|
|
3875
|
-
if (installed.length > 0) {
|
|
3876
|
-
return { name, status: "warn", detail: `only ${installed.join(", ")} installed`, fix: "Run `outerlayer init` to (re)install all lifecycle hooks." };
|
|
3877
|
-
}
|
|
3878
|
-
}
|
|
3879
|
-
const manifest = readManifest(home);
|
|
3880
|
-
const recorded = readablePaths.find((p) => manifest?.installs[p]);
|
|
3881
|
-
if (recorded) {
|
|
3882
|
-
const record = manifest.installs[recorded];
|
|
3883
|
-
return {
|
|
3884
|
-
name,
|
|
3885
|
-
status: "fail",
|
|
3886
|
-
detail: `hooks were installed into ${recorded} (recorded ${record.updatedAt}) but the settings file no longer contains them \u2014 it was rewritten or reset`,
|
|
3887
|
-
fix: "Run `outerlayer init` to reinstall."
|
|
3888
|
-
};
|
|
3889
|
-
}
|
|
3890
|
-
if (firingFresh) {
|
|
3891
|
-
return { name, status: "pass", detail: "not in user/project settings, but hooks are firing \u2014 installed in another scope (managed settings?)" };
|
|
3892
|
-
}
|
|
3893
|
-
return { name, status: "fail", detail: "no OuterLayer hooks found", fix: "Run `outerlayer init`." };
|
|
3894
|
-
}
|
|
3895
|
-
function checkWrappedHooks(home, cwd) {
|
|
3896
|
-
const manifest = readManifest(home);
|
|
3897
|
-
if (!manifest) return null;
|
|
3898
|
-
const missing = [];
|
|
3899
|
-
for (const scope of ["user", "project"]) {
|
|
3900
|
-
const path2 = settingsPath(scope, { home, cwd });
|
|
3901
|
-
const records = manifest.wrapped[path2];
|
|
3902
|
-
if (!records || records.length === 0) continue;
|
|
3903
|
-
let settings;
|
|
3904
|
-
try {
|
|
3905
|
-
settings = readSettings(path2);
|
|
3906
|
-
} catch {
|
|
3907
|
-
continue;
|
|
3908
|
-
}
|
|
3909
|
-
const present = /* @__PURE__ */ new Set();
|
|
3910
|
-
const hooks = settings?.hooks ?? {};
|
|
3911
|
-
for (const event of Object.keys(hooks)) {
|
|
3912
|
-
for (const block of hooks[event] ?? []) {
|
|
3913
|
-
for (const h of block.hooks ?? []) {
|
|
3914
|
-
const original = wrappedOriginalCommand(h);
|
|
3915
|
-
if (original !== null) present.add(`${event}\0${original}`);
|
|
3916
|
-
}
|
|
3917
|
-
}
|
|
3918
|
-
}
|
|
3919
|
-
for (const r of records) {
|
|
3920
|
-
if (!present.has(`${r.event}\0${r.original}`)) missing.push(`${r.event}: ${r.original}`);
|
|
3921
|
-
}
|
|
3922
|
-
}
|
|
3923
|
-
if (missing.length === 0) return null;
|
|
3924
|
-
return {
|
|
3925
|
-
name: "Wrapped hooks",
|
|
3926
|
-
status: "warn",
|
|
3927
|
-
detail: `recorded as wrapped but the settings no longer carry the wrapped form \u2014 ${missing.join("; ")}`,
|
|
3928
|
-
fix: "Run `outerlayer hooks wrap` to re-wrap, or `outerlayer hooks unwrap` if the wrap is no longer wanted."
|
|
3929
|
-
};
|
|
3930
|
-
}
|
|
3931
|
-
function checkHooksFiring(transcripts, lastFired, now) {
|
|
3932
|
-
const name = "Hooks firing";
|
|
3933
|
-
if (transcripts.count === 0) return null;
|
|
3934
|
-
if (lastFired === null) {
|
|
3935
|
-
return {
|
|
3936
|
-
name,
|
|
3937
|
-
status: "warn",
|
|
3938
|
-
detail: "no firing evidence recorded yet \u2014 every hook run writes a marker, and none exists",
|
|
3939
|
-
fix: "Start a Claude Code session, then re-run doctor."
|
|
3940
|
-
};
|
|
3941
|
-
}
|
|
3942
|
-
if (transcripts.newest - lastFired > HOOK_FIRING_GAP_MS) {
|
|
3943
|
-
return {
|
|
3944
|
-
name,
|
|
3945
|
-
status: "fail",
|
|
3946
|
-
detail: `sessions as recent as ${formatAgeH(now() - transcripts.newest)}, but no lifecycle hook has fired since ${formatAgeH(now() - lastFired)}`,
|
|
3947
|
-
fix: "Hooks are configured but not running \u2014 run `outerlayer init` and check `claude --debug` hook output."
|
|
3948
|
-
};
|
|
3949
|
-
}
|
|
3950
|
-
return { name, status: "pass", detail: `last fired ${formatAgeH(now() - lastFired)}` };
|
|
3951
|
-
}
|
|
3952
|
-
function checkDeadHooksState(cwd) {
|
|
3953
|
-
const dir = cwd ?? process.cwd();
|
|
3954
|
-
const state = deadHooksState(dir);
|
|
3955
|
-
if (state.hooksDir === void 0) return [];
|
|
3956
|
-
const name = "Git hooks directory";
|
|
3957
|
-
if (state.unreadableError) {
|
|
3958
|
-
return [
|
|
3959
|
-
{
|
|
3960
|
-
name,
|
|
3961
|
-
status: "fail",
|
|
3962
|
-
detail: `${sanitizeForContext(state.hooksDir)} could not be read (${state.unreadableError.code ?? state.unreadableError.message}) \u2014 git's hooks status is unknown, not confirmed healthy`,
|
|
3963
|
-
fix: "Fix the directory's permissions so it can be listed, then re-run `outerlayer doctor`."
|
|
3964
|
-
}
|
|
3965
|
-
];
|
|
3966
|
-
}
|
|
3967
|
-
if (!state.dead) {
|
|
3968
|
-
return [{ name, status: "pass", detail: `hooks present at ${sanitizeForContext(state.hooksDir)}` }];
|
|
3969
|
-
}
|
|
3970
|
-
const fix = `Run \`${preparePackageManagerCommand(state.repoRoot ?? dir)}\` to (re)generate the hooks directory.`;
|
|
3971
|
-
return [
|
|
3972
|
-
{
|
|
3973
|
-
name,
|
|
3974
|
-
status: "fail",
|
|
3975
|
-
detail: `${sanitizeForContext(state.hooksDir)} is missing or empty \u2014 core.hooksPath points at a dead directory, so git runs no hooks at all`,
|
|
3976
|
-
fix
|
|
3977
|
-
}
|
|
3978
|
-
];
|
|
3979
|
-
}
|
|
3980
|
-
function checkSpoolWritable(home) {
|
|
3981
|
-
const dir = spoolDir(home);
|
|
3982
|
-
try {
|
|
3983
|
-
if (!existsSync(dir)) return { name: "Spool writable", status: "warn", detail: "spool not created yet", fix: "Run `outerlayer init` (creates the spool on first hook)." };
|
|
3984
|
-
accessSync(dir, constants.W_OK);
|
|
3985
|
-
return { name: "Spool writable", status: "pass", detail: dir };
|
|
3986
|
-
} catch {
|
|
3987
|
-
return { name: "Spool writable", status: "fail", detail: `${dir} not writable`, fix: "Fix permissions on ~/.outerlayer/spool." };
|
|
3988
|
-
}
|
|
3989
|
-
}
|
|
3990
|
-
function checkLastSync(home, now) {
|
|
3991
|
-
const name = "Cloud sync";
|
|
3992
|
-
if (!existsSync(join(home, ".outerlayer", "config.json"))) {
|
|
3993
|
-
return { name, status: "warn", detail: "not configured \u2014 sessions stay on this machine", fix: "Run `outerlayer sync --url <url> --api-key <key> --app-id <id>` once to connect." };
|
|
3994
|
-
}
|
|
3995
|
-
const health = assessSyncHealth(readSyncStatus(home), now(), SYNC_STALE_AFTER_MS);
|
|
3996
|
-
const detail = [health.headline, ...health.details].join("; ");
|
|
3997
|
-
if (health.level === "error") {
|
|
3998
|
-
return { name, status: "fail", detail, fix: "Check the destination and API key in ~/.outerlayer/config.json, then run `outerlayer sync` to see the full error." };
|
|
3999
|
-
}
|
|
4000
|
-
if (health.level === "warn" || health.level === "unknown") {
|
|
4001
|
-
return { name, status: "warn", detail, fix: "Run `outerlayer sync` to confirm the connection works." };
|
|
4002
|
-
}
|
|
4003
|
-
return { name, status: "pass", detail };
|
|
4004
|
-
}
|
|
4005
|
-
function checkDaemon(home) {
|
|
4006
|
-
const pidfile = join(home, ".outerlayer", "daemon.pid");
|
|
4007
|
-
const heartbeat = join(home, ".outerlayer", "daemon.heartbeat");
|
|
4008
|
-
if (!existsSync(pidfile)) {
|
|
4009
|
-
return { name: "Daemon running", status: "warn", detail: "not running", fix: "Start it with `outerlayer daemon` (or a login agent)." };
|
|
4010
|
-
}
|
|
4011
|
-
try {
|
|
4012
|
-
const beatMs = statSync(heartbeat).mtimeMs;
|
|
4013
|
-
const staleMin = (Date.now() - beatMs) / 6e4;
|
|
4014
|
-
if (staleMin > 30) {
|
|
4015
|
-
return { name: "Daemon running", status: "warn", detail: `heartbeat ${Math.round(staleMin)}m stale`, fix: "Restart `outerlayer daemon`." };
|
|
4016
|
-
}
|
|
4017
|
-
return { name: "Daemon running", status: "pass", detail: "heartbeat fresh" };
|
|
4018
|
-
} catch {
|
|
4019
|
-
return { name: "Daemon running", status: "warn", detail: "pidfile present, no heartbeat", fix: "Restart `outerlayer daemon`." };
|
|
4020
|
-
}
|
|
4021
|
-
}
|
|
4022
|
-
function checkCleanupPeriod(home) {
|
|
4023
|
-
const path2 = join(home, ".claude", "settings.json");
|
|
4024
|
-
let settings;
|
|
4025
|
-
try {
|
|
4026
|
-
settings = readSettings(path2);
|
|
4027
|
-
} catch {
|
|
4028
|
-
return { name: "Retention (cleanupPeriodDays)", status: "warn", detail: "settings unreadable" };
|
|
4029
|
-
}
|
|
4030
|
-
const raw = settings?.cleanupPeriodDays;
|
|
4031
|
-
const days = typeof raw === "number" ? raw : 30;
|
|
4032
|
-
if (days === 0) {
|
|
4033
|
-
return {
|
|
4034
|
-
name: "Retention (cleanupPeriodDays)",
|
|
4035
|
-
status: "fail",
|
|
4036
|
-
detail: "set to 0 \u2014 this DISABLES transcript persistence (upstream anthropics/claude-code#23710 and #59248)",
|
|
4037
|
-
fix: "Remove cleanupPeriodDays or set it \u226514; run `outerlayer daemon` to mirror before deletion."
|
|
4038
|
-
};
|
|
4039
|
-
}
|
|
4040
|
-
if (days < 14) {
|
|
4041
|
-
return { name: "Retention (cleanupPeriodDays)", status: "warn", detail: `${days}d \u2014 transcripts deleted quickly`, fix: "Run `outerlayer daemon` so raws are mirrored before cleanup." };
|
|
4042
|
-
}
|
|
4043
|
-
return { name: "Retention (cleanupPeriodDays)", status: "pass", detail: `${days}d` };
|
|
4044
|
-
}
|
|
4045
|
-
function checkDiskHeadroom(home) {
|
|
4046
|
-
try {
|
|
4047
|
-
accessSync(home, constants.W_OK);
|
|
4048
|
-
return { name: "Disk headroom", status: "pass", detail: "home writable (raw mirror capped at 5 GiB, LRU)" };
|
|
4049
|
-
} catch {
|
|
4050
|
-
return { name: "Disk headroom", status: "fail", detail: "home not writable", fix: "Free space / fix permissions on your home directory." };
|
|
4051
|
-
}
|
|
4052
|
-
}
|
|
4053
|
-
function checkClaudeVersion(env) {
|
|
4054
|
-
let version;
|
|
4055
|
-
try {
|
|
4056
|
-
version = (env.claudeVersion ?? defaultClaudeVersion)();
|
|
4057
|
-
} catch {
|
|
4058
|
-
return { name: "Claude Code version", status: "warn", detail: "could not run `claude --version`", fix: "Ensure `claude` is on PATH." };
|
|
4059
|
-
}
|
|
4060
|
-
const parsed = version.match(/\d+\.\d+\.\d+/)?.[0];
|
|
4061
|
-
if (!parsed) return { name: "Claude Code version", status: "warn", detail: `unrecognized: ${version}` };
|
|
4062
|
-
if (compareVersions(parsed, SUPPORTED_VERSIONS.max) > 0) {
|
|
4063
|
-
return { name: "Claude Code version", status: "warn", detail: `${parsed} is newer than validated (${SUPPORTED_VERSIONS.max}) \u2014 parser runs best-effort`, fix: "Update OuterLayer if you see parse warnings." };
|
|
4064
|
-
}
|
|
4065
|
-
if (compareVersions(parsed, SUPPORTED_VERSIONS.min) < 0) {
|
|
4066
|
-
return { name: "Claude Code version", status: "warn", detail: `${parsed} is older than validated (${SUPPORTED_VERSIONS.min})` };
|
|
4067
|
-
}
|
|
4068
|
-
return { name: "Claude Code version", status: "pass", detail: parsed };
|
|
4069
|
-
}
|
|
4070
|
-
function checkSettingsValid(home, cwd) {
|
|
4071
|
-
for (const scope of ["user", "project"]) {
|
|
4072
|
-
const path2 = settingsPath(scope, { home, cwd });
|
|
4073
|
-
if (!existsSync(path2)) continue;
|
|
4074
|
-
try {
|
|
4075
|
-
readSettings(path2);
|
|
4076
|
-
} catch (err) {
|
|
4077
|
-
return { name: "Settings JSON valid", status: "fail", detail: `${path2} is corrupt`, fix: err instanceof Error ? err.message : "Fix the JSON syntax." };
|
|
4078
|
-
}
|
|
4079
|
-
}
|
|
4080
|
-
return { name: "Settings JSON valid", status: "pass", detail: "all settings files parse" };
|
|
4081
|
-
}
|
|
4082
|
-
function checkInstalls(env) {
|
|
4083
|
-
let installs;
|
|
4084
|
-
try {
|
|
4085
|
-
installs = (env.claudeInstalls ?? defaultClaudeInstalls)();
|
|
4086
|
-
} catch {
|
|
4087
|
-
return [];
|
|
4088
|
-
}
|
|
4089
|
-
if (installs.length > 1) {
|
|
4090
|
-
return [{ name: "Claude Code installs", status: "warn", detail: `multiple found: ${installs.join(", ")}`, fix: "Multiple installs can diverge in version; keep one." }];
|
|
4091
|
-
}
|
|
4092
|
-
return [];
|
|
4093
|
-
}
|
|
4094
|
-
function defaultClaudeVersion() {
|
|
4095
|
-
return execFileSync("claude", ["--version"], { encoding: "utf8", timeout: 5e3 }).trim();
|
|
4096
|
-
}
|
|
4097
|
-
function defaultClaudeInstalls() {
|
|
4098
|
-
try {
|
|
4099
|
-
const out = execFileSync(process.platform === "win32" ? "where" : "which", ["-a", "claude"], { encoding: "utf8", timeout: 5e3 });
|
|
4100
|
-
return out.split(/\r?\n/).filter((l) => l.trim().length > 0);
|
|
4101
|
-
} catch {
|
|
4102
|
-
return [];
|
|
4103
|
-
}
|
|
4104
|
-
}
|
|
4105
|
-
function contextDegradedFix(kind) {
|
|
4106
|
-
switch (kind) {
|
|
4107
|
-
case "unreachable":
|
|
4108
|
-
return "Check network/VPN access to the control plane, then run `outerlayer context materialize`.";
|
|
4109
|
-
case "no-access":
|
|
4110
|
-
return "Check your git credentials (SSH key or token) and org/SSO access to the control plane, then retry.";
|
|
4111
|
-
case "designation-unverified":
|
|
4112
|
-
return "Check OuterLayer cloud connectivity/config (~/.outerlayer/config.json), then run `outerlayer context materialize`.";
|
|
4113
|
-
case "not-governed":
|
|
4114
|
-
return "Expected if the control plane's `governs` list changed intentionally; otherwise re-designate the source.";
|
|
4115
|
-
case "adoption-conflict":
|
|
4116
|
-
return "Remove the committed AGENTS.md/.claude/.outerlayer files in a commit, then run `outerlayer context materialize`.";
|
|
4117
|
-
case "conflict":
|
|
4118
|
-
return "Revoke the designation from all but one of the claiming factories, then run `outerlayer context materialize`.";
|
|
4119
|
-
}
|
|
4120
|
-
}
|
|
4121
|
-
function checkContextSource(home, cwd) {
|
|
4122
|
-
const dir = cwd ?? process.cwd();
|
|
4123
|
-
let repoRoot;
|
|
4124
|
-
let gitCommonDir;
|
|
4125
|
-
try {
|
|
4126
|
-
repoRoot = execFileSync("git", ["-C", dir, "rev-parse", "--show-toplevel"], {
|
|
4127
|
-
encoding: "utf8",
|
|
4128
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
4129
|
-
timeout: 2e3
|
|
4130
|
-
}).trim();
|
|
4131
|
-
gitCommonDir = execFileSync("git", ["-C", dir, "rev-parse", "--path-format=absolute", "--git-common-dir"], {
|
|
4132
|
-
encoding: "utf8",
|
|
4133
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
4134
|
-
timeout: 2e3
|
|
4135
|
-
}).trim();
|
|
4136
|
-
} catch {
|
|
4137
|
-
return [];
|
|
4138
|
-
}
|
|
4139
|
-
if (!repoRoot || !gitCommonDir) return [];
|
|
4140
|
-
const pin = readPin(repoRoot);
|
|
4141
|
-
const manifest = readContextManifest(gitCommonDir);
|
|
4142
|
-
const degraded = readDegraded(gitCommonDir);
|
|
4143
|
-
const sourceCache = readSourceCache(gitCommonDir);
|
|
4144
|
-
const looksGoverned = pin !== void 0 || sourceCache?.status === "governed";
|
|
4145
|
-
if (!pin && !manifest && !degraded && !looksGoverned) return [];
|
|
4146
|
-
const checks = [];
|
|
4147
|
-
const name = "Context source";
|
|
4148
|
-
if (pin && manifest && pin.sha !== manifest.sha) {
|
|
4149
|
-
checks.push({
|
|
4150
|
-
name,
|
|
4151
|
-
status: "warn",
|
|
4152
|
-
detail: `pin (${pin.sha.slice(0, 7)}) and manifest (${manifest.sha.slice(0, 7)}) disagree`,
|
|
4153
|
-
fix: "Run `outerlayer context materialize` to reconcile."
|
|
4154
|
-
});
|
|
4155
|
-
} else if (pin ?? manifest) {
|
|
4156
|
-
const sha = (pin ?? manifest).sha;
|
|
4157
|
-
checks.push({ name, status: degraded ? "warn" : "pass", detail: `materialized at ${sha.slice(0, 7)}` });
|
|
4158
|
-
}
|
|
4159
|
-
if (degraded) {
|
|
4160
|
-
checks.push({
|
|
4161
|
-
name: "Context source degradation",
|
|
4162
|
-
status: degraded.kind === "adoption-conflict" || degraded.kind === "conflict" ? "fail" : "warn",
|
|
4163
|
-
detail: degraded.message,
|
|
4164
|
-
fix: contextDegradedFix(degraded.kind)
|
|
4165
|
-
});
|
|
4166
|
-
}
|
|
4167
|
-
if ((pin || manifest) && !contextExcludesPresent(gitCommonDir)) {
|
|
4168
|
-
checks.push({
|
|
4169
|
-
name: "Context exclude block",
|
|
4170
|
-
status: "warn",
|
|
4171
|
-
detail: "materialized paths are not excluded in .git/info/exclude",
|
|
4172
|
-
fix: "Run `outerlayer context materialize` to repair it."
|
|
4173
|
-
});
|
|
4174
|
-
}
|
|
4175
|
-
if (pin && !readCloudConfig(home).url) {
|
|
4176
|
-
checks.push({
|
|
4177
|
-
name,
|
|
4178
|
-
status: "warn",
|
|
4179
|
-
detail: "a pin exists but OuterLayer cloud sync isn't configured \u2014 the designation cannot be confirmed",
|
|
4180
|
-
fix: "Run `outerlayer sync --url <url> --api-key <key> --app-id <id>` once to connect."
|
|
4181
|
-
});
|
|
4182
|
-
}
|
|
4183
|
-
if (looksGoverned) {
|
|
4184
|
-
let tracked = "";
|
|
4185
|
-
try {
|
|
4186
|
-
tracked = execFileSync("git", ["-C", repoRoot, "ls-files", "--", "AGENTS.md", ".claude", ".outerlayer"], {
|
|
4187
|
-
encoding: "utf8",
|
|
4188
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
4189
|
-
timeout: 2e3
|
|
4190
|
-
}).trim();
|
|
4191
|
-
} catch {
|
|
4192
|
-
tracked = "";
|
|
4193
|
-
}
|
|
4194
|
-
if (tracked) {
|
|
4195
|
-
const paths = tracked.split("\n");
|
|
4196
|
-
const sample = paths.slice(0, 3).join(", ") + (paths.length > 3 ? ", \u2026" : "");
|
|
4197
|
-
checks.push({
|
|
4198
|
-
name,
|
|
4199
|
-
status: "fail",
|
|
4200
|
-
detail: `committed context files shadow the control plane: ${sample}`,
|
|
4201
|
-
fix: "Remove the committed AGENTS.md/.claude/.outerlayer files in a commit, then run `outerlayer context materialize`."
|
|
4202
|
-
});
|
|
4203
|
-
}
|
|
4204
|
-
}
|
|
4205
|
-
return checks;
|
|
4206
|
-
}
|
|
4207
|
-
function doctorExitCode(checks) {
|
|
4208
|
-
return checks.some((c) => c.status === "fail") ? 1 : 0;
|
|
4209
|
-
}
|
|
4210
|
-
var FALLBACK_VERSION = "0.1.0";
|
|
4211
|
-
function readJson(path2) {
|
|
4212
|
-
if (!existsSync(path2)) return null;
|
|
4213
|
-
try {
|
|
4214
|
-
return JSON.parse(readFileSync(path2, "utf8"));
|
|
4215
|
-
} catch {
|
|
4216
|
-
return null;
|
|
4217
|
-
}
|
|
4218
|
-
}
|
|
4219
|
-
function resolveVersion(entryPath = fileURLToPath(import.meta.url)) {
|
|
4220
|
-
const distDir = dirname(entryPath);
|
|
4221
|
-
const pkg = readJson(join(dirname(distDir), "package.json"));
|
|
4222
|
-
const version = typeof pkg?.version === "string" ? pkg.version : FALLBACK_VERSION;
|
|
4223
|
-
const buildInfo = readJson(join(distDir, "build-info.json"));
|
|
4224
|
-
const buildId = typeof buildInfo?.id === "string" ? buildInfo.id : null;
|
|
4225
|
-
return buildId ? `${version} (build ${buildId})` : version;
|
|
4226
|
-
}
|
|
4227
3609
|
|
|
4228
3610
|
// src/cli.ts
|
|
4229
3611
|
function resolveCliBin() {
|
|
@@ -4234,11 +3616,6 @@ function resolveCliBin() {
|
|
|
4234
3616
|
}
|
|
4235
3617
|
}
|
|
4236
3618
|
var SUPPORT_TEXT = "\nDocs: https://github.com/outerlayerai/outerlayer#readme\nIssues: https://github.com/outerlayerai/outerlayer/issues\n";
|
|
4237
|
-
function icon(status) {
|
|
4238
|
-
if (status === "pass") return `${C.green}\u2713${C.reset}`;
|
|
4239
|
-
if (status === "warn") return `${C.yellow}!${C.reset}`;
|
|
4240
|
-
return `${C.red}\u2717${C.reset}`;
|
|
4241
|
-
}
|
|
4242
3619
|
function fail(message) {
|
|
4243
3620
|
process.stderr.write(`${C.red}\u2717${C.reset} ${message}
|
|
4244
3621
|
`);
|
|
@@ -4254,7 +3631,18 @@ function apiKeyOption() {
|
|
|
4254
3631
|
function warnIfApiKeyFlag(opts) {
|
|
4255
3632
|
if (opts.apiKey !== void 0) warnDeprecated(API_KEY_FLAG_DEPRECATION);
|
|
4256
3633
|
}
|
|
4257
|
-
function
|
|
3634
|
+
async function installStarterPackInRepo() {
|
|
3635
|
+
const { installStarterPack, formatStarterPackOutput } = await import('./starter-pack-LIZYMKYQ.js');
|
|
3636
|
+
const cwd = process.cwd();
|
|
3637
|
+
let repoRoot = cwd;
|
|
3638
|
+
try {
|
|
3639
|
+
repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || cwd;
|
|
3640
|
+
} catch {
|
|
3641
|
+
}
|
|
3642
|
+
const result = installStarterPack({ repoRoot });
|
|
3643
|
+
return { result, output: formatStarterPackOutput(result) };
|
|
3644
|
+
}
|
|
3645
|
+
function formatInitOutput(result, opts = {}) {
|
|
4258
3646
|
const lines = [];
|
|
4259
3647
|
if (result.removed) {
|
|
4260
3648
|
lines.push(
|
|
@@ -4324,15 +3712,12 @@ cannot be disabled.
|
|
|
4324
3712
|
`
|
|
4325
3713
|
);
|
|
4326
3714
|
lines.push(
|
|
4327
|
-
`
|
|
3715
|
+
(opts.next === false ? "\n" : `
|
|
4328
3716
|
Next: launch a session naming its work item, then sync it:
|
|
4329
3717
|
${C.yellow}OUTERLAYER_WORK=42 claude${C.reset}
|
|
4330
3718
|
${C.yellow}outerlayer sync${C.reset}
|
|
4331
3719
|
|
|
4332
|
-
Once you have run outerlayer login
|
|
4333
|
-
automatically in the background after each agent turn and at the end
|
|
4334
|
-
of the session, at most once every five minutes.
|
|
4335
|
-
`
|
|
3720
|
+
`) + (opts.connected ? "This repository is connected to a factory, so launched sessions sync\nautomatically in the background after each agent turn and at the end\nof the session, at most once every five minutes.\n" : "Once you have run outerlayer login and outerlayer init in this repository,\nlaunched sessions sync automatically in the background after each agent\nturn and at the end of the session, at most once every five minutes.\n")
|
|
4336
3721
|
);
|
|
4337
3722
|
return lines.join("");
|
|
4338
3723
|
}
|
|
@@ -4369,110 +3754,171 @@ function formatGuardInstallOutput(guard) {
|
|
|
4369
3754
|
return { lines: [], error: "not inside a git repository." };
|
|
4370
3755
|
}
|
|
4371
3756
|
}
|
|
3757
|
+
async function installHooks(opts, resolvedBin, connected, starterTemplate, withNext = true) {
|
|
3758
|
+
const scope = opts.project ? "project" : "user";
|
|
3759
|
+
let cliBin;
|
|
3760
|
+
try {
|
|
3761
|
+
cliBin = initHookBin({ home: homeFromEnv(), runningBin: resolvedBin, remove: opts.remove === true });
|
|
3762
|
+
} catch (err) {
|
|
3763
|
+
const text = `could not copy the CLI to ~/.outerlayer/cli: ${err instanceof Error ? err.message : String(err)}
|
|
3764
|
+
Hooks were not changed. Fix the permissions on ~/.outerlayer, then re-run ${C.yellow}outerlayer init${C.reset}.`;
|
|
3765
|
+
return { ok: false, text, detail: "could not copy the CLI to ~/.outerlayer/cli" };
|
|
3766
|
+
}
|
|
3767
|
+
try {
|
|
3768
|
+
const result = runInit({
|
|
3769
|
+
scope,
|
|
3770
|
+
remove: opts.remove,
|
|
3771
|
+
cliBin,
|
|
3772
|
+
addGitignore: opts.gitignore,
|
|
3773
|
+
wrapHooks: opts.wrapHooks,
|
|
3774
|
+
statusline: opts.statusline
|
|
3775
|
+
});
|
|
3776
|
+
if (result.cliBinUnresolved) {
|
|
3777
|
+
const text = `the resolved CLI path (${cliBin}) does not exist \u2014 refusing to write hook entries pointing at it.
|
|
3778
|
+
Reinstall OuterLayer, then re-run ${C.yellow}outerlayer init${C.reset}.`;
|
|
3779
|
+
return { ok: false, text, detail: "the CLI path does not exist" };
|
|
3780
|
+
}
|
|
3781
|
+
const starter = starterTemplate === void 0 ? null : await installStarterPackInRepo();
|
|
3782
|
+
return {
|
|
3783
|
+
ok: true,
|
|
3784
|
+
text: formatInitOutput(result, { connected: connected === true, next: withNext }) + (starter === null ? "" : `
|
|
3785
|
+
${starter.output}`),
|
|
3786
|
+
detail: result.changed ? `installed in ${result.path}` : `already installed in ${result.path}`,
|
|
3787
|
+
result: starter === null ? result : { ...result, starterPack: starter.result }
|
|
3788
|
+
};
|
|
3789
|
+
} catch (err) {
|
|
3790
|
+
if (err instanceof SettingsParseError) return { ok: false, text: err.message, detail: err.message };
|
|
3791
|
+
throw err;
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
async function realConnectDepsForCwd() {
|
|
3795
|
+
const { realConnectDeps } = await import('./connect-cmd-ZTJONP37.js');
|
|
3796
|
+
const { resolveRepoIdentity } = await import('./dist-44CQVWGT.js');
|
|
3797
|
+
return {
|
|
3798
|
+
...realConnectDeps(),
|
|
3799
|
+
home: homeFromEnv(),
|
|
3800
|
+
cwd: process.cwd(),
|
|
3801
|
+
// Prompts need a person at both ends: stdin to answer on, stderr to ask on.
|
|
3802
|
+
isTerminal: Boolean(process.stdin.isTTY && process.stderr.isTTY),
|
|
3803
|
+
resolveRepository: (cwd) => resolveRepoIdentity(cwd).gitRepo
|
|
3804
|
+
};
|
|
3805
|
+
}
|
|
4372
3806
|
function invokedCommandToken(argv) {
|
|
4373
3807
|
return argv.slice(2).find((token) => !token.startsWith("-"));
|
|
4374
3808
|
}
|
|
4375
3809
|
function buildProgram(argv = process.argv) {
|
|
4376
3810
|
const program2 = new Command();
|
|
4377
3811
|
program2.name("outerlayer").description("OuterLayer \u2014 see what your coding agents actually do").enablePositionalOptions().version(resolveVersion()).option(NO_COLOR_FLAG, "disable color (NO_COLOR=1 does the same)").addHelpText("after", SUPPORT_TEXT);
|
|
4378
|
-
program2.command("init").description("
|
|
3812
|
+
program2.command("init").description("Connect this repository, install hooks and agent skills, run doctor").option("--user", "install into user settings [default]").option("--project", "install into project settings (./.claude/settings.json)").option("--org", "print a managed-settings.json snippet for MDM/GPO rollout").option("--remove", "uninstall OuterLayer hooks").option("--local", "install the hooks only: no connect step and no network call").option("--factory <org/factory>", "the factory to connect this repository to (see `outerlayer connect`)").option("--gitignore", "also add .outerlayer/ to .gitignore (project scope)").option("--template <name>", "install the starter skills and a policy file (only: default)").option("--no-wrap-hooks", "leave PreToolUse/PostToolUse hooks unwrapped").option("--statusline", "install the status-line segment [default]").option("--no-statusline", "leave the statusLine slot untouched").option("--json", "machine-readable JSON output").addHelpText(
|
|
4379
3813
|
"after",
|
|
4380
|
-
"\nIdempotent: settings are backed up before any write, and a second run\nchanges nothing. Wrapping adds one spawn per hook firing; undo it with\n`outerlayer hooks unwrap`. An occupied status-line slot is wrapped, never\nreplaced.\n"
|
|
4381
|
-
).action((opts) => {
|
|
4382
|
-
let
|
|
3814
|
+
"\nIn a repository, init runs four steps: connect the repository to a\nfactory (`outerlayer connect`), install the hooks, set up the coding agent,\nand run `outerlayer doctor`. It never signs in and asks nothing. With no\nlogin it skips the connect step and says to run `outerlayer login`. With\n--json it prints one document naming the outcome of each step: done,\nskipped or failed. The exit code is 1 when a step failed.\n\nThe agent setup step installs the starter skills and the `outerlayer` skill\ninto .outerlayer/skills/, writes .outerlayer/config.json for Claude Code when\nthere is none, runs `outerlayer context emit` so the skills appear in\n.claude/skills/, and adds the OuterLayer MCP server to .mcp.json. Nothing you\nwrote is changed: an existing config, policy, edited skill, MCP entry or root\nCLAUDE.md and AGENTS.md is left as it is, and a hand-written file the emit\nwould replace is kept and listed. The files are left uncommitted.\nThe step is skipped outside a git repository and in a repository whose\ncontext comes from a control plane, where it arrives at session start.\n\n--local installs only the hooks: no connect step, no agent setup and no\nnetwork call. Add `--template default` to also write the starter skills and\na policy file into .outerlayer/ without compiling them; `outerlayer context\nemit` does that.\n\nIdempotent: settings are backed up before any write, and a second run\nchanges nothing. Wrapping adds one spawn per hook firing; undo it with\n`outerlayer hooks unwrap`. An occupied status-line slot is wrapped, never\nreplaced.\n\nThree of the starter skills are copied once and are yours to edit. The\nothers are rewritten from this CLI on every `context emit`.\n"
|
|
3815
|
+
).action(async (opts) => {
|
|
3816
|
+
let starterTemplate;
|
|
3817
|
+
if (opts.template !== void 0) {
|
|
3818
|
+
const { assertKnownTemplate, StarterPackError } = await import('./starter-pack-LIZYMKYQ.js');
|
|
3819
|
+
try {
|
|
3820
|
+
assertKnownTemplate(opts.template);
|
|
3821
|
+
} catch (err) {
|
|
3822
|
+
if (err instanceof StarterPackError) {
|
|
3823
|
+
fail(err.message);
|
|
3824
|
+
return;
|
|
3825
|
+
}
|
|
3826
|
+
throw err;
|
|
3827
|
+
}
|
|
3828
|
+
if (opts.remove || opts.org) {
|
|
3829
|
+
fail("--template installs into a repository, so it cannot be combined with --remove or --org.");
|
|
3830
|
+
return;
|
|
3831
|
+
}
|
|
3832
|
+
starterTemplate = opts.template;
|
|
3833
|
+
}
|
|
3834
|
+
const cliBin = resolveCliBin();
|
|
4383
3835
|
if (opts.org) {
|
|
4384
3836
|
process.stdout.write(orgRolloutSnippet(cliBin) + "\n");
|
|
4385
3837
|
return;
|
|
4386
3838
|
}
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
fail(
|
|
4392
|
-
`could not copy the CLI to ~/.outerlayer/cli: ${err instanceof Error ? err.message : String(err)}
|
|
4393
|
-
Hooks were not changed. Fix the permissions on ~/.outerlayer, then re-run ${C.yellow}outerlayer init${C.reset}.`
|
|
4394
|
-
);
|
|
4395
|
-
return;
|
|
4396
|
-
}
|
|
4397
|
-
try {
|
|
4398
|
-
const result = runInit({
|
|
4399
|
-
scope,
|
|
4400
|
-
remove: opts.remove,
|
|
4401
|
-
cliBin,
|
|
4402
|
-
addGitignore: opts.gitignore,
|
|
4403
|
-
wrapHooks: opts.wrapHooks,
|
|
4404
|
-
statusline: opts.statusline
|
|
4405
|
-
});
|
|
4406
|
-
if (result.cliBinUnresolved) {
|
|
4407
|
-
fail(
|
|
4408
|
-
`the resolved CLI path (${cliBin}) does not exist \u2014 refusing to write hook entries pointing at it.
|
|
4409
|
-
Reinstall OuterLayer, then re-run ${C.yellow}outerlayer init${C.reset}.`
|
|
4410
|
-
);
|
|
4411
|
-
return;
|
|
4412
|
-
}
|
|
4413
|
-
if (opts.json) {
|
|
4414
|
-
writeJson(result);
|
|
4415
|
-
return;
|
|
4416
|
-
}
|
|
4417
|
-
process.stdout.write(formatInitOutput(result));
|
|
4418
|
-
} catch (err) {
|
|
4419
|
-
if (err instanceof SettingsParseError) {
|
|
4420
|
-
fail(err.message);
|
|
3839
|
+
if (opts.remove || opts.local) {
|
|
3840
|
+
const outcome = await installHooks(opts, cliBin, void 0, starterTemplate);
|
|
3841
|
+
if (!outcome.ok) {
|
|
3842
|
+
fail(outcome.text);
|
|
4421
3843
|
return;
|
|
4422
3844
|
}
|
|
4423
|
-
|
|
3845
|
+
if (opts.json) writeJson(outcome.result);
|
|
3846
|
+
else process.stdout.write(outcome.text);
|
|
3847
|
+
return;
|
|
4424
3848
|
}
|
|
3849
|
+
const { runInitCommand } = await import('./init-cmd-WLST2F7G.js');
|
|
3850
|
+
const code = await runInitCommand(
|
|
3851
|
+
{ json: opts.json, factory: opts.factory },
|
|
3852
|
+
{
|
|
3853
|
+
connect: await realConnectDepsForCwd(),
|
|
3854
|
+
stdout: process.stdout,
|
|
3855
|
+
stderr: process.stderr,
|
|
3856
|
+
runHooks: async (hooksOpts) => {
|
|
3857
|
+
const { ok, text, detail } = await installHooks(opts, cliBin, hooksOpts?.connected, void 0, false);
|
|
3858
|
+
return { ok, text, detail };
|
|
3859
|
+
},
|
|
3860
|
+
runAgentSetup: async () => {
|
|
3861
|
+
const { runAgentSetup } = await import('./agent-setup-IFH3FB5X.js');
|
|
3862
|
+
return runAgentSetup({ cwd: process.cwd() });
|
|
3863
|
+
},
|
|
3864
|
+
runDoctorChecks: gatherDoctorChecks
|
|
3865
|
+
}
|
|
3866
|
+
);
|
|
3867
|
+
if (code !== 0) process.exitCode = code;
|
|
3868
|
+
});
|
|
3869
|
+
program2.command("connect").description("Choose the factory this repository's work goes to").option("--factory <org/factory>", "the factory to use: <org>/<factory> as in its dashboard address, or a factory id").option("--json", "write one JSON document to stdout; the text goes to stderr").addHelpText(
|
|
3870
|
+
"after",
|
|
3871
|
+
"\nRun it in a repository, after `outerlayer login`. With a login, connect lists\nthe factories you can use and takes the first rule that leaves one answer:\n 1. --factory <org>/<factory>, or a factory id\n 2. the factory this repository is already connected to\n 3. the one factory this repository is linked to\n 4. the one factory you can use\n 5. on a terminal, ask; with no terminal, exit 1 and list each --factory value\n\nIt says what it chose and why. The choice is saved in this repository's git\ndirectory (outerlayer/connection.json), shared by every worktree of the\nclone and never committed. A factory key already belongs to one factory, so\nconnect changes nothing for it. Connect never links a repository.\n\nAfter it saves the choice, connect checks that this repository is linked to\nthe factory. If it is not, connect says so, prints the factory's Work page\nlink where you link it, and exits 2. The choice is saved either way. Run\nconnect again once the repository is linked. With --json, the document has\nlinked (true, false, or null when it could not be checked) and, when false,\nlinkUrl.\n"
|
|
3872
|
+
).action(async (opts) => {
|
|
3873
|
+
const { runConnectCommand } = await import('./connect-cmd-ZTJONP37.js');
|
|
3874
|
+
const code = await runConnectCommand({ factory: opts.factory, json: opts.json }, await realConnectDepsForCwd(), {
|
|
3875
|
+
stdout: process.stdout,
|
|
3876
|
+
stderr: process.stderr
|
|
3877
|
+
});
|
|
3878
|
+
if (code !== 0) process.exitCode = code;
|
|
4425
3879
|
});
|
|
4426
|
-
program2.command("doctor").description("Diagnose the capture setup; every failing check names its fix").option("--json", "machine-readable JSON output").action((opts) => {
|
|
4427
|
-
const checks =
|
|
4428
|
-
const fails = checks.filter((c) => c.status === "fail").length;
|
|
4429
|
-
const warns = checks.filter((c) => c.status === "warn").length;
|
|
4430
|
-
const passed = checks.length - fails - warns;
|
|
3880
|
+
program2.command("doctor").description("Diagnose the capture setup; every failing check names its fix").option("--json", "machine-readable JSON output").action(async (opts) => {
|
|
3881
|
+
const checks = await gatherDoctorChecks();
|
|
4431
3882
|
process.exitCode = doctorExitCode(checks);
|
|
4432
3883
|
if (opts.json) {
|
|
4433
|
-
writeJson({ checks, summary:
|
|
3884
|
+
writeJson({ checks, summary: summarizeChecks(checks) });
|
|
4434
3885
|
return;
|
|
4435
3886
|
}
|
|
4436
|
-
process.stdout.write(
|
|
4437
|
-
for (const c of checks) {
|
|
4438
|
-
process.stdout.write(` ${icon(c.status)} ${c.name}${C.dim} \u2014 ${c.detail}${C.reset}
|
|
4439
|
-
`);
|
|
4440
|
-
if (c.fix && c.status !== "pass") process.stdout.write(` ${C.dim}\u21B3 ${c.fix}${C.reset}
|
|
4441
|
-
`);
|
|
4442
|
-
}
|
|
4443
|
-
process.stdout.write(`
|
|
4444
|
-
${fails === 0 ? `${C.green}\u2713${C.reset}` : `${C.red}\u2717${C.reset}`} ${passed} passed, ${warns} warnings, ${fails} failures
|
|
4445
|
-
`);
|
|
3887
|
+
process.stdout.write(renderDoctorText(checks));
|
|
4446
3888
|
});
|
|
4447
|
-
program2.command("login").description("
|
|
3889
|
+
program2.command("login").description("Sign in to your account by browser approval, or save a piped factory key").option("--dashboard <address>", "the dashboard to sign in on (default: https://app.outerlayer.ai)").option("--no-browser", "print the approve link without opening a browser").option("--check <id>", "check a login that is still waiting for approval, once").option("--url <url>", "gateway base URL for a piped key (browser login gets it from the dashboard)").option("--app-id <id>", "factory id a piped key is bound to (Settings \u2192 General)").option("--no-input", "never wait on a person; fail if the key is not on stdin").option("--json", "machine-readable JSON output").addHelpText(
|
|
4448
3890
|
"after",
|
|
4449
|
-
'\
|
|
3891
|
+
'\nWith no key on stdin, login prints an approve link and a code. A signed-in\nperson approves it on the dashboard. The login is a token for that person\'s\naccount, made for this machine, saved here and never printed. It picks no\nfactory: run `outerlayer connect` for that. `outerlayer logout` revokes it.\n\nWhen stdout is not a terminal, login does not wait. It prints one JSON\ndocument with the link, the code and a `login --check <id>` command, and\nexits 75. Run the check until it stops exiting 75: 0 is signed in, 1 is\ndenied or expired. A dropped connection or a 5xx also exits 75.\n\nA factory key piped on stdin is saved as it always was, never as a flag and\nnever printed back. A factory key always wins over the login, so CI and\nrunners keep acting as their key.\n\n echo "$OUTERLAYER_KEY" | outerlayer login --url <gateway url> --app-id <factory id>\n\nEvery later command reads the saved file, so none of them needs a key on\nits command line. A saved key is checked for real on the first `sync`.\n'
|
|
4450
3892
|
).action(async (opts) => {
|
|
4451
|
-
const {
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
3893
|
+
const { runLoginCommand, realLoginDeps } = await import('./login-browser-7XGSV7MA.js');
|
|
3894
|
+
const code = await runLoginCommand(
|
|
3895
|
+
{
|
|
3896
|
+
browser: opts.browser !== false,
|
|
3897
|
+
dashboard: opts.dashboard,
|
|
3898
|
+
json: opts.json,
|
|
3899
|
+
check: opts.check,
|
|
3900
|
+
noInput: opts.input === false,
|
|
3901
|
+
url: opts.url,
|
|
3902
|
+
appId: opts.appId
|
|
3903
|
+
},
|
|
3904
|
+
realLoginDeps()
|
|
3905
|
+
);
|
|
3906
|
+
if (code !== 0) process.exitCode = code;
|
|
3907
|
+
});
|
|
3908
|
+
program2.command("logout").description("Revoke this machine's login and remove it").addHelpText(
|
|
3909
|
+
"after",
|
|
3910
|
+
"\nRevokes the token `outerlayer login` saved, then deletes it from the config\nfile. A factory key and the chosen factory are left alone. When the\ndashboard cannot be reached the local copy is still deleted and the exit\ncode is 1: revoke the token on /profile/security.\n"
|
|
3911
|
+
).action(async () => {
|
|
3912
|
+
const { runLogoutCommand, realLogoutDeps } = await import('./logout-cmd-QAUFDJ6J.js');
|
|
3913
|
+
const code = await runLogoutCommand(realLogoutDeps());
|
|
3914
|
+
if (code !== 0) process.exitCode = code;
|
|
4469
3915
|
});
|
|
4470
|
-
program2.command("sync").description("Send local coding-agent sessions to OuterLayer cloud").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "
|
|
3916
|
+
program2.command("sync").description("Send local coding-agent sessions to OuterLayer cloud").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").option("--tier <tier>", "capture tier: metrics | redacted | full (default: full)").option("--dry-run", "print what would leave this machine, send nothing").option("--all", "ignore the sync checkpoint and re-send everything").option("--limit <n>", "only the N most recent sessions", (v) => parseInt(v, 10)).option("--json", "machine-readable JSON output").option("--quiet", "no output; exit code only").option("--root <dir>", "Claude Code transcript root (default ~/.claude/projects)").option("--codex-root <dir>", "Codex sessions root (default ~/.codex/sessions)").option("--cursor-root <dir>", "Cursor chats root (default ~/.cursor/chats)").addHelpText(
|
|
4471
3917
|
"after",
|
|
4472
3918
|
"\nIncremental: only sessions newer than the last checkpoint ship. The tier\nis applied on this machine before anything leaves it; `--dry-run` prints\nexactly what would. The hook-triggered background sync runs `--quiet`.\nExit 2 means the server rejected at least one session.\n"
|
|
4473
3919
|
).action(async (opts) => {
|
|
4474
3920
|
warnIfApiKeyFlag(opts);
|
|
4475
|
-
const { runSync, SyncConfigError, SyncTransportError } = await import('./sync-cmd-
|
|
3921
|
+
const { runSync, SyncConfigError, SyncTransportError } = await import('./sync-cmd-4G4A7EVN.js');
|
|
4476
3922
|
try {
|
|
4477
3923
|
const result = await runSync({
|
|
4478
3924
|
url: opts.url,
|
|
@@ -4501,7 +3947,7 @@ ${fails === 0 ? `${C.green}\u2713${C.reset}` : `${C.red}\u2717${C.reset}`} ${pas
|
|
|
4501
3947
|
if (invokedCommandToken(argv) === "watch") {
|
|
4502
3948
|
warnDeprecated("`outerlayer watch` is now `outerlayer daemon`; the old name will be removed in a future release.");
|
|
4503
3949
|
}
|
|
4504
|
-
const { runWatch } = await import('./watch-
|
|
3950
|
+
const { runWatch } = await import('./watch-7SKJKGAA.js');
|
|
4505
3951
|
await runWatch({ once: opts.once });
|
|
4506
3952
|
});
|
|
4507
3953
|
program2.command("hook <event>", { hidden: true }).description("capture-hook fast path, installed by `init`").action(() => {
|
|
@@ -4510,6 +3956,59 @@ ${fails === 0 ? `${C.green}\u2713${C.reset}` : `${C.red}\u2717${C.reset}`} ${pas
|
|
|
4510
3956
|
});
|
|
4511
3957
|
program2.command("hook-wrap", { hidden: true }).description("runs a hook wrapped by `hooks wrap`, recording exec evidence").option("--id <base64>", "the original hook command, base64-encoded").option("--capture-output", "tee stdout/stderr into a bounded tail").action(() => {
|
|
4512
3958
|
});
|
|
3959
|
+
program2.command("broker-relay", { hidden: true }).description("relay a build's loopback connections to the runner's tunnel and destinations").requiredOption("--port <port>", "the loopback port to listen on").requiredOption("--socket <path>", "the runner's tunnel socket").option("--forward <port=socket>", "also relay a loopback port to a destination's socket; repeatable", (value, previous) => [...previous, value], []).action(async (opts) => {
|
|
3960
|
+
const { startRelay } = await import('./relay-L76HDX72.js');
|
|
3961
|
+
const port = Number(opts.port);
|
|
3962
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
3963
|
+
fail(`--port must be a port number, not ${opts.port}`);
|
|
3964
|
+
return;
|
|
3965
|
+
}
|
|
3966
|
+
const forwards = [];
|
|
3967
|
+
for (const spec of opts.forward) {
|
|
3968
|
+
const eq = spec.indexOf("=");
|
|
3969
|
+
const forwardPort = Number(spec.slice(0, eq));
|
|
3970
|
+
if (eq === -1 || !Number.isInteger(forwardPort) || forwardPort < 1 || forwardPort > 65535 || eq === spec.length - 1) {
|
|
3971
|
+
fail(`--forward must be <port>=<socket path>, not ${spec}`);
|
|
3972
|
+
return;
|
|
3973
|
+
}
|
|
3974
|
+
forwards.push({ port: forwardPort, socketPath: spec.slice(eq + 1) });
|
|
3975
|
+
}
|
|
3976
|
+
await startRelay({ port, socketPath: opts.socket, forwards });
|
|
3977
|
+
});
|
|
3978
|
+
program2.command("vm-exec", { hidden: true }).description("run a command in a microVM build's guest through its exec agent").requiredOption("--socket <path>", "the Unix socket Firecracker serves the build's vsock device on").option("--user <user>", "the user to run as", "").option("--cwd <dir>", "the working directory", "/").option("--env <NAME=value>", "an environment value the command line carries; repeatable", (value, previous) => [...previous, value], []).option("--pass <NAME>", "an environment variable whose value is read from this process; repeatable", (value, previous) => [...previous, value], []).argument("<argv...>", "the command and its arguments").action(async (argv2, opts) => {
|
|
3979
|
+
const { runVmExec } = await import('./exec-client-4XGXV3XN.js');
|
|
3980
|
+
const env = {};
|
|
3981
|
+
for (const entry of opts.env) {
|
|
3982
|
+
const eq = entry.indexOf("=");
|
|
3983
|
+
if (eq < 1) {
|
|
3984
|
+
fail(`--env must be NAME=value, not ${entry}`);
|
|
3985
|
+
return;
|
|
3986
|
+
}
|
|
3987
|
+
env[entry.slice(0, eq)] = entry.slice(eq + 1);
|
|
3988
|
+
}
|
|
3989
|
+
const abort = new AbortController();
|
|
3990
|
+
for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"]) process.on(signal, () => abort.abort());
|
|
3991
|
+
try {
|
|
3992
|
+
const status = await runVmExec(
|
|
3993
|
+
{ socket: opts.socket, user: opts.user, cwd: opts.cwd, env, pass: opts.pass, argv: argv2 },
|
|
3994
|
+
{ stdin: process.stdin, stdout: (chunk) => void process.stdout.write(chunk), stderr: (chunk) => void process.stderr.write(chunk), env: process.env, signal: abort.signal }
|
|
3995
|
+
);
|
|
3996
|
+
process.exit(status < 0 ? 255 : status);
|
|
3997
|
+
} catch (err) {
|
|
3998
|
+
process.stderr.write(`vm-exec: ${err instanceof Error ? err.message : String(err)}
|
|
3999
|
+
`);
|
|
4000
|
+
process.exit(255);
|
|
4001
|
+
}
|
|
4002
|
+
});
|
|
4003
|
+
program2.command("vm-init", { hidden: true }).description("lay out a microVM build's guest; the guest helper runs it").action(async () => {
|
|
4004
|
+
const { realGuestHost, runGuestInit } = await import('./guest-init-GENRHNP7.js');
|
|
4005
|
+
const { readFileSync: readFileSync3 } = await import('fs');
|
|
4006
|
+
try {
|
|
4007
|
+
runGuestInit(realGuestHost(), readFileSync3("/proc/cmdline", "utf8"));
|
|
4008
|
+
} catch (err) {
|
|
4009
|
+
fail(`vm-init failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
4010
|
+
}
|
|
4011
|
+
});
|
|
4513
4012
|
registerEmitCommands(program2);
|
|
4514
4013
|
registerImportCommands(program2);
|
|
4515
4014
|
registerHooksCommands(program2);
|
|
@@ -4519,13 +4018,17 @@ ${fails === 0 ? `${C.green}\u2713${C.reset}` : `${C.red}\u2717${C.reset}`} ${pas
|
|
|
4519
4018
|
registerRunnerCommands(program2);
|
|
4520
4019
|
return program2;
|
|
4521
4020
|
}
|
|
4021
|
+
function withoutItemKey(job) {
|
|
4022
|
+
const { itemKey: _itemKey, ...rest } = job;
|
|
4023
|
+
return rest;
|
|
4024
|
+
}
|
|
4522
4025
|
async function runCli(processArgv) {
|
|
4523
4026
|
const { argv, noColor } = stripNoColor(processArgv);
|
|
4524
4027
|
if (noColor) process.env.NO_COLOR = "1";
|
|
4525
4028
|
await buildProgram(argv).parseAsync(argv);
|
|
4526
4029
|
}
|
|
4527
4030
|
async function compileContext(opts) {
|
|
4528
|
-
const { runEmit, EmitError } = await import('./emit-cmd-
|
|
4031
|
+
const { runEmit, EmitError } = await import('./emit-cmd-DCERAU27.js');
|
|
4529
4032
|
try {
|
|
4530
4033
|
const result = runEmit({ cwd: opts.dir, check: opts.check, json: opts.json });
|
|
4531
4034
|
if (result.exitCode !== 0) process.exitCode = result.exitCode;
|
|
@@ -4538,9 +4041,9 @@ async function compileContext(opts) {
|
|
|
4538
4041
|
}
|
|
4539
4042
|
}
|
|
4540
4043
|
function registerEmitCommands(program2) {
|
|
4541
|
-
const emitCmd = program2.command("emit").enablePositionalOptions().description("Record evidence for a work item: a named check result").argument("[name]", "the check name a validator declares (e.g. smoke.pass)").option("--link <url>", "proof link, usually the CI run URL").option("--body <text>", "one sentence recording your judgment").option("--body-file <path>", "read the sentence from a file; - reads standard input").option("--result <pass|fail>", "check outcome (default: pass)").option("--item <number>", "work item to record on directly, by its number in this factory").option("--
|
|
4044
|
+
const emitCmd = program2.command("emit").enablePositionalOptions().description("Record evidence for a work item: a named check result").argument("[name]", "the check name a validator declares (e.g. smoke.pass)").option("--link <url>", "proof link, usually the CI run URL").option("--body <text>", "one sentence recording your judgment").option("--body-file <path>", "read the sentence from a file; - reads standard input").option("--result <pass|fail>", "check outcome (default: pass)").option("--item <number>", "work item to record on directly, by its number in this factory").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").addOption(new Option("--check", "deprecated: use `context emit --check`").hideHelp()).addOption(new Option("--dir <path>", "deprecated: use `context emit --dir`").hideHelp()).addHelpText(
|
|
4542
4045
|
"after",
|
|
4543
|
-
|
|
4046
|
+
"\n outerlayer emit smoke.pass --item 412 --link https://ci.example.com/run/42\n outerlayer emit code-review --result fail --item 412 --body \"The button should be red.\"\n\nRecords that a named check passed or failed, anchored to a work item.\n--item is always required, in or out of a recorded session \u2014 there is no\nsession fallback. A check that ran carries the run URL as proof (--link);\na judgment you are making yourself carries one sentence (--body, or\n--body-file to read it from a file or standard input). A fail needs at\nleast one of the two. Run from inside a session, the check is stored as\nthe session's; a session cannot record over a person's fail. With\nnothing to attach it to, the command refuses.\n\nWho recorded a check is decided by your API key, never by what you send:\na key bound to your membership records it as yours, and nothing automated\ncan then clear it.\n\nA person's pass or fail on one criterion's proof is recorded on that\ncriterion's thread instead \u2014 see `outerlayer work comment`.\n\nSubcommands: `emit artifact <file>` for screenshots, recordings, reports and\nlogs; `emit finding` / `emit findings <file>` to record a problem in the factory;\n`emit criteria <file>` to record a work item's acceptance criteria.\nCompiling .outerlayer/ moved to `outerlayer context emit`.\n"
|
|
4544
4047
|
).action(async (name, opts) => {
|
|
4545
4048
|
if (name === void 0) {
|
|
4546
4049
|
warnDeprecated("`outerlayer emit` with no name now lives at `outerlayer context emit`; the bare form will be removed in a future release.");
|
|
@@ -4548,7 +4051,7 @@ function registerEmitCommands(program2) {
|
|
|
4548
4051
|
return;
|
|
4549
4052
|
}
|
|
4550
4053
|
warnIfApiKeyFlag(opts);
|
|
4551
|
-
const { runEmitResult, EmitResultError } = await import('./emit-result-cmd-
|
|
4054
|
+
const { runEmitResult, EmitResultError } = await import('./emit-result-cmd-4CPDBMN2.js');
|
|
4552
4055
|
try {
|
|
4553
4056
|
const result = await runEmitResult({
|
|
4554
4057
|
name,
|
|
@@ -4557,7 +4060,6 @@ function registerEmitCommands(program2) {
|
|
|
4557
4060
|
bodyFile: opts.bodyFile,
|
|
4558
4061
|
result: opts.result,
|
|
4559
4062
|
item: opts.item,
|
|
4560
|
-
artifact: opts.artifact,
|
|
4561
4063
|
json: opts.json,
|
|
4562
4064
|
url: opts.url,
|
|
4563
4065
|
apiKey: opts.apiKey,
|
|
@@ -4572,12 +4074,35 @@ function registerEmitCommands(program2) {
|
|
|
4572
4074
|
throw err;
|
|
4573
4075
|
}
|
|
4574
4076
|
});
|
|
4575
|
-
emitCmd.command("artifact <file>").description("Emit a proof artifact: screenshot, recording, report, or log").requiredOption("--caption <text>", "what this artifact shows or proves").option("--for <criterion-id>", "id of the criterion this artifact proves (e.g. LOGIN-01)").option(
|
|
4077
|
+
emitCmd.command("artifact <file>").description("Emit a proof artifact: screenshot, recording, report, or log").requiredOption("--caption <text>", "what this artifact shows or proves").option("--for <criterion-id>", "id of the criterion this artifact proves (e.g. LOGIN-01)").option(
|
|
4078
|
+
"--test <name[=path:line]>",
|
|
4079
|
+
"bind only this test of JUnit results; repeatable; =path:line sets its location",
|
|
4080
|
+
(value, previous = []) => [...previous, value]
|
|
4081
|
+
).option("--replaces <ids>", "artifact id(s) this one corrects; comma-separate 2-20 to retire them all").option("--pr <number>", "pull request number to anchor to").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
|
|
4576
4082
|
"after",
|
|
4577
|
-
|
|
4083
|
+
`
|
|
4084
|
+
outerlayer emit artifact shot.png --caption "the dot row, current dot marked" --for LOGIN-01
|
|
4085
|
+
|
|
4086
|
+
Inside a recorded Claude Code session the artifact spools locally and ships,
|
|
4087
|
+
bound to that session, on the next \`outerlayer sync\` \u2014 but only if that
|
|
4088
|
+
session was launched with OUTERLAYER_WORK; without it nothing the session
|
|
4089
|
+
produces may leave the machine, and the command refuses rather than queue an
|
|
4090
|
+
artifact that can never ship. Otherwise it uploads immediately, anchored to a
|
|
4091
|
+
pull request (--pr, or CI's PR context) or to the current git checkout. With
|
|
4092
|
+
nothing to attach it to, the command refuses.
|
|
4093
|
+
|
|
4094
|
+
A JUnit XML file (.xml, root <testsuites> or <testsuite>) bound with --for proves a
|
|
4095
|
+
criterion that declares test proof once every bound test passed. Without --test every
|
|
4096
|
+
test in the file is bound; each --test binds only that one. Give a test's location with
|
|
4097
|
+
--test "name=src/a.test.ts:42" when the runner's file does not carry it.
|
|
4098
|
+
|
|
4099
|
+
--replaces: an already-uploaded target is marked superseded and hidden
|
|
4100
|
+
everywhere, bytes and deep link untouched; a still-queued one is cancelled
|
|
4101
|
+
locally so it never uploads. One correction can retire several duplicates.
|
|
4102
|
+
`
|
|
4578
4103
|
).action(async (file, opts) => {
|
|
4579
4104
|
warnIfApiKeyFlag(opts);
|
|
4580
|
-
const { runEmitArtifact, EmitArtifactError } = await import('./emit-artifact-cmd-
|
|
4105
|
+
const { runEmitArtifact, EmitArtifactError } = await import('./emit-artifact-cmd-AKZP7TMF.js');
|
|
4581
4106
|
let pr;
|
|
4582
4107
|
if (opts.pr !== void 0) {
|
|
4583
4108
|
pr = parseTrackerNumber(opts.pr);
|
|
@@ -4592,6 +4117,7 @@ function registerEmitCommands(program2) {
|
|
|
4592
4117
|
caption: opts.caption,
|
|
4593
4118
|
criterionId: opts.for,
|
|
4594
4119
|
replaces: opts.replaces,
|
|
4120
|
+
tests: opts.test,
|
|
4595
4121
|
pr,
|
|
4596
4122
|
json: opts.json,
|
|
4597
4123
|
url: opts.url,
|
|
@@ -4606,28 +4132,24 @@ function registerEmitCommands(program2) {
|
|
|
4606
4132
|
throw err;
|
|
4607
4133
|
}
|
|
4608
4134
|
});
|
|
4609
|
-
emitCmd.command("finding").description("Record one finding:
|
|
4135
|
+
emitCmd.command("finding").description("Record one finding: a problem you hit in the factory").requiredOption("--id <id>", "client id \u2014 re-emitting the same id replaces it").option("--category <context|flaky-test|tooling|environment|defect|other>", "what kind of problem this is").requiredOption("--title <text>", "one sentence").requiredOption("--file <path>", "the file the problem is in, or the instruction's own path").option("--line <number>", "line number in --file").option("--rule-path <path>", "the instruction's own path \u2014 required for a context finding").option("--rule-line <number>", "line number in --rule-path").option("--rule-quote <text>", "the instruction's quoted sentence \u2014 required unless --rule-relation is missing").option("--rule-relation <wrong|broken|missing>", "how the instruction fails \u2014 required for a context finding").requiredOption("--where <label>", 'where this was recorded, e.g. "build session"').option("--trace-id <id>", "the session's trace id, when there is one").option("--turn <number>", "turn index within the session").option("--round <number>", "round number, when there is one").addOption(new Option("--subject <value>").hideHelp()).addOption(new Option("--kind <value>").hideHelp()).addOption(new Option("--severity <value>").hideHelp()).addOption(new Option("--verdict <value>").hideHelp()).addOption(new Option("--fixed").hideHelp()).addOption(new Option("--source <value>").hideHelp()).option("--head-sha <sha>", "the commit this batch was recorded against").option("--context-source-sha <sha>", "the context-source commit this batch was recorded against").option("--item <number>", "work item to record on directly, by its number in this factory").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
|
|
4610
4136
|
"after",
|
|
4611
|
-
'\n outerlayer emit finding --id
|
|
4137
|
+
'\n outerlayer emit finding --id gate-skip --category tooling \\\n --title "The pre-push gate prints green when a step was skipped" \\\n --file scripts/git/pre-push-checks.mjs --where "build session"\n\nRecords one problem you hit in the factory: an instruction that is wrong or\nmissing (context), a flaky test, a misbehaving script (tooling), a local\nsetup trap (environment), a bug outside your own change (defect), or\nanything else. A problem in your own change is fixed, not recorded.\nA context finding needs --rule-path and --rule-relation, and --rule-quote\nunless the relation is missing.\n--item names the work item; without it, inside a recorded session, the\nitem that session was launched for is used automatically.\n'
|
|
4612
4138
|
).action(async (opts) => {
|
|
4613
4139
|
warnIfApiKeyFlag(opts);
|
|
4614
|
-
const { runEmitFinding, EmitFindingError } = await import('./emit-finding-cmd-
|
|
4140
|
+
const { runEmitFinding, refuseRemovedFindingFlags, EmitFindingError } = await import('./emit-finding-cmd-N2FTGCVC.js');
|
|
4615
4141
|
try {
|
|
4142
|
+
refuseRemovedFindingFlags(opts);
|
|
4616
4143
|
const result = await runEmitFinding({
|
|
4617
4144
|
id: opts.id,
|
|
4618
|
-
|
|
4145
|
+
category: opts.category,
|
|
4619
4146
|
title: opts.title,
|
|
4620
4147
|
file: opts.file,
|
|
4621
4148
|
line: opts.line,
|
|
4622
|
-
severity: opts.severity,
|
|
4623
|
-
kind: opts.kind,
|
|
4624
|
-
verdict: opts.verdict,
|
|
4625
|
-
fixed: opts.fixed,
|
|
4626
4149
|
rulePath: opts.rulePath,
|
|
4627
4150
|
ruleLine: opts.ruleLine,
|
|
4628
4151
|
ruleQuote: opts.ruleQuote,
|
|
4629
4152
|
ruleRelation: opts.ruleRelation,
|
|
4630
|
-
source: opts.source,
|
|
4631
4153
|
where: opts.where,
|
|
4632
4154
|
traceId: opts.traceId,
|
|
4633
4155
|
turn: opts.turn,
|
|
@@ -4649,12 +4171,12 @@ function registerEmitCommands(program2) {
|
|
|
4649
4171
|
throw err;
|
|
4650
4172
|
}
|
|
4651
4173
|
});
|
|
4652
|
-
emitCmd.command("findings <file>").description("Record a batch of findings from a FindingBatch JSON file (- for standard input)").option("--item <number>", "work item to record on directly, by its number in this factory").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "
|
|
4174
|
+
emitCmd.command("findings <file>").description("Record a batch of findings from a FindingBatch JSON file (- for standard input)").option("--item <number>", "work item to record on directly, by its number in this factory").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
|
|
4653
4175
|
"after",
|
|
4654
4176
|
"\n outerlayer emit findings review.json --item 412\n\nRecords a whole batch at once \u2014 a FindingBatch JSON file (schemaVersion,\noptional headSha / contextSourceSha, and 1-500 findings), validated against\nthe same contract `emit finding` uses before anything is sent. Re-emitting\nan id already on the item replaces that row rather than adding a second one.\n--item names the work item; without it, inside a recorded session, the item\nthat session was launched for is used automatically.\n"
|
|
4655
4177
|
).action(async (file, opts) => {
|
|
4656
4178
|
warnIfApiKeyFlag(opts);
|
|
4657
|
-
const { runEmitFindings, EmitFindingError } = await import('./emit-finding-cmd-
|
|
4179
|
+
const { runEmitFindings, EmitFindingError } = await import('./emit-finding-cmd-N2FTGCVC.js');
|
|
4658
4180
|
try {
|
|
4659
4181
|
const result = await runEmitFindings({
|
|
4660
4182
|
file,
|
|
@@ -4673,17 +4195,41 @@ function registerEmitCommands(program2) {
|
|
|
4673
4195
|
throw err;
|
|
4674
4196
|
}
|
|
4675
4197
|
});
|
|
4198
|
+
emitCmd.command("criteria <file>").description("Record a work item's acceptance criteria from a JSON file (- for stdin)").option("--item <number>", "work item to record on directly, by its number in this factory").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
|
|
4199
|
+
"after",
|
|
4200
|
+
'\n outerlayer emit criteria criteria.json --item 412\n\nRecords the item\'s acceptance criteria as one list. The file is JSON:\n { "criteria": [ { "id": "AC-1", "text": "Given ...", "proof": null },\n { "id": "login.redirect", "text": "...", "proof": "screenshot" } ] }\nAn id is 1-64 characters of A-Z a-z 0-9 . _ : - and is the id evidence is bound to with\n`emit artifact --for`. proof is video, screenshot, report, log, test, file or null (any bound\nartifact proves it). The list holds 1 to 200 criteria, each text at most 2,000\ncharacters, and no id twice. The list is validated before anything is sent.\nOnce an item has a list, only a person can replace it: run the command outside an\nagent session, with your own login. A key bound to no member cannot replace it either.\n--item names the work item; without it, inside a recorded session, the item that\nsession was launched for is used automatically.\n'
|
|
4201
|
+
).action(async (file, opts) => {
|
|
4202
|
+
warnIfApiKeyFlag(opts);
|
|
4203
|
+
const { runEmitCriteria, EmitCriteriaError } = await import('./emit-criteria-cmd-7MXQVCKD.js');
|
|
4204
|
+
try {
|
|
4205
|
+
const result = await runEmitCriteria({
|
|
4206
|
+
file,
|
|
4207
|
+
item: opts.item,
|
|
4208
|
+
json: opts.json,
|
|
4209
|
+
url: opts.url,
|
|
4210
|
+
apiKey: opts.apiKey,
|
|
4211
|
+
appId: opts.appId
|
|
4212
|
+
});
|
|
4213
|
+
if (result.exitCode !== 0) process.exitCode = result.exitCode;
|
|
4214
|
+
} catch (err) {
|
|
4215
|
+
if (err instanceof EmitCriteriaError) {
|
|
4216
|
+
fail(err.message);
|
|
4217
|
+
return;
|
|
4218
|
+
}
|
|
4219
|
+
throw err;
|
|
4220
|
+
}
|
|
4221
|
+
});
|
|
4676
4222
|
}
|
|
4677
4223
|
function registerWorkCommands(program2) {
|
|
4678
|
-
const workCmd = program2.command("work").description("
|
|
4679
|
-
workCmd.command("
|
|
4224
|
+
const workCmd = program2.command("work").description("Start, remove, or read what the factory is working on (the Work page)");
|
|
4225
|
+
workCmd.command("build").description("Start work on an item: a host builds it, or with --local you do").option("--issue <number>", "issue number, or a Linear/Jira key like ENG-123 (adds it to Work first)").option("--tracker <linear|jira>", "tracker for a keyed --issue \u2014 required if more than one is connected").option("--item <number>", "the item's own number inside this factory \u2014 must already exist").option("--local", "you build it in a session you start; no host takes it").option("--repo <owner/name>", "repository \u2014 the checkout's remote for a number, never guessed for a key").option("--note <text>", "a short note stored with the request").option("--session-id <id>", "session id to record as-is, skipping detection").option("--retry <count>", "retries after a gateway answer worth trying again").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
|
|
4680
4226
|
"after",
|
|
4681
|
-
"\n outerlayer work
|
|
4227
|
+
"\n outerlayer work build --issue 42 # a host builds it\n outerlayer work build --item 7 # retry an item already on Work\n outerlayer work build --issue 42 --local # you build it\n outerlayer work build --issue ENG-123 --tracker linear # a Linear issue\n\nWithout --local, records a build request: a host takes the item when it\nhas a free slot. It never links the calling session and never takes a\nlease, even run from inside one.\n\nWith --local, puts the item on Work with no build request, so no host\ntakes it, and prints the line that starts your session on it. That\nsession links itself and holds the item's lease while it works.\n"
|
|
4682
4228
|
).action(async (opts) => {
|
|
4683
4229
|
warnIfApiKeyFlag(opts);
|
|
4684
|
-
const {
|
|
4230
|
+
const { runWorkBuild, WorkCommandError } = await import('./work-cmd-QAUZ7MTD.js');
|
|
4685
4231
|
try {
|
|
4686
|
-
await
|
|
4232
|
+
await runWorkBuild(opts);
|
|
4687
4233
|
} catch (err) {
|
|
4688
4234
|
if (err instanceof WorkCommandError) {
|
|
4689
4235
|
fail(err.message);
|
|
@@ -4692,14 +4238,14 @@ function registerWorkCommands(program2) {
|
|
|
4692
4238
|
throw err;
|
|
4693
4239
|
}
|
|
4694
4240
|
});
|
|
4695
|
-
workCmd.command("link-session").description("Link the calling session to a work item that already exists").argument("<number>", "the item's number inside this factory").option("--session-id <id>", "session id to record as-is, skipping detection (used by the SessionStart hook)").option("--retry <count>", "resend after a failure that may pass, with backoff").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "
|
|
4241
|
+
workCmd.command("link-session").description("Link the calling session to a work item that already exists").argument("<number>", "the item's number inside this factory").option("--session-id <id>", "session id to record as-is, skipping detection (used by the SessionStart hook)").option("--retry <count>", "resend after a failure that may pass, with backoff").option("--no-lease", "link only \u2014 ask for no session lease (used by a host's own job)").option("--ended", "release this session's lease instead of taking or renewing one").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
|
|
4696
4242
|
"after",
|
|
4697
|
-
"\n outerlayer work link-session 42\n\nNames an item that already exists on the calling session \u2014 it never\ncreates one. Create
|
|
4243
|
+
"\n outerlayer work link-session 42\n\nNames an item that already exists on the calling session \u2014 it never\ncreates one. Create one with `outerlayer work build --local`. Linking\nthe same session to the same item twice is a no-op. Also takes or\nrenews that session's own 20-minute lease, unless --no-lease or\n--ended is given, or another worker already holds a live one.\n"
|
|
4698
4244
|
).action(async (number, opts) => {
|
|
4699
4245
|
warnIfApiKeyFlag(opts);
|
|
4700
|
-
const { runWorkLinkSession, WorkCommandError } = await import('./work-cmd-
|
|
4246
|
+
const { runWorkLinkSession, WorkCommandError } = await import('./work-cmd-QAUZ7MTD.js');
|
|
4701
4247
|
try {
|
|
4702
|
-
await runWorkLinkSession({ ...opts, number });
|
|
4248
|
+
await runWorkLinkSession({ ...opts, number, noLease: opts.lease === false });
|
|
4703
4249
|
} catch (err) {
|
|
4704
4250
|
if (err instanceof WorkCommandError) {
|
|
4705
4251
|
fail(err.message);
|
|
@@ -4708,18 +4254,61 @@ function registerWorkCommands(program2) {
|
|
|
4708
4254
|
throw err;
|
|
4709
4255
|
}
|
|
4710
4256
|
});
|
|
4711
|
-
workCmd.command("
|
|
4257
|
+
workCmd.command("comment").description("Post a comment on a work item's general thread or one of its criterion threads").requiredOption("--item <number>", "the item's number inside this factory").option("--criterion <id>", "the criterion thread to post on (default: the general thread)").option("--body <text>", "the comment's text").option("--artifact <id>", "artifact id \u2014 required with --attach, naming the criterion's new proof").option("--pass", "record a pass (a person's verdict; refused from an agent session or a key)").option("--fail", "record a fail (a person's verdict; refused from an agent session or a key)").option("--attach", "make --artifact the named criterion's proof").option("--ready", "hand the thread back to a person (refused from a person)").option("--session-id <id>", "session id to record as-is, skipping detection (used by the SessionStart hook)").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
|
|
4258
|
+
"after",
|
|
4259
|
+
`
|
|
4260
|
+
outerlayer work comment --item 7 --criterion AC-9-01 --body "fixed" --artifact 3f2a1c9d --attach
|
|
4261
|
+
|
|
4262
|
+
Posts one comment. Omitting --criterion posts on the general thread;
|
|
4263
|
+
naming one posts on that criterion's own thread. --pass/--fail are a
|
|
4264
|
+
person's verdict on the whole item, so only on the general thread: the
|
|
4265
|
+
gateway refuses them with --criterion. --attach makes --artifact the
|
|
4266
|
+
criterion's new proof; --ready hands the thread back to a person. At
|
|
4267
|
+
most one action flag may be given.
|
|
4268
|
+
`
|
|
4269
|
+
).action(async (opts) => {
|
|
4270
|
+
warnIfApiKeyFlag(opts);
|
|
4271
|
+
const { runWorkComment, WorkCommentCommandError } = await import('./work-comment-cmd-BNCSBTLH.js');
|
|
4272
|
+
const { WorkCommandError } = await import('./work-cmd-QAUZ7MTD.js');
|
|
4273
|
+
try {
|
|
4274
|
+
await runWorkComment(opts);
|
|
4275
|
+
} catch (err) {
|
|
4276
|
+
if (err instanceof WorkCommentCommandError || err instanceof WorkCommandError) {
|
|
4277
|
+
fail(err.message);
|
|
4278
|
+
return;
|
|
4279
|
+
}
|
|
4280
|
+
throw err;
|
|
4281
|
+
}
|
|
4282
|
+
});
|
|
4283
|
+
workCmd.command("threads").description("Read a work item's general thread and its criterion threads").requiredOption("--item <number>", "the item's number inside this factory").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
|
|
4284
|
+
"after",
|
|
4285
|
+
"\n outerlayer work threads --item 7\n\nPrints the item's general thread, then one thread per acceptance\ncriterion that carries proof or a comment, each with who is waiting\non whom next.\n"
|
|
4286
|
+
).action(async (opts) => {
|
|
4287
|
+
warnIfApiKeyFlag(opts);
|
|
4288
|
+
const { runWorkThreads, WorkCommentCommandError } = await import('./work-comment-cmd-BNCSBTLH.js');
|
|
4289
|
+
const { WorkCommandError } = await import('./work-cmd-QAUZ7MTD.js');
|
|
4290
|
+
try {
|
|
4291
|
+
await runWorkThreads(opts);
|
|
4292
|
+
} catch (err) {
|
|
4293
|
+
if (err instanceof WorkCommentCommandError || err instanceof WorkCommandError) {
|
|
4294
|
+
fail(err.message);
|
|
4295
|
+
return;
|
|
4296
|
+
}
|
|
4297
|
+
throw err;
|
|
4298
|
+
}
|
|
4299
|
+
});
|
|
4300
|
+
workCmd.command("remove").description("Withdraw a source's addition \u2014 never deletes the item").option("--item <number>", "the item's number inside this factory").option("--issue <number>", "issue number (default: named by --item instead)").option("--repo <owner/name>", "repository (default: the current checkout's remote)").requiredOption("--reason <text>", "why this source is no longer working on it").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
|
|
4712
4301
|
"after",
|
|
4713
4302
|
`
|
|
4714
4303
|
outerlayer work remove --item 42 --reason "done"
|
|
4715
4304
|
|
|
4716
4305
|
Withdraws the caller's own addition, recording the reason. Never
|
|
4717
|
-
deletes anything \u2014 the item is removed from
|
|
4306
|
+
deletes anything \u2014 the item is removed from Work only once no
|
|
4718
4307
|
live addition remains on it.
|
|
4719
4308
|
`
|
|
4720
4309
|
).action(async (opts) => {
|
|
4721
4310
|
warnIfApiKeyFlag(opts);
|
|
4722
|
-
const { runWorkRemove, WorkCommandError } = await import('./work-cmd-
|
|
4311
|
+
const { runWorkRemove, WorkCommandError } = await import('./work-cmd-QAUZ7MTD.js');
|
|
4723
4312
|
try {
|
|
4724
4313
|
await runWorkRemove(opts);
|
|
4725
4314
|
} catch (err) {
|
|
@@ -4730,9 +4319,9 @@ live addition remains on it.
|
|
|
4730
4319
|
throw err;
|
|
4731
4320
|
}
|
|
4732
4321
|
});
|
|
4733
|
-
workCmd.command("status").description("Show one item's stage, gate ledger, and additions").option("--item <number>", "the item's number inside this factory").option("--issue <number>", "issue number").option("--repo <owner/name>", "repository (default: the current checkout's remote)").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "
|
|
4322
|
+
workCmd.command("status").description("Show one item's stage, gate ledger, and additions").option("--item <number>", "the item's number inside this factory").option("--issue <number>", "issue number").option("--repo <owner/name>", "repository (default: the current checkout's remote)").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").action(async (opts) => {
|
|
4734
4323
|
warnIfApiKeyFlag(opts);
|
|
4735
|
-
const { runWorkStatus, WorkCommandError } = await import('./work-cmd-
|
|
4324
|
+
const { runWorkStatus, WorkCommandError } = await import('./work-cmd-QAUZ7MTD.js');
|
|
4736
4325
|
try {
|
|
4737
4326
|
await runWorkStatus(opts);
|
|
4738
4327
|
} catch (err) {
|
|
@@ -4743,9 +4332,9 @@ live addition remains on it.
|
|
|
4743
4332
|
throw err;
|
|
4744
4333
|
}
|
|
4745
4334
|
});
|
|
4746
|
-
workCmd.command("list").description("List live work items for this factory").option("--stage <spec|build|review>", "filter by stage").option("--section <attention|waiting|flowing>", "filter by Queue section").option("--repo <owner/name>", "repository (default: the current checkout's remote)").option("--unclaimed", "only items with no live
|
|
4335
|
+
workCmd.command("list").description("List live work items for this factory").option("--stage <spec|build|review>", "filter by stage").option("--section <attention|waiting|flowing>", "filter by Queue section").option("--repo <owner/name>", "repository (default: the current checkout's remote)").option("--unclaimed", "only items with no live lease").option("--claimed", "only items with a live lease, held by a host or a local session").option("--startable", "only items with an unused build request and no live lease").option("--needs <amend>", "only items with a thread waiting on an agent").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").action(async (opts) => {
|
|
4747
4336
|
warnIfApiKeyFlag(opts);
|
|
4748
|
-
const { runWorkList, WorkCommandError } = await import('./work-cmd-
|
|
4337
|
+
const { runWorkList, WorkCommandError } = await import('./work-cmd-QAUZ7MTD.js');
|
|
4749
4338
|
try {
|
|
4750
4339
|
await runWorkList(opts);
|
|
4751
4340
|
} catch (err) {
|
|
@@ -4756,12 +4345,12 @@ live addition remains on it.
|
|
|
4756
4345
|
throw err;
|
|
4757
4346
|
}
|
|
4758
4347
|
});
|
|
4759
|
-
workCmd.command("claim").description("Claim a work item under this host's lease before working on it").option("--item <number>", "the item's number inside this factory").option("--issue <number>", "issue number (default: named by --item instead)").option("--repo <owner/name>", "repository (default: the current checkout's remote)").requiredOption("--kind <implement|amend>", "what this host is doing with the item").option("--host <name>", "the claiming host (default: this machine's hostname)").option("--seconds <n>", "lease length in seconds, clamped to 900 (default: 900)").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "
|
|
4348
|
+
workCmd.command("claim").description("Claim a work item under this host's lease before working on it").option("--item <number>", "the item's number inside this factory").option("--issue <number>", "issue number (default: named by --item instead)").option("--repo <owner/name>", "repository (default: the current checkout's remote)").requiredOption("--kind <implement|amend>", "what this host is doing with the item").option("--host <name>", "the claiming host (default: this machine's hostname)").option("--seconds <n>", "lease length in seconds, clamped to 900 (default: 900)").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
|
|
4760
4349
|
"after",
|
|
4761
4350
|
"\n outerlayer work claim --item 42 --kind implement\n\nRecords a lease for this host. A live lease already held by a\ndifferent host is refused; claiming again under the same host extends\nit.\n"
|
|
4762
4351
|
).action(async (opts) => {
|
|
4763
4352
|
warnIfApiKeyFlag(opts);
|
|
4764
|
-
const { runWorkClaim, WorkCommandError } = await import('./work-claim-cmd-
|
|
4353
|
+
const { runWorkClaim, WorkCommandError } = await import('./work-claim-cmd-5F7VZ42N.js');
|
|
4765
4354
|
try {
|
|
4766
4355
|
await runWorkClaim(opts);
|
|
4767
4356
|
} catch (err) {
|
|
@@ -4772,12 +4361,12 @@ live addition remains on it.
|
|
|
4772
4361
|
throw err;
|
|
4773
4362
|
}
|
|
4774
4363
|
});
|
|
4775
|
-
workCmd.command("renew").description("Extend this host's lease on a work item").option("--item <number>", "the item's number inside this factory").option("--issue <number>", "issue number (default: named by --item instead)").option("--repo <owner/name>", "repository (default: the current checkout's remote)").option("--host <name>", "the claiming host (default: this machine's hostname)").option("--seconds <n>", "lease length in seconds, clamped to 900 (default: 900)").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "
|
|
4364
|
+
workCmd.command("renew").description("Extend this host's lease on a work item").option("--item <number>", "the item's number inside this factory").option("--issue <number>", "issue number (default: named by --item instead)").option("--repo <owner/name>", "repository (default: the current checkout's remote)").option("--host <name>", "the claiming host (default: this machine's hostname)").option("--seconds <n>", "lease length in seconds, clamped to 900 (default: 900)").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
|
|
4776
4365
|
"after",
|
|
4777
4366
|
"\n outerlayer work renew --item 42\n\nExtends this host's own live lease. Refused with a 410 if the lease\nhas expired, or a 403 if a different host holds it.\n"
|
|
4778
4367
|
).action(async (opts) => {
|
|
4779
4368
|
warnIfApiKeyFlag(opts);
|
|
4780
|
-
const { runWorkRenew, WorkCommandError } = await import('./work-claim-cmd-
|
|
4369
|
+
const { runWorkRenew, WorkCommandError } = await import('./work-claim-cmd-5F7VZ42N.js');
|
|
4781
4370
|
try {
|
|
4782
4371
|
await runWorkRenew(opts);
|
|
4783
4372
|
} catch (err) {
|
|
@@ -4789,14 +4378,14 @@ live addition remains on it.
|
|
|
4789
4378
|
}
|
|
4790
4379
|
});
|
|
4791
4380
|
workCmd.command("release").description("Release this host's lease on a work item").option("--item <number>", "the item's number inside this factory").option("--issue <number>", "issue number (default: named by --item instead)").option("--repo <owner/name>", "repository (default: the current checkout's remote)").option("--host <name>", "the claiming host (default: this machine's hostname)").option(
|
|
4792
|
-
"--outcome <ok|failed|timed_out|idle|lease_lost|interrupted|stopped>",
|
|
4381
|
+
"--outcome <ok|incomplete|failed|timed_out|idle|lease_lost|interrupted|stopped>",
|
|
4793
4382
|
"how the attempt ended (default: none)"
|
|
4794
|
-
).option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "
|
|
4383
|
+
).option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
|
|
4795
4384
|
"after",
|
|
4796
4385
|
"\n outerlayer work release --item 42 --outcome ok\n\nMarks this host's lease released, optionally with an outcome.\nReleasing an already-released lease is a no-op that returns the\nrecorded release time and outcome.\n"
|
|
4797
4386
|
).action(async (opts) => {
|
|
4798
4387
|
warnIfApiKeyFlag(opts);
|
|
4799
|
-
const { runWorkRelease, WorkCommandError } = await import('./work-claim-cmd-
|
|
4388
|
+
const { runWorkRelease, WorkCommandError } = await import('./work-claim-cmd-5F7VZ42N.js');
|
|
4800
4389
|
try {
|
|
4801
4390
|
await runWorkRelease(opts);
|
|
4802
4391
|
} catch (err) {
|
|
@@ -4807,12 +4396,12 @@ live addition remains on it.
|
|
|
4807
4396
|
throw err;
|
|
4808
4397
|
}
|
|
4809
4398
|
});
|
|
4810
|
-
workCmd.command("pr <number>").description("Declare that a pull request belongs to the work item this session is on").option("--repo <owner/name>", "repository (default: the current checkout's remote)").option("--session-id <id>", "session id to record as-is, skipping detection").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "
|
|
4399
|
+
workCmd.command("pr <number>").description("Declare that a pull request belongs to the work item this session is on").option("--repo <owner/name>", "repository (default: the current checkout's remote)").option("--session-id <id>", "session id to record as-is, skipping detection").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
|
|
4811
4400
|
"after",
|
|
4812
4401
|
"\n outerlayer work pr 481\n\nRun inside a session already on a work item. Records that pull\nrequest 481 in the checkout's repository belongs to that item \u2014 the\nsame fact a `pr-link` in the session's own transcript records on\nsync. Idempotent: declaring the same pull request twice is a no-op.\n"
|
|
4813
4402
|
).action(async (number, opts) => {
|
|
4814
4403
|
warnIfApiKeyFlag(opts);
|
|
4815
|
-
const { runWorkPr, WorkPrCommandError } = await import('./work-pr-cmd-
|
|
4404
|
+
const { runWorkPr, WorkPrCommandError } = await import('./work-pr-cmd-PV32ZLSI.js');
|
|
4816
4405
|
try {
|
|
4817
4406
|
await runWorkPr({ ...opts, number });
|
|
4818
4407
|
} catch (err) {
|
|
@@ -4823,17 +4412,35 @@ live addition remains on it.
|
|
|
4823
4412
|
throw err;
|
|
4824
4413
|
}
|
|
4825
4414
|
});
|
|
4415
|
+
workCmd.command("open-pr").description("Open, or edit, the pull request for the work item this session is on").requiredOption("--title <title>", "the pull request's title").requiredOption("--body-file <path>", "a file holding the pull request's description").option("--repo <owner/name>", "repository, on the gh path (default: the current checkout's remote)").option("--session-id <id>", "session id to record as-is, on the gh path, skipping detection").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
|
|
4416
|
+
"after",
|
|
4417
|
+
'\n outerlayer work open-pr --title "Add the thing" --body-file pr.md\n\nIn a host build, which holds an item key, the gateway opens the pull\nrequest as the factory\'s GitHub App, from the branch the claim named,\nor edits the one already open on that branch. No gh command runs.\nIn your own session it runs `gh pr create` (or `gh pr edit`) under your\nlogin, then declares the pull request on the item, as `work pr` does.\nRun it again after a change to the title or description.\n'
|
|
4418
|
+
).action(async (opts) => {
|
|
4419
|
+
warnIfApiKeyFlag(opts);
|
|
4420
|
+
const { runWorkOpenPr, WorkOpenPrCommandError } = await import('./work-open-pr-cmd-VFYZHIIW.js');
|
|
4421
|
+
const { WorkCommandError } = await import('./work-cmd-QAUZ7MTD.js');
|
|
4422
|
+
try {
|
|
4423
|
+
await runWorkOpenPr(opts);
|
|
4424
|
+
} catch (err) {
|
|
4425
|
+
if (err instanceof WorkOpenPrCommandError || err instanceof WorkCommandError) {
|
|
4426
|
+
fail(err.message);
|
|
4427
|
+
return;
|
|
4428
|
+
}
|
|
4429
|
+
throw err;
|
|
4430
|
+
}
|
|
4431
|
+
});
|
|
4826
4432
|
}
|
|
4827
4433
|
function registerRunnerCommands(program2) {
|
|
4828
4434
|
const runnerCmd = program2.command("runner").description("Take work from the implement and amend queues on this machine");
|
|
4829
4435
|
const configOption = "the cloud config file to run against (default: ~/.outerlayer/config.json)";
|
|
4830
4436
|
runnerCmd.command("start").description("Run the loop: claim work, run it, report, clean up, repeat").option("--config <path>", configOption).action(async (opts) => {
|
|
4831
|
-
const { defaultRunnerConfigPath, RunnerConfigError } = await import('./config-
|
|
4832
|
-
const { start, RunnerStartError } = await import('./loop-
|
|
4833
|
-
const { RunnerAlreadyRunningError } = await import('./pidfile-
|
|
4437
|
+
const { defaultRunnerConfigPath, RunnerConfigError } = await import('./config-XVJYZ4IQ.js');
|
|
4438
|
+
const { start, RunnerStartError } = await import('./loop-JFIBP4B7.js');
|
|
4439
|
+
const { RunnerAlreadyRunningError } = await import('./pidfile-UZRH774M.js');
|
|
4834
4440
|
const configPath = opts.config ?? defaultRunnerConfigPath();
|
|
4835
4441
|
try {
|
|
4836
|
-
|
|
4442
|
+
const { realUpdateDeps } = await import('./real-deps-SU24ZA2K.js');
|
|
4443
|
+
await start(configPath, { update: realUpdateDeps(), fetchImpl: fetch });
|
|
4837
4444
|
} catch (err) {
|
|
4838
4445
|
if (err instanceof RunnerConfigError || err instanceof RunnerAlreadyRunningError || err instanceof RunnerStartError) {
|
|
4839
4446
|
fail(err.message);
|
|
@@ -4843,89 +4450,214 @@ function registerRunnerCommands(program2) {
|
|
|
4843
4450
|
}
|
|
4844
4451
|
});
|
|
4845
4452
|
runnerCmd.command("check").description("Validate the config, the hooks and the key, and print the effective settings").option("--config <path>", configOption).action(async (opts) => {
|
|
4846
|
-
const { defaultRunnerConfigPath
|
|
4847
|
-
const {
|
|
4453
|
+
const { defaultRunnerConfigPath } = await import('./config-XVJYZ4IQ.js');
|
|
4454
|
+
const { runRunnerCheck } = await import('./check-7MFH6HNC.js');
|
|
4848
4455
|
const configPath = opts.config ?? defaultRunnerConfigPath();
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
} catch (err) {
|
|
4853
|
-
fail(err instanceof RunnerConfigError ? err.message : String(err));
|
|
4854
|
-
return;
|
|
4855
|
-
}
|
|
4856
|
-
const hookProblem = checkHooks2(config);
|
|
4857
|
-
if (hookProblem) {
|
|
4858
|
-
fail(`${configPath}: ${hookProblem}`);
|
|
4859
|
-
return;
|
|
4860
|
-
}
|
|
4861
|
-
const probe = await probeCredentials(config);
|
|
4862
|
-
if (!probe.ok && probe.fatal) {
|
|
4863
|
-
fail(probe.reason);
|
|
4456
|
+
const result = await runRunnerCheck(configPath);
|
|
4457
|
+
if (!result.ok) {
|
|
4458
|
+
fail(result.message);
|
|
4864
4459
|
return;
|
|
4865
4460
|
}
|
|
4866
4461
|
process.stdout.write(
|
|
4867
|
-
|
|
4868
|
-
` : `${C.yellow}!${C.reset} credentials unverified \xB7 ${
|
|
4462
|
+
result.unverifiedReason === void 0 ? `${C.green}\u2713${C.reset} credentials ok \xB7 ${result.summary}
|
|
4463
|
+
` : `${C.yellow}!${C.reset} credentials unverified \xB7 ${result.summary} \xB7 ${result.unverifiedReason}
|
|
4869
4464
|
`
|
|
4870
4465
|
);
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
`hook cleanup ${config.hooks.cleanup}`,
|
|
4882
|
-
`hook report ${config.hooks.report ?? "(none)"}`,
|
|
4883
|
-
`repos include ${config.repos?.include?.join(", ") ?? "(all)"}`,
|
|
4884
|
-
`repos exclude ${config.repos?.exclude?.join(", ") ?? "(none)"}`
|
|
4885
|
-
];
|
|
4886
|
-
process.stdout.write(`${lines.join("\n")}
|
|
4466
|
+
process.stdout.write(`${result.lines.join("\n")}
|
|
4467
|
+
`);
|
|
4468
|
+
});
|
|
4469
|
+
runnerCmd.command("install").description("Put this CLI into the runner's install layout, so the runner can update itself").option("--config <path>", configOption).action(async (opts) => {
|
|
4470
|
+
const { defaultRunnerConfigPath } = await import('./config-XVJYZ4IQ.js');
|
|
4471
|
+
const { runRunnerInstall, RunnerInstallError } = await import('./install-cmd-7WXOIFDO.js');
|
|
4472
|
+
const configPath = opts.config ?? defaultRunnerConfigPath();
|
|
4473
|
+
try {
|
|
4474
|
+
const result = runRunnerInstall({ configPath, entryPath: process.argv[1] ?? "", nodePath: process.execPath });
|
|
4475
|
+
process.stdout.write(`${C.green}\u2713${C.reset} installed ${result.version} for the runner; \`current\` names it
|
|
4887
4476
|
`);
|
|
4477
|
+
process.stdout.write(
|
|
4478
|
+
`
|
|
4479
|
+
Start the runner from the install layout by putting this line in the service unit, then restart the service:
|
|
4480
|
+
${C.yellow}${result.execStart}${C.reset}
|
|
4481
|
+
`
|
|
4482
|
+
);
|
|
4483
|
+
} catch (err) {
|
|
4484
|
+
if (err instanceof RunnerInstallError) {
|
|
4485
|
+
fail(err.message);
|
|
4486
|
+
return;
|
|
4487
|
+
}
|
|
4488
|
+
throw err;
|
|
4489
|
+
}
|
|
4888
4490
|
});
|
|
4889
|
-
runnerCmd.command("init").description("Write a runner block
|
|
4890
|
-
const { defaultRunnerConfigPath } = await import('./config-
|
|
4891
|
-
const { runRunnerInit, RunnerInitError } = await import('./init-
|
|
4491
|
+
runnerCmd.command("init").description("Write a runner block that fits this machine; on macOS, offers a Linux VM").option("--config <path>", configOption).option("--vm", "on macOS, run the runner in a Linux VM (Lima), with container builds").option("--no-vm", "on macOS, skip the VM and run builds as unisolated processes").option("--allow-process-builds", "where builds cannot be isolated, run them as processes anyway").addOption(new Option("--config-stdin", "the VM-side hand-off: read the whole config as JSON on stdin").hideHelp()).addOption(new Option("--vm-cli-package <path>", "install this packed CLI tarball in the VM instead of the published version").hideHelp()).action(async (opts) => {
|
|
4492
|
+
const { defaultRunnerConfigPath, RunnerConfigError } = await import('./config-XVJYZ4IQ.js');
|
|
4493
|
+
const { runRunnerInit, RunnerInitError } = await import('./init-55Y4LHWF.js');
|
|
4494
|
+
const { detectMachine } = await import('./machine-ZBPT2J3R.js');
|
|
4495
|
+
const { chooseVm, runVmInit, VmInitError } = await import('./init-XXEKWZQP.js');
|
|
4892
4496
|
const configPath = opts.config ?? defaultRunnerConfigPath();
|
|
4893
4497
|
try {
|
|
4894
|
-
const
|
|
4498
|
+
const machine = detectMachine();
|
|
4499
|
+
if (opts.configStdin === true) {
|
|
4500
|
+
const chunks = [];
|
|
4501
|
+
for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
|
|
4502
|
+
const result2 = runRunnerInit(configPath, { machine, configFromStdin: Buffer.concat(chunks).toString("utf8"), requireContainer: true });
|
|
4503
|
+
const { ensureHostKey: ensureHostKey2 } = await import('./host-key-KDYZ5ECC.js');
|
|
4504
|
+
const hostKey2 = await ensureHostKey2(configPath);
|
|
4505
|
+
process.stdout.write(`host key ${hostKey2.fingerprint}
|
|
4506
|
+
hooks: ${result2.hooks}
|
|
4507
|
+
`);
|
|
4508
|
+
return;
|
|
4509
|
+
}
|
|
4510
|
+
let declinedVm = opts.vm === false;
|
|
4511
|
+
const useVm = await chooseVm({
|
|
4512
|
+
platform: machine.platform,
|
|
4513
|
+
flag: opts.vm,
|
|
4514
|
+
isTTY: process.stdin.isTTY === true,
|
|
4515
|
+
ask: async () => {
|
|
4516
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
4517
|
+
const answer = await rl.question(
|
|
4518
|
+
"Run the runner in a Linux VM, so builds are isolated in containers? It needs Lima (brew install lima). Answer no to run builds as processes under your user, without isolation. [Y/n] "
|
|
4519
|
+
);
|
|
4520
|
+
rl.close();
|
|
4521
|
+
const yes = answer.trim() === "" || /^y(es)?$/i.test(answer.trim());
|
|
4522
|
+
declinedVm = !yes;
|
|
4523
|
+
return yes;
|
|
4524
|
+
}
|
|
4525
|
+
});
|
|
4526
|
+
if (useVm) {
|
|
4527
|
+
const { realLima } = await import('./lima-XN65D7GN.js');
|
|
4528
|
+
const { cpus, totalmem } = await import('os');
|
|
4529
|
+
const { resolvePackageVersion } = await import('./version-BWM6VLDI.js');
|
|
4530
|
+
const result2 = await runVmInit(configPath, {
|
|
4531
|
+
lima: realLima(),
|
|
4532
|
+
home: homeFromEnv(),
|
|
4533
|
+
hostCpus: cpus().length,
|
|
4534
|
+
hostMemoryBytes: totalmem(),
|
|
4535
|
+
version: resolvePackageVersion(),
|
|
4536
|
+
cliPackage: opts.vmCliPackage,
|
|
4537
|
+
progress: (line) => process.stdout.write(`${line}
|
|
4538
|
+
`)
|
|
4539
|
+
});
|
|
4540
|
+
for (const line of result2.lines) process.stdout.write(`${C.green}\u2713${C.reset} ${line}
|
|
4541
|
+
`);
|
|
4542
|
+
return;
|
|
4543
|
+
}
|
|
4544
|
+
const allowProcess = opts.allowProcessBuilds === true || machine.platform === "darwin" && declinedVm;
|
|
4545
|
+
const result = runRunnerInit(configPath, { machine, allowProcess });
|
|
4546
|
+
const { ensureHostKey } = await import('./host-key-KDYZ5ECC.js');
|
|
4547
|
+
const hostKey = await ensureHostKey(configPath);
|
|
4895
4548
|
process.stdout.write(`${C.green}\u2713${C.reset} wrote a runner block into ${result.configPath}
|
|
4896
4549
|
`);
|
|
4897
|
-
|
|
4898
|
-
|
|
4550
|
+
process.stdout.write(
|
|
4551
|
+
`${C.green}\u2713${C.reset} host key ${hostKey.fingerprint}: the runner key binds to it on its first claim
|
|
4552
|
+
`
|
|
4553
|
+
);
|
|
4554
|
+
process.stdout.write(`${C.green}\u2713${C.reset} hooks: ${result.hooks}${result.hooks === "builtin:vm" ? " (builds run in microVMs)" : result.hooks === "builtin:container" ? " (builds run in containers)" : " (builds run as processes)"}
|
|
4555
|
+
`);
|
|
4556
|
+
if (result.note !== void 0) process.stdout.write(`
|
|
4557
|
+
${C.yellow}${result.note}${C.reset}
|
|
4558
|
+
`);
|
|
4559
|
+
if (machine.platform === "darwin" && opts.vm === void 0) {
|
|
4560
|
+
process.stdout.write(`
|
|
4561
|
+
To isolate builds in containers instead, run ${C.yellow}outerlayer runner init --vm${C.reset}.
|
|
4562
|
+
`);
|
|
4563
|
+
}
|
|
4564
|
+
const { runRunnerInstall, RunnerInstallError } = await import('./install-cmd-7WXOIFDO.js');
|
|
4565
|
+
let execStart;
|
|
4566
|
+
try {
|
|
4567
|
+
const installed = runRunnerInstall({ configPath, entryPath: process.argv[1] ?? "", nodePath: process.execPath });
|
|
4568
|
+
execStart = installed.execStart;
|
|
4569
|
+
process.stdout.write(`${C.green}\u2713${C.reset} installed ${installed.version} for the runner, so it can update itself
|
|
4570
|
+
`);
|
|
4571
|
+
} catch (installErr) {
|
|
4572
|
+
if (!(installErr instanceof RunnerInstallError)) throw installErr;
|
|
4573
|
+
process.stdout.write(`
|
|
4574
|
+
${C.yellow}The runner will not update itself: ${installErr.message}${C.reset}
|
|
4899
4575
|
`);
|
|
4900
4576
|
}
|
|
4901
4577
|
process.stdout.write(
|
|
4902
4578
|
`
|
|
4903
4579
|
Next:
|
|
4904
4580
|
1. Edit the command lines in ${result.configPath} to start your agent.
|
|
4905
|
-
2.
|
|
4906
|
-
3. ${C.yellow}outerlayer runner
|
|
4907
|
-
|
|
4908
|
-
|
|
4581
|
+
2. ${C.yellow}outerlayer runner check${C.reset}
|
|
4582
|
+
3. ${C.yellow}outerlayer runner start${C.reset}
|
|
4583
|
+
` + (execStart === void 0 ? "" : ` To run it as a service that updates itself, start it with this line in the unit:
|
|
4584
|
+
${C.yellow}${execStart}${C.reset}
|
|
4585
|
+
`)
|
|
4909
4586
|
);
|
|
4910
4587
|
} catch (err) {
|
|
4911
|
-
if (err instanceof RunnerInitError) {
|
|
4588
|
+
if (err instanceof RunnerInitError || err instanceof VmInitError || err instanceof RunnerConfigError) {
|
|
4912
4589
|
fail(err.message);
|
|
4913
4590
|
return;
|
|
4914
4591
|
}
|
|
4915
4592
|
throw err;
|
|
4916
4593
|
}
|
|
4917
4594
|
});
|
|
4595
|
+
runnerCmd.command("image").description("Build a repository's image from its default branch's devcontainer file").requiredOption("--repository <owner/name>", "the repository to build the image for").option("--remote <url>", "where to fetch from (default: the repository's GitHub URL)").option("--config <path>", configOption).option("--json", "machine-readable JSON output").addHelpText(
|
|
4596
|
+
"after",
|
|
4597
|
+
"\nFetches the head of the default branch into the runner's cache, reads its\n.devcontainer/devcontainer.json, and builds the image with the devcontainer CLI. Settings\nthat reach the host or widen the container's privileges are ignored and listed. The\nimage holds this CLI and a pinned Claude Code. Nothing runs inside the image.\n"
|
|
4598
|
+
).action(async (opts) => {
|
|
4599
|
+
const { defaultRunnerConfigPath } = await import('./config-XVJYZ4IQ.js');
|
|
4600
|
+
const { buildRepositoryImage, RecipeError } = await import('./build-TZUUTRGH.js');
|
|
4601
|
+
const { currentCliBuild } = await import('./cli-build-K56DK4DS.js');
|
|
4602
|
+
const { createDockerHost } = await import('./docker-NEGDLU6D.js');
|
|
4603
|
+
try {
|
|
4604
|
+
const result = await buildRepositoryImage(
|
|
4605
|
+
{
|
|
4606
|
+
repository: opts.repository,
|
|
4607
|
+
remote: opts.remote,
|
|
4608
|
+
configPath: opts.config ?? defaultRunnerConfigPath(),
|
|
4609
|
+
cli: currentCliBuild()
|
|
4610
|
+
},
|
|
4611
|
+
{ host: createDockerHost() }
|
|
4612
|
+
);
|
|
4613
|
+
if (opts.json) {
|
|
4614
|
+
writeJson(result);
|
|
4615
|
+
return;
|
|
4616
|
+
}
|
|
4617
|
+
const { report } = result;
|
|
4618
|
+
const what = result.reused ? "reused" : result.toolsBuilt && !result.recipeBuilt ? "rebuilt the tools layer of" : "built";
|
|
4619
|
+
process.stdout.write(`${C.green}\u2713${C.reset} ${what} ${report.tag}
|
|
4620
|
+
`);
|
|
4621
|
+
process.stdout.write(` recipe: ${report.recipe} at ${report.sourceCommit.slice(0, 12)} on ${report.defaultBranch}
|
|
4622
|
+
`);
|
|
4623
|
+
if (result.checkedCommit !== report.sourceCommit) {
|
|
4624
|
+
process.stdout.write(` checked: ${result.checkedCommit.slice(0, 12)}, the recipe has not changed since
|
|
4625
|
+
`);
|
|
4626
|
+
}
|
|
4627
|
+
process.stdout.write(` record: ${result.recordDir}
|
|
4628
|
+
`);
|
|
4629
|
+
if (report.ignored.length > 0) {
|
|
4630
|
+
process.stdout.write(` ignored:
|
|
4631
|
+
`);
|
|
4632
|
+
for (const { property, source } of report.ignored) process.stdout.write(` ${property} (from ${source})
|
|
4633
|
+
`);
|
|
4634
|
+
}
|
|
4635
|
+
if (result.removed.length > 0) process.stdout.write(` pruned ${result.removed.length} older image(s)
|
|
4636
|
+
`);
|
|
4637
|
+
} catch (err) {
|
|
4638
|
+
if (err instanceof RecipeError) {
|
|
4639
|
+
if (opts.json) {
|
|
4640
|
+
writeJson({ error: { code: err.code, message: err.message, ...err.logPath ? { logPath: err.logPath } : {} } });
|
|
4641
|
+
process.exitCode = 1;
|
|
4642
|
+
return;
|
|
4643
|
+
}
|
|
4644
|
+
fail(`${err.code}: ${err.message}`);
|
|
4645
|
+
return;
|
|
4646
|
+
}
|
|
4647
|
+
throw err;
|
|
4648
|
+
}
|
|
4649
|
+
});
|
|
4918
4650
|
runnerCmd.command("stop").description("Take no more work; wait for running jobs to finish, then exit").option("--config <path>", "the cloud config file naming the runner to stop").option("--now", "end running jobs immediately, with outcome stopped").action(async (opts) => {
|
|
4919
|
-
const { defaultRunnerConfigPath } = await import('./config-
|
|
4920
|
-
const { requestStop } = await import('./loop-
|
|
4651
|
+
const { defaultRunnerConfigPath } = await import('./config-XVJYZ4IQ.js');
|
|
4652
|
+
const { requestStop } = await import('./loop-JFIBP4B7.js');
|
|
4921
4653
|
const configPath = opts.config ?? defaultRunnerConfigPath();
|
|
4922
4654
|
await requestStop(configPath, Boolean(opts.now));
|
|
4923
4655
|
});
|
|
4924
4656
|
runnerCmd.command("status").description("List running jobs; with --recent, finished ones too").option("--config <path>", "the cloud config file naming the runner to inspect").option("--recent", "also list finished jobs, newest first, capped at 20").option("--json", "machine-readable JSON output").action(async (opts) => {
|
|
4925
|
-
const { defaultRunnerConfigPath, loadRunnerConfig } = await import('./config-
|
|
4926
|
-
const { jobsDir } = await import('./paths-
|
|
4927
|
-
const { listJobFiles, runnerProcessState } = await import('./loop-
|
|
4928
|
-
const { renderStatus } = await import('./status-
|
|
4657
|
+
const { defaultRunnerConfigPath, loadRunnerConfig } = await import('./config-XVJYZ4IQ.js');
|
|
4658
|
+
const { jobsDir } = await import('./paths-OYKMVYJP.js');
|
|
4659
|
+
const { listJobFiles, runnerProcessState } = await import('./loop-JFIBP4B7.js');
|
|
4660
|
+
const { renderStatus } = await import('./status-UJXWZRN5.js');
|
|
4929
4661
|
const path2 = await import('path');
|
|
4930
4662
|
const configPath = opts.config ?? defaultRunnerConfigPath();
|
|
4931
4663
|
const all = listJobFiles(configPath);
|
|
@@ -4944,7 +4676,7 @@ Next:
|
|
|
4944
4676
|
configPath,
|
|
4945
4677
|
process: processState,
|
|
4946
4678
|
concurrency,
|
|
4947
|
-
jobs: all.map(({ dirName, job }) => ({ ...job, jobDir: path2.join(jobsDir(configPath), dirName) }))
|
|
4679
|
+
jobs: all.map(({ dirName, job }) => ({ ...withoutItemKey(job), jobDir: path2.join(jobsDir(configPath), dirName) }))
|
|
4948
4680
|
});
|
|
4949
4681
|
return;
|
|
4950
4682
|
}
|
|
@@ -4962,9 +4694,9 @@ Next:
|
|
|
4962
4694
|
);
|
|
4963
4695
|
});
|
|
4964
4696
|
runnerCmd.command("logs").argument("<item>", "the work item's number in the factory").description("Print the newest attempt's log for one item").option("--config <path>", "the cloud config file naming the runner to inspect").option("-f, --follow", "keep printing as the log grows").action(async (itemArg, opts) => {
|
|
4965
|
-
const { defaultRunnerConfigPath } = await import('./config-
|
|
4966
|
-
const { listJobFiles } = await import('./loop-
|
|
4967
|
-
const { newestAttempt, logPathFor, tail, RunnerLogsError } = await import('./logs-
|
|
4697
|
+
const { defaultRunnerConfigPath } = await import('./config-XVJYZ4IQ.js');
|
|
4698
|
+
const { listJobFiles } = await import('./loop-JFIBP4B7.js');
|
|
4699
|
+
const { newestAttempt, logPathFor, tail, RunnerLogsError } = await import('./logs-7RYUH5A6.js');
|
|
4968
4700
|
const configPath = opts.config ?? defaultRunnerConfigPath();
|
|
4969
4701
|
const item = parseTrackerNumber(itemArg);
|
|
4970
4702
|
if (item === void 0) {
|
|
@@ -5183,11 +4915,11 @@ ${C.dim}${file.path}${C.reset}
|
|
|
5183
4915
|
}
|
|
5184
4916
|
function registerMcpCommands(program2) {
|
|
5185
4917
|
const mcpCmd = program2.command("mcp").description("Configure MCP clients against the OuterLayer gateway");
|
|
5186
|
-
mcpCmd.command("install").description("Write or update the OuterLayer entry in .mcp.json").option("--transport <stdio|http>", "how the client connects (default: stdio)").option("--command <exe>", "stdio only: executable the client spawns (default: outerlayer)").option("--url <url>", "gateway MCP endpoint (defaults to the gateway you logged in to)").option("--name <name>", 'mcpServers key to write under (default: "outerlayer")').option("--dir <path>", "repo root to write into (default: cwd)").option("--app-id <uuid>", "self-host only:
|
|
4918
|
+
mcpCmd.command("install").description("Write or update the OuterLayer entry in .mcp.json").option("--transport <stdio|http>", "how the client connects (default: stdio)").option("--command <exe>", "stdio only: executable the client spawns (default: outerlayer)").option("--url <url>", "gateway MCP endpoint (defaults to the gateway you logged in to)").option("--name <name>", 'mcpServers key to write under (default: "outerlayer")').option("--dir <path>", "repo root to write into (default: cwd)").option("--app-id <uuid>", "self-host only: factory id sent as X-Outerlayer-App-Id").option("--json", "machine-readable JSON output").addHelpText(
|
|
5187
4919
|
"after",
|
|
5188
4920
|
"\nThe API key is never a flag and never written to .mcp.json as a literal value.\n stdio (default): the client spawns `outerlayer mcp serve`, which reads the key\n from ~/.outerlayer/config.json (or OUTERLAYER_API_KEY) on every connect, so a\n rotated key is picked up without rewriting the entry.\n http: the entry carries the ${OUTERLAYER_API_KEY} placeholder, which the client\n resolves from the environment it was launched with.\n\n--command: use an absolute or ${HOME}-relative path when the CLI is not on PATH.\n--url: with stdio the endpoint resolves from OUTERLAYER_URL / the config file at\nconnect time; for self-host pass your gateway-node origin + /v1/mcp.\nSelf-host deployments also need --app-id: without a key service to resolve the\ncaller from the bearer token, the gateway answers 401 without that header.\n"
|
|
5189
4921
|
).action(async (opts) => {
|
|
5190
|
-
const { runMcpInstall, handleMcpInstallError, McpInstallError } = await import('./mcp-install-cmd-
|
|
4922
|
+
const { runMcpInstall, handleMcpInstallError, McpInstallError } = await import('./mcp-install-cmd-RLK4SO3N.js');
|
|
5191
4923
|
try {
|
|
5192
4924
|
if (opts.transport !== void 0 && opts.transport !== "stdio" && opts.transport !== "http") {
|
|
5193
4925
|
throw new McpInstallError(`--transport must be "stdio" or "http", got "${opts.transport}"`);
|
|
@@ -5205,11 +4937,11 @@ function registerMcpCommands(program2) {
|
|
|
5205
4937
|
handleMcpInstallError(err);
|
|
5206
4938
|
}
|
|
5207
4939
|
});
|
|
5208
|
-
mcpCmd.command("serve").description("Run a stdio MCP server that bridges to the OuterLayer gateway").option("--url <url>", "gateway MCP endpoint (default: OUTERLAYER_URL or config + /v1/mcp)").option("--app-id <uuid>", "
|
|
4940
|
+
mcpCmd.command("serve").description("Run a stdio MCP server that bridges to the OuterLayer gateway").option("--url <url>", "gateway MCP endpoint (default: OUTERLAYER_URL or config + /v1/mcp)").option("--app-id <uuid>", "factory id sent as X-Outerlayer-App-Id (default: config)").addHelpText(
|
|
5209
4941
|
"after",
|
|
5210
4942
|
"\nWhat the stdio .mcp.json entry spawns. Reads newline-delimited JSON-RPC 2.0 on\nstdin and writes responses on stdout. The API key comes from OUTERLAYER_API_KEY\nor ~/.outerlayer/config.json at start-up; with neither the command exits 1 and\nprints why, which the MCP client shows as the connect error.\n"
|
|
5211
4943
|
).action(async (opts) => {
|
|
5212
|
-
const { runMcpServe, handleMcpServeError } = await import('./mcp-serve-cmd-
|
|
4944
|
+
const { runMcpServe, handleMcpServeError } = await import('./mcp-serve-cmd-N6UD2KBX.js');
|
|
5213
4945
|
try {
|
|
5214
4946
|
await runMcpServe({ url: opts.url, appId: opts.appId });
|
|
5215
4947
|
} catch (err) {
|
|
@@ -5220,7 +4952,7 @@ function registerMcpCommands(program2) {
|
|
|
5220
4952
|
function registerImportCommands(program2) {
|
|
5221
4953
|
const importCmd = program2.command("import").description("Import an existing config format into .outerlayer/");
|
|
5222
4954
|
importCmd.command("ruler").description("Port a .ruler/ tree into the equivalent .outerlayer/ tree").option("--dir <path>", "repo root to read from (default: cwd)").option("--json", "machine-readable JSON output").addHelpText("after", "\nRuler: https://github.com/intellectronica/ruler. Mostly a rename.\n").action(async (opts) => {
|
|
5223
|
-
const { runImportRuler, ImportRulerError } = await import('./import-ruler-cmd-
|
|
4955
|
+
const { runImportRuler, ImportRulerError } = await import('./import-ruler-cmd-GUHL6IY5.js');
|
|
5224
4956
|
try {
|
|
5225
4957
|
const result = runImportRuler({ cwd: opts.dir, json: opts.json });
|
|
5226
4958
|
if (result.exitCode !== 0) process.exitCode = result.exitCode;
|
|
@@ -5232,11 +4964,11 @@ function registerImportCommands(program2) {
|
|
|
5232
4964
|
throw err;
|
|
5233
4965
|
}
|
|
5234
4966
|
});
|
|
5235
|
-
importCmd.command("capture").description("Install the
|
|
4967
|
+
importCmd.command("capture").description("Install the maintained skills into .outerlayer/").option("--dir <path>", "repo root to write into (default: cwd)").option("--json", "machine-readable JSON output").addHelpText(
|
|
5236
4968
|
"after",
|
|
5237
|
-
"\
|
|
4969
|
+
"\nInstalls the four maintained skills (emitting-evidence, amend,\nreporting-findings, outerlayer) and an AGENTS.md snippet teaching agents to run\n`emit artifact`. A skill that differs from this CLI's bundle is rewritten to it, and the output says so.\n"
|
|
5238
4970
|
).action(async (opts) => {
|
|
5239
|
-
const { runImportCapture, ImportCaptureError } = await import('./import-capture-cmd-
|
|
4971
|
+
const { runImportCapture, ImportCaptureError } = await import('./import-capture-cmd-AVZ4VIVJ.js');
|
|
5240
4972
|
try {
|
|
5241
4973
|
runImportCapture({ cwd: opts.dir, json: opts.json });
|
|
5242
4974
|
} catch (err) {
|
|
@@ -5274,15 +5006,21 @@ function registerContextCommands(program2) {
|
|
|
5274
5006
|
const contextCmd = program2.command("context").description("Compile and materialize agent context");
|
|
5275
5007
|
contextCmd.command("emit").description("Compile .outerlayer/ into each configured target tool's native files").option("--check", "compare against disk, write nothing, exit 1 on drift (CI mode)").option("--dir <path>", "repo root to read from (default: cwd)").option("--json", "machine-readable JSON output").addHelpText(
|
|
5276
5008
|
"after",
|
|
5277
|
-
'\nTargets come from .outerlayer/config.json only; there is no --target flag.\nMissing or empty config: {"targets": [...]}, e.g. {"targets": ["claude-code"]}.\nKnown targets: claude-code, cursor, codex, copilot, factory.\n\nOrphan detection (--check) only covers header-carrying outputs: every markdown\ntarget file gets a generated-by comment, but .mcp.json and .cursor/mcp.json are\nJSON with no comment syntax, so they can never be flagged as orphaned; missing\nand content drift still apply to them.\n'
|
|
5009
|
+
'\nTargets come from .outerlayer/config.json only; there is no --target flag.\nMissing or empty config: {"targets": [...]}, e.g. {"targets": ["claude-code"]}.\nKnown targets: claude-code, cursor, codex, copilot, factory.\n\nThe maintained skills (emitting-evidence, amend, reporting-findings, outerlayer)\nfollow this CLI: emit rewrites one that differs from its bundle, and --check reports it as\ndrift. Templates (spec, writing-specs, build) are never rewritten or reported.\n\nOrphan detection (--check) only covers header-carrying outputs: every markdown\ntarget file gets a generated-by comment, but .mcp.json and .cursor/mcp.json are\nJSON with no comment syntax, so they can never be flagged as orphaned; missing\nand content drift still apply to them.\n'
|
|
5278
5010
|
).action(async (opts) => {
|
|
5279
5011
|
await compileContext({ dir: opts.dir, check: opts.check, json: opts.json });
|
|
5280
5012
|
});
|
|
5013
|
+
contextCmd.command("adopt <stagedDir>", { hidden: true }).description("Copy context the runner staged into this checkout (container builds)").option("--json", "machine-readable JSON output").action(async (stagedDir) => {
|
|
5014
|
+
const { runContextAdopt } = await import('./context-adopt-UMA4O5NY.js');
|
|
5015
|
+
const { exitCode, stdout } = runContextAdopt(stagedDir, process.cwd());
|
|
5016
|
+
process.stdout.write(stdout);
|
|
5017
|
+
process.exitCode = exitCode;
|
|
5018
|
+
});
|
|
5281
5019
|
contextCmd.command("materialize").description("Materialize this repository's governed context from its control plane").option("--json", "machine-readable JSON output").addHelpText(
|
|
5282
5020
|
"after",
|
|
5283
5021
|
"\nThe same flow SessionStart runs automatically, exposed for a manual retry or a\nfirst check outside a session. Writes AGENTS.md, .claude/**, and .outerlayer/**.\n"
|
|
5284
5022
|
).action(async (opts) => {
|
|
5285
|
-
const { materializeContext } = await import('./context-materialize-
|
|
5023
|
+
const { materializeContext } = await import('./context-materialize-G2FUFC72.js');
|
|
5286
5024
|
const result = await materializeContext({ cwd: process.cwd(), sessionId: `manual-${Date.now()}` });
|
|
5287
5025
|
const output = formatContextMaterializeOutput(result);
|
|
5288
5026
|
if (output.exitCode !== void 0) process.exitCode = output.exitCode;
|