@lannguyensi/harness 0.32.0 → 0.34.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 +40 -0
- package/README.md +8 -5
- package/dist/cli/adopt/index.d.ts +7 -0
- package/dist/cli/adopt/index.js +10 -0
- package/dist/cli/adopt/index.js.map +1 -1
- package/dist/cli/apply/apply.d.ts +14 -0
- package/dist/cli/apply/apply.js +21 -6
- package/dist/cli/apply/apply.js.map +1 -1
- package/dist/cli/approve/branch-protection.d.ts +69 -0
- package/dist/cli/approve/branch-protection.js +157 -0
- package/dist/cli/approve/branch-protection.js.map +1 -0
- package/dist/cli/approve/understanding.d.ts +12 -0
- package/dist/cli/approve/understanding.js +50 -3
- package/dist/cli/approve/understanding.js.map +1 -1
- package/dist/cli/gc/index.d.ts +47 -0
- package/dist/cli/gc/index.js +148 -0
- package/dist/cli/gc/index.js.map +1 -0
- package/dist/cli/index.js +174 -14
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init/composer.js +11 -5
- package/dist/cli/init/composer.js.map +1 -1
- package/dist/cli/init/profiles.d.ts +2 -2
- package/dist/cli/init/profiles.js +2 -2
- package/dist/cli/init/templates.d.ts +1 -1
- package/dist/cli/init/templates.js +8 -4
- package/dist/cli/init/templates.js.map +1 -1
- package/dist/cli/pack/hook-branch-protection.d.ts +8 -0
- package/dist/cli/pack/hook-branch-protection.js +59 -15
- package/dist/cli/pack/hook-branch-protection.js.map +1 -1
- package/dist/cli/pack/hook-pre-tool-use.js +31 -2
- package/dist/cli/pack/hook-pre-tool-use.js.map +1 -1
- package/dist/cli/pack/hook-solution-acceptance.d.ts +2 -0
- package/dist/cli/pack/hook-solution-acceptance.js +24 -10
- package/dist/cli/pack/hook-solution-acceptance.js.map +1 -1
- package/dist/cli/pack/read-only-bash.js +127 -4
- package/dist/cli/pack/read-only-bash.js.map +1 -1
- package/dist/cli/uninstall/index.d.ts +24 -5
- package/dist/cli/uninstall/index.js +73 -6
- package/dist/cli/uninstall/index.js.map +1 -1
- package/dist/policy-packs/builtin/branch-protection-runtime.d.ts +47 -6
- package/dist/policy-packs/builtin/branch-protection-runtime.js +53 -6
- package/dist/policy-packs/builtin/branch-protection-runtime.js.map +1 -1
- package/dist/policy-packs/builtin/branch-protection.js +24 -14
- package/dist/policy-packs/builtin/branch-protection.js.map +1 -1
- package/dist/policy-packs/builtin/solution-acceptance-runtime.d.ts +18 -0
- package/dist/policy-packs/builtin/solution-acceptance-runtime.js +32 -0
- package/dist/policy-packs/builtin/solution-acceptance-runtime.js.map +1 -1
- package/dist/policy-packs/builtin/understanding-before-execution-runtime.d.ts +82 -10
- package/dist/policy-packs/builtin/understanding-before-execution-runtime.js +88 -22
- package/dist/policy-packs/builtin/understanding-before-execution-runtime.js.map +1 -1
- package/dist/policy-packs/builtin/understanding-before-execution.d.ts +11 -0
- package/dist/policy-packs/builtin/understanding-before-execution.js +15 -0
- package/dist/policy-packs/builtin/understanding-before-execution.js.map +1 -1
- package/package.json +3 -3
package/dist/cli/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import { Command } from "commander";
|
|
4
5
|
// Production version probe for `harness doctor`: synchronous --version
|
|
@@ -33,6 +34,7 @@ import { runPackHookCodexPreToolUseCli } from "./pack/hook-codex-pre-tool-use.js
|
|
|
33
34
|
import { runPackHookCodexStopCli } from "./pack/hook-codex-stop.js";
|
|
34
35
|
import { runPackHookCodexUserPromptSubmitCli } from "./pack/hook-codex-user-prompt-submit.js";
|
|
35
36
|
import { isRuntime, KNOWN_RUNTIMES } from "../policy-packs/index.js";
|
|
37
|
+
import { approveBranchProtection } from "./approve/branch-protection.js";
|
|
36
38
|
import { approveRisk } from "./approve/risk.js";
|
|
37
39
|
import { approveUnderstanding } from "./approve/understanding.js";
|
|
38
40
|
import { describe, isPillar } from "./describe.js";
|
|
@@ -64,6 +66,7 @@ import { runPackHookSolutionAcceptanceWriteguardCli } from "./pack/hook-solution
|
|
|
64
66
|
import { runPackHookRuntimeRealityCli } from "./pack/hook-runtime-reality.js";
|
|
65
67
|
import { gateDisable, GateDisableError } from "./gate/disable.js";
|
|
66
68
|
import { gateEnable, GateEnableError } from "./gate/enable.js";
|
|
69
|
+
import { DEFAULT_RETENTION_DAYS, gc } from "./gc/index.js";
|
|
67
70
|
import { uninstall, UninstallError } from "./uninstall/index.js";
|
|
68
71
|
import { migrateHome } from "./migrate-home/index.js";
|
|
69
72
|
import { pause as pauseHarness, resume as resumeHarness } from "./pause/index.js";
|
|
@@ -127,6 +130,7 @@ export function buildProgram(opts = {}) {
|
|
|
127
130
|
.option("--project <name>", "apply per-project overrides for this project name")
|
|
128
131
|
.option("--strict", "promote warnings to errors")
|
|
129
132
|
.option("--check-lock", "surface harness.lock asset-content drift as warnings (or errors with --strict)")
|
|
133
|
+
.option("--json", "emit a structured JSON report ({ diagnostics, errorCount, warningCount }) instead of prose")
|
|
130
134
|
.action((options) => {
|
|
131
135
|
const result = validate({
|
|
132
136
|
configPath: options.config,
|
|
@@ -134,11 +138,22 @@ export function buildProgram(opts = {}) {
|
|
|
134
138
|
strict: options.strict,
|
|
135
139
|
checkLock: options.checkLock,
|
|
136
140
|
});
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
141
|
+
if (options.json) {
|
|
142
|
+
// JSON goes to stdout regardless of outcome so pipelines can
|
|
143
|
+
// always parse it; the exit code still carries pass/fail.
|
|
144
|
+
stdout(`${JSON.stringify({
|
|
145
|
+
diagnostics: result.diagnostics,
|
|
146
|
+
errorCount: result.errorCount,
|
|
147
|
+
warningCount: result.warningCount,
|
|
148
|
+
}, null, 2)}\n`);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
const report = formatReport(result);
|
|
152
|
+
if (result.diagnostics.length > 0)
|
|
153
|
+
stderr(report);
|
|
154
|
+
else
|
|
155
|
+
stdout(report);
|
|
156
|
+
}
|
|
142
157
|
if (result.errorCount > 0) {
|
|
143
158
|
throw new HarnessExitError("", EX_FAIL);
|
|
144
159
|
}
|
|
@@ -463,7 +478,8 @@ export function buildProgram(opts = {}) {
|
|
|
463
478
|
.option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
|
|
464
479
|
.option("--project <name>", "apply per-project overrides for this project name")
|
|
465
480
|
.option("--dry-run", "print the would-be diff + restart hints; do not write")
|
|
466
|
-
.option("--overwrite-drift", "discard any on-disk hand-edits to harness.generated/ files (prompts for `yes
|
|
481
|
+
.option("--overwrite-drift", "discard any on-disk hand-edits to harness.generated/ files (prompts for `yes`; pair with --yes for non-interactive runs)")
|
|
482
|
+
.option("--yes", "with --overwrite-drift, skip the confirmation prompt (for non-interactive use)")
|
|
467
483
|
.option("--strict-lock", "refuse with exit 1 (no write) when harness.lock asset drift is detected; dry-run wins")
|
|
468
484
|
.option("--target <path>", "additionally write the generated settings.json to <path> (e.g. .claude/settings.local.json)")
|
|
469
485
|
.option("--merge", "with --target, 3-way merge into an existing target file (replace owned keys, preserve others)")
|
|
@@ -494,6 +510,7 @@ export function buildProgram(opts = {}) {
|
|
|
494
510
|
...(options.project !== undefined ? { project: options.project } : {}),
|
|
495
511
|
...(options.dryRun ? { dryRun: true } : {}),
|
|
496
512
|
...(options.overwriteDrift ? { overwriteDrift: true } : {}),
|
|
513
|
+
...(options.yes ? { yes: true } : {}),
|
|
497
514
|
...(options.strictLock ? { strictLock: true } : {}),
|
|
498
515
|
...(options.target !== undefined ? { target: options.target } : {}),
|
|
499
516
|
...(options.merge ? { merge: true } : {}),
|
|
@@ -835,7 +852,8 @@ export function buildProgram(opts = {}) {
|
|
|
835
852
|
.command("branch-protection")
|
|
836
853
|
.description("PreToolUse blocker for the branch-protection pack: read tool-event JSON from stdin, consult the " +
|
|
837
854
|
"evidence ledger, emit a deny envelope on protected branches unless either a fresh " +
|
|
838
|
-
"`branch:non-protected` tag (within 5m) or
|
|
855
|
+
"`branch:non-protected` tag (within 5m) or the operator-only override marker " +
|
|
856
|
+
"(written by `harness approve branch-protection`) is present.")
|
|
839
857
|
.option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
|
|
840
858
|
.option("--project <name>", "apply per-project overrides")
|
|
841
859
|
.option("--ledger-timeout <ms>", "per-call ledger timeout in milliseconds")
|
|
@@ -1064,6 +1082,11 @@ export function buildProgram(opts = {}) {
|
|
|
1064
1082
|
? "; stamped sessionId"
|
|
1065
1083
|
: "";
|
|
1066
1084
|
lines.push(`report: ✓ ${result.persistedReport.filePath} (approvalStatus: ${prev} → approved${stampNote})`);
|
|
1085
|
+
const fb = result.persistedReport.fallbackAdopted;
|
|
1086
|
+
if (fb) {
|
|
1087
|
+
lines.push(` ⚠ adopted via sessionId-less fallback: created ${fb.createdAt ?? "<unknown>"} (${fb.ageMinutes}m ago).`);
|
|
1088
|
+
lines.push(" The live session's report was never persisted, or an older producer", " wrote it without a sessionId. Verify this is the report you just read", " before trusting the approval.");
|
|
1089
|
+
}
|
|
1067
1090
|
}
|
|
1068
1091
|
else {
|
|
1069
1092
|
lines.push(`report: ⚠ skipped (${result.persistedReport.reason})`);
|
|
@@ -1170,6 +1193,58 @@ export function buildProgram(opts = {}) {
|
|
|
1170
1193
|
}
|
|
1171
1194
|
stdout(`${lines.join("\n")}\n`);
|
|
1172
1195
|
});
|
|
1196
|
+
approveCmd
|
|
1197
|
+
.command("branch-protection")
|
|
1198
|
+
.description("Bless a deliberate protected-branch edit for one session. Writes the canonical " +
|
|
1199
|
+
"operator-only approval marker under harness.generated/.approvals/ that the " +
|
|
1200
|
+
"branch-protection blocker consults, plus a best-effort branch-protection-ack " +
|
|
1201
|
+
"ledger row for audit. Operator action: the marker (not the ledger tag) is the " +
|
|
1202
|
+
"trusted override, because the ledger is agent-writable.")
|
|
1203
|
+
.option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
|
|
1204
|
+
.option("--project <name>", "apply per-project overrides")
|
|
1205
|
+
.option("--session <id>", "explicit session id (default: $CLAUDE_CODE_SESSION_ID, then $CLAUDE_SESSION_ID, then $CODEX_SESSION_ID, then staged .pending-approval)")
|
|
1206
|
+
.option("--reason <text>", "free-form note recorded in the audit ledger tag (why the override fired)")
|
|
1207
|
+
.option("--approved-by <actor>", "actor to record on the marker (default: harness-approve-cli)")
|
|
1208
|
+
.action(async (options) => {
|
|
1209
|
+
const cliOpts = {};
|
|
1210
|
+
if (options.config)
|
|
1211
|
+
cliOpts.configPath = options.config;
|
|
1212
|
+
if (options.project)
|
|
1213
|
+
cliOpts.project = options.project;
|
|
1214
|
+
if (options.session)
|
|
1215
|
+
cliOpts.session = options.session;
|
|
1216
|
+
if (options.reason)
|
|
1217
|
+
cliOpts.reason = options.reason;
|
|
1218
|
+
if (options.approvedBy)
|
|
1219
|
+
cliOpts.approvedBy = options.approvedBy;
|
|
1220
|
+
const result = await approveBranchProtection(cliOpts);
|
|
1221
|
+
const lines = [];
|
|
1222
|
+
const sourceNote = result.sessionSource === "pending-approval"
|
|
1223
|
+
? " (resolved from .pending-approval staged by the gate hook)"
|
|
1224
|
+
: result.sessionSource === "env-claude-code"
|
|
1225
|
+
? " (from $CLAUDE_CODE_SESSION_ID)"
|
|
1226
|
+
: result.sessionSource === "env-claude"
|
|
1227
|
+
? " (from $CLAUDE_SESSION_ID)"
|
|
1228
|
+
: result.sessionSource === "env-codex"
|
|
1229
|
+
? " (from $CODEX_SESSION_ID)"
|
|
1230
|
+
: "";
|
|
1231
|
+
lines.push(`session: ${result.sessionId}${sourceNote}`);
|
|
1232
|
+
if (result.marker.ok) {
|
|
1233
|
+
lines.push(`marker: ✓ ${result.marker.filePath} (canonical gate signal)`);
|
|
1234
|
+
lines.push(" the branch-protection gate now allows protected-branch edits for this session.");
|
|
1235
|
+
}
|
|
1236
|
+
else {
|
|
1237
|
+
lines.push(`marker: ✗ FAILED (${result.marker.reason})`);
|
|
1238
|
+
lines.push(" the gate WILL keep blocking the next tool call until the marker exists.");
|
|
1239
|
+
}
|
|
1240
|
+
if (result.ledger.ok) {
|
|
1241
|
+
lines.push(`ledger: ✓ wrote ${result.ledger.tag} (audit only)`);
|
|
1242
|
+
}
|
|
1243
|
+
else {
|
|
1244
|
+
lines.push(`ledger: ⚠ skipped (${result.ledger.reason ?? "unknown"}) (audit only)`);
|
|
1245
|
+
}
|
|
1246
|
+
stdout(`${lines.join("\n")}\n`);
|
|
1247
|
+
});
|
|
1173
1248
|
const VALID_DECISION_FILTERS = [
|
|
1174
1249
|
"allow",
|
|
1175
1250
|
"warn",
|
|
@@ -1635,18 +1710,77 @@ export function buildProgram(opts = {}) {
|
|
|
1635
1710
|
throw err;
|
|
1636
1711
|
}
|
|
1637
1712
|
});
|
|
1713
|
+
program
|
|
1714
|
+
.command("gc")
|
|
1715
|
+
.description("Retention-based cleanup of harness-owned gate state: terminal " +
|
|
1716
|
+
"(approved/expired) understanding-gate reports, parse-error logs, and " +
|
|
1717
|
+
"approval markers older than the retention window. Pending reports and " +
|
|
1718
|
+
"anything outside the enumerated harness-owned dirs are never touched " +
|
|
1719
|
+
"(the evidence ledger and solution-acceptance verdict dirs are owned by " +
|
|
1720
|
+
"their producers). Dry-run by default; pass --apply to delete.")
|
|
1721
|
+
.option("--config <path>", "manifest path (default: ~/.harness/harness.yaml; legacy fallback ~/.claude/harness.yaml)")
|
|
1722
|
+
.option("--retention-days <n>", `delete artifacts older than this many days (default: ${DEFAULT_RETENTION_DAYS})`)
|
|
1723
|
+
.option("--apply", "delete the listed artifacts (default: dry-run listing only)")
|
|
1724
|
+
.action((options) => {
|
|
1725
|
+
const cliOpts = {};
|
|
1726
|
+
if (options.config)
|
|
1727
|
+
cliOpts.configPath = options.config;
|
|
1728
|
+
if (options.apply)
|
|
1729
|
+
cliOpts.apply = true;
|
|
1730
|
+
if (options.retentionDays !== undefined) {
|
|
1731
|
+
const parsed = Number(options.retentionDays);
|
|
1732
|
+
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
1733
|
+
stderr(`--retention-days must be a positive number, got ${JSON.stringify(options.retentionDays)}\n`);
|
|
1734
|
+
throw new HarnessExitError("", EX_USAGE);
|
|
1735
|
+
}
|
|
1736
|
+
cliOpts.retentionDays = parsed;
|
|
1737
|
+
}
|
|
1738
|
+
const result = gc(cliOpts);
|
|
1739
|
+
const sweptDirs = [
|
|
1740
|
+
result.reportsDir,
|
|
1741
|
+
...(result.parseErrorsDir !== null ? [result.parseErrorsDir] : []),
|
|
1742
|
+
result.approvalsDir,
|
|
1743
|
+
];
|
|
1744
|
+
if (result.parseErrorsDir === null) {
|
|
1745
|
+
stderr("gc: skipping the parse-errors sweep (reports dir does not have the conventional .understanding-gate/reports shape)\n");
|
|
1746
|
+
}
|
|
1747
|
+
if (result.candidates.length === 0) {
|
|
1748
|
+
stdout(`gc: nothing older than ${result.retentionDays}d (cutoff ${result.cutoffIso}) under\n` +
|
|
1749
|
+
sweptDirs.map((d) => ` ${d}\n`).join("") +
|
|
1750
|
+
`${result.keptCount} artifact(s) inspected and kept.\n`);
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
const verb = result.applied ? "removing" : "would remove";
|
|
1754
|
+
stdout(`gc: ${verb} ${result.candidates.length} artifact(s) older than ${result.retentionDays}d (cutoff ${result.cutoffIso}); keeping ${result.keptCount}:\n`);
|
|
1755
|
+
for (const c of result.candidates) {
|
|
1756
|
+
stdout(` [${c.category}] ${c.filePath} (${c.reason})\n`);
|
|
1757
|
+
}
|
|
1758
|
+
if (!result.applied) {
|
|
1759
|
+
stdout(`\nDry-run; pass --apply to delete.\n`);
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
stdout(`removed ${result.removed.length} file(s).\n`);
|
|
1763
|
+
if (result.failures.length > 0) {
|
|
1764
|
+
for (const f of result.failures) {
|
|
1765
|
+
stderr(`gc: failed to remove ${f.filePath}: ${f.reason}\n`);
|
|
1766
|
+
}
|
|
1767
|
+
throw new HarnessExitError(`${result.failures.length} deletion(s) failed`, EX_FAIL);
|
|
1768
|
+
}
|
|
1769
|
+
});
|
|
1638
1770
|
program
|
|
1639
1771
|
.command("uninstall")
|
|
1640
1772
|
.description("Clean teardown of a harness installation. Inventories harness-owned " +
|
|
1641
|
-
"
|
|
1642
|
-
"and mcpServers in settings.json)
|
|
1643
|
-
"
|
|
1773
|
+
"state (manifest, lock, harness.generated/, .understanding-gate/ under " +
|
|
1774
|
+
"the state root; hook groups and mcpServers in ~/.claude/settings.json) " +
|
|
1775
|
+
"and prints it. With --apply, removes it after writing a reversible " +
|
|
1776
|
+
"settings.json backup + snapshot. " +
|
|
1644
1777
|
"settings.json.pre-harness-<TS> backups are listed but never deleted, " +
|
|
1645
1778
|
"so the operator can hand them to --restore-from <path> (atomic restore " +
|
|
1646
1779
|
"from that file) or `rm` them manually.")
|
|
1647
1780
|
.option("--apply", "execute the teardown (default: dry-run listing only)")
|
|
1648
1781
|
.option("--restore-from <path>", "atomic restore: copy this file over settings.json instead of selective removal (implies --apply)")
|
|
1649
|
-
.option("--home <path>", "override ~/.claude/ root
|
|
1782
|
+
.option("--home <path>", "override ~/.claude/ (settings home; without --state it also overrides the state root, for tests / non-default installs)")
|
|
1783
|
+
.option("--state <path>", "override the harness state root (default: ~/.harness/, legacy fallback ~/.claude/)")
|
|
1650
1784
|
.option("--settings <path>", "override ~/.claude/settings.json")
|
|
1651
1785
|
.action(async (options) => {
|
|
1652
1786
|
const cliOpts = {};
|
|
@@ -1656,6 +1790,8 @@ export function buildProgram(opts = {}) {
|
|
|
1656
1790
|
cliOpts.restoreFrom = options.restoreFrom;
|
|
1657
1791
|
if (options.home)
|
|
1658
1792
|
cliOpts.homeDir = options.home;
|
|
1793
|
+
if (options.state)
|
|
1794
|
+
cliOpts.stateDir = options.state;
|
|
1659
1795
|
if (options.settings)
|
|
1660
1796
|
cliOpts.settingsPath = options.settings;
|
|
1661
1797
|
try {
|
|
@@ -1665,22 +1801,28 @@ export function buildProgram(opts = {}) {
|
|
|
1665
1801
|
const nothing = inv.manifestPath === null &&
|
|
1666
1802
|
inv.lockPath === null &&
|
|
1667
1803
|
inv.generatedDir === null &&
|
|
1804
|
+
inv.gateStateDir === null &&
|
|
1668
1805
|
inv.hookGroups.length === 0 &&
|
|
1669
1806
|
inv.mcpServers.length === 0 &&
|
|
1670
1807
|
inv.preHarnessBackups.length === 0;
|
|
1808
|
+
const rootsLabel = inv.stateDir === inv.homeDir
|
|
1809
|
+
? inv.homeDir
|
|
1810
|
+
: `${inv.stateDir} (state) + ${inv.homeDir} (settings)`;
|
|
1671
1811
|
if (nothing) {
|
|
1672
|
-
stdout(`no harness install found under ${
|
|
1812
|
+
stdout(`no harness install found under ${rootsLabel}; nothing to do.\n`);
|
|
1673
1813
|
for (const w of inv.warnings)
|
|
1674
1814
|
stderr(`warning: ${w}\n`);
|
|
1675
1815
|
return;
|
|
1676
1816
|
}
|
|
1677
|
-
stdout(`harness install under ${
|
|
1817
|
+
stdout(`harness install under ${rootsLabel}:\n`);
|
|
1678
1818
|
if (inv.manifestPath)
|
|
1679
1819
|
stdout(` manifest: ${inv.manifestPath}\n`);
|
|
1680
1820
|
if (inv.lockPath)
|
|
1681
1821
|
stdout(` lock: ${inv.lockPath}\n`);
|
|
1682
1822
|
if (inv.generatedDir)
|
|
1683
1823
|
stdout(` generated: ${inv.generatedDir}/\n`);
|
|
1824
|
+
if (inv.gateStateDir)
|
|
1825
|
+
stdout(` gate: ${inv.gateStateDir}/ (understanding-gate state)\n`);
|
|
1684
1826
|
if (inv.hookGroups.length > 0) {
|
|
1685
1827
|
stdout(` hook groups in ${inv.settingsPath}:\n`);
|
|
1686
1828
|
for (const g of inv.hookGroups) {
|
|
@@ -1734,11 +1876,29 @@ export function buildProgram(opts = {}) {
|
|
|
1734
1876
|
stdout(`removed from disk:\n`);
|
|
1735
1877
|
for (const f of result.removedFiles)
|
|
1736
1878
|
stdout(` ${f}\n`);
|
|
1879
|
+
// Explicit kept-list: name whatever survives under the state
|
|
1880
|
+
// root (machines/ + projects/ override layers are
|
|
1881
|
+
// operator-authored; foreign files are not ours to judge) so
|
|
1882
|
+
// the operator never has to discover residue by accident.
|
|
1883
|
+
try {
|
|
1884
|
+
const residue = fs
|
|
1885
|
+
.readdirSync(inv.stateDir)
|
|
1886
|
+
.filter((name) => !name.startsWith("settings.json"));
|
|
1887
|
+
if (residue.length > 0) {
|
|
1888
|
+
stdout(`kept under ${inv.stateDir}: ${residue.join(", ")} (not removed; operator-authored or out of scope)\n`);
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
catch {
|
|
1892
|
+
/* state root itself may be gone or unreadable; nothing to report */
|
|
1893
|
+
}
|
|
1737
1894
|
}
|
|
1738
1895
|
if (result.backupPath === null &&
|
|
1739
1896
|
result.snapshotPath === null &&
|
|
1740
1897
|
result.removedFiles.length === 0) {
|
|
1741
|
-
|
|
1898
|
+
const rootsLabel = inv.stateDir === inv.homeDir
|
|
1899
|
+
? inv.homeDir
|
|
1900
|
+
: `${inv.stateDir} (state) + ${inv.homeDir} (settings)`;
|
|
1901
|
+
stdout(`no harness install found under ${rootsLabel}; nothing to remove.\n`);
|
|
1742
1902
|
}
|
|
1743
1903
|
else {
|
|
1744
1904
|
stdout(`\nTo finish: \`npm uninstall -g @lannguyensi/harness\` (uninstall does not touch the npm install).\n`);
|