@tech-leads-club/harness-toolkit 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +95 -0
- package/LICENSE +84 -0
- package/NOTICE +9 -0
- package/README.md +480 -0
- package/bin/tlc +10 -0
- package/bin/tlc-build +80 -0
- package/bin/tlc-cli.ts +1509 -0
- package/bin/tlc-exec +10 -0
- package/bin/tlc-exec.cmd +4 -0
- package/bin/tlc-exec.d.mts +60 -0
- package/bin/tlc-exec.mjs +248 -0
- package/bin/tlc.cmd +4 -0
- package/bin/tlc.mjs +7 -0
- package/bin/write-user-hooks.d.mts +40 -0
- package/bin/write-user-hooks.mjs +123 -0
- package/capabilities/catalog.json +418 -0
- package/config.example.json +51 -0
- package/dist/compact-before.mjs +7839 -0
- package/dist/doctor.mjs +8263 -0
- package/dist/help-topic.mjs +82 -0
- package/dist/init-project.mjs +993 -0
- package/dist/install-runtime.mjs +969 -0
- package/dist/lessons-cli.mjs +7258 -0
- package/dist/obs-cli.mjs +7025 -0
- package/dist/price-lookup.mjs +221 -0
- package/dist/prompt-submit.mjs +7860 -0
- package/dist/refresh-model-prices.mjs +158 -0
- package/dist/response-after.mjs +7879 -0
- package/dist/run.mjs +7830 -0
- package/dist/session-end.mjs +7937 -0
- package/dist/session-start.mjs +8056 -0
- package/dist/shim.mjs +52 -0
- package/dist/stop.mjs +8494 -0
- package/dist/subagent-start.mjs +7861 -0
- package/dist/subagent-stop.mjs +7851 -0
- package/dist/support.mjs +7045 -0
- package/dist/tlc-cli.mjs +8063 -0
- package/dist/tool-after.mjs +8113 -0
- package/dist/tool-before.mjs +7985 -0
- package/dist/tool-failure.mjs +7850 -0
- package/dist/uninstall-runtime.mjs +1307 -0
- package/docs/architecture.md +270 -0
- package/docs/concepts.md +562 -0
- package/docs/coverage.md +55 -0
- package/docs/decisions/ad-001.md +32 -0
- package/docs/decisions/ad-002.md +51 -0
- package/docs/decisions/ad-003.md +30 -0
- package/docs/decisions/ad-004.md +37 -0
- package/docs/decisions/ad-005.md +36 -0
- package/docs/decisions/ad-006.md +49 -0
- package/docs/decisions/ad-007.md +36 -0
- package/docs/decisions/ad-008.md +54 -0
- package/docs/decisions/ad-009.md +61 -0
- package/docs/decisions/ad-010.md +45 -0
- package/docs/decisions/ad-011.md +59 -0
- package/docs/decisions/ad-012.md +71 -0
- package/docs/decisions/ad-013.md +87 -0
- package/docs/decisions/ad-014.md +56 -0
- package/docs/decisions/ad-015.md +33 -0
- package/docs/decisions/ad-016.md +98 -0
- package/docs/decisions/ad-017.md +65 -0
- package/docs/decisions/ad-018.md +77 -0
- package/docs/decisions/ad-019.md +75 -0
- package/docs/decisions/ad-020.md +88 -0
- package/docs/decisions/ad-021.md +57 -0
- package/docs/decisions/ad-022.md +120 -0
- package/docs/decisions/ad-023.md +87 -0
- package/docs/decisions/ad-024.md +85 -0
- package/docs/decisions/ad-025.md +92 -0
- package/docs/decisions/ad-026.md +131 -0
- package/docs/decisions/ad-027.md +90 -0
- package/docs/decisions/ad-028.md +90 -0
- package/docs/decisions/ad-029.md +73 -0
- package/docs/decisions/ad-030.md +97 -0
- package/docs/decisions/ad-031.md +94 -0
- package/docs/decisions/ad-032.md +83 -0
- package/docs/decisions/ad-033.md +89 -0
- package/docs/decisions/ad-034.md +86 -0
- package/docs/decisions/ad-035.md +86 -0
- package/docs/decisions/ad-036.md +68 -0
- package/docs/decisions/ad-037.md +47 -0
- package/docs/decisions/ad-038.md +52 -0
- package/docs/decisions/ad-039.md +69 -0
- package/docs/decisions/ad-040.md +89 -0
- package/docs/decisions/ad-041.md +98 -0
- package/docs/decisions/ad-042.md +82 -0
- package/docs/decisions/ad-043.md +79 -0
- package/docs/decisions/ad-044.md +61 -0
- package/docs/decisions/ad-045.md +94 -0
- package/docs/decisions/ad-046.md +111 -0
- package/docs/decisions/ad-047.md +96 -0
- package/docs/decisions/ad-048.md +85 -0
- package/docs/decisions/ad-049.md +66 -0
- package/docs/decisions/ad-050.md +94 -0
- package/docs/decisions/ad-051.md +69 -0
- package/docs/decisions/ad-052.md +69 -0
- package/docs/decisions/ad-053.md +78 -0
- package/docs/decisions/ad-054.md +98 -0
- package/docs/decisions/ad-055.md +74 -0
- package/docs/decisions/ad-056.md +85 -0
- package/docs/decisions/ad-057.md +68 -0
- package/docs/decisions/ad-058.md +97 -0
- package/docs/decisions/ad-059.md +82 -0
- package/docs/decisions/ad-060.md +75 -0
- package/docs/decisions/ad-061.md +68 -0
- package/docs/decisions/ad-062.md +72 -0
- package/docs/decisions/ad-063.md +84 -0
- package/docs/decisions/ad-064.md +79 -0
- package/docs/decisions/ad-065.md +81 -0
- package/docs/decisions/ad-066.md +111 -0
- package/docs/decisions/ad-067.md +64 -0
- package/docs/decisions/ad-068.md +79 -0
- package/docs/decisions/ad-069.md +74 -0
- package/docs/decisions/ad-070.md +86 -0
- package/docs/decisions/ad-071.md +93 -0
- package/docs/decisions/ad-072.md +82 -0
- package/docs/decisions/ad-073.md +102 -0
- package/docs/decisions/ad-074.md +91 -0
- package/docs/decisions/ad-075.md +79 -0
- package/docs/decisions/ad-076.md +102 -0
- package/docs/decisions/ad-077.md +94 -0
- package/docs/decisions/ad-078.md +84 -0
- package/docs/decisions/ad-079.md +73 -0
- package/docs/decisions/ad-080.md +86 -0
- package/docs/decisions/ad-081.md +70 -0
- package/docs/decisions/ad-082.md +79 -0
- package/docs/decisions/ad-083.md +88 -0
- package/docs/decisions/index.md +111 -0
- package/docs/diagnose.md +248 -0
- package/docs/index.md +52 -0
- package/docs/init.md +101 -0
- package/docs/lessons.md +294 -0
- package/docs/log.md +139 -0
- package/docs/measure.md +178 -0
- package/docs/providers/claude-code.md +118 -0
- package/docs/providers/cursor.md +110 -0
- package/docs/providers/index.md +71 -0
- package/docs/troubleshooting.md +138 -0
- package/model-aliases.json +12 -0
- package/model-prices.cursor.json +410 -0
- package/model-prices.json +1 -0
- package/package.json +57 -0
- package/skills/harness-init/SKILL.md +241 -0
- package/skills/harness-init/evals/trigger_evals.json +86 -0
- package/skills/harness-init/references/capabilities.md +113 -0
- package/src/contracts/capabilities.ts +30 -0
- package/src/contracts/decision.ts +21 -0
- package/src/contracts/effort.ts +15 -0
- package/src/contracts/harness-event.ts +87 -0
- package/src/contracts/index.ts +6 -0
- package/src/contracts/policy-defaults.ts +14 -0
- package/src/contracts/wiring.ts +20 -0
- package/src/core/attest/attest.service.ts +0 -0
- package/src/core/capability/capability.service.ts +62 -0
- package/src/core/capability/capability.store.ts +51 -0
- package/src/core/capability/capability.types.ts +56 -0
- package/src/core/comment-policy/comment-policy.service.ts +307 -0
- package/src/core/comment-policy/comment-policy.types.ts +6 -0
- package/src/core/comment-policy/comment-resolvability.ts +134 -0
- package/src/core/comment-policy/comment-syntax.catalog.ts +310 -0
- package/src/core/comment-policy/comment-syntax.store.ts +65 -0
- package/src/core/comment-policy/comment-syntax.types.ts +24 -0
- package/src/core/core.facade.ts +461 -0
- package/src/core/duplication/duplication.service.ts +219 -0
- package/src/core/floor/floor.catalog.ts +54 -0
- package/src/core/floor/floor.paths.ts +90 -0
- package/src/core/floor/floor.policy-surface.ts +311 -0
- package/src/core/floor/floor.service.ts +306 -0
- package/src/core/floor/floor.tokenize.ts +179 -0
- package/src/core/floor/floor.verb.ts +28 -0
- package/src/core/floor/index.ts +1 -0
- package/src/core/gate/gate.artifact.ts +157 -0
- package/src/core/gate/gate.command.ts +130 -0
- package/src/core/gate/gate.findings.ts +201 -0
- package/src/core/gate/gate.inputs.ts +0 -0
- package/src/core/gate/gate.lock.ts +283 -0
- package/src/core/gate/gate.service.ts +28 -0
- package/src/core/gate/gate.types.ts +67 -0
- package/src/core/handoff/handoff.service.ts +43 -0
- package/src/core/handoff/handoff.store.ts +59 -0
- package/src/core/handoff/handoff.types.ts +89 -0
- package/src/core/index.ts +52 -0
- package/src/core/integrity/state-seal.ts +81 -0
- package/src/core/lesson/lesson.authored.ts +96 -0
- package/src/core/lesson/lesson.credit.ts +51 -0
- package/src/core/lesson/lesson.garden.ts +283 -0
- package/src/core/lesson/lesson.link.ts +93 -0
- package/src/core/lesson/lesson.score.ts +61 -0
- package/src/core/lesson/lesson.select.ts +209 -0
- package/src/core/lesson/lesson.service.ts +110 -0
- package/src/core/lesson/lesson.store.ts +317 -0
- package/src/core/lesson/lesson.sync.ts +70 -0
- package/src/core/lesson/lesson.types.ts +92 -0
- package/src/core/lesson/lesson.validity.ts +44 -0
- package/src/core/observability/observability.report.ts +326 -0
- package/src/core/observability/observability.service.ts +328 -0
- package/src/core/observability/observability.store.ts +269 -0
- package/src/core/observability/observability.types.ts +193 -0
- package/src/core/observability/observability.why.ts +206 -0
- package/src/core/observe/observe.service.ts +76 -0
- package/src/core/plan/plan.detect.ts +47 -0
- package/src/core/plan/plan.service.ts +81 -0
- package/src/core/plan/plan.types.ts +25 -0
- package/src/core/policy/policy.defaults.ts +103 -0
- package/src/core/policy/policy.guard.ts +34 -0
- package/src/core/policy/policy.integrity.ts +238 -0
- package/src/core/policy/policy.loader.ts +118 -0
- package/src/core/policy/policy.operator.ts +82 -0
- package/src/core/policy/policy.posture.ts +79 -0
- package/src/core/policy/policy.rails.ts +45 -0
- package/src/core/policy/policy.types.ts +157 -0
- package/src/core/presence/presence.service.ts +111 -0
- package/src/core/presence/presence.store.ts +59 -0
- package/src/core/presence/presence.types.ts +9 -0
- package/src/core/release/release.decisions.ts +147 -0
- package/src/core/release/release.seen.ts +38 -0
- package/src/core/shell-policy/shell-policy.service.ts +215 -0
- package/src/core/shell-policy/shell-policy.stall.ts +57 -0
- package/src/core/shell-policy/shell-policy.types.ts +26 -0
- package/src/core/ship/ship.ledger.ts +76 -0
- package/src/core/ship/ship.service.ts +137 -0
- package/src/core/ship/ship.types.ts +19 -0
- package/src/core/stagnation/stagnation.resolution.ts +87 -0
- package/src/core/stagnation/stagnation.service.ts +20 -0
- package/src/core/stagnation/stagnation.store.ts +49 -0
- package/src/core/stagnation/stagnation.types.ts +12 -0
- package/src/core/subagent-policy/subagent-policy.parent-model.ts +202 -0
- package/src/core/subagent-policy/subagent-policy.service.ts +139 -0
- package/src/core/subagent-policy/subagent-policy.types.ts +22 -0
- package/src/core/supply-chain/supply-chain.catalog.ts +51 -0
- package/src/core/supply-chain/supply-chain.service.ts +217 -0
- package/src/core/turn/turn.activity.ts +94 -0
- package/src/core/turn/turn.autopilot.ts +131 -0
- package/src/core/turn/turn.failure-signals.ts +193 -0
- package/src/core/turn/turn.loop-counter.ts +71 -0
- package/src/core/turn/turn.types.ts +14 -0
- package/src/core/untrusted/untrusted.detect.ts +53 -0
- package/src/core/untrusted/untrusted.recall.ts +87 -0
- package/src/core/untrusted/untrusted.service.ts +129 -0
- package/src/core/untrusted/untrusted.store.ts +75 -0
- package/src/core/untrusted/untrusted.types.ts +42 -0
- package/src/entrypoints/compact-before.ts +14 -0
- package/src/entrypoints/prompt-submit.ts +25 -0
- package/src/entrypoints/response-after.ts +60 -0
- package/src/entrypoints/run.ts +179 -0
- package/src/entrypoints/session-end.ts +58 -0
- package/src/entrypoints/session-start.ts +172 -0
- package/src/entrypoints/shim.ts +46 -0
- package/src/entrypoints/stop.ts +876 -0
- package/src/entrypoints/subagent-start.ts +16 -0
- package/src/entrypoints/subagent-stop.ts +32 -0
- package/src/entrypoints/support.ts +182 -0
- package/src/entrypoints/tool-after.ts +111 -0
- package/src/entrypoints/tool-before.ts +186 -0
- package/src/entrypoints/tool-failure.ts +26 -0
- package/src/platform/backoff.ts +49 -0
- package/src/platform/cli-output.ts +30 -0
- package/src/platform/env-scope.ts +22 -0
- package/src/platform/fs-atomic.ts +142 -0
- package/src/platform/fs-jsonl.ts +24 -0
- package/src/platform/git.ts +187 -0
- package/src/platform/paths.ts +80 -0
- package/src/platform/pricing.ts +229 -0
- package/src/platform/process.ts +64 -0
- package/src/platform/sanitize.ts +23 -0
- package/src/platform/screen.ts +46 -0
- package/src/platform/style.ts +103 -0
- package/src/providers/claude/claude.capabilities.ts +25 -0
- package/src/providers/claude/claude.detect.ts +14 -0
- package/src/providers/claude/claude.inbound.ts +252 -0
- package/src/providers/claude/claude.lessons-view.ts +33 -0
- package/src/providers/claude/claude.outbound.ts +65 -0
- package/src/providers/claude/claude.policy-defaults.ts +9 -0
- package/src/providers/claude/claude.transcript.ts +62 -0
- package/src/providers/claude/claude.wiring.ts +245 -0
- package/src/providers/claude/index.ts +17 -0
- package/src/providers/cursor/cursor.capabilities.ts +28 -0
- package/src/providers/cursor/cursor.detect.ts +14 -0
- package/src/providers/cursor/cursor.inbound.ts +203 -0
- package/src/providers/cursor/cursor.lessons-view.ts +30 -0
- package/src/providers/cursor/cursor.outbound.ts +43 -0
- package/src/providers/cursor/cursor.policy-defaults.ts +9 -0
- package/src/providers/cursor/cursor.wiring.ts +214 -0
- package/src/providers/cursor/index.ts +17 -0
- package/src/providers/index.ts +9 -0
- package/src/providers/provider.degrade.ts +152 -0
- package/src/providers/provider.port.ts +20 -0
- package/src/providers/provider.registry.ts +28 -0
- package/tools/doctor.ts +631 -0
- package/tools/help-topic.ts +74 -0
- package/tools/init-project.ts +277 -0
- package/tools/install-runtime.ts +164 -0
- package/tools/lessons-cli.ts +474 -0
- package/tools/obs-cli.ts +175 -0
- package/tools/price-lookup.ts +57 -0
- package/tools/refresh-model-prices.ts +210 -0
- package/tools/test-env.mjs +28 -0
- package/tools/test-env.names.d.mts +1 -0
- package/tools/test-env.names.mjs +14 -0
- package/tools/uninstall-runtime.ts +463 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
lstatSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
readlinkSync,
|
|
6
|
+
realpathSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
statSync,
|
|
9
|
+
unlinkSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
14
|
+
import { NPM_MARKER, NPM_PACKAGE } from "../bin/tlc-cli.ts";
|
|
15
|
+
import { claudeConfigDir, cursorConfigDir, runtimeHome } from "../src/platform/paths.ts";
|
|
16
|
+
import { type Row, render, type Screen } from "../src/platform/screen.ts";
|
|
17
|
+
import { createStyle, PLAIN, type Style } from "../src/platform/style.ts";
|
|
18
|
+
import { removeClaudeWiring, unmergeClaudeSettings } from "../src/providers/claude/claude.wiring.ts";
|
|
19
|
+
import { unwireCursorHooks } from "../src/providers/cursor/cursor.wiring.ts";
|
|
20
|
+
import { OPERATOR_OWNED, RUNTIME_PAYLOAD } from "./install-runtime.ts";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* `unmerge` rewrites a file the operator owns, keeping everything that is not ours. `unlink` removes a symlink
|
|
24
|
+
* without reading through it. `remove` deletes a path the installer created. `keep` is an artefact examined and
|
|
25
|
+
* deliberately left. `manual` is work this command will not do, printed so it is not silently skipped.
|
|
26
|
+
*/
|
|
27
|
+
export type ItemAction = "unmerge" | "unlink" | "remove" | "keep" | "manual";
|
|
28
|
+
|
|
29
|
+
export type PlanItem = { action: ItemAction; target: string; detail: string };
|
|
30
|
+
|
|
31
|
+
export type UninstallPlan = {
|
|
32
|
+
items: PlanItem[];
|
|
33
|
+
purge: boolean;
|
|
34
|
+
/** hazard: a symlinked home points at somebody's working clone. Nothing inside it may be removed. */
|
|
35
|
+
homeIsLink: boolean;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type UninstallTargets = {
|
|
39
|
+
home: string;
|
|
40
|
+
binLink: string;
|
|
41
|
+
claudeSettings: string;
|
|
42
|
+
cursorHooks: string;
|
|
43
|
+
skillLinks: string[];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* hazard: `install.ps1` does not write the same artefacts as `install.sh`. It resolves the home from
|
|
48
|
+
* `USERPROFILE`, **copies** `tlc.cmd` into the bin directory instead of linking it, and puts one skill junction
|
|
49
|
+
* at `~/.tlc/skills/harness-init` rather than one inside each provider's directory. Reading the POSIX layout on
|
|
50
|
+
* Windows finds none of them and reports a clean machine ([/decisions/ad-066.md](/decisions/ad-066.md)).
|
|
51
|
+
*/
|
|
52
|
+
export function uninstallTargets(
|
|
53
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
54
|
+
platform: NodeJS.Platform = process.platform,
|
|
55
|
+
): UninstallTargets {
|
|
56
|
+
const windows = platform === "win32";
|
|
57
|
+
const userHome = (windows ? env.USERPROFILE : env.HOME)?.trim() || homedir();
|
|
58
|
+
const binDir = env.TLC_BIN_DIR?.trim() || join(userHome, ".local", "bin");
|
|
59
|
+
return {
|
|
60
|
+
home: runtimeHome(env),
|
|
61
|
+
binLink: join(binDir, windows ? "tlc.cmd" : "tlc"),
|
|
62
|
+
claudeSettings: join(claudeConfigDir(), "settings.json"),
|
|
63
|
+
cursorHooks: join(cursorConfigDir(), "hooks.json"),
|
|
64
|
+
skillLinks: windows
|
|
65
|
+
? [join(userHome, ".tlc", "skills", "harness-init")]
|
|
66
|
+
: [
|
|
67
|
+
join(claudeConfigDir(), "skills", "harness-init"),
|
|
68
|
+
join(cursorConfigDir(), "skills", "harness-init"),
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isSymlink(path: string): boolean {
|
|
74
|
+
try {
|
|
75
|
+
return lstatSync(path).isSymbolicLink();
|
|
76
|
+
} catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The real path of `path`, resolving as much of it as exists on disk and re-appending the rest.
|
|
83
|
+
*
|
|
84
|
+
* hazard: `realpathSync` throws on a path whose tail is absent, and a link into a runtime home already removed is
|
|
85
|
+
* exactly the state a partial run leaves. Returning the literal text in that case compared a resolved path against
|
|
86
|
+
* an unresolved one — and on macOS the OS temp directory sits under `/var`, itself a symlink to `/private/var`, so
|
|
87
|
+
* the two forms never matched and a dangling link of ours read as somebody else's. Green on Linux, red on macOS
|
|
88
|
+
* CI ([/decisions/ad-066.md](/decisions/ad-066.md)).
|
|
89
|
+
*/
|
|
90
|
+
export function canonicalise(path: string): string {
|
|
91
|
+
let head = resolve(path);
|
|
92
|
+
const tail: string[] = [];
|
|
93
|
+
for (;;) {
|
|
94
|
+
try {
|
|
95
|
+
return join(realpathSync(head), ...tail);
|
|
96
|
+
} catch {
|
|
97
|
+
const parent = dirname(head);
|
|
98
|
+
if (parent === head) {
|
|
99
|
+
return resolve(path);
|
|
100
|
+
}
|
|
101
|
+
tail.unshift(basename(head));
|
|
102
|
+
head = parent;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function resolveLink(path: string): string {
|
|
108
|
+
try {
|
|
109
|
+
return realpathSync(path);
|
|
110
|
+
} catch {
|
|
111
|
+
try {
|
|
112
|
+
const target = readlinkSync(path);
|
|
113
|
+
return canonicalise(isAbsolute(target) ? target : resolve(dirname(path), target));
|
|
114
|
+
} catch {
|
|
115
|
+
return canonicalise(path);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* hazard: this was `target.startsWith(`${root}/`)`, and Windows separates with `\`. Every path landed outside
|
|
122
|
+
* every root, so on Windows CI the harness's own links read as somebody else's. `relative` is separator-aware and
|
|
123
|
+
* case-folds the drive the way the platform does; the `pathApi` parameter is what lets the win32 rules be tested
|
|
124
|
+
* from any machine ([/decisions/ad-066.md](/decisions/ad-066.md)).
|
|
125
|
+
*/
|
|
126
|
+
export function isInsideRoot(
|
|
127
|
+
target: string,
|
|
128
|
+
root: string,
|
|
129
|
+
pathApi: Pick<typeof import("node:path"), "relative" | "isAbsolute"> = { relative, isAbsolute },
|
|
130
|
+
): boolean {
|
|
131
|
+
if (target === root) {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
const step = pathApi.relative(root, target);
|
|
135
|
+
return step !== "" && !step.startsWith("..") && !pathApi.isAbsolute(step);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// invariant: a link is ours only when it lands inside the runtime home. A `tlc` on PATH belonging to something
|
|
139
|
+
// else keeps its name, and this command is not the place to argue about it. Both sides go through the same
|
|
140
|
+
// resolution, because comparing a resolved path with an unresolved one is how this broke.
|
|
141
|
+
function pointsInto(link: string, home: string): boolean {
|
|
142
|
+
return isInsideRoot(resolveLink(link), canonicalise(home));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* `target` — the link is ours only when it lands inside the runtime home. `location` — the path itself is the
|
|
147
|
+
* installer's artefact and the target is irrelevant.
|
|
148
|
+
*
|
|
149
|
+
* hazard: both rules were `target` at first, and running the command on the machine it was written on found a
|
|
150
|
+
* `~/.claude/skills/harness-init` pointing at a `/tmp` install deleted weeks earlier. Under the target rule that
|
|
151
|
+
* dangling link is "not ours" and survives every uninstall forever. `tlc` on PATH is a name anybody may own, so
|
|
152
|
+
* it keeps the target rule; `skills/harness-init` is a path only this installer writes
|
|
153
|
+
* ([/decisions/ad-066.md](/decisions/ad-066.md)).
|
|
154
|
+
*/
|
|
155
|
+
type LinkOwnership = "target" | "location";
|
|
156
|
+
|
|
157
|
+
const LAUNCHER_MARKER = "tlc-exec.mjs";
|
|
158
|
+
|
|
159
|
+
function carriesLauncherMarker(path: string): boolean {
|
|
160
|
+
try {
|
|
161
|
+
return statSync(path).size < 4096 && readFileSync(path, "utf8").includes(LAUNCHER_MARKER);
|
|
162
|
+
} catch {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function planLink(
|
|
168
|
+
items: PlanItem[],
|
|
169
|
+
path: string,
|
|
170
|
+
home: string,
|
|
171
|
+
label: string,
|
|
172
|
+
ownership: LinkOwnership,
|
|
173
|
+
): void {
|
|
174
|
+
if (!existsSync(path) && !isSymlink(path)) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (!isSymlink(path)) {
|
|
178
|
+
// why: on Windows the launcher is a copy, not a link, so "not a link" is not the same as "not ours". The
|
|
179
|
+
// copy carries the launcher name in its one command line, which is the same marker every other artefact
|
|
180
|
+
// identifies itself by.
|
|
181
|
+
if (carriesLauncherMarker(path)) {
|
|
182
|
+
items.push({ action: "remove", target: path, detail: `${label}, installed as a copy` });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
items.push({
|
|
186
|
+
action: "keep",
|
|
187
|
+
target: path,
|
|
188
|
+
detail: `${label} is a real file the installer did not write`,
|
|
189
|
+
});
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (ownership === "target" && !pointsInto(path, home)) {
|
|
193
|
+
items.push({ action: "keep", target: path, detail: `points at ${resolveLink(path)} — not ours` });
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const stale = ownership === "location" && !pointsInto(path, home);
|
|
197
|
+
items.push({
|
|
198
|
+
action: "unlink",
|
|
199
|
+
target: path,
|
|
200
|
+
detail: stale ? `${label}, stale — points at ${resolveLink(path)}` : label,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function planClaude(items: PlanItem[], settingsPath: string): void {
|
|
205
|
+
if (!existsSync(settingsPath)) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const result = unmergeClaudeSettings(readFileSync(settingsPath, "utf8"));
|
|
209
|
+
if (!result.ok) {
|
|
210
|
+
items.push({
|
|
211
|
+
action: "keep",
|
|
212
|
+
target: settingsPath,
|
|
213
|
+
detail: `left untouched — it does not parse as JSON: ${result.error}`,
|
|
214
|
+
});
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (!result.changed) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
items.push({
|
|
221
|
+
action: "unmerge",
|
|
222
|
+
target: settingsPath,
|
|
223
|
+
detail: "drop the harness hook groups, keep every other key and every foreign hook",
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function planCursor(items: PlanItem[], hooksPath: string): void {
|
|
228
|
+
const text = existsSync(hooksPath) ? readFileSync(hooksPath, "utf8") : null;
|
|
229
|
+
const result = unwireCursorHooks(text);
|
|
230
|
+
switch (result.kind) {
|
|
231
|
+
case "absent":
|
|
232
|
+
return;
|
|
233
|
+
case "unparsed":
|
|
234
|
+
items.push({ action: "keep", target: hooksPath, detail: "left untouched — it does not parse as JSON" });
|
|
235
|
+
return;
|
|
236
|
+
case "empty":
|
|
237
|
+
if (result.removed > 0) {
|
|
238
|
+
items.push({ action: "remove", target: hooksPath, detail: `${result.removed} entries, all ours` });
|
|
239
|
+
}
|
|
240
|
+
return;
|
|
241
|
+
default:
|
|
242
|
+
items.push({
|
|
243
|
+
action: "unmerge",
|
|
244
|
+
target: hooksPath,
|
|
245
|
+
detail: `drop ${result.removed} harness entries, keep the rest`,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function planRuntime(items: PlanItem[], home: string, purge: boolean): boolean {
|
|
251
|
+
const homeIsLink = isSymlink(home);
|
|
252
|
+
if (homeIsLink) {
|
|
253
|
+
// hazard: this is the contributor's checkout on every development machine. `rm -rf` through the link deletes
|
|
254
|
+
// the repository. The installer refuses to touch it for the same reason ([/decisions/ad-046.md](/decisions/ad-046.md)) and so does this.
|
|
255
|
+
items.push({
|
|
256
|
+
action: "unlink",
|
|
257
|
+
target: home,
|
|
258
|
+
detail: `a link to ${resolveLink(home)} — the checkout it points at is left exactly as it is`,
|
|
259
|
+
});
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
if (!existsSync(home)) {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
for (const entry of RUNTIME_PAYLOAD) {
|
|
266
|
+
const path = join(home, entry);
|
|
267
|
+
if (existsSync(path)) {
|
|
268
|
+
items.push({ action: "remove", target: path, detail: "runtime payload" });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const marker = join(home, NPM_MARKER);
|
|
272
|
+
if (existsSync(marker)) {
|
|
273
|
+
items.push({ action: "remove", target: marker, detail: "install marker" });
|
|
274
|
+
}
|
|
275
|
+
for (const entry of OPERATOR_OWNED) {
|
|
276
|
+
const path = join(home, entry);
|
|
277
|
+
if (!existsSync(path)) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
items.push(
|
|
281
|
+
purge
|
|
282
|
+
? { action: "remove", target: path, detail: "--purge" }
|
|
283
|
+
: { action: "keep", target: path, detail: "yours — add --purge to remove it" },
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function planManual(items: PlanItem[], home: string): void {
|
|
290
|
+
if (existsSync(join(home, NPM_MARKER))) {
|
|
291
|
+
items.push({
|
|
292
|
+
action: "manual",
|
|
293
|
+
target: `npm uninstall -g ${NPM_PACKAGE}`,
|
|
294
|
+
// why: a global prefix owned by root needs sudo, and an npm call failing halfway through a teardown leaves
|
|
295
|
+
// a worse state than one that never started ([/decisions/ad-066.md](/decisions/ad-066.md)).
|
|
296
|
+
detail: "the global package is reported, never removed for you",
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
items.push({
|
|
300
|
+
action: "manual",
|
|
301
|
+
target: "rm -rf .tlc/ in each repository",
|
|
302
|
+
detail: "per-project config and state — this command does not search your disk for them",
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function planUninstall(targets: UninstallTargets, options: { purge?: boolean } = {}): UninstallPlan {
|
|
307
|
+
const purge = options.purge === true;
|
|
308
|
+
const items: PlanItem[] = [];
|
|
309
|
+
|
|
310
|
+
planClaude(items, targets.claudeSettings);
|
|
311
|
+
planCursor(items, targets.cursorHooks);
|
|
312
|
+
for (const link of targets.skillLinks) {
|
|
313
|
+
planLink(items, link, targets.home, "skill link", "location");
|
|
314
|
+
}
|
|
315
|
+
planLink(items, targets.binLink, targets.home, "the tlc launcher on PATH", "target");
|
|
316
|
+
const homeIsLink = planRuntime(items, targets.home, purge);
|
|
317
|
+
planManual(items, targets.home);
|
|
318
|
+
|
|
319
|
+
return { items, purge, homeIsLink };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Everything the plan would change. `keep` and `manual` are reported, not counted. */
|
|
323
|
+
export function pendingItems(plan: UninstallPlan): PlanItem[] {
|
|
324
|
+
return plan.items.filter(
|
|
325
|
+
(item) => item.action === "unmerge" || item.action === "unlink" || item.action === "remove",
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export type UninstallResult = { applied: PlanItem[]; failed: { item: PlanItem; reason: string }[] };
|
|
330
|
+
|
|
331
|
+
export function applyUninstall(plan: UninstallPlan, targets: UninstallTargets): UninstallResult {
|
|
332
|
+
const applied: PlanItem[] = [];
|
|
333
|
+
const failed: { item: PlanItem; reason: string }[] = [];
|
|
334
|
+
|
|
335
|
+
for (const item of pendingItems(plan)) {
|
|
336
|
+
try {
|
|
337
|
+
if (item.action === "unmerge" && item.target === targets.claudeSettings) {
|
|
338
|
+
removeClaudeWiring(item.target);
|
|
339
|
+
} else if (item.action === "unmerge") {
|
|
340
|
+
const result = unwireCursorHooks(readFileSync(item.target, "utf8"));
|
|
341
|
+
if (result.kind === "rewritten") {
|
|
342
|
+
writeFileSync(item.target, result.text, "utf8");
|
|
343
|
+
}
|
|
344
|
+
} else if (item.action === "unlink") {
|
|
345
|
+
// why: `unlinkSync` states the intent — remove the link, never what it points at. Measured: `rmSync`
|
|
346
|
+
// with `recursive` happens to agree, unlinking a symlink rather than descending it. The guard that
|
|
347
|
+
// actually prevents the data loss is `planRuntime` returning before any payload path is planned, since
|
|
348
|
+
// `home/src` under a linked home resolves inside the checkout.
|
|
349
|
+
unlinkSync(item.target);
|
|
350
|
+
} else {
|
|
351
|
+
rmSync(item.target, { recursive: true, force: true });
|
|
352
|
+
}
|
|
353
|
+
applied.push(item);
|
|
354
|
+
} catch (error) {
|
|
355
|
+
failed.push({ item, reason: error instanceof Error ? error.message : String(error) });
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return { applied, failed };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const ACTION_LEVEL = {
|
|
362
|
+
unmerge: "warn",
|
|
363
|
+
unlink: "warn",
|
|
364
|
+
remove: "warn",
|
|
365
|
+
keep: "ok",
|
|
366
|
+
manual: "info",
|
|
367
|
+
} as const;
|
|
368
|
+
|
|
369
|
+
export function uninstallScreen(plan: UninstallPlan, result: UninstallResult | null): Screen {
|
|
370
|
+
const pending = pendingItems(plan);
|
|
371
|
+
const applied = result !== null;
|
|
372
|
+
|
|
373
|
+
if (pending.length === 0) {
|
|
374
|
+
return {
|
|
375
|
+
title: "harness uninstall",
|
|
376
|
+
summary: ["nothing wired"],
|
|
377
|
+
sections: [
|
|
378
|
+
{
|
|
379
|
+
rows: [{ label: "state", value: "no harness artefact found — nothing to undo", level: "ok" }],
|
|
380
|
+
},
|
|
381
|
+
{ title: "STILL YOURS TO DO", rows: manualRows(plan) },
|
|
382
|
+
],
|
|
383
|
+
footer: `already clean · reinstall with npm i -g ${NPM_PACKAGE}, then \`tlc harness install\``,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const changes: Row[] = pending.map((item) => ({
|
|
388
|
+
label: item.action,
|
|
389
|
+
value: `${item.target} — ${item.detail}`,
|
|
390
|
+
level: ACTION_LEVEL[item.action],
|
|
391
|
+
}));
|
|
392
|
+
const kept = plan.items.filter((item) => item.action === "keep");
|
|
393
|
+
|
|
394
|
+
const sections = [
|
|
395
|
+
{ title: applied ? "REMOVED" : "WOULD REMOVE", rows: changes },
|
|
396
|
+
...(kept.length > 0
|
|
397
|
+
? [
|
|
398
|
+
{
|
|
399
|
+
title: "KEPT",
|
|
400
|
+
rows: kept.map((item) => ({
|
|
401
|
+
label: "keep",
|
|
402
|
+
value: `${item.target} — ${item.detail}`,
|
|
403
|
+
level: "ok" as const,
|
|
404
|
+
})),
|
|
405
|
+
},
|
|
406
|
+
]
|
|
407
|
+
: []),
|
|
408
|
+
{ title: "STILL YOURS TO DO", rows: manualRows(plan) },
|
|
409
|
+
...(result !== null && result.failed.length > 0
|
|
410
|
+
? [
|
|
411
|
+
{
|
|
412
|
+
title: "FAILED",
|
|
413
|
+
rows: result.failed.map((entry) => ({
|
|
414
|
+
label: "error",
|
|
415
|
+
value: `${entry.item.target} — ${entry.reason}`,
|
|
416
|
+
level: "fail" as const,
|
|
417
|
+
})),
|
|
418
|
+
},
|
|
419
|
+
]
|
|
420
|
+
: []),
|
|
421
|
+
];
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
title: "harness uninstall",
|
|
425
|
+
summary: [
|
|
426
|
+
applied ? `${result.applied.length} applied` : `${pending.length} pending`,
|
|
427
|
+
plan.purge ? "purge: state included" : "purge: state kept",
|
|
428
|
+
plan.homeIsLink ? "runtime: linked checkout, unlinked only" : "runtime: owned by the installer",
|
|
429
|
+
],
|
|
430
|
+
sections,
|
|
431
|
+
footer: applied
|
|
432
|
+
? `reinstall any time with npm i -g ${NPM_PACKAGE}, then \`tlc harness install\``
|
|
433
|
+
: "nothing was changed · re-run with --yes to apply this plan",
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function manualRows(plan: UninstallPlan): Row[] {
|
|
438
|
+
return plan.items
|
|
439
|
+
.filter((item) => item.action === "manual")
|
|
440
|
+
.map((item) => ({ label: "run", value: `${item.target} — ${item.detail}`, level: "info" as const }));
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export function uninstallReportText(
|
|
444
|
+
plan: UninstallPlan,
|
|
445
|
+
result: UninstallResult | null,
|
|
446
|
+
style: Style = PLAIN,
|
|
447
|
+
): string {
|
|
448
|
+
return render(uninstallScreen(plan, result), style);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
452
|
+
const targets = uninstallTargets();
|
|
453
|
+
const plan = planUninstall(targets, { purge: argv.includes("--purge") });
|
|
454
|
+
// why: the plan is the confirmation. A prompt needs a TTY, and the operator reaching for this is as likely to
|
|
455
|
+
// be in CI or a shell that is already half-broken ([/decisions/ad-066.md](/decisions/ad-066.md)).
|
|
456
|
+
const result = argv.includes("--yes") ? applyUninstall(plan, targets) : null;
|
|
457
|
+
console.log(uninstallReportText(plan, result, createStyle()));
|
|
458
|
+
return result !== null && result.failed.length > 0 ? 1 : 0;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (import.meta.main) {
|
|
462
|
+
process.exitCode = main();
|
|
463
|
+
}
|