@outerlayer/cli 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +124 -0
- package/README.md +226 -59
- 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-WQP2CZDW.js +94 -0
- package/dist/chunk-2E54Z3P5.js +232 -0
- package/dist/chunk-3TT7RQYB.js +55 -0
- package/dist/{chunk-U32VSRLO.js → chunk-3YPKXE5L.js} +90 -632
- package/dist/{chunk-Q6CL5THG.js → chunk-5DLBAXY5.js} +208 -61
- package/dist/{chunk-R5KBGVII.js → chunk-5QRP3MVS.js} +30 -14
- package/dist/chunk-5Y3QQRUH.js +300 -0
- package/dist/chunk-6Y63SXFO.js +383 -0
- package/dist/chunk-774HEQL6.js +178 -0
- package/dist/chunk-7A6RVXPZ.js +2233 -0
- package/dist/chunk-7YIOOFVJ.js +71 -0
- 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-CBZB6XY6.js +102 -0
- package/dist/{chunk-65WXAANR.js → chunk-DVDEBNQJ.js} +17 -2
- package/dist/{work-pr-cmd-AZQPWH4H.js → chunk-DVYHCMB2.js} +9 -26
- 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/chunk-FAJSS2WN.js +1357 -0
- package/dist/chunk-FS2VYGZT.js +1764 -0
- package/dist/chunk-FV2CCGXK.js +9 -0
- package/dist/{chunk-4TNZMO7V.js → chunk-G7FSV7JX.js} +3452 -751
- package/dist/{emit-cmd-TWSYYEZI.js → chunk-H5NCNVDA.js} +68 -17
- package/dist/chunk-HE2D4EY5.js +83 -0
- package/dist/chunk-L2DBRKRA.js +94 -0
- package/dist/chunk-LF6MLJCY.js +38 -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-NYC54VBD.js +1084 -0
- package/dist/chunk-OGFGZQA3.js +23 -0
- package/dist/chunk-QOZKPLVJ.js +226 -0
- package/dist/chunk-RA3O54FC.js +30 -0
- package/dist/chunk-TBT347UY.js +41 -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-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-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-NHUXABYH.js} +590 -876
- package/dist/{paths-D2VGWWFI.js → cli-build-K56DK4DS.js} +1 -1
- package/dist/config-XVJYZ4IQ.js +9 -0
- package/dist/connect-cmd-OOG4TPU7.js +16 -0
- package/dist/context-adopt-UMA4O5NY.js +105 -0
- package/dist/context-materialize-G2FUFC72.js +19 -0
- package/dist/dist-RW4UPPC2.js +6 -0
- package/dist/docker-NEGDLU6D.js +7 -0
- package/dist/doctor-53F2PYY7.js +43 -0
- package/dist/doctor-UFVL4PZY.js +426 -0
- package/dist/{emit-artifact-cmd-UQVT3OKW.js → emit-artifact-cmd-KTYW2TN2.js} +24 -20
- package/dist/emit-cmd-DCERAU27.js +9 -0
- package/dist/emit-criteria-cmd-VAUUSAYD.js +166 -0
- package/dist/{emit-finding-cmd-FDOU2OPD.js → emit-finding-cmd-UR4ID45Z.js} +34 -31
- package/dist/{emit-result-cmd-H2T4C2K3.js → emit-result-cmd-FDTUKGKE.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-DXBCOAJK.js +36 -0
- package/dist/init-YEXK35CF.js +221 -0
- package/dist/init-cmd-NHR7PRSQ.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-VZOLL4WQ.js +43 -0
- package/dist/machine-WSG52J75.js +35 -0
- package/dist/mcp-install-cmd-RLK4SO3N.js +9 -0
- package/dist/{mcp-serve-cmd-57EZZOTL.js → mcp-serve-cmd-JPTP5FMS.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-2UZKKNB7.js} +30 -13
- package/dist/{statusline-fast-3C5OXHDD.js → statusline-fast-SVTMBMD7.js} +3 -2
- package/dist/sync-cmd-VPPYCNNM.js +28 -0
- package/dist/version-BWM6VLDI.js +6 -0
- package/dist/{watch-3DTPJETH.js → watch-5C4ZBBGO.js} +19 -8
- package/dist/{work-claim-cmd-5AUNCLS2.js → work-claim-cmd-LSO2B2EX.js} +33 -20
- package/dist/work-cmd-UOQO2AD4.js +17 -0
- package/dist/work-comment-cmd-WF3EOLAE.js +106 -0
- package/dist/{work-launch-LT663PB3.js → work-launch-YI4CJDAP.js} +1 -1
- package/dist/work-open-pr-cmd-4SSQZMYG.js +156 -0
- package/dist/work-pr-cmd-6AN6RRQJ.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 +99 -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-NYC54VBD.js';
|
|
4
|
+
import './chunk-3TT7RQYB.js';
|
|
5
|
+
import './chunk-F47JBIAW.js';
|
|
6
|
+
import './chunk-BKO6JEZI.js';
|
|
7
|
+
import './chunk-5DLBAXY5.js';
|
|
8
|
+
import './chunk-BFESLKPP.js';
|
|
9
|
+
import './chunk-6Y63SXFO.js';
|
|
10
|
+
import './chunk-G7FSV7JX.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
|
+
}
|
|
3445
|
+
}
|
|
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
|
+
}
|
|
3414
3458
|
}
|
|
3415
|
-
if (
|
|
3416
|
-
rename(join(staged, "dist", "index.js"), bin);
|
|
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-OOG4TPU7.js');
|
|
3796
|
+
const { resolveRepoIdentity } = await import('./dist-RW4UPPC2.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-NHR7PRSQ.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;
|
|
4425
3868
|
});
|
|
4426
|
-
program2.command("
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
const
|
|
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-OOG4TPU7.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;
|
|
3879
|
+
});
|
|
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-VPPYCNNM.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-5C4ZBBGO.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-FDTUKGKE.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,12 @@ 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("--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>", "
|
|
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("--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
4078
|
"after",
|
|
4577
4079
|
'\n outerlayer emit artifact shot.png --caption "the dot row, current dot marked" --for LOGIN-01\n\nInside a recorded Claude Code session the artifact spools locally and ships,\nbound to that session, on the next `outerlayer sync` \u2014 but only if that\nsession was launched with OUTERLAYER_WORK; without it nothing the session\nproduces may leave the machine, and the command refuses rather than queue an\nartifact that can never ship. Otherwise it uploads immediately, anchored to a\npull request (--pr, or CI\'s PR context) or to the current git checkout. With\nnothing to attach it to, the command refuses.\n\n--replaces: an already-uploaded target is marked superseded and hidden\neverywhere, bytes and deep link untouched; a still-queued one is cancelled\nlocally so it never uploads. One correction can retire several duplicates.\n'
|
|
4578
4080
|
).action(async (file, opts) => {
|
|
4579
4081
|
warnIfApiKeyFlag(opts);
|
|
4580
|
-
const { runEmitArtifact, EmitArtifactError } = await import('./emit-artifact-cmd-
|
|
4082
|
+
const { runEmitArtifact, EmitArtifactError } = await import('./emit-artifact-cmd-KTYW2TN2.js');
|
|
4581
4083
|
let pr;
|
|
4582
4084
|
if (opts.pr !== void 0) {
|
|
4583
4085
|
pr = parseTrackerNumber(opts.pr);
|
|
@@ -4606,28 +4108,24 @@ function registerEmitCommands(program2) {
|
|
|
4606
4108
|
throw err;
|
|
4607
4109
|
}
|
|
4608
4110
|
});
|
|
4609
|
-
emitCmd.command("finding").description("Record one finding:
|
|
4111
|
+
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
4112
|
"after",
|
|
4611
|
-
'\n outerlayer emit finding --id
|
|
4113
|
+
'\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
4114
|
).action(async (opts) => {
|
|
4613
4115
|
warnIfApiKeyFlag(opts);
|
|
4614
|
-
const { runEmitFinding, EmitFindingError } = await import('./emit-finding-cmd-
|
|
4116
|
+
const { runEmitFinding, refuseRemovedFindingFlags, EmitFindingError } = await import('./emit-finding-cmd-UR4ID45Z.js');
|
|
4615
4117
|
try {
|
|
4118
|
+
refuseRemovedFindingFlags(opts);
|
|
4616
4119
|
const result = await runEmitFinding({
|
|
4617
4120
|
id: opts.id,
|
|
4618
|
-
|
|
4121
|
+
category: opts.category,
|
|
4619
4122
|
title: opts.title,
|
|
4620
4123
|
file: opts.file,
|
|
4621
4124
|
line: opts.line,
|
|
4622
|
-
severity: opts.severity,
|
|
4623
|
-
kind: opts.kind,
|
|
4624
|
-
verdict: opts.verdict,
|
|
4625
|
-
fixed: opts.fixed,
|
|
4626
4125
|
rulePath: opts.rulePath,
|
|
4627
4126
|
ruleLine: opts.ruleLine,
|
|
4628
4127
|
ruleQuote: opts.ruleQuote,
|
|
4629
4128
|
ruleRelation: opts.ruleRelation,
|
|
4630
|
-
source: opts.source,
|
|
4631
4129
|
where: opts.where,
|
|
4632
4130
|
traceId: opts.traceId,
|
|
4633
4131
|
turn: opts.turn,
|
|
@@ -4649,12 +4147,12 @@ function registerEmitCommands(program2) {
|
|
|
4649
4147
|
throw err;
|
|
4650
4148
|
}
|
|
4651
4149
|
});
|
|
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>", "
|
|
4150
|
+
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
4151
|
"after",
|
|
4654
4152
|
"\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
4153
|
).action(async (file, opts) => {
|
|
4656
4154
|
warnIfApiKeyFlag(opts);
|
|
4657
|
-
const { runEmitFindings, EmitFindingError } = await import('./emit-finding-cmd-
|
|
4155
|
+
const { runEmitFindings, EmitFindingError } = await import('./emit-finding-cmd-UR4ID45Z.js');
|
|
4658
4156
|
try {
|
|
4659
4157
|
const result = await runEmitFindings({
|
|
4660
4158
|
file,
|
|
@@ -4673,17 +4171,41 @@ function registerEmitCommands(program2) {
|
|
|
4673
4171
|
throw err;
|
|
4674
4172
|
}
|
|
4675
4173
|
});
|
|
4174
|
+
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(
|
|
4175
|
+
"after",
|
|
4176
|
+
'\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, 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'
|
|
4177
|
+
).action(async (file, opts) => {
|
|
4178
|
+
warnIfApiKeyFlag(opts);
|
|
4179
|
+
const { runEmitCriteria, EmitCriteriaError } = await import('./emit-criteria-cmd-VAUUSAYD.js');
|
|
4180
|
+
try {
|
|
4181
|
+
const result = await runEmitCriteria({
|
|
4182
|
+
file,
|
|
4183
|
+
item: opts.item,
|
|
4184
|
+
json: opts.json,
|
|
4185
|
+
url: opts.url,
|
|
4186
|
+
apiKey: opts.apiKey,
|
|
4187
|
+
appId: opts.appId
|
|
4188
|
+
});
|
|
4189
|
+
if (result.exitCode !== 0) process.exitCode = result.exitCode;
|
|
4190
|
+
} catch (err) {
|
|
4191
|
+
if (err instanceof EmitCriteriaError) {
|
|
4192
|
+
fail(err.message);
|
|
4193
|
+
return;
|
|
4194
|
+
}
|
|
4195
|
+
throw err;
|
|
4196
|
+
}
|
|
4197
|
+
});
|
|
4676
4198
|
}
|
|
4677
4199
|
function registerWorkCommands(program2) {
|
|
4678
|
-
const workCmd = program2.command("work").description("
|
|
4679
|
-
workCmd.command("
|
|
4200
|
+
const workCmd = program2.command("work").description("Start, remove, or read what the factory is working on (the Work page)");
|
|
4201
|
+
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
4202
|
"after",
|
|
4681
|
-
"\n outerlayer work
|
|
4203
|
+
"\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
4204
|
).action(async (opts) => {
|
|
4683
4205
|
warnIfApiKeyFlag(opts);
|
|
4684
|
-
const {
|
|
4206
|
+
const { runWorkBuild, WorkCommandError } = await import('./work-cmd-UOQO2AD4.js');
|
|
4685
4207
|
try {
|
|
4686
|
-
await
|
|
4208
|
+
await runWorkBuild(opts);
|
|
4687
4209
|
} catch (err) {
|
|
4688
4210
|
if (err instanceof WorkCommandError) {
|
|
4689
4211
|
fail(err.message);
|
|
@@ -4692,14 +4214,14 @@ function registerWorkCommands(program2) {
|
|
|
4692
4214
|
throw err;
|
|
4693
4215
|
}
|
|
4694
4216
|
});
|
|
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>", "
|
|
4217
|
+
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
4218
|
"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
|
|
4219
|
+
"\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
4220
|
).action(async (number, opts) => {
|
|
4699
4221
|
warnIfApiKeyFlag(opts);
|
|
4700
|
-
const { runWorkLinkSession, WorkCommandError } = await import('./work-cmd-
|
|
4222
|
+
const { runWorkLinkSession, WorkCommandError } = await import('./work-cmd-UOQO2AD4.js');
|
|
4701
4223
|
try {
|
|
4702
|
-
await runWorkLinkSession({ ...opts, number });
|
|
4224
|
+
await runWorkLinkSession({ ...opts, number, noLease: opts.lease === false });
|
|
4703
4225
|
} catch (err) {
|
|
4704
4226
|
if (err instanceof WorkCommandError) {
|
|
4705
4227
|
fail(err.message);
|
|
@@ -4708,18 +4230,61 @@ function registerWorkCommands(program2) {
|
|
|
4708
4230
|
throw err;
|
|
4709
4231
|
}
|
|
4710
4232
|
});
|
|
4711
|
-
workCmd.command("
|
|
4233
|
+
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(
|
|
4234
|
+
"after",
|
|
4235
|
+
`
|
|
4236
|
+
outerlayer work comment --item 7 --criterion AC-9-01 --body "fixed" --artifact 3f2a1c9d --attach
|
|
4237
|
+
|
|
4238
|
+
Posts one comment. Omitting --criterion posts on the general thread;
|
|
4239
|
+
naming one posts on that criterion's own thread. --pass/--fail are a
|
|
4240
|
+
person's verdict on the whole item, so only on the general thread: the
|
|
4241
|
+
gateway refuses them with --criterion. --attach makes --artifact the
|
|
4242
|
+
criterion's new proof; --ready hands the thread back to a person. At
|
|
4243
|
+
most one action flag may be given.
|
|
4244
|
+
`
|
|
4245
|
+
).action(async (opts) => {
|
|
4246
|
+
warnIfApiKeyFlag(opts);
|
|
4247
|
+
const { runWorkComment, WorkCommentCommandError } = await import('./work-comment-cmd-WF3EOLAE.js');
|
|
4248
|
+
const { WorkCommandError } = await import('./work-cmd-UOQO2AD4.js');
|
|
4249
|
+
try {
|
|
4250
|
+
await runWorkComment(opts);
|
|
4251
|
+
} catch (err) {
|
|
4252
|
+
if (err instanceof WorkCommentCommandError || err instanceof WorkCommandError) {
|
|
4253
|
+
fail(err.message);
|
|
4254
|
+
return;
|
|
4255
|
+
}
|
|
4256
|
+
throw err;
|
|
4257
|
+
}
|
|
4258
|
+
});
|
|
4259
|
+
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(
|
|
4260
|
+
"after",
|
|
4261
|
+
"\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"
|
|
4262
|
+
).action(async (opts) => {
|
|
4263
|
+
warnIfApiKeyFlag(opts);
|
|
4264
|
+
const { runWorkThreads, WorkCommentCommandError } = await import('./work-comment-cmd-WF3EOLAE.js');
|
|
4265
|
+
const { WorkCommandError } = await import('./work-cmd-UOQO2AD4.js');
|
|
4266
|
+
try {
|
|
4267
|
+
await runWorkThreads(opts);
|
|
4268
|
+
} catch (err) {
|
|
4269
|
+
if (err instanceof WorkCommentCommandError || err instanceof WorkCommandError) {
|
|
4270
|
+
fail(err.message);
|
|
4271
|
+
return;
|
|
4272
|
+
}
|
|
4273
|
+
throw err;
|
|
4274
|
+
}
|
|
4275
|
+
});
|
|
4276
|
+
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
4277
|
"after",
|
|
4713
4278
|
`
|
|
4714
4279
|
outerlayer work remove --item 42 --reason "done"
|
|
4715
4280
|
|
|
4716
4281
|
Withdraws the caller's own addition, recording the reason. Never
|
|
4717
|
-
deletes anything \u2014 the item is removed from
|
|
4282
|
+
deletes anything \u2014 the item is removed from Work only once no
|
|
4718
4283
|
live addition remains on it.
|
|
4719
4284
|
`
|
|
4720
4285
|
).action(async (opts) => {
|
|
4721
4286
|
warnIfApiKeyFlag(opts);
|
|
4722
|
-
const { runWorkRemove, WorkCommandError } = await import('./work-cmd-
|
|
4287
|
+
const { runWorkRemove, WorkCommandError } = await import('./work-cmd-UOQO2AD4.js');
|
|
4723
4288
|
try {
|
|
4724
4289
|
await runWorkRemove(opts);
|
|
4725
4290
|
} catch (err) {
|
|
@@ -4730,9 +4295,9 @@ live addition remains on it.
|
|
|
4730
4295
|
throw err;
|
|
4731
4296
|
}
|
|
4732
4297
|
});
|
|
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>", "
|
|
4298
|
+
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
4299
|
warnIfApiKeyFlag(opts);
|
|
4735
|
-
const { runWorkStatus, WorkCommandError } = await import('./work-cmd-
|
|
4300
|
+
const { runWorkStatus, WorkCommandError } = await import('./work-cmd-UOQO2AD4.js');
|
|
4736
4301
|
try {
|
|
4737
4302
|
await runWorkStatus(opts);
|
|
4738
4303
|
} catch (err) {
|
|
@@ -4743,9 +4308,9 @@ live addition remains on it.
|
|
|
4743
4308
|
throw err;
|
|
4744
4309
|
}
|
|
4745
4310
|
});
|
|
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
|
|
4311
|
+
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
4312
|
warnIfApiKeyFlag(opts);
|
|
4748
|
-
const { runWorkList, WorkCommandError } = await import('./work-cmd-
|
|
4313
|
+
const { runWorkList, WorkCommandError } = await import('./work-cmd-UOQO2AD4.js');
|
|
4749
4314
|
try {
|
|
4750
4315
|
await runWorkList(opts);
|
|
4751
4316
|
} catch (err) {
|
|
@@ -4756,12 +4321,12 @@ live addition remains on it.
|
|
|
4756
4321
|
throw err;
|
|
4757
4322
|
}
|
|
4758
4323
|
});
|
|
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>", "
|
|
4324
|
+
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
4325
|
"after",
|
|
4761
4326
|
"\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
4327
|
).action(async (opts) => {
|
|
4763
4328
|
warnIfApiKeyFlag(opts);
|
|
4764
|
-
const { runWorkClaim, WorkCommandError } = await import('./work-claim-cmd-
|
|
4329
|
+
const { runWorkClaim, WorkCommandError } = await import('./work-claim-cmd-LSO2B2EX.js');
|
|
4765
4330
|
try {
|
|
4766
4331
|
await runWorkClaim(opts);
|
|
4767
4332
|
} catch (err) {
|
|
@@ -4772,12 +4337,12 @@ live addition remains on it.
|
|
|
4772
4337
|
throw err;
|
|
4773
4338
|
}
|
|
4774
4339
|
});
|
|
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>", "
|
|
4340
|
+
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
4341
|
"after",
|
|
4777
4342
|
"\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
4343
|
).action(async (opts) => {
|
|
4779
4344
|
warnIfApiKeyFlag(opts);
|
|
4780
|
-
const { runWorkRenew, WorkCommandError } = await import('./work-claim-cmd-
|
|
4345
|
+
const { runWorkRenew, WorkCommandError } = await import('./work-claim-cmd-LSO2B2EX.js');
|
|
4781
4346
|
try {
|
|
4782
4347
|
await runWorkRenew(opts);
|
|
4783
4348
|
} catch (err) {
|
|
@@ -4791,12 +4356,12 @@ live addition remains on it.
|
|
|
4791
4356
|
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
4357
|
"--outcome <ok|failed|timed_out|idle|lease_lost|interrupted|stopped>",
|
|
4793
4358
|
"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>", "
|
|
4359
|
+
).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
4360
|
"after",
|
|
4796
4361
|
"\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
4362
|
).action(async (opts) => {
|
|
4798
4363
|
warnIfApiKeyFlag(opts);
|
|
4799
|
-
const { runWorkRelease, WorkCommandError } = await import('./work-claim-cmd-
|
|
4364
|
+
const { runWorkRelease, WorkCommandError } = await import('./work-claim-cmd-LSO2B2EX.js');
|
|
4800
4365
|
try {
|
|
4801
4366
|
await runWorkRelease(opts);
|
|
4802
4367
|
} catch (err) {
|
|
@@ -4807,12 +4372,12 @@ live addition remains on it.
|
|
|
4807
4372
|
throw err;
|
|
4808
4373
|
}
|
|
4809
4374
|
});
|
|
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>", "
|
|
4375
|
+
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
4376
|
"after",
|
|
4812
4377
|
"\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
4378
|
).action(async (number, opts) => {
|
|
4814
4379
|
warnIfApiKeyFlag(opts);
|
|
4815
|
-
const { runWorkPr, WorkPrCommandError } = await import('./work-pr-cmd-
|
|
4380
|
+
const { runWorkPr, WorkPrCommandError } = await import('./work-pr-cmd-6AN6RRQJ.js');
|
|
4816
4381
|
try {
|
|
4817
4382
|
await runWorkPr({ ...opts, number });
|
|
4818
4383
|
} catch (err) {
|
|
@@ -4823,17 +4388,35 @@ live addition remains on it.
|
|
|
4823
4388
|
throw err;
|
|
4824
4389
|
}
|
|
4825
4390
|
});
|
|
4391
|
+
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(
|
|
4392
|
+
"after",
|
|
4393
|
+
'\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'
|
|
4394
|
+
).action(async (opts) => {
|
|
4395
|
+
warnIfApiKeyFlag(opts);
|
|
4396
|
+
const { runWorkOpenPr, WorkOpenPrCommandError } = await import('./work-open-pr-cmd-4SSQZMYG.js');
|
|
4397
|
+
const { WorkCommandError } = await import('./work-cmd-UOQO2AD4.js');
|
|
4398
|
+
try {
|
|
4399
|
+
await runWorkOpenPr(opts);
|
|
4400
|
+
} catch (err) {
|
|
4401
|
+
if (err instanceof WorkOpenPrCommandError || err instanceof WorkCommandError) {
|
|
4402
|
+
fail(err.message);
|
|
4403
|
+
return;
|
|
4404
|
+
}
|
|
4405
|
+
throw err;
|
|
4406
|
+
}
|
|
4407
|
+
});
|
|
4826
4408
|
}
|
|
4827
4409
|
function registerRunnerCommands(program2) {
|
|
4828
4410
|
const runnerCmd = program2.command("runner").description("Take work from the implement and amend queues on this machine");
|
|
4829
4411
|
const configOption = "the cloud config file to run against (default: ~/.outerlayer/config.json)";
|
|
4830
4412
|
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-
|
|
4413
|
+
const { defaultRunnerConfigPath, RunnerConfigError } = await import('./config-XVJYZ4IQ.js');
|
|
4414
|
+
const { start, RunnerStartError } = await import('./loop-VZOLL4WQ.js');
|
|
4415
|
+
const { RunnerAlreadyRunningError } = await import('./pidfile-UZRH774M.js');
|
|
4834
4416
|
const configPath = opts.config ?? defaultRunnerConfigPath();
|
|
4835
4417
|
try {
|
|
4836
|
-
|
|
4418
|
+
const { realUpdateDeps } = await import('./real-deps-SU24ZA2K.js');
|
|
4419
|
+
await start(configPath, { update: realUpdateDeps(), fetchImpl: fetch });
|
|
4837
4420
|
} catch (err) {
|
|
4838
4421
|
if (err instanceof RunnerConfigError || err instanceof RunnerAlreadyRunningError || err instanceof RunnerStartError) {
|
|
4839
4422
|
fail(err.message);
|
|
@@ -4843,89 +4426,214 @@ function registerRunnerCommands(program2) {
|
|
|
4843
4426
|
}
|
|
4844
4427
|
});
|
|
4845
4428
|
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 {
|
|
4429
|
+
const { defaultRunnerConfigPath } = await import('./config-XVJYZ4IQ.js');
|
|
4430
|
+
const { runRunnerCheck } = await import('./check-WQP2CZDW.js');
|
|
4848
4431
|
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);
|
|
4432
|
+
const result = await runRunnerCheck(configPath);
|
|
4433
|
+
if (!result.ok) {
|
|
4434
|
+
fail(result.message);
|
|
4864
4435
|
return;
|
|
4865
4436
|
}
|
|
4866
4437
|
process.stdout.write(
|
|
4867
|
-
|
|
4868
|
-
` : `${C.yellow}!${C.reset} credentials unverified \xB7 ${
|
|
4438
|
+
result.unverifiedReason === void 0 ? `${C.green}\u2713${C.reset} credentials ok \xB7 ${result.summary}
|
|
4439
|
+
` : `${C.yellow}!${C.reset} credentials unverified \xB7 ${result.summary} \xB7 ${result.unverifiedReason}
|
|
4869
4440
|
`
|
|
4870
4441
|
);
|
|
4871
|
-
|
|
4872
|
-
`config ${configPath}`,
|
|
4873
|
-
`host ${config.host}`,
|
|
4874
|
-
`queues ${config.queues.join(", ")}`,
|
|
4875
|
-
`concurrency ${config.concurrency}`,
|
|
4876
|
-
`poll ${config.pollSeconds}s`,
|
|
4877
|
-
`time limit ${config.timeLimitMinutes}m`,
|
|
4878
|
-
`idle limit ${config.idleLimitMinutes}m`,
|
|
4879
|
-
...config.queues.map((queue) => `command ${queue.padEnd(11)} ${config.commands[queue] ?? "(none)"}`),
|
|
4880
|
-
`hook provision ${config.hooks.provision}`,
|
|
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")}
|
|
4442
|
+
process.stdout.write(`${result.lines.join("\n")}
|
|
4887
4443
|
`);
|
|
4888
4444
|
});
|
|
4889
|
-
runnerCmd.command("
|
|
4890
|
-
const { defaultRunnerConfigPath } = await import('./config-
|
|
4891
|
-
const {
|
|
4445
|
+
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) => {
|
|
4446
|
+
const { defaultRunnerConfigPath } = await import('./config-XVJYZ4IQ.js');
|
|
4447
|
+
const { runRunnerInstall, RunnerInstallError } = await import('./install-cmd-7WXOIFDO.js');
|
|
4892
4448
|
const configPath = opts.config ?? defaultRunnerConfigPath();
|
|
4893
4449
|
try {
|
|
4894
|
-
const result =
|
|
4450
|
+
const result = runRunnerInstall({ configPath, entryPath: process.argv[1] ?? "", nodePath: process.execPath });
|
|
4451
|
+
process.stdout.write(`${C.green}\u2713${C.reset} installed ${result.version} for the runner; \`current\` names it
|
|
4452
|
+
`);
|
|
4453
|
+
process.stdout.write(
|
|
4454
|
+
`
|
|
4455
|
+
Start the runner from the install layout by putting this line in the service unit, then restart the service:
|
|
4456
|
+
${C.yellow}${result.execStart}${C.reset}
|
|
4457
|
+
`
|
|
4458
|
+
);
|
|
4459
|
+
} catch (err) {
|
|
4460
|
+
if (err instanceof RunnerInstallError) {
|
|
4461
|
+
fail(err.message);
|
|
4462
|
+
return;
|
|
4463
|
+
}
|
|
4464
|
+
throw err;
|
|
4465
|
+
}
|
|
4466
|
+
});
|
|
4467
|
+
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) => {
|
|
4468
|
+
const { defaultRunnerConfigPath, RunnerConfigError } = await import('./config-XVJYZ4IQ.js');
|
|
4469
|
+
const { runRunnerInit, RunnerInitError } = await import('./init-DXBCOAJK.js');
|
|
4470
|
+
const { detectMachine } = await import('./machine-WSG52J75.js');
|
|
4471
|
+
const { chooseVm, runVmInit, VmInitError } = await import('./init-YEXK35CF.js');
|
|
4472
|
+
const configPath = opts.config ?? defaultRunnerConfigPath();
|
|
4473
|
+
try {
|
|
4474
|
+
const machine = detectMachine();
|
|
4475
|
+
if (opts.configStdin === true) {
|
|
4476
|
+
const chunks = [];
|
|
4477
|
+
for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
|
|
4478
|
+
const result2 = runRunnerInit(configPath, { machine, configFromStdin: Buffer.concat(chunks).toString("utf8"), requireContainer: true });
|
|
4479
|
+
const { ensureHostKey: ensureHostKey2 } = await import('./host-key-KDYZ5ECC.js');
|
|
4480
|
+
const hostKey2 = await ensureHostKey2(configPath);
|
|
4481
|
+
process.stdout.write(`host key ${hostKey2.fingerprint}
|
|
4482
|
+
hooks: ${result2.hooks}
|
|
4483
|
+
`);
|
|
4484
|
+
return;
|
|
4485
|
+
}
|
|
4486
|
+
let declinedVm = opts.vm === false;
|
|
4487
|
+
const useVm = await chooseVm({
|
|
4488
|
+
platform: machine.platform,
|
|
4489
|
+
flag: opts.vm,
|
|
4490
|
+
isTTY: process.stdin.isTTY === true,
|
|
4491
|
+
ask: async () => {
|
|
4492
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
4493
|
+
const answer = await rl.question(
|
|
4494
|
+
"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] "
|
|
4495
|
+
);
|
|
4496
|
+
rl.close();
|
|
4497
|
+
const yes = answer.trim() === "" || /^y(es)?$/i.test(answer.trim());
|
|
4498
|
+
declinedVm = !yes;
|
|
4499
|
+
return yes;
|
|
4500
|
+
}
|
|
4501
|
+
});
|
|
4502
|
+
if (useVm) {
|
|
4503
|
+
const { realLima } = await import('./lima-XN65D7GN.js');
|
|
4504
|
+
const { cpus, totalmem } = await import('os');
|
|
4505
|
+
const { resolvePackageVersion } = await import('./version-BWM6VLDI.js');
|
|
4506
|
+
const result2 = await runVmInit(configPath, {
|
|
4507
|
+
lima: realLima(),
|
|
4508
|
+
home: homeFromEnv(),
|
|
4509
|
+
hostCpus: cpus().length,
|
|
4510
|
+
hostMemoryBytes: totalmem(),
|
|
4511
|
+
version: resolvePackageVersion(),
|
|
4512
|
+
cliPackage: opts.vmCliPackage,
|
|
4513
|
+
progress: (line) => process.stdout.write(`${line}
|
|
4514
|
+
`)
|
|
4515
|
+
});
|
|
4516
|
+
for (const line of result2.lines) process.stdout.write(`${C.green}\u2713${C.reset} ${line}
|
|
4517
|
+
`);
|
|
4518
|
+
return;
|
|
4519
|
+
}
|
|
4520
|
+
const allowProcess = opts.allowProcessBuilds === true || machine.platform === "darwin" && declinedVm;
|
|
4521
|
+
const result = runRunnerInit(configPath, { machine, allowProcess });
|
|
4522
|
+
const { ensureHostKey } = await import('./host-key-KDYZ5ECC.js');
|
|
4523
|
+
const hostKey = await ensureHostKey(configPath);
|
|
4895
4524
|
process.stdout.write(`${C.green}\u2713${C.reset} wrote a runner block into ${result.configPath}
|
|
4896
4525
|
`);
|
|
4897
|
-
|
|
4898
|
-
|
|
4526
|
+
process.stdout.write(
|
|
4527
|
+
`${C.green}\u2713${C.reset} host key ${hostKey.fingerprint}: the runner key binds to it on its first claim
|
|
4528
|
+
`
|
|
4529
|
+
);
|
|
4530
|
+
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)"}
|
|
4531
|
+
`);
|
|
4532
|
+
if (result.note !== void 0) process.stdout.write(`
|
|
4533
|
+
${C.yellow}${result.note}${C.reset}
|
|
4534
|
+
`);
|
|
4535
|
+
if (machine.platform === "darwin" && opts.vm === void 0) {
|
|
4536
|
+
process.stdout.write(`
|
|
4537
|
+
To isolate builds in containers instead, run ${C.yellow}outerlayer runner init --vm${C.reset}.
|
|
4538
|
+
`);
|
|
4539
|
+
}
|
|
4540
|
+
const { runRunnerInstall, RunnerInstallError } = await import('./install-cmd-7WXOIFDO.js');
|
|
4541
|
+
let execStart;
|
|
4542
|
+
try {
|
|
4543
|
+
const installed = runRunnerInstall({ configPath, entryPath: process.argv[1] ?? "", nodePath: process.execPath });
|
|
4544
|
+
execStart = installed.execStart;
|
|
4545
|
+
process.stdout.write(`${C.green}\u2713${C.reset} installed ${installed.version} for the runner, so it can update itself
|
|
4546
|
+
`);
|
|
4547
|
+
} catch (installErr) {
|
|
4548
|
+
if (!(installErr instanceof RunnerInstallError)) throw installErr;
|
|
4549
|
+
process.stdout.write(`
|
|
4550
|
+
${C.yellow}The runner will not update itself: ${installErr.message}${C.reset}
|
|
4899
4551
|
`);
|
|
4900
4552
|
}
|
|
4901
4553
|
process.stdout.write(
|
|
4902
4554
|
`
|
|
4903
4555
|
Next:
|
|
4904
4556
|
1. Edit the command lines in ${result.configPath} to start your agent.
|
|
4905
|
-
2.
|
|
4906
|
-
3. ${C.yellow}outerlayer runner
|
|
4907
|
-
|
|
4908
|
-
|
|
4557
|
+
2. ${C.yellow}outerlayer runner check${C.reset}
|
|
4558
|
+
3. ${C.yellow}outerlayer runner start${C.reset}
|
|
4559
|
+
` + (execStart === void 0 ? "" : ` To run it as a service that updates itself, start it with this line in the unit:
|
|
4560
|
+
${C.yellow}${execStart}${C.reset}
|
|
4561
|
+
`)
|
|
4909
4562
|
);
|
|
4910
4563
|
} catch (err) {
|
|
4911
|
-
if (err instanceof RunnerInitError) {
|
|
4564
|
+
if (err instanceof RunnerInitError || err instanceof VmInitError || err instanceof RunnerConfigError) {
|
|
4912
4565
|
fail(err.message);
|
|
4913
4566
|
return;
|
|
4914
4567
|
}
|
|
4915
4568
|
throw err;
|
|
4916
4569
|
}
|
|
4917
4570
|
});
|
|
4571
|
+
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(
|
|
4572
|
+
"after",
|
|
4573
|
+
"\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"
|
|
4574
|
+
).action(async (opts) => {
|
|
4575
|
+
const { defaultRunnerConfigPath } = await import('./config-XVJYZ4IQ.js');
|
|
4576
|
+
const { buildRepositoryImage, RecipeError } = await import('./build-TZUUTRGH.js');
|
|
4577
|
+
const { currentCliBuild } = await import('./cli-build-K56DK4DS.js');
|
|
4578
|
+
const { createDockerHost } = await import('./docker-NEGDLU6D.js');
|
|
4579
|
+
try {
|
|
4580
|
+
const result = await buildRepositoryImage(
|
|
4581
|
+
{
|
|
4582
|
+
repository: opts.repository,
|
|
4583
|
+
remote: opts.remote,
|
|
4584
|
+
configPath: opts.config ?? defaultRunnerConfigPath(),
|
|
4585
|
+
cli: currentCliBuild()
|
|
4586
|
+
},
|
|
4587
|
+
{ host: createDockerHost() }
|
|
4588
|
+
);
|
|
4589
|
+
if (opts.json) {
|
|
4590
|
+
writeJson(result);
|
|
4591
|
+
return;
|
|
4592
|
+
}
|
|
4593
|
+
const { report } = result;
|
|
4594
|
+
const what = result.reused ? "reused" : result.toolsBuilt && !result.recipeBuilt ? "rebuilt the tools layer of" : "built";
|
|
4595
|
+
process.stdout.write(`${C.green}\u2713${C.reset} ${what} ${report.tag}
|
|
4596
|
+
`);
|
|
4597
|
+
process.stdout.write(` recipe: ${report.recipe} at ${report.sourceCommit.slice(0, 12)} on ${report.defaultBranch}
|
|
4598
|
+
`);
|
|
4599
|
+
if (result.checkedCommit !== report.sourceCommit) {
|
|
4600
|
+
process.stdout.write(` checked: ${result.checkedCommit.slice(0, 12)}, the recipe has not changed since
|
|
4601
|
+
`);
|
|
4602
|
+
}
|
|
4603
|
+
process.stdout.write(` record: ${result.recordDir}
|
|
4604
|
+
`);
|
|
4605
|
+
if (report.ignored.length > 0) {
|
|
4606
|
+
process.stdout.write(` ignored:
|
|
4607
|
+
`);
|
|
4608
|
+
for (const { property, source } of report.ignored) process.stdout.write(` ${property} (from ${source})
|
|
4609
|
+
`);
|
|
4610
|
+
}
|
|
4611
|
+
if (result.removed.length > 0) process.stdout.write(` pruned ${result.removed.length} older image(s)
|
|
4612
|
+
`);
|
|
4613
|
+
} catch (err) {
|
|
4614
|
+
if (err instanceof RecipeError) {
|
|
4615
|
+
if (opts.json) {
|
|
4616
|
+
writeJson({ error: { code: err.code, message: err.message, ...err.logPath ? { logPath: err.logPath } : {} } });
|
|
4617
|
+
process.exitCode = 1;
|
|
4618
|
+
return;
|
|
4619
|
+
}
|
|
4620
|
+
fail(`${err.code}: ${err.message}`);
|
|
4621
|
+
return;
|
|
4622
|
+
}
|
|
4623
|
+
throw err;
|
|
4624
|
+
}
|
|
4625
|
+
});
|
|
4918
4626
|
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-
|
|
4627
|
+
const { defaultRunnerConfigPath } = await import('./config-XVJYZ4IQ.js');
|
|
4628
|
+
const { requestStop } = await import('./loop-VZOLL4WQ.js');
|
|
4921
4629
|
const configPath = opts.config ?? defaultRunnerConfigPath();
|
|
4922
4630
|
await requestStop(configPath, Boolean(opts.now));
|
|
4923
4631
|
});
|
|
4924
4632
|
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-
|
|
4633
|
+
const { defaultRunnerConfigPath, loadRunnerConfig } = await import('./config-XVJYZ4IQ.js');
|
|
4634
|
+
const { jobsDir } = await import('./paths-OYKMVYJP.js');
|
|
4635
|
+
const { listJobFiles, runnerProcessState } = await import('./loop-VZOLL4WQ.js');
|
|
4636
|
+
const { renderStatus } = await import('./status-2UZKKNB7.js');
|
|
4929
4637
|
const path2 = await import('path');
|
|
4930
4638
|
const configPath = opts.config ?? defaultRunnerConfigPath();
|
|
4931
4639
|
const all = listJobFiles(configPath);
|
|
@@ -4944,7 +4652,7 @@ Next:
|
|
|
4944
4652
|
configPath,
|
|
4945
4653
|
process: processState,
|
|
4946
4654
|
concurrency,
|
|
4947
|
-
jobs: all.map(({ dirName, job }) => ({ ...job, jobDir: path2.join(jobsDir(configPath), dirName) }))
|
|
4655
|
+
jobs: all.map(({ dirName, job }) => ({ ...withoutItemKey(job), jobDir: path2.join(jobsDir(configPath), dirName) }))
|
|
4948
4656
|
});
|
|
4949
4657
|
return;
|
|
4950
4658
|
}
|
|
@@ -4962,9 +4670,9 @@ Next:
|
|
|
4962
4670
|
);
|
|
4963
4671
|
});
|
|
4964
4672
|
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-
|
|
4673
|
+
const { defaultRunnerConfigPath } = await import('./config-XVJYZ4IQ.js');
|
|
4674
|
+
const { listJobFiles } = await import('./loop-VZOLL4WQ.js');
|
|
4675
|
+
const { newestAttempt, logPathFor, tail, RunnerLogsError } = await import('./logs-7RYUH5A6.js');
|
|
4968
4676
|
const configPath = opts.config ?? defaultRunnerConfigPath();
|
|
4969
4677
|
const item = parseTrackerNumber(itemArg);
|
|
4970
4678
|
if (item === void 0) {
|
|
@@ -5183,11 +4891,11 @@ ${C.dim}${file.path}${C.reset}
|
|
|
5183
4891
|
}
|
|
5184
4892
|
function registerMcpCommands(program2) {
|
|
5185
4893
|
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:
|
|
4894
|
+
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
4895
|
"after",
|
|
5188
4896
|
"\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
4897
|
).action(async (opts) => {
|
|
5190
|
-
const { runMcpInstall, handleMcpInstallError, McpInstallError } = await import('./mcp-install-cmd-
|
|
4898
|
+
const { runMcpInstall, handleMcpInstallError, McpInstallError } = await import('./mcp-install-cmd-RLK4SO3N.js');
|
|
5191
4899
|
try {
|
|
5192
4900
|
if (opts.transport !== void 0 && opts.transport !== "stdio" && opts.transport !== "http") {
|
|
5193
4901
|
throw new McpInstallError(`--transport must be "stdio" or "http", got "${opts.transport}"`);
|
|
@@ -5205,11 +4913,11 @@ function registerMcpCommands(program2) {
|
|
|
5205
4913
|
handleMcpInstallError(err);
|
|
5206
4914
|
}
|
|
5207
4915
|
});
|
|
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>", "
|
|
4916
|
+
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
4917
|
"after",
|
|
5210
4918
|
"\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
4919
|
).action(async (opts) => {
|
|
5212
|
-
const { runMcpServe, handleMcpServeError } = await import('./mcp-serve-cmd-
|
|
4920
|
+
const { runMcpServe, handleMcpServeError } = await import('./mcp-serve-cmd-JPTP5FMS.js');
|
|
5213
4921
|
try {
|
|
5214
4922
|
await runMcpServe({ url: opts.url, appId: opts.appId });
|
|
5215
4923
|
} catch (err) {
|
|
@@ -5220,7 +4928,7 @@ function registerMcpCommands(program2) {
|
|
|
5220
4928
|
function registerImportCommands(program2) {
|
|
5221
4929
|
const importCmd = program2.command("import").description("Import an existing config format into .outerlayer/");
|
|
5222
4930
|
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-
|
|
4931
|
+
const { runImportRuler, ImportRulerError } = await import('./import-ruler-cmd-GUHL6IY5.js');
|
|
5224
4932
|
try {
|
|
5225
4933
|
const result = runImportRuler({ cwd: opts.dir, json: opts.json });
|
|
5226
4934
|
if (result.exitCode !== 0) process.exitCode = result.exitCode;
|
|
@@ -5232,11 +4940,11 @@ function registerImportCommands(program2) {
|
|
|
5232
4940
|
throw err;
|
|
5233
4941
|
}
|
|
5234
4942
|
});
|
|
5235
|
-
importCmd.command("capture").description("Install the
|
|
4943
|
+
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
4944
|
"after",
|
|
5237
|
-
"\
|
|
4945
|
+
"\nInstalls the three maintained skills (emitting-evidence, amend,\nreporting-findings) 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
4946
|
).action(async (opts) => {
|
|
5239
|
-
const { runImportCapture, ImportCaptureError } = await import('./import-capture-cmd-
|
|
4947
|
+
const { runImportCapture, ImportCaptureError } = await import('./import-capture-cmd-AVZ4VIVJ.js');
|
|
5240
4948
|
try {
|
|
5241
4949
|
runImportCapture({ cwd: opts.dir, json: opts.json });
|
|
5242
4950
|
} catch (err) {
|
|
@@ -5274,15 +4982,21 @@ function registerContextCommands(program2) {
|
|
|
5274
4982
|
const contextCmd = program2.command("context").description("Compile and materialize agent context");
|
|
5275
4983
|
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
4984
|
"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'
|
|
4985
|
+
'\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)\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
4986
|
).action(async (opts) => {
|
|
5279
4987
|
await compileContext({ dir: opts.dir, check: opts.check, json: opts.json });
|
|
5280
4988
|
});
|
|
4989
|
+
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) => {
|
|
4990
|
+
const { runContextAdopt } = await import('./context-adopt-UMA4O5NY.js');
|
|
4991
|
+
const { exitCode, stdout } = runContextAdopt(stagedDir, process.cwd());
|
|
4992
|
+
process.stdout.write(stdout);
|
|
4993
|
+
process.exitCode = exitCode;
|
|
4994
|
+
});
|
|
5281
4995
|
contextCmd.command("materialize").description("Materialize this repository's governed context from its control plane").option("--json", "machine-readable JSON output").addHelpText(
|
|
5282
4996
|
"after",
|
|
5283
4997
|
"\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
4998
|
).action(async (opts) => {
|
|
5285
|
-
const { materializeContext } = await import('./context-materialize-
|
|
4999
|
+
const { materializeContext } = await import('./context-materialize-G2FUFC72.js');
|
|
5286
5000
|
const result = await materializeContext({ cwd: process.cwd(), sessionId: `manual-${Date.now()}` });
|
|
5287
5001
|
const output = formatContextMaterializeOutput(result);
|
|
5288
5002
|
if (output.exitCode !== void 0) process.exitCode = output.exitCode;
|