@adrrr/tarmac 0.1.2 → 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 +109 -47
- package/dist/cli.js +20 -5
- package/dist/collect.js +17 -9
- package/dist/fleet.js +11 -0
- package/dist/install.js +349 -15
- package/dist/map.js +110 -0
- package/dist/reap.js +9 -4
- package/dist/render.js +285 -18
- package/dist/server.js +12 -2
- package/dist/sessions.js +21 -1
- package/dist/snapshots.js +19 -2
- package/dist/wrapper.js +108 -14
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -1,62 +1,111 @@
|
|
|
1
1
|
# tarmac
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@adrrr/tarmac)
|
|
4
|
+
[](https://github.com/adrrr/tarmac/actions/workflows/ci.yml)
|
|
4
5
|

|
|
5
6
|

|
|
6
7
|

|
|
7
8
|
|
|
8
9
|
**Fleet observability for Claude Code.** One table for every session you have running —
|
|
9
|
-
busy or idle, how full its context is, which model, what it has cost so far.
|
|
10
|
+
busy or idle, how full its context is, which model, what it has cost so far. It reads
|
|
11
|
+
documented surfaces only, never an internal format.
|
|
10
12
|
|
|
11
13
|
```
|
|
12
|
-
$ npx @adrrr/tarmac
|
|
14
|
+
$ npx @adrrr/tarmac
|
|
13
15
|
|
|
14
16
|
PROJECT STATE CTX AS OF MODEL EFFORT COST UP
|
|
15
|
-
apollo busy 28% 7m Fable 5 max $
|
|
16
|
-
mercury-dashboard busy 27% 22m ! Fable 5 max $
|
|
17
|
-
gemini idle 12% 3h ! Opus 5 high $
|
|
17
|
+
apollo busy 28% 7m Fable 5 max $41.20 8h
|
|
18
|
+
mercury-dashboard busy 27% 22m ! Fable 5 max $62.75 8h
|
|
19
|
+
gemini idle 12% 3h ! Opus 5 high $5.40 8h
|
|
18
20
|
atlas idle — fresh 8h ! Opus 5 high $0.00 8h
|
|
19
21
|
|
|
20
|
-
! 3 reading(s) marked "!" are older than
|
|
22
|
+
! 3 reading(s) marked "!" are older than 10m (--stale-after)
|
|
21
23
|
|
|
22
|
-
4 sessions · 2 busy · $
|
|
24
|
+
4 sessions · 2 busy · $109.35
|
|
23
25
|
```
|
|
24
26
|
|
|
27
|
+
That is the table with the status line chained. Without it — nothing installed at all — the
|
|
28
|
+
same command still lists every session, its state and its uptime, straight from
|
|
29
|
+
`claude agents --json`; the context column reads `— absent`, model, effort and cost fall to
|
|
30
|
+
`—`, and the line under the table counts how many sessions are covered.
|
|
31
|
+
|
|
32
|
+
Node ≥ 20. **Zero runtime dependencies** — no framework, no bundler, nothing to audit.
|
|
33
|
+
|
|
34
|
+
[Manual](docs/MANUAL.md) · [Changelog](CHANGELOG.md) · [Issues](https://github.com/adrrr/tarmac/issues)
|
|
35
|
+
|
|
25
36
|
## Install
|
|
26
37
|
|
|
27
38
|
```bash
|
|
28
|
-
npx @adrrr/tarmac
|
|
29
|
-
npx @adrrr/tarmac
|
|
30
|
-
npx @adrrr/tarmac
|
|
31
|
-
npx @adrrr/tarmac
|
|
32
|
-
npx @adrrr/tarmac uninstall #
|
|
39
|
+
npx @adrrr/tarmac # one-shot fleet table
|
|
40
|
+
npx @adrrr/tarmac --watch # the same table, redrawn every 5s until ^C
|
|
41
|
+
npx @adrrr/tarmac serve # the same fleet in the browser
|
|
42
|
+
npx @adrrr/tarmac install # chain the status line: unlocks ctx, model, effort and cost
|
|
43
|
+
npx @adrrr/tarmac uninstall # hand your status line back
|
|
33
44
|
```
|
|
34
45
|
|
|
35
|
-
|
|
46
|
+
`install` changes the `statusLine` key of `~/.claude/settings.json`, and never on your
|
|
47
|
+
say-so alone. It prints the whole plan first — including the exact command that undoes it —
|
|
48
|
+
and waits for a **typed word** (`y` is not an answer; scripts pass `--yes`, deliberately):
|
|
36
49
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
50
|
+
```
|
|
51
|
+
tarmac install — your home
|
|
52
|
+
|
|
53
|
+
file /Users/jane/.claude/settings.json
|
|
54
|
+
statusLine now ~/bin/my-line.sh
|
|
55
|
+
statusLine next /Users/jane/.claude/tarmac/statusline.sh
|
|
56
|
+
↳ which calls ~/bin/my-line.sh (your display is unchanged)
|
|
57
|
+
snapshots /Users/jane/.local/state/tarmac/snapshots
|
|
58
|
+
undo tarmac uninstall
|
|
59
|
+
|
|
60
|
+
Type "install" to proceed, anything else to abort:
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
A status line you already had is **wrapped, not replaced**: its display stays byte-identical,
|
|
64
|
+
and `uninstall` names which of its four restore modes ran — `bytes`, the usual one, puts the
|
|
65
|
+
original file back exactly. Note that the write re-serialises `settings.json`, so a
|
|
66
|
+
version-controlled one shows a formatting diff, not a one-line diff.
|
|
67
|
+
|
|
68
|
+
The only things that land under `~/.claude/` are that wrapper and the `backup.json` that
|
|
69
|
+
undoes it, neither of which changes at runtime: the snapshots the wrapper writes at every
|
|
70
|
+
frame go to `$XDG_STATE_HOME/tarmac/snapshots` (`~/.local/state/tarmac/snapshots` by
|
|
71
|
+
default), because `~/.claude` is a directory people commit. Coming from 0.1.x, `install`
|
|
72
|
+
clears the payloads an older version left in there, says how many and from where, and — when
|
|
73
|
+
`~/.claude` is a git repository — prints the `.gitignore` line worth adding.
|
|
42
74
|
|
|
43
75
|
## The dashboard
|
|
44
76
|
|
|
45
77
|
`tarmac serve` puts the same fleet in the browser — every session a row, ages that keep
|
|
46
|
-
climbing, and a banner the moment a refresh fails instead of a table quietly going stale
|
|
78
|
+
climbing, and a banner the moment a refresh fails instead of a table quietly going stale. It
|
|
79
|
+
prints the settings it resolved, then the URL it got:
|
|
47
80
|
|
|
48
81
|
```
|
|
49
82
|
tarmac serving http://127.0.0.1:4477
|
|
50
83
|
```
|
|
51
84
|
|
|
52
|
-
It binds to loopback and refuses any request whose `Host` is not loopback
|
|
53
|
-
paths and costs never leave the machine.
|
|
85
|
+
It binds to loopback and refuses any request whose `Host` is not loopback, or that a browser
|
|
86
|
+
does not mark same-origin — your cwd paths and costs never leave the machine. A busy
|
|
87
|
+
**default** port walks up to the next free one and says so; a port you chose yourself — flag,
|
|
88
|
+
environment or config file — refuses instead, because you chose it.
|
|
89
|
+
|
|
90
|
+
### The map
|
|
91
|
+
|
|
92
|
+
The tab in the header swaps the table for the same fleet as nodes — one per session, the arc
|
|
93
|
+
its context, the shape by the name its state, and a single halo when a reading for it landed
|
|
94
|
+
moments ago. It is the same reading in the same fragment, so the two views can never disagree.
|
|
95
|
+
|
|
96
|
+
The rules the table follows, the map follows: a reading past the freshness threshold is drawn
|
|
97
|
+
thin, amber and dated `! 3h ago` rather than as a live one, and a percentage nobody measured
|
|
98
|
+
is an empty dotted dial that names which kind of nothing it is — never a ring at zero, and
|
|
99
|
+
never a halo, however new the file it came in. A background agent is placed beside the session
|
|
100
|
+
sharing its working directory, because the working directory is the only thing the two provably
|
|
101
|
+
share; nothing is nested, and no edge is drawn for a relationship the sources do not publish.
|
|
102
|
+
Details in [the manual](docs/MANUAL.md#the-map).
|
|
54
103
|
|
|
55
104
|
## Why it does not break
|
|
56
105
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
106
|
+
The usual ways to watch a Claude Code fleet read something Claude Code never promised would
|
|
107
|
+
stay put: transcript files, terminal panes, undocumented paths. Those break on an update,
|
|
108
|
+
and — worse — they break *quietly*, reporting a calm empty fleet.
|
|
60
109
|
|
|
61
110
|
tarmac reads two things instead:
|
|
62
111
|
|
|
@@ -66,47 +115,60 @@ tarmac reads two things instead:
|
|
|
66
115
|
| the status line payload | context %, model, effort, cost | the JSON Claude Code hands to your own `statusLine.command` on every frame — **observed, not published as a schema** |
|
|
67
116
|
|
|
68
117
|
That second line is the honest caveat, and it is the reason the real defence is not
|
|
69
|
-
immunity, it is **visible degradation
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
118
|
+
immunity, it is **visible degradation**. A missing measurement is never a confident `0`: it
|
|
119
|
+
is an em dash that names which kind of missing it is — `absent` for a session no status line
|
|
120
|
+
ever wrote for, `fresh` for one that has taken no turn yet, `drift` for a release that moved
|
|
121
|
+
the payload out from under us — and the fleet-wide count sits under the table, as
|
|
122
|
+
`! statusline chained on 0/4 sessions`. A stale reading keeps its value and gets **dated**
|
|
123
|
+
with a `!`. And when a session shows up on a Claude Code build no fixture covers, tarmac
|
|
124
|
+
names that version *before* anything breaks, and keeps reporting. The full state-by-state
|
|
125
|
+
table is in [`docs/MANUAL.md`](docs/MANUAL.md).
|
|
74
126
|
|
|
75
127
|
## Commands
|
|
76
128
|
|
|
77
129
|
| Command | What it does | Options |
|
|
78
130
|
|---|---|---|
|
|
79
|
-
| `tarmac list` | one-shot fleet table | `--home`, `--stale-after`, `--snapshots-dir`, `--claude-bin`, `--json`, `--watch` |
|
|
80
|
-
| `tarmac serve` | local dashboard, `GET /` for the
|
|
131
|
+
| `tarmac list` | one-shot fleet table — the default, so bare `tarmac` runs it | `--home`, `--stale-after`, `--snapshots-dir`, `--claude-bin`, `--json`, `--watch` |
|
|
132
|
+
| `tarmac serve` | local dashboard, `GET /` for the table, `GET /map` for the map, `GET /live` for the fragment both refresh from, `GET /api/fleet` for JSON | `--home`, `--port`, `--stale-after`, `--snapshots-dir`, `--claude-bin` |
|
|
81
133
|
| `tarmac install` | chain the status line under `<home>/.claude/settings.json`, after confirmation | `--home`, `--yes` |
|
|
82
134
|
| `tarmac uninstall` | restore it, and say which of the four restore modes ran | `--home`, `--yes` |
|
|
83
135
|
|
|
84
136
|
`--help` works everywhere. An option handed to a command that does not read it is an
|
|
85
|
-
**error**, not something quietly ignored
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
banners with names, and a table is never thrown away for one. How each failure mode is
|
|
90
|
-
kept from looking like health: `docs/MANUAL.md`.
|
|
137
|
+
**error**, not something quietly ignored — and the error names the commands it does belong
|
|
138
|
+
to. Both live views tell you **when the last good reading arrived** and **whether the last
|
|
139
|
+
refresh failed**: ages keep climbing whether or not the refresh works, failures are banners
|
|
140
|
+
with names, and a table is never thrown away for one.
|
|
91
141
|
|
|
92
142
|
## Configuration
|
|
93
143
|
|
|
94
144
|
Three numbers are opinions, not truths, so all three are yours; everything else is
|
|
95
145
|
deliberately not configurable, and all of it works with no configuration at all.
|
|
96
146
|
|
|
97
|
-
| Setting | Flag | Environment |
|
|
147
|
+
| Setting | Flag | Environment | `<home>/.claude/tarmac/config.json` | Default |
|
|
98
148
|
|---|---|---|---|---|
|
|
99
149
|
| freshness threshold | `--stale-after 90s` \| `15m` \| `2h` | `TARMAC_STALE_AFTER` | `"staleAfterMs": 90000` | `10m` |
|
|
100
150
|
| port | `--port 8080` | `TARMAC_PORT` | `"port": 8080` | `4477` |
|
|
101
|
-
| snapshots dir (read side) | `--snapshots-dir DIR` | `TARMAC_SNAPSHOTS_DIR` | `"snapshotsDir": "DIR"` | `<home>/.
|
|
151
|
+
| snapshots dir (read side) | `--snapshots-dir DIR` | `TARMAC_SNAPSHOTS_DIR` | `"snapshotsDir": "DIR"` | the path frozen into the installed wrapper — failing that `$XDG_STATE_HOME/tarmac/snapshots`, when it is absolute *and* the target home is your own, else `<home>/.local/state/tarmac/snapshots` |
|
|
152
|
+
|
|
153
|
+
**Flag beats environment beats config file beats default**, settled per setting; `serve`
|
|
154
|
+
opens by printing each effective value and where it came from. Nothing is silently dropped
|
|
155
|
+
or silently corrected — a value that will not parse stops the run and says what it got,
|
|
156
|
+
where it came from, and what would have worked, *including* values that were going to lose
|
|
157
|
+
the precedence fight anyway.
|
|
158
|
+
|
|
159
|
+
That last default is read out of the installed wrapper, never recomputed: a reader that
|
|
160
|
+
recomputed it would disagree with the writer the moment the two saw different environments
|
|
161
|
+
— `XDG_STATE_HOME` exported in your shell, absent from a LaunchAgent or cron — and the
|
|
162
|
+
symptom is a healthy, empty fleet, the one failure a fleet monitor may not have. Anything
|
|
163
|
+
else reading those payloads can ask where they are: `tarmac list --json` reports the path as
|
|
164
|
+
`health.snapshotsDir`. Full rules and edge cases: [`docs/MANUAL.md`](docs/MANUAL.md).
|
|
102
165
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
going to lose the precedence fight anyway. Full rules and edge cases: `docs/MANUAL.md`.
|
|
166
|
+
`tarmac list --json` also reports `health.unfilable`: how many live sessions carry an id
|
|
167
|
+
tarmac will never file a snapshot under, so a reader can tell telemetry that is *late* from
|
|
168
|
+
telemetry that is *not coming*. See [`docs/MANUAL.md`](docs/MANUAL.md) for what makes an id
|
|
169
|
+
filable.
|
|
108
170
|
|
|
109
|
-
## What
|
|
171
|
+
## What it deliberately does not do
|
|
110
172
|
|
|
111
173
|
- **No "waiting for you" signal.** The obvious missing column — which session is blocked on
|
|
112
174
|
a human — is deliberately absent: the status line payload carries nothing that means it,
|
|
@@ -126,8 +188,8 @@ npm run build # flat JavaScript into dist/
|
|
|
126
188
|
CI runs the suite on Node 22 and 24, on Linux and macOS, with `TARMAC_REQUIRE_DASH=1` so a
|
|
127
189
|
machine without dash cannot report a green build it did not earn; a separate job builds
|
|
128
190
|
`dist/` and runs it on Node 20 — the oldest version `engines` promises, and the only place
|
|
129
|
-
the published artefact is ever executed. Releases are cut by hand (`PUBLISHING.md`);
|
|
130
|
-
capturing fixtures for a new Claude Code build: `docs/MANUAL.md
|
|
191
|
+
the published artefact is ever executed. Releases are cut by hand ([`PUBLISHING.md`](PUBLISHING.md));
|
|
192
|
+
capturing fixtures for a new Claude Code build: [`docs/MANUAL.md`](docs/MANUAL.md).
|
|
131
193
|
|
|
132
194
|
The suite runs the TypeScript sources directly through Node's type stripping, so it needs
|
|
133
195
|
Node ≥ 22.18 to *develop*; what ships in `dist/` is plain ES2022 and runs on Node ≥ 20.
|
package/dist/cli.js
CHANGED
|
@@ -13,7 +13,7 @@ import { parseArgs } from './args.js';
|
|
|
13
13
|
import { collectFleet } from './collect.js';
|
|
14
14
|
import { readConfigFile, resolveConfig } from './config.js';
|
|
15
15
|
import { createFleetServer, listenFleetServer } from './server.js';
|
|
16
|
-
import { install, uninstall, paths, planInstall, planUninstall } from './install.js';
|
|
16
|
+
import { install, uninstall, paths, planInstall, planUninstall, installedSnapshotsDir, wrapperIsOurs } from './install.js';
|
|
17
17
|
import { confirmTyped } from './prompt.js';
|
|
18
18
|
import { reapOrphanedTemps } from './reap.js';
|
|
19
19
|
import { renderPlan, renderSettings, renderTable, restoreMeaning, servingLine } from './render.js';
|
|
@@ -39,7 +39,8 @@ const USAGE = `tarmac — fleet observability for Claude Code
|
|
|
39
39
|
--port the dashboard's port (default: 4477 — a busy default walks up to the
|
|
40
40
|
next free port, a port named here refuses instead)
|
|
41
41
|
--snapshots-dir where the chained statusline drops its payloads
|
|
42
|
-
(default:
|
|
42
|
+
(default: $XDG_STATE_HOME/tarmac/snapshots, or
|
|
43
|
+
<home>/.local/state/tarmac/snapshots)
|
|
43
44
|
--claude-bin path to the claude CLI (default: claude)
|
|
44
45
|
|
|
45
46
|
Those three settings can also be set, in decreasing order of precedence, by the
|
|
@@ -78,6 +79,11 @@ try {
|
|
|
78
79
|
console.log(res.alreadyInstalled
|
|
79
80
|
? `install: already installed — wrapper regenerated, settings.json left alone`
|
|
80
81
|
: `install: statusLine wrapped — undo with \`${plan.undo}\``);
|
|
82
|
+
// What the plan promised, as it actually went: a frame of the OLD wrapper can land in
|
|
83
|
+
// that directory between the two, so the count is reported rather than assumed.
|
|
84
|
+
if (res.legacy !== null)
|
|
85
|
+
console.log(`install: cleared ${res.legacy.payloads} runtime payload(s) from ${res.legacy.dir} — they belong in ${res.snapshots}` +
|
|
86
|
+
(res.legacy.kept > 0 ? ` (${res.legacy.kept} file(s) kept, so the directory stays)` : ''));
|
|
81
87
|
}
|
|
82
88
|
else {
|
|
83
89
|
const { mode } = uninstall({ home });
|
|
@@ -88,11 +94,20 @@ try {
|
|
|
88
94
|
// Only the reading commands resolve settings, and only they read the config file: a
|
|
89
95
|
// typo in it must not be what stands between a user and `tarmac uninstall`.
|
|
90
96
|
const p = paths(args.home ?? os.homedir());
|
|
97
|
+
// The installed wrapper's own path, when there is one: the default is where the WRITER
|
|
98
|
+
// writes, not where a reader's environment would have put it. Recomputing it here made
|
|
99
|
+
// `XDG_STATE_HOME` in one process and not the other a silent split.
|
|
100
|
+
const frozen = installedSnapshotsDir(p);
|
|
101
|
+
if (frozen === null && wrapperIsOurs(p))
|
|
102
|
+
console.error(`tarmac: ${p.wrapper} is ours but does not say where it writes — falling back to ${p.snapshots}`);
|
|
91
103
|
const config = resolveConfig({
|
|
92
104
|
flags: { staleAfter: args.staleAfter, port: args.port, snapshotsDir: args.snapshotsDir },
|
|
93
105
|
env: process.env,
|
|
94
106
|
file: readConfigFile(p.config),
|
|
95
|
-
|
|
107
|
+
// The installed wrapper's own path, when there is one: the default is where the
|
|
108
|
+
// WRITER writes, not where a reader's environment would have put it. Recomputing it
|
|
109
|
+
// here made `XDG_STATE_HOME` in one process and not the other a silent split.
|
|
110
|
+
defaultSnapshotsDir: frozen ?? p.snapshots,
|
|
96
111
|
});
|
|
97
112
|
const snapshotsDir = config.snapshotsDir.value;
|
|
98
113
|
const staleAfterMs = config.staleAfterMs.value;
|
|
@@ -108,7 +123,7 @@ try {
|
|
|
108
123
|
if (failed > 0)
|
|
109
124
|
console.error(`tarmac: could not remove ${failed} orphaned temp file(s) under ${snapshotsDir}`);
|
|
110
125
|
const server = createFleetServer({
|
|
111
|
-
collect: () => collectFleet({ claudeBin: args.claudeBin, snapshotsDir, staleAfterMs, snapshotsDirSource: config.snapshotsDir.source }),
|
|
126
|
+
collect: () => collectFleet({ claudeBin: args.claudeBin, snapshotsDir, staleAfterMs, snapshotsDirSource: config.snapshotsDir.source, installed: frozen !== null }),
|
|
112
127
|
});
|
|
113
128
|
// A port nobody chose is not worth failing over: this walks past a busy 4477 and says
|
|
114
129
|
// where it landed. A port that WAS chosen refuses instead, and the refusal leaves
|
|
@@ -123,7 +138,7 @@ try {
|
|
|
123
138
|
console.log(servingLine(bound));
|
|
124
139
|
}
|
|
125
140
|
else {
|
|
126
|
-
const collect = () => collectFleet({ claudeBin: args.claudeBin, snapshotsDir, staleAfterMs, snapshotsDirSource: config.snapshotsDir.source });
|
|
141
|
+
const collect = () => collectFleet({ claudeBin: args.claudeBin, snapshotsDir, staleAfterMs, snapshotsDirSource: config.snapshotsDir.source, installed: frozen !== null });
|
|
127
142
|
if (args.watch) {
|
|
128
143
|
// One redraws a screen, the other is meant to be piped once. Silently letting one win
|
|
129
144
|
// is how someone ends up parsing a frame of terminal art.
|
package/dist/collect.js
CHANGED
|
@@ -3,22 +3,30 @@ import { SOURCE_PHRASE } from './config.js';
|
|
|
3
3
|
import { discoverSessions } from './discover.js';
|
|
4
4
|
import { readSnapshots } from './snapshots.js';
|
|
5
5
|
import { buildFleet } from './fleet.js';
|
|
6
|
-
export async function collectFleet({ claudeBin, snapshotsDir, now = Date.now(), staleAfterMs, snapshotsDirSource = 'default', }) {
|
|
6
|
+
export async function collectFleet({ claudeBin, snapshotsDir, now = Date.now(), staleAfterMs, snapshotsDirSource = 'default', installed = false, }) {
|
|
7
7
|
const { sessions, health: discovery } = await discoverSessions({ claudeBin });
|
|
8
8
|
const { snapshots, dirError, unreadable, duplicates, dirMissing } = readSnapshots(snapshotsDir, { now });
|
|
9
9
|
const fleet = buildFleet({ sessions, snapshots, now, discovery, staleAfterMs });
|
|
10
10
|
// Both blind spots travel with the data: a directory we could not read and files we
|
|
11
11
|
// could not parse are OUR failures to report, not silence to render as "all clear".
|
|
12
12
|
//
|
|
13
|
-
// An absent directory is judged HERE, because this is the layer that knows who chose it
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
// An absent directory is judged HERE, because this is the layer that knows who chose it.
|
|
14
|
+
// A path someone typed — flag, environment, or a config file edited months ago — that is
|
|
15
|
+
// not there is a setting pointing at nothing; rendered as "not chained yet" it sends the
|
|
16
|
+
// user to run `tarmac install`, which cannot fix it: install writes where install writes.
|
|
17
|
+
//
|
|
18
|
+
// Exactly ONE case is innocent, and it is narrower than it used to be: no install here at
|
|
19
|
+
// all. Since #20 the default is the path the install FROZE into the wrapper — a directory
|
|
20
|
+
// that was chosen and created by a run that happened — so its absence means the writer and
|
|
21
|
+
// the reader have parted company, which renders as a healthy, empty fleet. That is the one
|
|
22
|
+
// failure this tool may not have.
|
|
23
|
+
fleet.health.snapshotsError = !dirMissing
|
|
24
|
+
? dirError
|
|
25
|
+
: snapshotsDirSource !== 'default'
|
|
20
26
|
? `ENOENT: ${snapshotsDir} does not exist — set by ${SOURCE_PHRASE[snapshotsDirSource]}`
|
|
21
|
-
:
|
|
27
|
+
: installed
|
|
28
|
+
? `ENOENT: ${snapshotsDir} does not exist — the installed wrapper writes there`
|
|
29
|
+
: dirError;
|
|
22
30
|
fleet.health.snapshotsUnreadable = unreadable;
|
|
23
31
|
fleet.health.snapshotsDuplicates = duplicates;
|
|
24
32
|
fleet.health.snapshotsDir = snapshotsDir;
|
package/dist/fleet.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { DEFAULT_STALE_AFTER_MS } from './config.js';
|
|
15
15
|
import { guardVersions } from './schema.js';
|
|
16
|
+
import { SID_NAME } from './wrapper.js';
|
|
16
17
|
export function buildFleet({ sessions, snapshots, now, staleAfterMs = DEFAULT_STALE_AFTER_MS, discovery = null, }) {
|
|
17
18
|
const rows = sessions.map((s) => {
|
|
18
19
|
const t = (s.sessionId && snapshots.get(s.sessionId)) || null;
|
|
@@ -22,6 +23,7 @@ export function buildFleet({ sessions, snapshots, now, staleAfterMs = DEFAULT_ST
|
|
|
22
23
|
project: s.cwd ? path.basename(s.cwd) : null,
|
|
23
24
|
cwd: s.cwd,
|
|
24
25
|
pid: s.pid,
|
|
26
|
+
kind: s.kind,
|
|
25
27
|
status: s.status,
|
|
26
28
|
busy: s.busy,
|
|
27
29
|
uptimeMs: typeof s.startedAt === 'number' ? now - s.startedAt : null,
|
|
@@ -39,6 +41,14 @@ export function buildFleet({ sessions, snapshots, now, staleAfterMs = DEFAULT_ST
|
|
|
39
41
|
});
|
|
40
42
|
rows.sort((a, b) => rank(a) - rank(b) || (b.ctxPct ?? -1) - (a.ctxPct ?? -1));
|
|
41
43
|
const covered = rows.filter((r) => r.ctxState !== 'absent').length;
|
|
44
|
+
// Blind AND unfilable, in that order — this number exists to say how many of the blind will
|
|
45
|
+
// stay blind, and both renderers subtract it from them. A session can be unfilable and
|
|
46
|
+
// covered at the same time: a snapshot written by a pre-upgrade wrapper under a non-UUID
|
|
47
|
+
// name is still READ, because the reader keys on the `session_id` inside the file rather
|
|
48
|
+
// than on the filename. Counting that one would push this past the blind count and make the
|
|
49
|
+
// renderers explain away someone else's missing telemetry.
|
|
50
|
+
// A null id is `noSessionId`'s business — a discovery failure, not a naming one.
|
|
51
|
+
const unfilable = rows.filter((r) => r.ctxState === 'absent' && r.sessionId !== null && !SID_NAME.test(r.sessionId)).length;
|
|
42
52
|
const drift = rows.filter((r) => r.ctxState === 'drift').length;
|
|
43
53
|
// Having a snapshot and having a cost are different facts, and only the second one is
|
|
44
54
|
// allowed to feed the total.
|
|
@@ -55,6 +65,7 @@ export function buildFleet({ sessions, snapshots, now, staleAfterMs = DEFAULT_ST
|
|
|
55
65
|
health: {
|
|
56
66
|
sessions: rows.length,
|
|
57
67
|
covered,
|
|
68
|
+
unfilable,
|
|
58
69
|
drift,
|
|
59
70
|
stale: rows.filter((r) => r.stale).length,
|
|
60
71
|
// Discovery's own blind spots. Dropping them turns a renamed `sessionId` into the
|