agent-dag 3.22.0 → 3.22.3
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 +6 -477
- package/package.json +14 -48
- package/shim.js +107 -0
- package/LICENSE +0 -661
- package/LICENSING.md +0 -82
- package/THIRD_PARTY_NOTICES.md +0 -395
- package/bin/agent-dag.js +0 -626
- package/bin/deck.js +0 -1805
- package/dist/web/assets/index-3FWd7g_W.css +0 -1
- package/dist/web/assets/index-BOwtoP02.js +0 -266
- package/dist/web/index.html +0 -49
- package/hook/hook.js +0 -542
- package/release-notes.json +0 -392
- package/src/server/activity.mjs +0 -52
- package/src/server/agent-activity.mjs +0 -522
- package/src/server/args.mjs +0 -183
- package/src/server/auto-update.mjs +0 -79
- package/src/server/block-notify.mjs +0 -173
- package/src/server/boot-deadline.mjs +0 -127
- package/src/server/brand.mjs +0 -16
- package/src/server/browser-history.mjs +0 -497
- package/src/server/browser-presence.mjs +0 -211
- package/src/server/browser-profiles.mjs +0 -279
- package/src/server/browser-react.mjs +0 -284
- package/src/server/browser-watch-store.mjs +0 -350
- package/src/server/browser-watch.mjs +0 -905
- package/src/server/ccusage.mjs +0 -1168
- package/src/server/claude-accounts.mjs +0 -951
- package/src/server/claude-dir.mjs +0 -213
- package/src/server/codex-auth.mjs +0 -388
- package/src/server/codex-dir.mjs +0 -171
- package/src/server/codex-quota.mjs +0 -449
- package/src/server/codex-usage.mjs +0 -512
- package/src/server/cswap-admin.mjs +0 -1562
- package/src/server/cswap-auto.mjs +0 -658
- package/src/server/cswap-install.mjs +0 -641
- package/src/server/deck-home.mjs +0 -243
- package/src/server/deck-prefs.mjs +0 -301
- package/src/server/deck-probe.mjs +0 -111
- package/src/server/detach.mjs +0 -244
- package/src/server/exec.mjs +0 -996
- package/src/server/global-install.mjs +0 -67
- package/src/server/hwmonitor.mjs +0 -56
- package/src/server/index.mjs +0 -6043
- package/src/server/installer.mjs +0 -912
- package/src/server/invoked-as.mjs +0 -144
- package/src/server/lan-about.mjs +0 -119
- package/src/server/lan-engine.mjs +0 -952
- package/src/server/lan-reach.mjs +0 -256
- package/src/server/lan-socket.mjs +0 -682
- package/src/server/lan-sync.mjs +0 -941
- package/src/server/lhm-parse.mjs +0 -91
- package/src/server/log-tail.mjs +0 -139
- package/src/server/log-writer.mjs +0 -322
- package/src/server/login-service.mjs +0 -473
- package/src/server/macmon.mjs +0 -310
- package/src/server/npx.mjs +0 -264
- package/src/server/open-url.mjs +0 -242
- package/src/server/presence.mjs +0 -40
- package/src/server/quota.mjs +0 -792
- package/src/server/relay-guard.mjs +0 -507
- package/src/server/reset-label.mjs +0 -78
- package/src/server/retire-sound-hook.mjs +0 -349
- package/src/server/running-deck.mjs +0 -234
- package/src/server/self-update.mjs +0 -1380
- package/src/server/stop-deck.mjs +0 -171
- package/src/server/supervisor.mjs +0 -392
- package/src/server/system-metrics.mjs +0 -1825
- package/src/server/term.mjs +0 -686
- package/src/server/uv-bootstrap.mjs +0 -337
package/src/server/term.mjs
DELETED
|
@@ -1,686 +0,0 @@
|
|
|
1
|
-
// Everything the deck knows about the terminal it is printing into.
|
|
2
|
-
//
|
|
3
|
-
// The terminal is the first surface of this product and, for the seconds before
|
|
4
|
-
// the browser opens, the only one — so it gets the same treatment the canvas
|
|
5
|
-
// got. The rules below are the whole of it: detect what this terminal can do,
|
|
6
|
-
// then degrade, rather than picking the floor and printing the same 16 colours
|
|
7
|
-
// into a pipe, a CI log and a truecolor emulator alike.
|
|
8
|
-
//
|
|
9
|
-
// It lives here, apart from bin/deck.js, for the reason supervisor.mjs and
|
|
10
|
-
// npx.mjs do: bin/deck.js installs hooks and binds a port the moment it is
|
|
11
|
-
// imported, so none of this could be checked any other way — and "what does the
|
|
12
|
-
// output look like at 40 columns, with NO_COLOR, on a Windows console" is
|
|
13
|
-
// exactly the kind of question that is only ever answered by asserting it.
|
|
14
|
-
//
|
|
15
|
-
// Nothing here writes anything or reads process state on its own: every input
|
|
16
|
-
// (env, isTTY, platform, columns) is a parameter, defaulted from the real thing.
|
|
17
|
-
// The two exceptions are the cursor constants, which are strings the caller
|
|
18
|
-
// writes, because who restores the cursor and when is a lifecycle question and
|
|
19
|
-
// belongs with the lifecycle.
|
|
20
|
-
|
|
21
|
-
// ── colour profile ───────────────────────────────────────────────────────────
|
|
22
|
-
//
|
|
23
|
-
// Detected once, in the order the conventions themselves establish:
|
|
24
|
-
// NO_COLOR (no-color.org — present and non-empty means no colour, whatever the
|
|
25
|
-
// value) beats FORCE_COLOR beats what the terminal advertises. Truecolor is
|
|
26
|
-
// effectively universal now, and it is the only tier on which the deck's accent
|
|
27
|
-
// is actually the deck's accent rather than whatever the user's theme decided
|
|
28
|
-
// cyan is — so it is worth asking for, and worth degrading from cleanly.
|
|
29
|
-
|
|
30
|
-
/** Present and non-empty, which is what every one of these variables means. */
|
|
31
|
-
const set = (v) => v != null && v !== "";
|
|
32
|
-
|
|
33
|
-
/** Anything below a space, or DEL. A URL is going inside an escape sequence, so
|
|
34
|
-
* a control character in it would close that sequence early. */
|
|
35
|
-
const hasControl = (s) => [...String(s)].some((c) => c.charCodeAt(0) <= 0x20 || c.charCodeAt(0) === 0x7f);
|
|
36
|
-
|
|
37
|
-
/** Terminals that do truecolor without saying so in COLORTERM. */
|
|
38
|
-
const TRUECOLOR_PROGRAMS = new Set(["iTerm.app", "vscode", "WezTerm", "ghostty", "Hyper", "rio", "Tabby"]);
|
|
39
|
-
|
|
40
|
-
/** FORCE_COLOR's levels, as every CLI that honours it reads them. `undefined`
|
|
41
|
-
* means the variable is absent and detection continues. */
|
|
42
|
-
function forcedProfile(raw) {
|
|
43
|
-
if (raw == null) return undefined;
|
|
44
|
-
const v = String(raw).trim().toLowerCase();
|
|
45
|
-
if (v === "0" || v === "false") return "none";
|
|
46
|
-
if (v === "2") return "ansi256";
|
|
47
|
-
if (v === "3") return "truecolor";
|
|
48
|
-
return "ansi16"; // "", "1", "true", anything else: colour, at the safe floor
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* What this terminal can be asked for: "none" | "ansi16" | "ansi256" | "truecolor".
|
|
53
|
-
*
|
|
54
|
-
* A Windows TTY floors at ansi16 rather than none: libuv translates SGR into
|
|
55
|
-
* console API calls for consoles that cannot parse escapes themselves, so the
|
|
56
|
-
* 16 colours arrive on legacy conhost too. Nothing above that tier is assumed
|
|
57
|
-
* there — Windows Terminal says so through WT_SESSION, and conhost never does.
|
|
58
|
-
*/
|
|
59
|
-
export function colorProfile({ env = process.env, isTTY = false, platform = process.platform } = {}) {
|
|
60
|
-
if (set(env.NO_COLOR)) return "none";
|
|
61
|
-
const forced = forcedProfile(env.FORCE_COLOR);
|
|
62
|
-
if (forced !== undefined) return forced;
|
|
63
|
-
if (!isTTY) return "none";
|
|
64
|
-
|
|
65
|
-
const term = String(env.TERM ?? "").toLowerCase();
|
|
66
|
-
if (term === "dumb") return "none";
|
|
67
|
-
|
|
68
|
-
const colorterm = String(env.COLORTERM ?? "").toLowerCase();
|
|
69
|
-
if (colorterm === "truecolor" || colorterm === "24bit") return "truecolor";
|
|
70
|
-
if (set(env.WT_SESSION)) return "truecolor";
|
|
71
|
-
if (TRUECOLOR_PROGRAMS.has(String(env.TERM_PROGRAM ?? ""))) return "truecolor";
|
|
72
|
-
if (term === "xterm-kitty" || term === "alacritty" || term.includes("truecolor")) return "truecolor";
|
|
73
|
-
if (term.includes("256")) return "ansi256";
|
|
74
|
-
// A TTY that advertises nothing still takes colour on every platform we
|
|
75
|
-
// support; 16 is the tier nothing has to advertise to have.
|
|
76
|
-
return "ansi16";
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ── colours ──────────────────────────────────────────────────────────────────
|
|
80
|
-
|
|
81
|
-
/** rgb → the nearest xterm-256 index: the 6×6×6 cube, or the 24-step grey ramp
|
|
82
|
-
* for anything neutral, which is where the muted tones land. */
|
|
83
|
-
function to256(r, g, b) {
|
|
84
|
-
if (r === g && g === b) {
|
|
85
|
-
if (r < 8) return 16;
|
|
86
|
-
if (r > 248) return 231;
|
|
87
|
-
return Math.round(((r - 8) / 247) * 24) + 232;
|
|
88
|
-
}
|
|
89
|
-
return 16 + 36 * Math.round((r / 255) * 5) + 6 * Math.round((g / 255) * 5) + Math.round((b / 255) * 5);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* One colour, expressed as far as this profile reaches.
|
|
94
|
-
*
|
|
95
|
-
* `fallback` is the 16-colour SGR to use when that is all there is — chosen per
|
|
96
|
-
* colour rather than derived, because the nearest of eight hues to a pastel is
|
|
97
|
-
* a question with a taste answer, not an arithmetic one.
|
|
98
|
-
*/
|
|
99
|
-
export function fg([r, g, b], profile, fallback) {
|
|
100
|
-
if (profile === "truecolor") return `\x1b[38;2;${r};${g};${b}m`;
|
|
101
|
-
if (profile === "ansi256") return `\x1b[38;5;${to256(r, g, b)}m`;
|
|
102
|
-
if (profile === "ansi16") return fallback;
|
|
103
|
-
return "";
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// The canvas' own tokens (src/web/styles.css), so the terminal and the page are
|
|
107
|
-
// one product rather than two. The ramp is the wordmark's vertical gradient:
|
|
108
|
-
// lit at the top, settling into the deeper tone at the baseline.
|
|
109
|
-
const BRAND = {
|
|
110
|
-
accentSoft: [[186, 230, 253], "\x1b[96m"],
|
|
111
|
-
accent: [[125, 211, 252], "\x1b[96m"],
|
|
112
|
-
accentDeep: [[56, 189, 248], "\x1b[36m"],
|
|
113
|
-
ok: [[134, 239, 172], "\x1b[32m"],
|
|
114
|
-
warn: [[252, 211, 77], "\x1b[33m"],
|
|
115
|
-
err: [[252, 165, 165], "\x1b[31m"],
|
|
116
|
-
muted: [[126, 130, 140], "\x1b[2m"],
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* The semantic names every call site uses — no raw escape ever appears in
|
|
121
|
-
* bin/deck.js again. Empty strings under "none", so the same template literals
|
|
122
|
-
* produce escape-free output in a pipe without a branch at each one.
|
|
123
|
-
*/
|
|
124
|
-
export function palette(profile) {
|
|
125
|
-
const on = profile !== "none";
|
|
126
|
-
const out = {
|
|
127
|
-
reset: on ? "\x1b[0m" : "",
|
|
128
|
-
bold: on ? "\x1b[1m" : "",
|
|
129
|
-
dim: on ? "\x1b[2m" : "",
|
|
130
|
-
};
|
|
131
|
-
for (const [name, [rgb, fallback]] of Object.entries(BRAND)) out[name] = fg(rgb, profile, fallback);
|
|
132
|
-
return out;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// ── hyperlinks ───────────────────────────────────────────────────────────────
|
|
136
|
-
//
|
|
137
|
-
// OSC 8 is an allowlist rather than a probe: a terminal that does not know the
|
|
138
|
-
// sequence is supposed to ignore it, and most do, but "most" is not a promise
|
|
139
|
-
// worth making to a legacy Windows console — and tmux below 3.4 passes the
|
|
140
|
-
// escape through mangled, which is worse than plain text. So links are emitted
|
|
141
|
-
// only where they are known to arrive.
|
|
142
|
-
|
|
143
|
-
const HYPERLINK_PROGRAMS = new Set(["iTerm.app", "vscode", "WezTerm", "ghostty", "Hyper", "rio", "Tabby"]);
|
|
144
|
-
|
|
145
|
-
export function supportsHyperlinks({ env = process.env, profile = "truecolor" } = {}) {
|
|
146
|
-
// NO_COLOR takes hyperlinks with it: the convention is about decoration, and
|
|
147
|
-
// an escape sequence wrapped around a path is decoration.
|
|
148
|
-
if (profile === "none") return false;
|
|
149
|
-
const term = String(env.TERM ?? "").toLowerCase();
|
|
150
|
-
if (set(env.TMUX) || term.startsWith("screen")) return false;
|
|
151
|
-
if (HYPERLINK_PROGRAMS.has(String(env.TERM_PROGRAM ?? ""))) return true;
|
|
152
|
-
if (set(env.WT_SESSION)) return true;
|
|
153
|
-
if (set(env.KITTY_WINDOW_ID) || term === "xterm-kitty") return true;
|
|
154
|
-
if (set(env.KONSOLE_VERSION) || set(env.DOMTERM)) return true;
|
|
155
|
-
const vte = Number.parseInt(String(env.VTE_VERSION ?? ""), 10);
|
|
156
|
-
return Number.isFinite(vte) && vte >= 5000; // GNOME Terminal 3.26+
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/** `text` as a link to `url`, or just `text`. BEL-terminated, which is the form
|
|
160
|
-
* the widest set of terminals accepts. A url carrying a control character is
|
|
161
|
-
* dropped rather than escaped — it could only have come from a path we should
|
|
162
|
-
* not be linking anyway. */
|
|
163
|
-
export function link(text, url, enabled) {
|
|
164
|
-
if (!enabled || !url || hasControl(url)) return text;
|
|
165
|
-
return `\x1b]8;;${url}\x07${text}\x1b]8;;\x07`;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// ── glyphs ───────────────────────────────────────────────────────────────────
|
|
169
|
-
//
|
|
170
|
-
// Windows Terminal renders all of these; `cmd.exe` on a non-UTF-8 code page
|
|
171
|
-
// renders none of them, and this project's rule is that everything works on
|
|
172
|
-
// Windows. So there are two tiers and every glyph the deck prints — punctuation
|
|
173
|
-
// included, since an em dash is as absent from CP437 as a check mark — comes
|
|
174
|
-
// from one of them.
|
|
175
|
-
|
|
176
|
-
const UNICODE_GLYPHS = {
|
|
177
|
-
ok: "✓", fail: "✗", warn: "⚠", stop: "◉", restart: "↻", cancel: "✕",
|
|
178
|
-
up: "↑", play: "▶", pulse: "●",
|
|
179
|
-
arrow: "→", ellipsis: "…", dash: "—", bullet: "·",
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
const ASCII_GLYPHS = {
|
|
183
|
-
ok: "+", fail: "x", warn: "!", stop: "*", restart: "~", cancel: "x",
|
|
184
|
-
up: "^", play: ">", pulse: "*",
|
|
185
|
-
arrow: "->", ellipsis: "...", dash: "-", bullet: "-",
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Whether this terminal can be trusted with the box-drawing and arrow glyphs.
|
|
190
|
-
*
|
|
191
|
-
* Everywhere but Windows, yes — except the Linux virtual console, whose font is
|
|
192
|
-
* 256 glyphs and none of them are these. On Windows it is the other way round:
|
|
193
|
-
* an allowlist of the emulators that ship a real font and a UTF-8 code page,
|
|
194
|
-
* because the default console does neither.
|
|
195
|
-
*/
|
|
196
|
-
export function unicodeOK({ env = process.env, platform = process.platform } = {}) {
|
|
197
|
-
const term = String(env.TERM ?? "").toLowerCase();
|
|
198
|
-
if (platform !== "win32") return term !== "linux" && term !== "dumb";
|
|
199
|
-
if (set(env.WT_SESSION)) return true;
|
|
200
|
-
if (String(env.TERM_PROGRAM ?? "") === "vscode") return true;
|
|
201
|
-
if (set(env.ConEmuTask) || set(env.WSL_DISTRO_NAME)) return true;
|
|
202
|
-
if (String(env.TERMINAL_EMULATOR ?? "") === "JetBrains-JediTerm") return true;
|
|
203
|
-
return term.startsWith("xterm") || term === "alacritty";
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export function glyphs(unicode) {
|
|
207
|
-
return unicode ? { ...UNICODE_GLYPHS } : { ...ASCII_GLYPHS };
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// One definition, for the banner, the step spinner and anything later. The
|
|
211
|
-
// braille frames are the nicest thing a terminal can do with one cell; the
|
|
212
|
-
// ASCII tier gets the rotating bar, which is the only spinner CP437 has.
|
|
213
|
-
const BRAILLE_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
214
|
-
const ASCII_FRAMES = ["|", "/", "-", "\\"];
|
|
215
|
-
|
|
216
|
-
export function spinnerFrames(unicode) {
|
|
217
|
-
return unicode ? BRAILLE_FRAMES.slice() : ASCII_FRAMES.slice();
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/** When a spinner starts saying how long it has been going.
|
|
221
|
-
*
|
|
222
|
-
* #742: a spinner four seconds in looks exactly like one four hundred
|
|
223
|
-
* milliseconds in, and that is the whole of "is this thing stuck". A number
|
|
224
|
-
* answers it. Three seconds, because under that the number would be on screen
|
|
225
|
-
* for a blink on every ordinary boot and would be noise rather than an answer
|
|
226
|
-
* — nobody doubts a step that has not yet lasted as long as it takes to doubt
|
|
227
|
-
* one. */
|
|
228
|
-
export const SPINNER_ELAPSED_AFTER_MS = 3_000;
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* The seconds a spinner shows beside its label, or "" while it is too young to
|
|
232
|
-
* have anything worth saying.
|
|
233
|
-
*
|
|
234
|
-
* Whole seconds, floored, and never a tenth: a number that changes ten times a
|
|
235
|
-
* second is a second spinner rather than an answer about the first one. Here
|
|
236
|
-
* rather than in bin/deck.js because that file runs a deck when it is imported,
|
|
237
|
-
* and this is the one part of `step` worth holding still in a test.
|
|
238
|
-
*/
|
|
239
|
-
export function elapsedSuffix(ms, after = SPINNER_ELAPSED_AFTER_MS) {
|
|
240
|
-
return ms < after ? "" : ` ${Math.floor(ms / 1000)}s`;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* How long a deck has been up, in the two units that matter and no more.
|
|
245
|
-
*
|
|
246
|
-
* `--status` and `--stop` both print this, and the question behind it is always
|
|
247
|
-
* "is this the deck I started, or one I forgot about" — which "3h 12m" answers
|
|
248
|
-
* and "11543s" does not. Two units, because the second one stops being
|
|
249
|
-
* interesting as soon as the first is large: nobody reading "2d" wants the
|
|
250
|
-
* minutes.
|
|
251
|
-
*
|
|
252
|
-
* Seconds under a minute, because that is the unit a deck started moments ago
|
|
253
|
-
* is measured in and "up just now" is not an answer to "how long". A clock that
|
|
254
|
-
* went backwards, or a `startedAt` from a machine whose time has since been
|
|
255
|
-
* corrected, reads as `0s` rather than as a negative duration.
|
|
256
|
-
*/
|
|
257
|
-
export function sinceLabel(ms) {
|
|
258
|
-
const s = Math.floor(Number(ms) / 1000);
|
|
259
|
-
if (!Number.isFinite(s) || s < 0) return "0s";
|
|
260
|
-
if (s < 60) return `${s}s`;
|
|
261
|
-
const m = Math.floor(s / 60);
|
|
262
|
-
if (m < 60) return `${m}m`;
|
|
263
|
-
const h = Math.floor(m / 60);
|
|
264
|
-
if (h < 24) return h > 0 && m % 60 > 0 ? `${h}h ${m % 60}m` : `${h}h`;
|
|
265
|
-
const d = Math.floor(h / 24);
|
|
266
|
-
return h % 24 > 0 ? `${d}d ${h % 24}h` : `${d}d`;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// ── motion ───────────────────────────────────────────────────────────────────
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Whether anything is allowed to move — the terminal's `prefers-reduced-motion`.
|
|
273
|
-
*
|
|
274
|
-
* A pipe, a file, a CI log and a NO_COLOR terminal all get the same treatment:
|
|
275
|
-
* no sleeps, no `\r` repaints, no spinner, no pulse. Every one of those either
|
|
276
|
-
* cannot show motion or is somebody's log file, and a log file full of carriage
|
|
277
|
-
* returns is the artefact this rule exists to prevent.
|
|
278
|
-
*/
|
|
279
|
-
export function motionOK({ env = process.env, isTTY = false, profile = colorProfile({ env, isTTY }) } = {}) {
|
|
280
|
-
if (!isTTY || profile === "none") return false;
|
|
281
|
-
const ci = String(env.CI ?? "").trim().toLowerCase();
|
|
282
|
-
if (set(ci) && ci !== "0" && ci !== "false") return false;
|
|
283
|
-
return true;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// ── width ────────────────────────────────────────────────────────────────────
|
|
287
|
-
|
|
288
|
-
/** The visible text: SGR colour and OSC 8 hyperlinks removed. Everything that
|
|
289
|
-
* measures a line for layout measures it through here — a padded line that
|
|
290
|
-
* counted its own escapes is how a `\r` repaint starts leaving debris. */
|
|
291
|
-
export function stripAnsi(s) {
|
|
292
|
-
return String(s)
|
|
293
|
-
.replace(/\x1b\]8;;.*?(?:\x07|\x1b\\)/g, "")
|
|
294
|
-
.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, "");
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
export function visibleWidth(s) {
|
|
298
|
-
return stripAnsi(s).length;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/** How wide the terminal is, or 80 when it will not say — a pipe, a CI runner,
|
|
302
|
-
* a terminal that never sent SIGWINCH. 80 is the assumption every one of them
|
|
303
|
-
* is already built around, and the layout below never needs more than 40. */
|
|
304
|
-
export function termColumns(stream = process.stdout, env = process.env) {
|
|
305
|
-
const c = stream?.columns;
|
|
306
|
-
if (Number.isInteger(c) && c > 0) return c;
|
|
307
|
-
// COLUMNS, the POSIX spelling of this question, and the only answer a DETACHED
|
|
308
|
-
// deck has. Its stdout is the log file, so the stream knows nothing about a
|
|
309
|
-
// width — but the launcher is watching that file from a real terminal and
|
|
310
|
-
// passes its own width down, so the boot report is laid out for the terminal
|
|
311
|
-
// a person is actually reading it in rather than for the 80 below.
|
|
312
|
-
const n = Number.parseInt(String(env?.COLUMNS ?? "").trim(), 10);
|
|
313
|
-
return Number.isInteger(n) && n > 0 ? n : 80;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/** The status column, computed from the labels rather than counted into each
|
|
317
|
-
* string by hand. A label one character longer used to break the alignment of
|
|
318
|
-
* every row silently. */
|
|
319
|
-
export function labelColumn(labels) {
|
|
320
|
-
return labels.reduce((w, l) => Math.max(w, String(l).length), 0);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* `s` trimmed to `max` columns, from whichever end matters least.
|
|
325
|
-
*
|
|
326
|
-
* A path, a URL or a version has no spaces in it and is identified by its tail
|
|
327
|
-
* — `…/agent-dag/hook.js` still says which file — so the head goes. A sentence
|
|
328
|
-
* is identified by its head, and `…nel enabled)` says nothing at all, so its
|
|
329
|
-
* tail goes instead. Below the width where the ellipsis plus a few characters
|
|
330
|
-
* would still mean something, the detail is dropped rather than reduced to
|
|
331
|
-
* punctuation; the label beside it already says which row this is.
|
|
332
|
-
*/
|
|
333
|
-
export function fit(s, max, ellipsis = "…") {
|
|
334
|
-
const text = String(s);
|
|
335
|
-
if (text.length <= max) return text;
|
|
336
|
-
if (max < ellipsis.length + 3) return "";
|
|
337
|
-
const room = max - ellipsis.length;
|
|
338
|
-
return /\s/.test(text) ? text.slice(0, room) + ellipsis : ellipsis + text.slice(text.length - room);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* A failure, reduced to the one line it is safe to print while the deck is
|
|
343
|
-
* painting.
|
|
344
|
-
*
|
|
345
|
-
* Everything bin/deck.js writes after the banner is a `\r`-rewritten row — the
|
|
346
|
-
* spinner in `step`, the pulse line, which repaints every 800ms — so a write
|
|
347
|
-
* that arrives from an async callback lands in the middle of one of them. A
|
|
348
|
-
* single short line is survivable: it ends in a newline, the next repaint
|
|
349
|
-
* starts on a fresh row, and the report above it is still on screen. A
|
|
350
|
-
* subprocess's stack trace is not: fifteen lines scroll the entire startup
|
|
351
|
-
* report away and leave the pulse mid-frame. Reported from Windows (#432),
|
|
352
|
-
* where a broken npm shim put two Node stack traces over the boot output.
|
|
353
|
-
*
|
|
354
|
-
* Three things happen here, and the order matters.
|
|
355
|
-
*
|
|
356
|
-
* The most informative line is chosen rather than the first one, because the
|
|
357
|
-
* first line of a Node stack trace is `node:internal/modules/cjs/loader:1573`
|
|
358
|
-
* and the line that names the actual failure — `Error: Cannot find module …` —
|
|
359
|
-
* is five lines below it.
|
|
360
|
-
*
|
|
361
|
-
* Every control character is replaced, not just the newlines. A lone `\r` is a
|
|
362
|
-
* cursor jump to column 0, so a CRLF stack trace that only had its `\n`
|
|
363
|
-
* removed would still overwrite whatever the deck had drawn on that row — and
|
|
364
|
-
* CRLF is what a Windows child writes, which is the platform this exists for.
|
|
365
|
-
* An ESC would open an escape sequence out of a string the deck did not write.
|
|
366
|
-
*
|
|
367
|
-
* And the result is fitted to the room the caller has left, so the one line
|
|
368
|
-
* stays one line: a message that wraps is two rows, and the `\r` that follows
|
|
369
|
-
* only ever reaches the second of them. The ellipsis defaults to the ASCII one
|
|
370
|
-
* rather than `…`, because callers of this are error paths in modules that
|
|
371
|
-
* know nothing about the terminal's glyph tier — and a `…` on a cmd.exe code
|
|
372
|
-
* page is a question mark in the middle of the only clue the user got.
|
|
373
|
-
*
|
|
374
|
-
* @param room how many columns are free after whatever prefix the caller prints.
|
|
375
|
-
*/
|
|
376
|
-
export function oneLine(text, room = 80, ellipsis = "...") {
|
|
377
|
-
const lines = stripAnsi(String(text ?? ""))
|
|
378
|
-
.split("\n")
|
|
379
|
-
// eslint-disable-next-line no-control-regex
|
|
380
|
-
.map((l) => l.replace(/[\x00-\x1f\x7f]/g, " ").trim())
|
|
381
|
-
.filter(Boolean);
|
|
382
|
-
if (!lines.length) return "";
|
|
383
|
-
// "Error:", "TypeError:", "Error [ERR_MODULE_NOT_FOUND]:" — the line a reader
|
|
384
|
-
// would have picked out of the dump themselves. The optional prefix is what
|
|
385
|
-
// makes a bare "Error:" match as well as a named subclass.
|
|
386
|
-
const named = lines.find((l) => /^[\w$]*Error\b/.test(l));
|
|
387
|
-
return fit(named ?? lines[0], Math.max(8, room), ellipsis);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
const same = (s) => s;
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* One aligned status row: ` ✓ label detail`.
|
|
394
|
-
*
|
|
395
|
-
* Laid out on the plain text and painted afterwards, so colour and hyperlinks
|
|
396
|
-
* never enter the arithmetic. The last cell of the line is left empty: a
|
|
397
|
-
* terminal that fills its final column either wraps or leaves the cursor in a
|
|
398
|
-
* place the next `\r` cannot recover from.
|
|
399
|
-
*
|
|
400
|
-
* `keep` is for the one detail nobody can act on a fragment of — the URL. When
|
|
401
|
-
* it will not fit beside its label it moves to its own line under the gutter
|
|
402
|
-
* rather than losing its port to an ellipsis; a truncated address is not a
|
|
403
|
-
* shorter address, it is no address. The result then contains a newline, which
|
|
404
|
-
* is why callers write it whole.
|
|
405
|
-
*/
|
|
406
|
-
export function statusLine({
|
|
407
|
-
mark = " ", label = "", detail = "", labelWidth = 0, columns = 80, indent = 2, paint = {},
|
|
408
|
-
ellipsis = "…", keep = false,
|
|
409
|
-
} = {}) {
|
|
410
|
-
// A blank gutter is painted by nobody: colouring a space costs two escapes
|
|
411
|
-
// and shows nothing, and rows with no mark are the majority of the quiet ones.
|
|
412
|
-
const paintMark = stripAnsi(mark).trim() ? (paint.mark ?? same) : same;
|
|
413
|
-
const paintLabel = paint.label ?? same;
|
|
414
|
-
const paintDetail = paint.detail ?? same;
|
|
415
|
-
const pad = " ".repeat(indent);
|
|
416
|
-
const head = `${pad}${paintMark(mark)} ${paintLabel(label)}`;
|
|
417
|
-
|
|
418
|
-
if (!detail) return head;
|
|
419
|
-
|
|
420
|
-
const padded = label.padEnd(Math.max(labelWidth, label.length));
|
|
421
|
-
const used = indent + stripAnsi(mark).length + 2 + padded.length + 2;
|
|
422
|
-
const room = columns - used - 1;
|
|
423
|
-
const plain = stripAnsi(detail);
|
|
424
|
-
if (keep && plain.length > room) {
|
|
425
|
-
// Under the gutter if that still fits, then flush left, then wrapped —
|
|
426
|
-
// there is nothing else to try when the address is wider than the terminal.
|
|
427
|
-
const under = [indent + 3, indent, 0].find(n => n + plain.length < columns) ?? 0;
|
|
428
|
-
return `${head}\n${" ".repeat(under)}${paintDetail(detail)}`;
|
|
429
|
-
}
|
|
430
|
-
const fitted = plain.length <= room ? detail : fit(plain, room, ellipsis);
|
|
431
|
-
if (!fitted) return head;
|
|
432
|
-
return `${pad}${paintMark(mark)} ${paintLabel(padded)} ${paintDetail(fitted)}`;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// ── wordmark ─────────────────────────────────────────────────────────────────
|
|
436
|
-
//
|
|
437
|
-
// Drawn, not pasted out of a figlet font — six letters on a 4×6 pixel grid,
|
|
438
|
-
// printed two pixel rows to a text row with the half-block characters, which is
|
|
439
|
-
// what gives it a lowercase with real ascenders in three lines instead of six.
|
|
440
|
-
// The old banner said `agents-deck`, a name the repo, the command, the README
|
|
441
|
-
// and the docs stopped using; this one says what the product is called.
|
|
442
|
-
|
|
443
|
-
const LETTERS = {
|
|
444
|
-
c: ["....", "....", ".###", "#...", "#...", ".###"],
|
|
445
|
-
d: ["...#", "...#", ".###", "#..#", "#..#", ".###"],
|
|
446
|
-
e: ["....", "....", ".###", "####", "#...", ".###"],
|
|
447
|
-
k: ["#...", "#...", "#.##", "##..", "#.#.", "#..#"],
|
|
448
|
-
};
|
|
449
|
-
|
|
450
|
-
/** Two pixel rows into one text row: ▀ upper only, ▄ lower only, █ both. */
|
|
451
|
-
function squash(top, bottom) {
|
|
452
|
-
let out = "";
|
|
453
|
-
for (let i = 0; i < top.length; i++) {
|
|
454
|
-
const t = top[i] === "#";
|
|
455
|
-
const b = bottom[i] === "#";
|
|
456
|
-
out += t && b ? "█" : t ? "▀" : b ? "▄" : " ";
|
|
457
|
-
}
|
|
458
|
-
return out;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
function renderWord(word) {
|
|
462
|
-
const rows = ["", "", ""];
|
|
463
|
-
for (const [n, ch] of [...word].entries()) {
|
|
464
|
-
const px = LETTERS[ch];
|
|
465
|
-
for (let r = 0; r < 3; r++) rows[r] += (n ? " " : "") + squash(px[r * 2], px[r * 2 + 1]);
|
|
466
|
-
}
|
|
467
|
-
return rows.map((r) => r.replace(/\s+$/, ""));
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
/** The drawn mark itself, three text rows of half-blocks.
|
|
471
|
-
*
|
|
472
|
-
* Exported for one reader, and it is a test rather than a caller (#383). This
|
|
473
|
-
* is the ONLY place the art's true width can be measured: `renderWord` trims
|
|
474
|
-
* each row's trailing blanks, so the rows are not all the same length, and the
|
|
475
|
-
* arithmetic in WORDMARK_WIDTH below is a hand-computed prediction of the
|
|
476
|
-
* widest of them. Nothing in the banner path compares the two — `wordmark()`
|
|
477
|
-
* gates on WORDMARK_WIDTH and then prints THESE lines — so an under-counting
|
|
478
|
-
* width lets the art render into a terminal too narrow to hold it and the
|
|
479
|
-
* three rows wrap into six. See wordmark-art-width.test.ts, which is the
|
|
480
|
-
* comparison this export exists to make possible. */
|
|
481
|
-
export const WORDMARK_LINES = renderWord("ccdeck");
|
|
482
|
-
/** 29 columns of art, plus the two-space indent every other line uses. */
|
|
483
|
-
export const WORDMARK_WIDTH = 2 + LETTERS.c[0].length * 6 + 5;
|
|
484
|
-
|
|
485
|
-
/**
|
|
486
|
-
* The tagline, longest version that fits — the name and version are the part
|
|
487
|
-
* that must survive, the rest is context.
|
|
488
|
-
*
|
|
489
|
-
* `prefix` is how many columns are already spent on the line the tagline lands
|
|
490
|
-
* on, and it is a parameter because the three layouts below do not agree on it
|
|
491
|
-
* (#383). In the full banner the tagline gets a line of its own behind a
|
|
492
|
-
* two-space indent; in the compact and plain ones it shares the line with the
|
|
493
|
-
* product name, which costs eight or nine columns more. This budget was
|
|
494
|
-
* hard-coded at 2 for all three, so between 21 and 31 columns — the widths where
|
|
495
|
-
* the compact form is chosen and the medium tagline still looks affordable —
|
|
496
|
-
* the banner printed a 36-column line into a terminal that could not hold it and
|
|
497
|
-
* the first thing a user saw wrapped. `columns - 1` keeps the last cell empty,
|
|
498
|
-
* because a line ending exactly at the right margin makes some terminals wrap
|
|
499
|
-
* anyway.
|
|
500
|
-
*/
|
|
501
|
-
function tagline(version, columns, bullet, prefix) {
|
|
502
|
-
const v = `v${version}`;
|
|
503
|
-
const options = [
|
|
504
|
-
`${v} ${bullet} live agent DAG ${bullet} Claude Code + Codex`,
|
|
505
|
-
`${v} ${bullet} live agent DAG`,
|
|
506
|
-
v,
|
|
507
|
-
];
|
|
508
|
-
return options.find((o) => o.length + prefix <= columns - 1) ?? v;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// What each layout spends before the tagline starts, counted off the strings
|
|
512
|
-
// built in `wordmark` below: " ccdeck " for plain, " ccdeck " for compact,
|
|
513
|
-
// and the bare two-space indent for the full banner's own tagline line.
|
|
514
|
-
const PLAIN_PREFIX_W = " ccdeck ".length;
|
|
515
|
-
const COMPACT_PREFIX_W = " ccdeck ".length;
|
|
516
|
-
const TAG_INDENT_W = " ".length;
|
|
517
|
-
|
|
518
|
-
/**
|
|
519
|
-
* The wordmark, in the largest form this terminal can hold.
|
|
520
|
-
*
|
|
521
|
-
* "full" — the drawn mark, three rows, on a vertical gradient
|
|
522
|
-
* "compact" — the name in the accent, for a terminal too narrow for the art
|
|
523
|
-
* or a console without the half-block glyphs
|
|
524
|
-
* "plain" — no escapes at all: a pipe, a CI log, NO_COLOR
|
|
525
|
-
*
|
|
526
|
-
* Returns the lines to print, blank lines included, so the caller writes them
|
|
527
|
-
* and nothing else decides how much air the banner gets.
|
|
528
|
-
*/
|
|
529
|
-
export function wordmark({
|
|
530
|
-
columns = 80, version = "0.0.0", profile = "none", unicode = true, pal = palette(profile),
|
|
531
|
-
} = {}) {
|
|
532
|
-
const g = glyphs(unicode);
|
|
533
|
-
// Each layout asks for its own tagline, because each leaves it a different
|
|
534
|
-
// amount of room — see the prefix constants above.
|
|
535
|
-
const tag = (prefix) => tagline(version, columns, g.bullet, prefix);
|
|
536
|
-
|
|
537
|
-
if (profile === "none") return { kind: "plain", lines: ["", ` ccdeck ${tag(PLAIN_PREFIX_W)}`, ""] };
|
|
538
|
-
if (!unicode || columns < WORDMARK_WIDTH + 1) {
|
|
539
|
-
return { kind: "compact", lines: ["", ` ${pal.bold}${pal.accent}ccdeck${pal.reset} ${pal.muted}${tag(COMPACT_PREFIX_W)}${pal.reset}`, ""] };
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
const ramp = [pal.accentSoft, pal.accent, pal.accentDeep];
|
|
543
|
-
return {
|
|
544
|
-
kind: "full",
|
|
545
|
-
lines: [
|
|
546
|
-
"",
|
|
547
|
-
...WORDMARK_LINES.map((l, i) => ` ${ramp[i]}${l}${pal.reset}`),
|
|
548
|
-
"",
|
|
549
|
-
` ${pal.muted}${tag(TAG_INDENT_W)}${pal.reset}`,
|
|
550
|
-
"",
|
|
551
|
-
],
|
|
552
|
-
};
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
// ── the pulse line ───────────────────────────────────────────────────────────
|
|
556
|
-
|
|
557
|
-
/**
|
|
558
|
-
* The one line that stays on screen for hours, sized to the terminal it is on.
|
|
559
|
-
*
|
|
560
|
-
* It is redrawn over itself with `\r`, so both messages are padded to one width
|
|
561
|
-
* — the shorter has to cover the longer — and that width is clamped to the real
|
|
562
|
-
* terminal: at 40 columns the old 58-character message wrapped, after which the
|
|
563
|
-
* `\r` only ever reached the second row and every repaint left the first one
|
|
564
|
-
* behind. A deck no hook can find is not listening in any sense the user cares
|
|
565
|
-
* about, which is why that state has a message here at all.
|
|
566
|
-
*
|
|
567
|
-
* `claude` is what makes that last sentence conditional. A deck watching only
|
|
568
|
-
* Codex is not waiting on a hook: startCodexWatcher is started by startServer
|
|
569
|
-
* and never consults the discovery file, and writesCodexLog keeps a deck with no
|
|
570
|
-
* record on disk writing its log. Capture and persistence both work perfectly
|
|
571
|
-
* there, while this line used to repaint "hooks cannot find this deck" every
|
|
572
|
-
* 800ms, forever (#404) — a permanent alarm about a mechanism that deck does not
|
|
573
|
-
* use. The one-time report at boot still says what IS lost; see
|
|
574
|
-
* unregisteredDetail.
|
|
575
|
-
*/
|
|
576
|
-
export function pulseText({
|
|
577
|
-
registered = true, claude = true, columns = 80, unicode = true, indent = 2, busy = null,
|
|
578
|
-
} = {}) {
|
|
579
|
-
const g = glyphs(unicode);
|
|
580
|
-
const room = Math.max(4, columns - indent - 3 - 1);
|
|
581
|
-
const pick = (options) => options.find((o) => o.length <= room) ?? options[options.length - 1].slice(0, room);
|
|
582
|
-
|
|
583
|
-
const rest = pick([`listening ${g.dash} Ctrl+C to stop`, "listening"]);
|
|
584
|
-
const bad = pick([
|
|
585
|
-
`listening, but not registered ${g.dash} hooks cannot find this deck`,
|
|
586
|
-
`not registered ${g.dash} hooks cannot find this deck`,
|
|
587
|
-
"not registered",
|
|
588
|
-
]);
|
|
589
|
-
// Both branches are still measured, registered or not, because the line is
|
|
590
|
-
// redrawn over itself and the shorter message has to cover the longer one on
|
|
591
|
-
// the beat after a deck loses its registration.
|
|
592
|
-
//
|
|
593
|
-
// The width is deliberately computed from the two FIXED messages only. `busy`
|
|
594
|
-
// comes and goes on a single boot — an install starts, the line names it, the
|
|
595
|
-
// install ends and the line goes back to Ctrl+C — so a width that grew to fit
|
|
596
|
-
// the label would have to shrink again afterwards, and the shorter line would
|
|
597
|
-
// leave the tail of the longer one on screen. Instead the label is shown only
|
|
598
|
-
// where it already fits — 60 columns and wider, measured — and below that the
|
|
599
|
-
// line says the true thing it has always said.
|
|
600
|
-
const width = Math.min(room, Math.max(rest.length, bad.length));
|
|
601
|
-
// What is still happening, rather than what is always true. `Ctrl+C to stop`
|
|
602
|
-
// is the right thing to say to somebody with nothing left to wait for, and
|
|
603
|
-
// the wrong thing to say to somebody watching an install.
|
|
604
|
-
const label = typeof busy === "string" && busy.trim() ? busy.trim() : null;
|
|
605
|
-
const working = label ? `listening ${g.bullet} ${label}` : null;
|
|
606
|
-
const ok = working && working.length <= width ? working : rest;
|
|
607
|
-
return (registered || !claude ? ok : bad).padEnd(width);
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
/**
|
|
611
|
-
* Whether the line has anything left to say by moving.
|
|
612
|
-
*
|
|
613
|
-
* #742. The dot alternated green and grey every 800ms for as long as the deck
|
|
614
|
-
* ran, and a blinking indicator beside a status line is the vocabulary of
|
|
615
|
-
* "working on it" — so a boot that had finished in a second read as one that
|
|
616
|
-
* never finished, and people said so. The deck's own web UI already retired
|
|
617
|
-
* this once: the pill goes quiet at rest (#720). The terminal did not.
|
|
618
|
-
*
|
|
619
|
-
* Motion is now spent on the two states where something is genuinely
|
|
620
|
-
* outstanding — a deck no hook can find, and a background job still running —
|
|
621
|
-
* and nowhere else. At rest the dot is painted once, in the healthy colour, and
|
|
622
|
-
* left alone. Movement then means something changed, which is the only thing
|
|
623
|
-
* movement should ever mean on a line somebody leaves open for hours.
|
|
624
|
-
*/
|
|
625
|
-
export function pulseMoves({ registered = true, claude = true, busy = null } = {}) {
|
|
626
|
-
if (!registered && claude) return true;
|
|
627
|
-
return typeof busy === "string" && busy.trim() !== "";
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
/**
|
|
631
|
-
* Whether the dot is lit on this beat.
|
|
632
|
-
*
|
|
633
|
-
* `"on"` on every beat of a deck at rest, which is what makes the line still:
|
|
634
|
-
* bin/deck.js paints a beat only when the frame differs from the one already on
|
|
635
|
-
* screen, so a dot that is always lit is a line written once and then left
|
|
636
|
-
* alone. Alternating is reserved for the states pulseMoves admits.
|
|
637
|
-
*
|
|
638
|
-
* The beat is a parameter rather than counted here so this is a function of its
|
|
639
|
-
* inputs and nothing else — and so a test can ask what the twentieth beat of an
|
|
640
|
-
* idle deck looks like without waiting sixteen seconds for it.
|
|
641
|
-
*/
|
|
642
|
-
export function pulseDot(beat, { registered = true, claude = true, busy = null } = {}) {
|
|
643
|
-
if (!pulseMoves({ registered, claude, busy })) return "on";
|
|
644
|
-
return beat % 2 === 0 ? "on" : "off";
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
/**
|
|
648
|
-
* The second line of the "not registered" report, which bin/deck.js prints once
|
|
649
|
-
* per change of state rather than every beat.
|
|
650
|
-
*
|
|
651
|
-
* What a missing discovery file costs is not the same on the two capture paths,
|
|
652
|
-
* and the old sentence — "hooks find this deck through <file>, so until that
|
|
653
|
-
* file exists no events arrive" — stated the Claude Code answer as if it were
|
|
654
|
-
* both. On a Codex-only deck it is false twice over: no hook is looking for this
|
|
655
|
-
* deck, and events do keep arriving, because the rollout watcher reads the files
|
|
656
|
-
* directly. What that deck really loses is the writer election in
|
|
657
|
-
* log-writer.mjs, which is how several decks tailing one rollout agree on which
|
|
658
|
-
* of them appends it to a shared events log. A deck with no record on disk is
|
|
659
|
-
* assumed to be writing, so nothing is dropped — the log can gain the same line
|
|
660
|
-
* twice instead.
|
|
661
|
-
*
|
|
662
|
-
* @param file the discovery file that could not be written.
|
|
663
|
-
* @param claude whether this deck is watching Claude Code at all.
|
|
664
|
-
* @param dash the em dash, or the ASCII stand-in on a console that cannot draw
|
|
665
|
-
* one (#797). A parameter for the same reason `renameNotice` takes one: this
|
|
666
|
-
* module's own header says "every glyph the deck prints — punctuation
|
|
667
|
-
* included, since an em dash is as absent from CP437 as a check mark — comes
|
|
668
|
-
* from one of them", and this sentence had it hardcoded. On a legacy cmd.exe
|
|
669
|
-
* or the Linux virtual console the reader got a box in the middle of the one
|
|
670
|
-
* line explaining why no events are arriving.
|
|
671
|
-
*/
|
|
672
|
-
export function unregisteredDetail({ file, claude = true, dash = "—" }) {
|
|
673
|
-
if (claude) {
|
|
674
|
-
return `Claude Code hooks find this deck through ${file}, so until that file exists no Claude Code events arrive.`;
|
|
675
|
-
}
|
|
676
|
-
return `Codex capture does not use ${file} ${dash} the deck tails the rollout files itself ${dash} so events still arrive. Only decks sharing one events log need it, to agree on which of them records.`;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
// ── cursor ───────────────────────────────────────────────────────────────────
|
|
680
|
-
//
|
|
681
|
-
// Strings rather than writers: hiding the cursor is trivial and restoring it is
|
|
682
|
-
// not — it has to happen on the normal exit, on SIGINT/SIGTERM/SIGHUP and after
|
|
683
|
-
// an uncaught throw, and getting that half-right leaves the user's shell with
|
|
684
|
-
// no cursor after the deck is gone. The lifecycle owns it; see bin/deck.js.
|
|
685
|
-
export const CURSOR_HIDE = "\x1b[?25l";
|
|
686
|
-
export const CURSOR_SHOW = "\x1b[?25h";
|