@tech-leads-club/harness-toolkit 0.2.1 → 0.3.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/README.md +22 -26
- package/bin/tlc-build.mjs +93 -0
- package/bin/tlc-cli.ts +117 -58
- package/bin/tlc-exec.mjs +16 -13
- package/dist/compact-before.mjs +151 -15
- package/dist/doctor.mjs +288 -45
- package/dist/help-topic.mjs +0 -0
- package/dist/init-project.mjs +15 -14
- package/dist/install-runtime.mjs +100 -17
- package/dist/lessons-cli.mjs +152 -8
- package/dist/obs-cli.mjs +149 -8
- package/dist/price-lookup.mjs +45 -23
- package/dist/prompt-submit.mjs +151 -15
- package/dist/refresh-model-prices.mjs +7190 -46
- package/dist/response-after.mjs +151 -15
- package/dist/run.mjs +151 -15
- package/dist/session-end.mjs +151 -15
- package/dist/session-start.mjs +151 -15
- package/dist/shim.mjs +7040 -16
- package/dist/stop.mjs +151 -15
- package/dist/subagent-start.mjs +151 -15
- package/dist/subagent-stop.mjs +151 -15
- package/dist/support.mjs +149 -8
- package/dist/tlc-cli.mjs +289 -89
- package/dist/tool-after.mjs +196 -38
- package/dist/tool-before.mjs +151 -15
- package/dist/tool-failure.mjs +151 -15
- package/dist/uninstall-runtime.mjs +9 -10
- package/docs/log.md +7 -0
- package/docs/measure.md +35 -31
- package/package.json +6 -5
- package/src/core/core.facade.ts +18 -0
- package/src/core/index.ts +3 -0
- package/src/core/pricing/pricing.freshness.ts +118 -0
- package/src/core/release/release.version.ts +147 -0
- package/src/core/shim/shim.precedence.ts +72 -0
- package/src/core/skill/skill.link.ts +92 -0
- package/src/entrypoints/shim.ts +49 -11
- package/src/platform/fs-atomic.ts +61 -23
- package/src/platform/links.ts +73 -0
- package/src/platform/paths.ts +27 -0
- package/src/platform/pricing.ts +139 -31
- package/src/providers/cursor/cursor.wiring.ts +11 -8
- package/tools/doctor.ts +127 -10
- package/tools/init-project.ts +34 -14
- package/tools/install-runtime.ts +89 -6
- package/tools/refresh-model-prices.ts +242 -75
- package/tools/uninstall-runtime.ts +23 -19
- package/CHANGELOG.md +0 -95
- package/bin/tlc-build +0 -80
- package/bin/tlc-exec +0 -10
- package/bin/tlc-exec.cmd +0 -4
- package/docs/decisions/ad-001.md +0 -32
- package/docs/decisions/ad-002.md +0 -51
- package/docs/decisions/ad-003.md +0 -30
- package/docs/decisions/ad-004.md +0 -37
- package/docs/decisions/ad-005.md +0 -36
- package/docs/decisions/ad-006.md +0 -49
- package/docs/decisions/ad-007.md +0 -36
- package/docs/decisions/ad-008.md +0 -54
- package/docs/decisions/ad-009.md +0 -61
- package/docs/decisions/ad-010.md +0 -45
- package/docs/decisions/ad-011.md +0 -59
- package/docs/decisions/ad-012.md +0 -71
- package/docs/decisions/ad-013.md +0 -87
- package/docs/decisions/ad-014.md +0 -56
- package/docs/decisions/ad-015.md +0 -33
- package/docs/decisions/ad-016.md +0 -98
- package/docs/decisions/ad-017.md +0 -65
- package/docs/decisions/ad-018.md +0 -77
- package/docs/decisions/ad-019.md +0 -75
- package/docs/decisions/ad-020.md +0 -88
- package/docs/decisions/ad-021.md +0 -57
- package/docs/decisions/ad-022.md +0 -120
- package/docs/decisions/ad-023.md +0 -87
- package/docs/decisions/ad-024.md +0 -85
- package/docs/decisions/ad-025.md +0 -92
- package/docs/decisions/ad-026.md +0 -131
- package/docs/decisions/ad-027.md +0 -90
- package/docs/decisions/ad-028.md +0 -90
- package/docs/decisions/ad-029.md +0 -73
- package/docs/decisions/ad-030.md +0 -97
- package/docs/decisions/ad-031.md +0 -94
- package/docs/decisions/ad-032.md +0 -83
- package/docs/decisions/ad-033.md +0 -89
- package/docs/decisions/ad-034.md +0 -86
- package/docs/decisions/ad-035.md +0 -86
- package/docs/decisions/ad-036.md +0 -68
- package/docs/decisions/ad-037.md +0 -47
- package/docs/decisions/ad-038.md +0 -52
- package/docs/decisions/ad-039.md +0 -69
- package/docs/decisions/ad-040.md +0 -89
- package/docs/decisions/ad-041.md +0 -98
- package/docs/decisions/ad-042.md +0 -82
- package/docs/decisions/ad-043.md +0 -79
- package/docs/decisions/ad-044.md +0 -61
- package/docs/decisions/ad-045.md +0 -94
- package/docs/decisions/ad-046.md +0 -111
- package/docs/decisions/ad-047.md +0 -96
- package/docs/decisions/ad-048.md +0 -85
- package/docs/decisions/ad-049.md +0 -66
- package/docs/decisions/ad-050.md +0 -94
- package/docs/decisions/ad-051.md +0 -69
- package/docs/decisions/ad-052.md +0 -69
- package/docs/decisions/ad-053.md +0 -78
- package/docs/decisions/ad-054.md +0 -98
- package/docs/decisions/ad-055.md +0 -74
- package/docs/decisions/ad-056.md +0 -85
- package/docs/decisions/ad-057.md +0 -68
- package/docs/decisions/ad-058.md +0 -97
- package/docs/decisions/ad-059.md +0 -82
- package/docs/decisions/ad-060.md +0 -75
- package/docs/decisions/ad-061.md +0 -68
- package/docs/decisions/ad-062.md +0 -72
- package/docs/decisions/ad-063.md +0 -84
- package/docs/decisions/ad-064.md +0 -79
- package/docs/decisions/ad-065.md +0 -81
- package/docs/decisions/ad-066.md +0 -111
- package/docs/decisions/ad-067.md +0 -64
- package/docs/decisions/ad-068.md +0 -79
- package/docs/decisions/ad-069.md +0 -74
- package/docs/decisions/ad-070.md +0 -86
- package/docs/decisions/ad-071.md +0 -93
- package/docs/decisions/ad-072.md +0 -82
- package/docs/decisions/ad-073.md +0 -102
- package/docs/decisions/ad-074.md +0 -91
- package/docs/decisions/ad-075.md +0 -79
- package/docs/decisions/ad-076.md +0 -102
- package/docs/decisions/ad-077.md +0 -94
- package/docs/decisions/ad-078.md +0 -84
- package/docs/decisions/ad-079.md +0 -73
- package/docs/decisions/ad-080.md +0 -86
- package/docs/decisions/ad-081.md +0 -70
- package/docs/decisions/ad-082.md +0 -79
- package/docs/decisions/ad-083.md +0 -88
- package/docs/decisions/index.md +0 -111
- package/model-aliases.json +0 -12
- package/model-prices.cursor.json +0 -410
- package/model-prices.json +0 -1
- package/tools/test-env.mjs +0 -28
- package/tools/test-env.names.d.mts +0 -1
- package/tools/test-env.names.mjs +0 -14
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ npm i -g @tech-leads-club/harness-toolkit
|
|
|
16
16
|
tlc harness install
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
Then restart Cursor or Claude Code. That is the whole setup —
|
|
19
|
+
Then restart Cursor or Claude Code. That is the whole setup — `install` finds which of the two you
|
|
20
20
|
have and wires only those, and the harness works in every repository right away with a safe baseline.
|
|
21
21
|
|
|
22
22
|
To give one project its own rules, open it and say **"setup harness"** to the agent, or run
|
|
@@ -41,12 +41,11 @@ To give one project its own rules, open it and say **"setup harness"** to the ag
|
|
|
41
41
|
12. [Connect a project](#connect-a-project)
|
|
42
42
|
13. [Paths and shared state](#paths-and-shared-state)
|
|
43
43
|
14. [Ship claims](#ship-claims)
|
|
44
|
-
15. [
|
|
45
|
-
16. [
|
|
46
|
-
17. [
|
|
47
|
-
18. [
|
|
48
|
-
19. [
|
|
49
|
-
20. [License](#license)
|
|
44
|
+
15. [Prices](#prices)
|
|
45
|
+
16. [Troubleshooting](#troubleshooting)
|
|
46
|
+
17. [Documentation](#documentation)
|
|
47
|
+
18. [Contributing](#contributing)
|
|
48
|
+
19. [License](#license)
|
|
50
49
|
|
|
51
50
|
## Everything it validates
|
|
52
51
|
|
|
@@ -214,9 +213,12 @@ nothing reads — `src/providers/provider.degrade.ts`.
|
|
|
214
213
|
|
|
215
214
|
| Dependency | Notes |
|
|
216
215
|
|------------|--------|
|
|
217
|
-
| **Bun** *or* **Node.js 24+** | Either one is enough. Bun runs every hook directly
|
|
218
|
-
| **npm** | Delivers the package
|
|
219
|
-
|
|
216
|
+
| **Bun** *or* **Node.js 24+** | Either one is enough. Bun runs every hook directly (~1 ms/hook); Node runs the shipped bundles (~27 ms/hook). With neither, the install stops and names both fixes |
|
|
217
|
+
| **npm** | Delivers the package, and generates the `tlc` shim for the platform it runs on |
|
|
218
|
+
|
|
219
|
+
Linux, macOS and Windows, same commands and same code path. CI runs the full suite and the bundle build on all
|
|
220
|
+
three on every push; hooks firing inside an editor session on Windows are outside that coverage
|
|
221
|
+
([`docs/decisions/ad-006.md`](docs/decisions/ad-006.md), [`docs/decisions/ad-097.md`](docs/decisions/ad-097.md)).
|
|
220
222
|
|
|
221
223
|
## Install
|
|
222
224
|
|
|
@@ -301,8 +303,8 @@ Reload/restart the provider session afterward if hooks or the init skill should
|
|
|
301
303
|
| `managed checkout` | moves it to upstream with a hard reset. Do not develop there — a local change is discarded |
|
|
302
304
|
| `link to a working clone` | nothing in the clone. That is a contributor install; you pull it yourself |
|
|
303
305
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
+
The bundles are rebuilt only when one is missing, and only Bun builds them — a second bundler emits different
|
|
307
|
+
bytes for the same source.
|
|
306
308
|
|
|
307
309
|
**If `update` aborts on `dist/`, install the package again.** A stuck install cannot deliver its own fix — the fix
|
|
308
310
|
lives in the revision `update` has to fetch — so `npm i -g @tech-leads-club/harness-toolkit@latest` followed by
|
|
@@ -352,7 +354,8 @@ graph LR
|
|
|
352
354
|
| Project shim (per provider) | `<repo>/.cursor/hooks.json`, `<repo>/.claude/settings.json` |
|
|
353
355
|
|
|
354
356
|
Entrypoint: `bin/tlc-exec.mjs`.
|
|
355
|
-
Wrappers: `bin/tlc
|
|
357
|
+
Wrappers: `bin/tlc` and `bin/tlc.cmd`, kept for installs made before the package existed. Everything else
|
|
358
|
+
spawns `bin/tlc-exec.mjs` with the interpreter already running.
|
|
356
359
|
|
|
357
360
|
See `tlc harness help architecture` or [`docs/architecture.md`](docs/architecture.md).
|
|
358
361
|
|
|
@@ -373,7 +376,7 @@ See `tlc harness help architecture` or [`docs/architecture.md`](docs/architectur
|
|
|
373
376
|
| `tlc harness attest` | Tamper-evident record of what each session ran under |
|
|
374
377
|
| `tlc harness handoff` | Handoff state between turns and sessions — the sanctioned reader |
|
|
375
378
|
| `tlc harness obs live` / `obs report` | Signal / session rollup |
|
|
376
|
-
| `tlc harness prices refresh` / `lookup` |
|
|
379
|
+
| `tlc harness prices refresh` / `lookup` | The machine's price catalogue |
|
|
377
380
|
| `tlc harness lessons list` | Lessons across the three tiers, with staleness and effectiveness |
|
|
378
381
|
| `tlc harness lessons add "…" [--ref path:symbol] [--global] [--pin]` | Write a lesson; `--ref` retires it when that stops resolving, `--pin` puts it ahead of ranking |
|
|
379
382
|
| `tlc harness policy` / `policy accept <path>` | List a mid-session policy change; accept exactly those paths |
|
|
@@ -427,26 +430,19 @@ HARNESS_PLAN_DEVIATION: src/x.ts — the call site moved with the type
|
|
|
427
430
|
Both are protocol lines on purpose. A gate that fires on free-English "done" fires on the word, not the
|
|
428
431
|
claim.
|
|
429
432
|
|
|
430
|
-
##
|
|
433
|
+
## Prices
|
|
431
434
|
|
|
432
435
|
```bash
|
|
433
436
|
tlc harness prices refresh
|
|
434
437
|
tlc harness prices refresh cursor
|
|
435
|
-
tlc harness prices refresh litellm
|
|
436
438
|
tlc harness prices lookup <model-id> [provider]
|
|
437
439
|
```
|
|
438
440
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
441
|
+
Catalogue: `~/.tlc/harness/model-prices.json`, one plane per billing origin. Overrides:
|
|
442
|
+
`model-prices.local.json`. Neither is versioned. `install` fetches once, `update` refreshes past a 7-day TTL,
|
|
443
|
+
`doctor` reports the age.
|
|
442
444
|
|
|
443
|
-
|
|
444
|
-
atomic writes retry, and the CLI ships a `.cmd` shim alongside directory junctions
|
|
445
|
-
([`docs/decisions/ad-006.md`](docs/decisions/ad-006.md)).
|
|
446
|
-
|
|
447
|
-
CI runs the full suite and the `dist/` build on `windows-latest` on every push.
|
|
448
|
-
|
|
449
|
-
Outside CI coverage: `install.ps1`, and hooks firing inside a Cursor or Claude Code session on Windows.
|
|
445
|
+
See `tlc harness help prices` or [`docs/measure.md`](docs/measure.md).
|
|
450
446
|
|
|
451
447
|
## Troubleshooting
|
|
452
448
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Build the Node-runnable ESM bundles under `dist/`.
|
|
4
|
+
*
|
|
5
|
+
* why this is not a shell script any more: the bash version ran only where bash runs, so `update` could not
|
|
6
|
+
* rebuild a missing bundle on Windows — `spawnSync` cannot execute an extensionless bash file there — and CI had
|
|
7
|
+
* to invoke it through `shell: bash` on all three legs. Nothing it does needs a shell
|
|
8
|
+
* ([/decisions/ad-097.md](/decisions/ad-097.md)).
|
|
9
|
+
*
|
|
10
|
+
* why Bun only: `dist/` is committed and `check-dist-fresh` compares bytes, so the bundler is part of the
|
|
11
|
+
* artefact. The bash version preferred Bun and fell back to esbuild, which means a contributor without Bun
|
|
12
|
+
* produced different bytes for the same source — measured at 223,390 against 228,018 for one bundle
|
|
13
|
+
* ([/decisions/ad-046.md](/decisions/ad-046.md)). One bundler is the only reproducible answer.
|
|
14
|
+
*
|
|
15
|
+
* invariant: what to build is derived from disk, one level deep. A hardcoded list silently stops building a new
|
|
16
|
+
* entrypoint, and the missing bundle only surfaces when a hook fires on somebody's machine.
|
|
17
|
+
*/
|
|
18
|
+
import { spawnSync } from "node:child_process";
|
|
19
|
+
import { chmodSync, existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
|
|
20
|
+
import { basename, dirname, join } from "node:path";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
|
|
23
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
24
|
+
const dist = join(root, "dist");
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* why one level: `tools/dev/` holds the checks that validate *this* repository — a user's install has no
|
|
28
|
+
* `src/core` of ours to validate — so the directory boundary is the whole declaration and nothing under it is
|
|
29
|
+
* ever a bundle ([/decisions/ad-068.md](/decisions/ad-068.md)).
|
|
30
|
+
*/
|
|
31
|
+
function sourcesIn(dir) {
|
|
32
|
+
if (!existsSync(dir)) {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
36
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".ts") && !entry.name.endsWith(".test.ts"))
|
|
37
|
+
.map((entry) => ({ name: basename(entry.name, ".ts"), source: join(dir, entry.name) }));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function buildOne(source, out) {
|
|
41
|
+
const result = spawnSync(
|
|
42
|
+
"bun",
|
|
43
|
+
["build", "--target=node", "--format=esm", `--outfile=${out}`, source],
|
|
44
|
+
{ stdio: "inherit" },
|
|
45
|
+
);
|
|
46
|
+
if (result.error?.code === "ENOENT") {
|
|
47
|
+
console.error("tlc-build: Bun is not on PATH, and dist/ is committed so the bundler is part of the artefact.");
|
|
48
|
+
console.error(" curl -fsSL https://bun.sh/install | bash");
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
if ((result.status ?? 1) !== 0) {
|
|
52
|
+
process.exit(result.status ?? 1);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const targets = [
|
|
57
|
+
...sourcesIn(join(root, "src", "entrypoints")),
|
|
58
|
+
...sourcesIn(join(root, "tools")),
|
|
59
|
+
{ name: "tlc-cli", source: join(root, "bin", "tlc-cli.ts") },
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
mkdirSync(dist, { recursive: true });
|
|
63
|
+
console.log(`tlc-build → ${dist}`);
|
|
64
|
+
for (const target of targets) {
|
|
65
|
+
buildOne(target.source, join(dist, `${target.name}.mjs`));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// invariant: the launcher stays executable on a filesystem that tracks the bit. A no-op where it does not.
|
|
69
|
+
try {
|
|
70
|
+
chmodSync(join(root, "bin", "tlc"), 0o755);
|
|
71
|
+
} catch {
|
|
72
|
+
// a checkout on a filesystem without a mode bit is not a build failure
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* hazard: a bundle whose source moved or was deleted is not rebuilt, so it is never diffed either — it stays in
|
|
77
|
+
* `dist/` and ships. Deriving what to remove from the same disk that decides what to build closes that.
|
|
78
|
+
*/
|
|
79
|
+
const wanted = new Set(targets.map((target) => `${target.name}.mjs`));
|
|
80
|
+
let bundles = 0;
|
|
81
|
+
for (const entry of readdirSync(dist, { withFileTypes: true })) {
|
|
82
|
+
if (!entry.isFile() || !entry.name.endsWith(".mjs")) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (wanted.has(entry.name)) {
|
|
86
|
+
bundles += 1;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
console.log(`tlc-build: pruning ${entry.name} — no source`);
|
|
90
|
+
rmSync(join(dist, entry.name), { force: true });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log(`tlc-build: ok (${bundles} bundles)`);
|
package/bin/tlc-cli.ts
CHANGED
|
@@ -9,11 +9,17 @@ import {
|
|
|
9
9
|
rmSync,
|
|
10
10
|
writeFileSync,
|
|
11
11
|
} from "node:fs";
|
|
12
|
-
import { homedir } from "node:os";
|
|
13
12
|
import { delimiter, join } from "node:path";
|
|
14
13
|
import { coreFacade } from "../src/core/index.ts";
|
|
15
14
|
import { emitJson, JSON_FLAG, takeJsonFlag, unknownFlags } from "../src/platform/cli-output.ts";
|
|
16
|
-
import {
|
|
15
|
+
import { linkDir, seedConfig } from "../src/platform/links.ts";
|
|
16
|
+
import {
|
|
17
|
+
flagsDir,
|
|
18
|
+
projectConfigPath,
|
|
19
|
+
projectStateDir,
|
|
20
|
+
providerConfigDirs,
|
|
21
|
+
runtimeHome,
|
|
22
|
+
} from "../src/platform/paths.ts";
|
|
17
23
|
import { type Row, render, type Screen, type Section } from "../src/platform/screen.ts";
|
|
18
24
|
import { createStyle, PLAIN, type Style } from "../src/platform/style.ts";
|
|
19
25
|
|
|
@@ -482,7 +488,7 @@ export const NPM_PACKAGE = "@tech-leads-club/harness-toolkit";
|
|
|
482
488
|
export const NPM_MARKER = "installed-from-npm";
|
|
483
489
|
|
|
484
490
|
/**
|
|
485
|
-
* hazard: `install
|
|
491
|
+
* hazard: `install --link` points the runtime path at the clone it was run from, so on a contributor's machine
|
|
486
492
|
* `~/.tlc/harness` is a symlink to their working repository. The old failure message told them to run
|
|
487
493
|
* `git reset --hard` there, which would have destroyed uncommitted work. Verified on this machine.
|
|
488
494
|
*
|
|
@@ -514,7 +520,7 @@ export function classifyRuntimePath(
|
|
|
514
520
|
* hazard: an earlier version also treated "resolves elsewhere" as linked, to catch a symlinked ancestor. macOS CI
|
|
515
521
|
* refuted it: `/var` is a symlink to `/private/var`, so every path under the system temp directory resolves
|
|
516
522
|
* elsewhere and a **managed** checkout was classified as linked — which would silently stop updates on the very
|
|
517
|
-
* platform the reporter uses. Only the last hop decides, which is the one thing
|
|
523
|
+
* platform the reporter uses. Only the last hop decides, which is the one thing the install actually creates.
|
|
518
524
|
*/
|
|
519
525
|
export function runtimePathKind(dest: string): RuntimePathKind {
|
|
520
526
|
return classifyRuntimePath(dest, {
|
|
@@ -746,13 +752,16 @@ const GATE_FIELDS: Record<string, GateField> = {
|
|
|
746
752
|
|
|
747
753
|
// why: resolved without executing. Running the binary to see whether it exists would run it, which is not
|
|
748
754
|
// something a config write is allowed to do.
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
755
|
+
/**
|
|
756
|
+
* why one list rather than a platform branch reading PATHEXT: the extensions only exist on the platform that uses
|
|
757
|
+
* them, so trying all of them everywhere finds the same file and removes a branch nobody can test twice. The bare
|
|
758
|
+
* name is first, so a POSIX `foo` is never beaten by a stray `foo.exe`
|
|
759
|
+
* ([/decisions/ad-097.md](/decisions/ad-097.md)).
|
|
760
|
+
*/
|
|
761
|
+
const EXECUTABLE_EXTENSIONS = ["", ".exe", ".cmd", ".bat", ".ps1"];
|
|
762
|
+
|
|
763
|
+
export function resolveExecutable(name: string, env: NodeJS.ProcessEnv = process.env): string | null {
|
|
764
|
+
const candidates = (base: string): string[] => EXECUTABLE_EXTENSIONS.map((ext) => `${base}${ext}`);
|
|
756
765
|
|
|
757
766
|
if (name.includes("/") || name.includes("\\")) {
|
|
758
767
|
return candidates(name).find((candidate) => existsSync(candidate)) ?? null;
|
|
@@ -861,22 +870,24 @@ export function helpText(style: Style = PLAIN): string {
|
|
|
861
870
|
|
|
862
871
|
export function pricesHelpScreen(): Screen {
|
|
863
872
|
return {
|
|
864
|
-
title: "
|
|
873
|
+
title: "prices",
|
|
865
874
|
sections: [
|
|
866
875
|
{
|
|
867
|
-
lines: ` tlc harness prices refresh [all|cursor|litellm]
|
|
876
|
+
lines: ` tlc harness prices refresh [all|cursor|litellm] [--if-stale]
|
|
868
877
|
tlc harness prices lookup <model-id>
|
|
869
878
|
|
|
870
|
-
refresh / refresh all
|
|
871
|
-
refresh cursor
|
|
872
|
-
refresh litellm
|
|
879
|
+
refresh / refresh all both planes of model-prices.json
|
|
880
|
+
refresh cursor the provider's own rates
|
|
881
|
+
refresh litellm the vendors' list prices
|
|
882
|
+
--if-stale fetch only past the 7-day TTL
|
|
873
883
|
lookup <model-id> catalog key, pool, USD for 1M in + 1M out
|
|
874
884
|
|
|
875
|
-
|
|
885
|
+
Catalogue: <runtime home>/model-prices.json — fetched per machine, never versioned
|
|
886
|
+
Overrides: <runtime home>/model-prices.local.json — yours, hand-written
|
|
876
887
|
Documentation: tlc harness help prices`.split("\n"),
|
|
877
888
|
},
|
|
878
889
|
],
|
|
879
|
-
footer: "resolution:
|
|
890
|
+
footer: "resolution: your overrides → the asking provider's plane → the vendor plane → null",
|
|
880
891
|
};
|
|
881
892
|
}
|
|
882
893
|
|
|
@@ -893,12 +904,69 @@ export function resolveHarnessRoot(): string {
|
|
|
893
904
|
}
|
|
894
905
|
}
|
|
895
906
|
|
|
907
|
+
/**
|
|
908
|
+
* Everything an install has to put in place outside the runtime directory itself: the init skill where each
|
|
909
|
+
* provider reads it, the user-level hooks, and a seeded config.
|
|
910
|
+
*
|
|
911
|
+
* hazard: there were three implementations of this — bash, PowerShell, and the POSIX branch here — and they
|
|
912
|
+
* disagreed. The PowerShell one linked the init skill into `~/.tlc/skills/harness-init`, which no provider reads,
|
|
913
|
+
* so on Windows `update` refreshed a skill nothing could route to. That is the defect
|
|
914
|
+
* [/decisions/ad-095.md](/decisions/ad-095.md) fixed on the other side, still live on this one
|
|
915
|
+
* ([/decisions/ad-097.md](/decisions/ad-097.md)).
|
|
916
|
+
*
|
|
917
|
+
* invariant: one function, no platform branch, and the launcher on PATH is npm's business.
|
|
918
|
+
*/
|
|
919
|
+
export function wireRuntime(dest: string, home: string): { lines: string[]; missingSkill: boolean } {
|
|
920
|
+
const lines: string[] = [];
|
|
921
|
+
const seeded = seedConfig(dest);
|
|
922
|
+
if (seeded.seeded) {
|
|
923
|
+
lines.push(`config seeded → ${seeded.path}`);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
if (!existsSync(join(dest, "skills", "harness-init"))) {
|
|
927
|
+
return { lines, missingSkill: true };
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const links = coreFacade.skill.skillLinks(dest, providerConfigDirs(), existsSync);
|
|
931
|
+
if (links.length === 0) {
|
|
932
|
+
lines.push("no provider config dir found — skill not linked");
|
|
933
|
+
}
|
|
934
|
+
for (const link of links) {
|
|
935
|
+
const outcome = linkDir(link.source, link.target);
|
|
936
|
+
lines.push(
|
|
937
|
+
outcome.kind === "refused" ? `skill not linked — ${outcome.reason}` : `skill → ${outcome.target}`,
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const hooks = spawnSync(process.execPath, [join(dest, "bin", "write-user-hooks.mjs")], {
|
|
942
|
+
stdio: "inherit",
|
|
943
|
+
env: { ...process.env, TLC_HOME: home },
|
|
944
|
+
});
|
|
945
|
+
if ((hooks.status ?? 1) !== 0) {
|
|
946
|
+
lines.push("hooks unchanged (merge manually or: node bin/write-user-hooks.mjs --force)");
|
|
947
|
+
}
|
|
948
|
+
return { lines, missingSkill: false };
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* hazard: this was the extensionless bash wrapper, so every `runEntry` spawn — `doctor`, `prices refresh`,
|
|
953
|
+
* `install-runtime`, `price-lookup` — named a file Windows cannot execute. The hooks never had this problem
|
|
954
|
+
* because they name the `.mjs` ([/decisions/ad-097.md](/decisions/ad-097.md)).
|
|
955
|
+
*
|
|
956
|
+
* invariant: paired with `process.execPath`, so the entry runs under the interpreter that is already running.
|
|
957
|
+
*/
|
|
896
958
|
export function execBinPath(): string {
|
|
897
|
-
return join(resolveHarnessRoot(), "bin", "tlc-exec");
|
|
959
|
+
return join(resolveHarnessRoot(), "bin", "tlc-exec.mjs");
|
|
898
960
|
}
|
|
899
961
|
|
|
962
|
+
/**
|
|
963
|
+
* why the `.mjs` and not a wrapper: the wrapper was bash, so `spawnSync` could not run it on Windows and
|
|
964
|
+
* `update` there could never rebuild a missing bundle ([/decisions/ad-097.md](/decisions/ad-097.md)).
|
|
965
|
+
*
|
|
966
|
+
* invariant: spawned with `process.execPath`, so the interpreter running the CLI is the one that builds.
|
|
967
|
+
*/
|
|
900
968
|
export function buildBinPath(): string {
|
|
901
|
-
return join(resolveHarnessRoot(), "bin", "tlc-build");
|
|
969
|
+
return join(resolveHarnessRoot(), "bin", "tlc-build.mjs");
|
|
902
970
|
}
|
|
903
971
|
|
|
904
972
|
export type Action =
|
|
@@ -1217,13 +1285,18 @@ function runUpdate(root: string): never {
|
|
|
1217
1285
|
const bump = spawnSync("npm", ["install", "-g", `${NPM_PACKAGE}@latest`], {
|
|
1218
1286
|
stdio: "inherit",
|
|
1219
1287
|
env: process.env,
|
|
1220
|
-
|
|
1288
|
+
// why: `npm` is `npm.cmd` on Windows and a shell is how that resolves; on POSIX it costs one `/bin/sh`,
|
|
1289
|
+
// and the argv here is fixed ([/decisions/ad-097.md](/decisions/ad-097.md)).
|
|
1290
|
+
shell: true,
|
|
1221
1291
|
});
|
|
1222
1292
|
if ((bump.status ?? 1) !== 0) {
|
|
1223
1293
|
console.error(npmUpdateFailureMessage());
|
|
1224
1294
|
process.exit(bump.status ?? 1);
|
|
1225
1295
|
}
|
|
1226
|
-
const sync = spawnSync(execBinPath(),
|
|
1296
|
+
const sync = spawnSync(process.execPath, [execBinPath(), "install-runtime"], {
|
|
1297
|
+
stdio: "inherit",
|
|
1298
|
+
env: process.env,
|
|
1299
|
+
});
|
|
1227
1300
|
if ((sync.status ?? 1) !== 0) {
|
|
1228
1301
|
process.exit(sync.status ?? 1);
|
|
1229
1302
|
}
|
|
@@ -1262,38 +1335,13 @@ function runUpdate(root: string): never {
|
|
|
1262
1335
|
);
|
|
1263
1336
|
}
|
|
1264
1337
|
|
|
1265
|
-
const
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
if (
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
stdio: "inherit",
|
|
1273
|
-
env: { ...process.env, TLC_HOME: home },
|
|
1274
|
-
cwd: dest,
|
|
1275
|
-
});
|
|
1276
|
-
if ((r.status ?? 1) !== 0) {
|
|
1277
|
-
process.exit(r.status ?? 1);
|
|
1278
|
-
}
|
|
1279
|
-
} else {
|
|
1280
|
-
const tlcBin = join(dest, "bin", "tlc");
|
|
1281
|
-
const skillSrc = join(dest, "skills", "harness-init");
|
|
1282
|
-
const skillDest = join(home, "..", "skills", "harness-init");
|
|
1283
|
-
spawnSync("ln", ["-sfn", tlcBin, join(binDir, "tlc")], { stdio: "inherit" });
|
|
1284
|
-
if (!existsSync(skillSrc)) {
|
|
1285
|
-
console.error(`update: missing skill at ${skillSrc}`);
|
|
1286
|
-
process.exit(1);
|
|
1287
|
-
}
|
|
1288
|
-
spawnSync("ln", ["-sfn", skillSrc, skillDest], { stdio: "inherit" });
|
|
1289
|
-
console.log(`update: skill → ${skillDest}`);
|
|
1290
|
-
const hooks = spawnSync(process.execPath, [join(dest, "bin", "write-user-hooks.mjs")], {
|
|
1291
|
-
stdio: "inherit",
|
|
1292
|
-
env: { ...process.env, TLC_HOME: home },
|
|
1293
|
-
});
|
|
1294
|
-
if ((hooks.status ?? 1) !== 0) {
|
|
1295
|
-
console.log("update: hooks unchanged (merge manually or: node bin/write-user-hooks.mjs --force)");
|
|
1296
|
-
}
|
|
1338
|
+
const wired = wireRuntime(dest, home);
|
|
1339
|
+
for (const line of wired.lines) {
|
|
1340
|
+
console.log(`update: ${line}`);
|
|
1341
|
+
}
|
|
1342
|
+
if (wired.missingSkill) {
|
|
1343
|
+
console.error(`update: missing skill at ${join(dest, "skills", "harness-init")}`);
|
|
1344
|
+
process.exit(1);
|
|
1297
1345
|
}
|
|
1298
1346
|
|
|
1299
1347
|
// invariant: never build into the artifact when it is already complete. `dist/` is committed for the Node
|
|
@@ -1305,7 +1353,7 @@ function runUpdate(root: string): never {
|
|
|
1305
1353
|
console.log("update: dist/ complete — no rebuild, so the runtime path stays clean");
|
|
1306
1354
|
} else if (existsSync(buildBinPath())) {
|
|
1307
1355
|
console.log(`update: ${missing.length} bundle(s) missing — building`);
|
|
1308
|
-
const build = spawnSync(buildBinPath()
|
|
1356
|
+
const build = spawnSync(process.execPath, [buildBinPath()], { stdio: "inherit", env: process.env });
|
|
1309
1357
|
if ((build.status ?? 1) !== 0) {
|
|
1310
1358
|
console.log(`update: build failed — ${missing.length} bundle(s) still missing from dist/`);
|
|
1311
1359
|
}
|
|
@@ -1314,8 +1362,19 @@ function runUpdate(root: string): never {
|
|
|
1314
1362
|
announceNewCapabilities(root, dest);
|
|
1315
1363
|
announceLandedDecisions(root, dest, revisionBefore);
|
|
1316
1364
|
|
|
1365
|
+
/**
|
|
1366
|
+
* why: an update is the moment a machine is already reaching the network, so it is the natural place to notice
|
|
1367
|
+
* that its prices are a week old. `--if-stale` is what keeps this from being a fetch on every update, and the
|
|
1368
|
+
* failure is tolerated — a rate that could not be fetched is not a reason for a failed update
|
|
1369
|
+
* ([/decisions/ad-096.md](/decisions/ad-096.md)).
|
|
1370
|
+
*/
|
|
1371
|
+
spawnSync(process.execPath, [execBinPath(), "refresh-model-prices", "all", "--if-stale"], {
|
|
1372
|
+
stdio: "inherit",
|
|
1373
|
+
env: { ...process.env, TLC_PROJECT_DIR: root },
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1317
1376
|
console.log("update: running doctor…");
|
|
1318
|
-
const doctor = spawnSync(execBinPath(),
|
|
1377
|
+
const doctor = spawnSync(process.execPath, [execBinPath(), "doctor"], {
|
|
1319
1378
|
stdio: "inherit",
|
|
1320
1379
|
env: { ...process.env, TLC_PROJECT_DIR: root },
|
|
1321
1380
|
});
|
|
@@ -1324,7 +1383,7 @@ function runUpdate(root: string): never {
|
|
|
1324
1383
|
}
|
|
1325
1384
|
|
|
1326
1385
|
function runEntry(entry: string, toolArgs: string[], root: string): never {
|
|
1327
|
-
const r = spawnSync(execBinPath(),
|
|
1386
|
+
const r = spawnSync(process.execPath, [execBinPath(), entry, ...toolArgs], {
|
|
1328
1387
|
stdio: "inherit",
|
|
1329
1388
|
env: { ...process.env, TLC_PROJECT_DIR: root },
|
|
1330
1389
|
});
|
|
@@ -1425,7 +1484,7 @@ function main(argv: string[]): void {
|
|
|
1425
1484
|
console.log(helpText(createStyle()));
|
|
1426
1485
|
break;
|
|
1427
1486
|
case "build": {
|
|
1428
|
-
const r = spawnSync(buildBinPath()
|
|
1487
|
+
const r = spawnSync(process.execPath, [buildBinPath()], { stdio: "inherit", env: process.env });
|
|
1429
1488
|
process.exit(r.status ?? 1);
|
|
1430
1489
|
break;
|
|
1431
1490
|
}
|
package/bin/tlc-exec.mjs
CHANGED
|
@@ -62,20 +62,23 @@ export function resolveHarnessHome(
|
|
|
62
62
|
return candidate;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
/**
|
|
66
|
+
* why a list and not a branch: the only difference between platforms is which of these names exists on disk, and
|
|
67
|
+
* asking for both costs one extra `existsSync` per PATH entry. `bun.exe` never exists on Linux and `bun` never
|
|
68
|
+
* shadows it on Windows ([/decisions/ad-097.md](/decisions/ad-097.md)).
|
|
69
|
+
*/
|
|
70
|
+
export const BUN_EXECUTABLE_NAMES = ["bun", "bun.exe"];
|
|
68
71
|
|
|
69
|
-
export function findBunOnPath(env = process.env
|
|
70
|
-
const
|
|
71
|
-
const bunName = bunExecutableName(platform);
|
|
72
|
-
for (const dir of pathValue.split(delimiter)) {
|
|
72
|
+
export function findBunOnPath(env = process.env) {
|
|
73
|
+
for (const dir of (env.PATH ?? "").split(delimiter)) {
|
|
73
74
|
if (!dir) {
|
|
74
75
|
continue;
|
|
75
76
|
}
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
for (const name of BUN_EXECUTABLE_NAMES) {
|
|
78
|
+
const candidate = join(dir, name);
|
|
79
|
+
if (existsSync(candidate)) {
|
|
80
|
+
return candidate;
|
|
81
|
+
}
|
|
79
82
|
}
|
|
80
83
|
}
|
|
81
84
|
return null;
|
|
@@ -122,12 +125,12 @@ export function writeRuntimeCache(harnessHome, bunPath) {
|
|
|
122
125
|
return record;
|
|
123
126
|
}
|
|
124
127
|
|
|
125
|
-
export function resolveBunPath(harnessHome, env = process.env
|
|
128
|
+
export function resolveBunPath(harnessHome, env = process.env) {
|
|
126
129
|
const cached = readRuntimeCache(harnessHome);
|
|
127
130
|
if (cached) {
|
|
128
131
|
return cached.bunPath;
|
|
129
132
|
}
|
|
130
|
-
const found = findBunOnPath(env
|
|
133
|
+
const found = findBunOnPath(env);
|
|
131
134
|
writeRuntimeCache(harnessHome, found);
|
|
132
135
|
return found;
|
|
133
136
|
}
|
|
@@ -167,7 +170,7 @@ export function decideRuntime({ harnessHome, entry, bunPath, nodeMajor, distExis
|
|
|
167
170
|
status: 1,
|
|
168
171
|
message: [
|
|
169
172
|
`tlc: Node ${process.version} found, but dist/${entry}.mjs is missing.`,
|
|
170
|
-
` Run: ${join(harnessHome, "bin", "tlc-build")}`,
|
|
173
|
+
` Run: node ${join(harnessHome, "bin", "tlc-build.mjs")}`,
|
|
171
174
|
].join("\n"),
|
|
172
175
|
};
|
|
173
176
|
}
|