@tech-leads-club/harness-toolkit 0.4.0 → 0.4.2
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/bin/tlc-cli.ts +70 -18
- package/dist/compact-before.mjs +60 -60
- package/dist/doctor.mjs +77 -77
- package/dist/help-topic.mjs +3 -3
- package/dist/init-project.mjs +86 -86
- package/dist/install-runtime.mjs +76 -76
- package/dist/lessons-cli.mjs +82 -82
- package/dist/obs-cli.mjs +73 -73
- package/dist/price-lookup.mjs +2 -2
- package/dist/prompt-submit.mjs +60 -60
- package/dist/refresh-model-prices.mjs +69 -69
- package/dist/response-after.mjs +61 -61
- package/dist/run.mjs +61 -61
- package/dist/session-end.mjs +65 -65
- package/dist/session-start.mjs +66 -66
- package/dist/shim.mjs +74 -74
- package/dist/stop.mjs +70 -70
- package/dist/subagent-start.mjs +59 -59
- package/dist/subagent-stop.mjs +61 -61
- package/dist/support.mjs +71 -71
- package/dist/tlc-cli.mjs +94 -94
- package/dist/tool-after.mjs +58 -58
- package/dist/tool-before.mjs +69 -69
- package/dist/tool-failure.mjs +60 -60
- package/dist/uninstall-runtime.mjs +4 -4
- package/package.json +1 -1
- package/src/core/core.facade.ts +7 -0
- package/src/core/policy/policy.integrity.ts +13 -1
- package/src/core/policy/policy.shadow.ts +36 -40
- package/src/platform/paths.ts +85 -3
- package/src/providers/index.ts +1 -0
- package/tools/doctor.ts +12 -33
- package/tools/install-runtime.ts +15 -21
package/bin/tlc-cli.ts
CHANGED
|
@@ -9,18 +9,23 @@ import {
|
|
|
9
9
|
rmSync,
|
|
10
10
|
writeFileSync,
|
|
11
11
|
} from "node:fs";
|
|
12
|
-
import {
|
|
12
|
+
import { join } from "node:path";
|
|
13
13
|
import { coreFacade } from "../src/core/index.ts";
|
|
14
14
|
import { emitJson, JSON_FLAG, takeJsonFlag, unknownFlags } from "../src/platform/cli-output.ts";
|
|
15
15
|
import { linkDir, linkFile, seedConfig } from "../src/platform/links.ts";
|
|
16
16
|
import {
|
|
17
|
+
EXECUTABLE_EXTENSIONS,
|
|
18
|
+
executableOnPath,
|
|
19
|
+
findProjectRoot,
|
|
17
20
|
flagsDir,
|
|
18
21
|
isOnPath,
|
|
19
22
|
launcherBinDir,
|
|
23
|
+
machineHome,
|
|
20
24
|
projectConfigPath,
|
|
21
25
|
projectStateDir,
|
|
22
26
|
providerConfigDirs,
|
|
23
27
|
runtimeHome,
|
|
28
|
+
sameLocation,
|
|
24
29
|
} from "../src/platform/paths.ts";
|
|
25
30
|
import { type Row, render, type Screen, type Section } from "../src/platform/screen.ts";
|
|
26
31
|
import { createStyle, PLAIN, type Style } from "../src/platform/style.ts";
|
|
@@ -31,8 +36,17 @@ export class UsageError extends Error {}
|
|
|
31
36
|
// single door into core and the two cannot drift apart.
|
|
32
37
|
type Posture = ReturnType<typeof coreFacade.policy.resolveProjectPosture>;
|
|
33
38
|
|
|
39
|
+
/**
|
|
40
|
+
* invariant: an explicit `TLC_PROJECT_DIR` still wins — the hooks set it from the host's own payload, which knows
|
|
41
|
+
* the workspace better than a directory walk can. Everything else discovers the project the way `git` does
|
|
42
|
+
* ([/decisions/ad-101.md](/decisions/ad-101.md)).
|
|
43
|
+
*/
|
|
34
44
|
export function resolveProjectRoot(): string {
|
|
35
|
-
|
|
45
|
+
const declared = process.env.TLC_PROJECT_DIR;
|
|
46
|
+
if (declared) {
|
|
47
|
+
return declared;
|
|
48
|
+
}
|
|
49
|
+
return findProjectRoot(process.cwd()) ?? process.cwd();
|
|
36
50
|
}
|
|
37
51
|
|
|
38
52
|
export function modeFilePath(root: string): string {
|
|
@@ -386,9 +400,18 @@ export function acceptPolicy(root: string, paths: string[], interactive: boolean
|
|
|
386
400
|
const notHere = requested.filter((path) => !blocked.includes(path));
|
|
387
401
|
const outcome = coreFacade.policy.acceptPolicySources(root, requested);
|
|
388
402
|
if (outcome.kind === "not-a-source") {
|
|
403
|
+
/**
|
|
404
|
+
* hazard: this listed the sources and never said which project it had resolved. Run from a home directory,
|
|
405
|
+
* `projectConfigPath(root)` *is* the machine config path — so the list showed the same file twice, none of the
|
|
406
|
+
* repository's own paths, and no hint that the root was wrong. An operator read it as a defect in the product
|
|
407
|
+
* and lost the afternoon to it ([/decisions/ad-101.md](/decisions/ad-101.md)).
|
|
408
|
+
*
|
|
409
|
+
* invariant: the success path already names the project. The failure path is the one that needed it.
|
|
410
|
+
*/
|
|
389
411
|
throw new UsageError(
|
|
390
412
|
[
|
|
391
413
|
`not a policy source: ${outcome.paths.join(", ")}`,
|
|
414
|
+
`project: ${root} — pass TLC_PROJECT_DIR or run this from the repository whose session is blocked`,
|
|
392
415
|
"The sources the loader reads are:",
|
|
393
416
|
...outcome.sources.map((source) => ` ${source}`),
|
|
394
417
|
].join("\n"),
|
|
@@ -760,24 +783,16 @@ const GATE_FIELDS: Record<string, GateField> = {
|
|
|
760
783
|
* name is first, so a POSIX `foo` is never beaten by a stray `foo.exe`
|
|
761
784
|
* ([/decisions/ad-097.md](/decisions/ad-097.md)).
|
|
762
785
|
*/
|
|
763
|
-
|
|
764
|
-
|
|
786
|
+
/**
|
|
787
|
+
* why this still exists beside `executableOnPath`: it also answers for a name that is already a path, which a PATH
|
|
788
|
+
* walk has nothing to say about. The walk itself is not repeated here
|
|
789
|
+
* ([/decisions/ad-101.md](/decisions/ad-101.md)).
|
|
790
|
+
*/
|
|
765
791
|
export function resolveExecutable(name: string, env: NodeJS.ProcessEnv = process.env): string | null {
|
|
766
|
-
const candidates = (base: string): string[] => EXECUTABLE_EXTENSIONS.map((ext) => `${base}${ext}`);
|
|
767
|
-
|
|
768
792
|
if (name.includes("/") || name.includes("\\")) {
|
|
769
|
-
return
|
|
793
|
+
return EXECUTABLE_EXTENSIONS.map((ext) => `${name}${ext}`).find((c) => existsSync(c)) ?? null;
|
|
770
794
|
}
|
|
771
|
-
|
|
772
|
-
if (!dir) {
|
|
773
|
-
continue;
|
|
774
|
-
}
|
|
775
|
-
const found = candidates(join(dir, name)).find((candidate) => existsSync(candidate));
|
|
776
|
-
if (found) {
|
|
777
|
-
return found;
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
return null;
|
|
795
|
+
return executableOnPath(name, env);
|
|
781
796
|
}
|
|
782
797
|
|
|
783
798
|
/**
|
|
@@ -897,6 +912,29 @@ export function pricesHelpText(style: Style = PLAIN): string {
|
|
|
897
912
|
return render(pricesHelpScreen(), style);
|
|
898
913
|
}
|
|
899
914
|
|
|
915
|
+
/**
|
|
916
|
+
* The installed version, read from the runtime's own manifest.
|
|
917
|
+
*
|
|
918
|
+
* hazard: nothing showed it. `doctor` printed twenty rows and not one carried a version, and `update` on the npm
|
|
919
|
+
* route said `runtime → <path>` without naming what it was on or what it moved to
|
|
920
|
+
* ([/decisions/ad-101.md](/decisions/ad-101.md)).
|
|
921
|
+
*/
|
|
922
|
+
export function runtimeVersion(home: string): string | null {
|
|
923
|
+
try {
|
|
924
|
+
const raw = JSON.parse(readFileSync(join(home, "package.json"), "utf8")) as { version?: unknown };
|
|
925
|
+
return typeof raw.version === "string" ? raw.version : null;
|
|
926
|
+
} catch {
|
|
927
|
+
return null;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/** why one line: an unchanged version is the common case and reads better as a sentence than as two rows. */
|
|
932
|
+
export function versionMoveLine(before: string | null, after: string | null): string {
|
|
933
|
+
return before !== null && after !== null && before !== after
|
|
934
|
+
? `update: ${before} → ${after}`
|
|
935
|
+
: `update: already at ${after ?? before ?? "unknown"}`;
|
|
936
|
+
}
|
|
937
|
+
|
|
900
938
|
export function resolveHarnessRoot(): string {
|
|
901
939
|
const home = runtimeHome();
|
|
902
940
|
try {
|
|
@@ -995,6 +1033,18 @@ export function npmRootFailureMessage(home: string): string {
|
|
|
995
1033
|
* then reports it healthy while the command still does not exist.
|
|
996
1034
|
*/
|
|
997
1035
|
export function launcherLines(dest: string): string[] {
|
|
1036
|
+
/**
|
|
1037
|
+
* hazard: this linked unconditionally. An install to a throwaway `TLC_INSTALL_DEST` therefore pointed the
|
|
1038
|
+
* machine's `tlc` at that directory — measured: a proof-of-concept install into a temp directory left the
|
|
1039
|
+
* operator's command running from `/tmp`, and every `tlc harness ...` after it resolved its runtime there
|
|
1040
|
+
* ([/decisions/ad-101.md](/decisions/ad-101.md)).
|
|
1041
|
+
*
|
|
1042
|
+
* invariant: only the machine's own runtime home owns the command on `PATH`. Installing somewhere else is a
|
|
1043
|
+
* deliberate act and must not reach into anybody's shell.
|
|
1044
|
+
*/
|
|
1045
|
+
if (!sameLocation(dest, machineHome())) {
|
|
1046
|
+
return [`tlc not linked — ${dest} is not this machine's runtime home`];
|
|
1047
|
+
}
|
|
998
1048
|
const dir = launcherBinDir();
|
|
999
1049
|
const source = join(dest, "bin", "tlc");
|
|
1000
1050
|
// hazard: `symlinkSync` happily creates a link to a path that is not there, and `existsSync` on a dangling link
|
|
@@ -1369,7 +1419,8 @@ function runUpdate(root: string): never {
|
|
|
1369
1419
|
const dest = resolveHarnessRoot();
|
|
1370
1420
|
const revisionBefore = runtimeRevision(dest).revision;
|
|
1371
1421
|
const home = runtimeHome();
|
|
1372
|
-
|
|
1422
|
+
const versionBefore = runtimeVersion(dest);
|
|
1423
|
+
console.log(`update: runtime → ${dest} (${versionBefore ?? "unknown"})`);
|
|
1373
1424
|
|
|
1374
1425
|
if (!existsSync(join(dest, "bin", "tlc-exec.mjs"))) {
|
|
1375
1426
|
console.error(`update: missing install at ${home}`);
|
|
@@ -1414,6 +1465,7 @@ function runUpdate(root: string): never {
|
|
|
1414
1465
|
if ((sync.status ?? 1) !== 0) {
|
|
1415
1466
|
process.exit(sync.status ?? 1);
|
|
1416
1467
|
}
|
|
1468
|
+
console.log(versionMoveLine(versionBefore, runtimeVersion(dest)));
|
|
1417
1469
|
} else if (kind === "unmanaged") {
|
|
1418
1470
|
console.log(unmanagedRuntimeMessage(dest));
|
|
1419
1471
|
} else {
|