@promptctl/cc-candybar 1.42.1 → 1.43.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/dist/index.mjs +72 -71
- package/package.json +5 -6
- package/src/check.ts +0 -478
- package/src/cli-flags.ts +0 -8
- package/src/click/wire.ts +0 -158
- package/src/config/action.ts +0 -329
- package/src/config/cli.ts +0 -71
- package/src/config/default-dsl-config.ts +0 -1645
- package/src/config/disclosure.ts +0 -170
- package/src/config/dsl-loader.ts +0 -339
- package/src/config/dsl-types.ts +0 -581
- package/src/config/edit-chrome.ts +0 -559
- package/src/config/help.ts +0 -151
- package/src/config/ident.ts +0 -22
- package/src/config/layout-ops.ts +0 -177
- package/src/config/loader/actions.ts +0 -972
- package/src/config/loader/cache.ts +0 -206
- package/src/config/loader/cross-ref.ts +0 -714
- package/src/config/loader/cycles.ts +0 -148
- package/src/config/loader/diagnostics.ts +0 -99
- package/src/config/loader/discovery.ts +0 -182
- package/src/config/loader/edit-mode.ts +0 -137
- package/src/config/loader/emit-schema.ts +0 -68
- package/src/config/loader/globals.ts +0 -269
- package/src/config/loader/helpers.ts +0 -48
- package/src/config/loader/layout.ts +0 -693
- package/src/config/loader/looks.ts +0 -96
- package/src/config/loader/menu-synth.ts +0 -435
- package/src/config/loader/merge.ts +0 -115
- package/src/config/loader/persist-target.ts +0 -67
- package/src/config/loader/presets.ts +0 -119
- package/src/config/loader/refs.ts +0 -100
- package/src/config/loader/reserved-namespace.ts +0 -38
- package/src/config/loader/segments.ts +0 -120
- package/src/config/loader/validate-core.ts +0 -737
- package/src/config/loader/variables.ts +0 -260
- package/src/config/menu-keys.ts +0 -139
- package/src/config/option-domain.ts +0 -164
- package/src/config/presets.ts +0 -326
- package/src/config/settings-menu.ts +0 -775
- package/src/daemon/acquire.ts +0 -684
- package/src/daemon/cache/git.ts +0 -649
- package/src/daemon/cache/render.ts +0 -623
- package/src/daemon/cache/session-usage-store.ts +0 -720
- package/src/daemon/cache/watchers.ts +0 -249
- package/src/daemon/client-debug.ts +0 -120
- package/src/daemon/client-stats.ts +0 -130
- package/src/daemon/client-transport.ts +0 -273
- package/src/daemon/client.ts +0 -78
- package/src/daemon/config-overrides-store.ts +0 -663
- package/src/daemon/debug-types.ts +0 -91
- package/src/daemon/debug.ts +0 -264
- package/src/daemon/fork-bomb-breaker.ts +0 -351
- package/src/daemon/limits.ts +0 -211
- package/src/daemon/log.ts +0 -81
- package/src/daemon/parent-watchdog.ts +0 -87
- package/src/daemon/paths.ts +0 -211
- package/src/daemon/process-fingerprint.ts +0 -146
- package/src/daemon/protocol.ts +0 -292
- package/src/daemon/render-payload.ts +0 -1256
- package/src/daemon/server.ts +0 -1330
- package/src/daemon/session-state-file.ts +0 -108
- package/src/daemon/session-state.ts +0 -237
- package/src/daemon/socket-lease.ts +0 -209
- package/src/daemon/socket-ownership.ts +0 -209
- package/src/daemon/stats.ts +0 -235
- package/src/daemon/verbs/config-validators.ts +0 -250
- package/src/daemon/verbs/index.ts +0 -706
- package/src/daemon/verbs/state-validators.ts +0 -249
- package/src/daemon/verbs/validator-registry.ts +0 -457
- package/src/demo/dsl.ts +0 -143
- package/src/demo/mock-data.ts +0 -67
- package/src/demo/statusline.json5 +0 -94
- package/src/dsl/node-registry.ts +0 -374
- package/src/dsl/render.ts +0 -803
- package/src/help-text.ts +0 -90
- package/src/index.ts +0 -210
- package/src/install/currency.ts +0 -197
- package/src/install/index.ts +0 -557
- package/src/proc/launch.ts +0 -459
- package/src/proc/stats-handle.ts +0 -13
- package/src/render/action.ts +0 -883
- package/src/render/active-segment.ts +0 -78
- package/src/render/diagnostic-style.ts +0 -23
- package/src/render/diagnostic-text.ts +0 -77
- package/src/render/error-glyph.ts +0 -53
- package/src/render/menu.ts +0 -257
- package/src/render/outcome-plan.ts +0 -45
- package/src/render/picker.ts +0 -372
- package/src/render/segment-color.ts +0 -74
- package/src/render/split-lines.ts +0 -51
- package/src/render/strip.ts +0 -228
- package/src/segments/cache.ts +0 -131
- package/src/segments/context.ts +0 -190
- package/src/segments/git.ts +0 -1084
- package/src/segments/metrics.ts +0 -187
- package/src/segments/pricing.ts +0 -452
- package/src/segments/session.ts +0 -23
- package/src/segments/tmux.ts +0 -74
- package/src/template-engine/cells.ts +0 -90
- package/src/template-engine/colors.ts +0 -124
- package/src/template-engine/engine.ts +0 -108
- package/src/template-engine/funcs.ts +0 -232
- package/src/template-engine/index.ts +0 -11
- package/src/template-engine/layout.ts +0 -133
- package/src/template-engine/scope.ts +0 -62
- package/src/template-engine/sparkline.ts +0 -79
- package/src/themes/index.ts +0 -20
- package/src/themes/palette-resolvers.ts +0 -84
- package/src/themes/policy.ts +0 -393
- package/src/utils/cache.ts +0 -206
- package/src/utils/claude.ts +0 -683
- package/src/utils/color-support.ts +0 -118
- package/src/utils/formatters.ts +0 -99
- package/src/utils/logger.ts +0 -5
- package/src/utils/outcome.ts +0 -33
- package/src/utils/schema-validator.ts +0 -126
- package/src/utils/single-flight.ts +0 -57
- package/src/utils/terminal-width.ts +0 -51
- package/src/utils/terminal.ts +0 -11
- package/src/utils/transcript-fs.ts +0 -279
- package/src/var-system/index.ts +0 -24
- package/src/var-system/sources.ts +0 -1047
- package/src/var-system/store.ts +0 -223
- package/src/var-system/types.ts +0 -57
- package/src/version.ts +0 -17
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import process from "node:process";
|
|
2
|
-
import tty from "node:tty";
|
|
3
|
-
|
|
4
|
-
export function getColorSupport(): "none" | "ansi" | "ansi256" | "truecolor" {
|
|
5
|
-
const { env } = process;
|
|
6
|
-
|
|
7
|
-
let colorEnabled = true;
|
|
8
|
-
|
|
9
|
-
if (env.NO_COLOR && env.NO_COLOR !== "") {
|
|
10
|
-
colorEnabled = false;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const forceColor = env.FORCE_COLOR;
|
|
14
|
-
if (forceColor && forceColor !== "") {
|
|
15
|
-
if (forceColor === "false" || forceColor === "0") {
|
|
16
|
-
return "none";
|
|
17
|
-
}
|
|
18
|
-
if (forceColor === "true" || forceColor === "1") {
|
|
19
|
-
return "ansi";
|
|
20
|
-
}
|
|
21
|
-
if (forceColor === "2") {
|
|
22
|
-
return "ansi256";
|
|
23
|
-
}
|
|
24
|
-
if (forceColor === "3") {
|
|
25
|
-
return "truecolor";
|
|
26
|
-
}
|
|
27
|
-
return "ansi";
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (!colorEnabled) {
|
|
31
|
-
return "none";
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (env.TERM === "dumb") {
|
|
35
|
-
return "none";
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (env.CI) {
|
|
39
|
-
if (
|
|
40
|
-
["GITHUB_ACTIONS", "GITEA_ACTIONS", "CIRCLECI"].some((key) => key in env)
|
|
41
|
-
) {
|
|
42
|
-
return "truecolor";
|
|
43
|
-
}
|
|
44
|
-
return "ansi";
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (env.COLORTERM === "truecolor") {
|
|
48
|
-
return "truecolor";
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const truecolorTerminals = [
|
|
52
|
-
"xterm-kitty",
|
|
53
|
-
"xterm-ghostty",
|
|
54
|
-
"wezterm",
|
|
55
|
-
"alacritty",
|
|
56
|
-
"foot",
|
|
57
|
-
"contour",
|
|
58
|
-
];
|
|
59
|
-
|
|
60
|
-
if (truecolorTerminals.includes(env.TERM || "")) {
|
|
61
|
-
return "truecolor";
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (env.TERM_PROGRAM) {
|
|
65
|
-
switch (env.TERM_PROGRAM) {
|
|
66
|
-
case "iTerm.app":
|
|
67
|
-
return "truecolor";
|
|
68
|
-
case "Apple_Terminal":
|
|
69
|
-
return "ansi256";
|
|
70
|
-
case "vscode":
|
|
71
|
-
return "truecolor";
|
|
72
|
-
case "Tabby":
|
|
73
|
-
return "truecolor";
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (/-256(color)?$/i.test(env.TERM || "")) {
|
|
78
|
-
return "ansi256";
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (/-truecolor$/i.test(env.TERM || "")) {
|
|
82
|
-
return "truecolor";
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (
|
|
86
|
-
/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(
|
|
87
|
-
env.TERM || "",
|
|
88
|
-
)
|
|
89
|
-
) {
|
|
90
|
-
return "ansi";
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (env.COLORTERM) {
|
|
94
|
-
return "ansi";
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (tty?.WriteStream?.prototype?.hasColors) {
|
|
98
|
-
try {
|
|
99
|
-
const colors = tty.WriteStream.prototype.hasColors();
|
|
100
|
-
if (!colors) {
|
|
101
|
-
return "none";
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const has256Colors = tty.WriteStream.prototype.hasColors(256);
|
|
105
|
-
const has16mColors = tty.WriteStream.prototype.hasColors(16777216);
|
|
106
|
-
|
|
107
|
-
if (has16mColors) {
|
|
108
|
-
return "truecolor";
|
|
109
|
-
} else if (has256Colors) {
|
|
110
|
-
return "ansi256";
|
|
111
|
-
} else {
|
|
112
|
-
return "ansi";
|
|
113
|
-
}
|
|
114
|
-
} catch {}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return "ansi";
|
|
118
|
-
}
|
package/src/utils/formatters.ts
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
const CLAUDE_MODEL_PATTERN =
|
|
2
|
-
/^(?:(?:global|apac|au|eu|us|us-east-\d|us-west-\d|eu-west-\d|eu-central-\d)\.)?(?:anthropic\.|azure_ai\/|bedrock\/|vertex_ai\/)?claude-(?:(?<family>opus|sonnet|haiku)-(?<newMajor>\d+)(?:-(?<newMinor>\d))?|(?<oldMajor>\d+)(?:-(?<oldMinor>\d))?-(?<oldFamily>opus|sonnet|haiku))(?:[-@]\d{8})?(?:-v\d+:\d+)?(?:-latest)?$/i;
|
|
3
|
-
|
|
4
|
-
const FRIENDLY_MODEL_PATTERN =
|
|
5
|
-
/^(?<family>opus|sonnet|haiku)\s+(?<major>\d+)(?:\.(?<minor>\d))?$/i;
|
|
6
|
-
|
|
7
|
-
export function formatModelName(rawName: string): string {
|
|
8
|
-
if (!rawName) {
|
|
9
|
-
return "Claude";
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
// [LAW:one-source-of-truth] strip variant decorations (e.g. " (1M context)",
|
|
13
|
-
// "[1m]") so all callers see canonical "Family X.Y" output regardless of
|
|
14
|
-
// whether the input came from model.id or model.display_name.
|
|
15
|
-
const stripped = rawName
|
|
16
|
-
.trim()
|
|
17
|
-
.replace(/\s*\([^)]*\)\s*$/, "")
|
|
18
|
-
.replace(/\s*\[[^\]]*\]\s*$/, "")
|
|
19
|
-
.trim();
|
|
20
|
-
|
|
21
|
-
const match = stripped.match(CLAUDE_MODEL_PATTERN);
|
|
22
|
-
if (match?.groups) {
|
|
23
|
-
const { family, newMajor, newMinor, oldMajor, oldMinor, oldFamily } =
|
|
24
|
-
match.groups;
|
|
25
|
-
|
|
26
|
-
const modelFamily = family || oldFamily;
|
|
27
|
-
const major = newMajor || oldMajor;
|
|
28
|
-
const minor = newMinor || oldMinor;
|
|
29
|
-
|
|
30
|
-
if (modelFamily && major) {
|
|
31
|
-
const capitalizedFamily =
|
|
32
|
-
modelFamily.charAt(0).toUpperCase() +
|
|
33
|
-
modelFamily.slice(1).toLowerCase();
|
|
34
|
-
const version = minor ? `${major}.${minor}` : major;
|
|
35
|
-
return `${capitalizedFamily} ${version}`;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const friendly = stripped.match(FRIENDLY_MODEL_PATTERN);
|
|
40
|
-
if (friendly?.groups) {
|
|
41
|
-
const family = friendly.groups.family!;
|
|
42
|
-
const major = friendly.groups.major!;
|
|
43
|
-
const minor = friendly.groups.minor;
|
|
44
|
-
const capitalizedFamily =
|
|
45
|
-
family.charAt(0).toUpperCase() + family.slice(1).toLowerCase();
|
|
46
|
-
const version = minor ? `${major}.${minor}` : major;
|
|
47
|
-
return `${capitalizedFamily} ${version}`;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return stripped || rawName;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function shortenModelName(formatted: string): string {
|
|
54
|
-
// [LAW:one-type-per-behavior] same parser, different rendering — operates on
|
|
55
|
-
// the canonical output of formatModelName so callers don't reparse raw IDs.
|
|
56
|
-
const match = formatted.match(FRIENDLY_MODEL_PATTERN);
|
|
57
|
-
if (!match?.groups) return formatted;
|
|
58
|
-
const family = match.groups.family!;
|
|
59
|
-
const major = match.groups.major!;
|
|
60
|
-
const minor = match.groups.minor;
|
|
61
|
-
const initial = family.charAt(0).toUpperCase();
|
|
62
|
-
const version = minor ? `${major}.${minor}` : major;
|
|
63
|
-
return `${initial}${version}`;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// [LAW:decomposition] Fish-shell `prompt_pwd` abbreviation: collapse every path
|
|
67
|
-
// segment EXCEPT the leaf to its leading character, keeping any leading dots so
|
|
68
|
-
// `.config` → `.c`. A pure string→string transform that knows nothing about
|
|
69
|
-
// home-collapse or project-relative logic — those live in the directory
|
|
70
|
-
// template and this abbreviates whatever collapsed display path they produce.
|
|
71
|
-
// `~/code/cc-candybar` → `~/c/cc-candybar`; a leading "/" is preserved because
|
|
72
|
-
// the empty pre-slash segment abbreviates to empty (`/usr/local/bin` → `/u/l/bin`).
|
|
73
|
-
export function abbreviatePath(path: string): string {
|
|
74
|
-
const segments = path.split("/");
|
|
75
|
-
const lastIndex = segments.length - 1;
|
|
76
|
-
return segments
|
|
77
|
-
.map((seg, i) => {
|
|
78
|
-
if (i === lastIndex) return seg;
|
|
79
|
-
// Leading dot-run plus the first following char (fish keeps dotfiles
|
|
80
|
-
// legible: `.config` → `.c`, `..` → `..`). No following char (empty
|
|
81
|
-
// segment) means no match — return it unchanged.
|
|
82
|
-
const abbreviated = seg.match(/^\.*./);
|
|
83
|
-
return abbreviated ? abbreviated[0] : seg;
|
|
84
|
-
})
|
|
85
|
-
.join("/");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// [LAW:one-source-of-truth] Locale-grouped integer rendering. Callers that
|
|
89
|
-
// want "50,000" instead of "50000" go through this rather than calling
|
|
90
|
-
// toLocaleString() ad-hoc — the legacy context segment used the latter
|
|
91
|
-
// pattern inline, and the DSL formatter (template-engine/funcs.ts)
|
|
92
|
-
// delegates here so the two producers agree by construction.
|
|
93
|
-
//
|
|
94
|
-
// No locale argument: the default-locale behaviour is exactly what the
|
|
95
|
-
// legacy renderer did (`n.toLocaleString()`), so byte-parity holds with
|
|
96
|
-
// whatever locale the host process picks at startup.
|
|
97
|
-
export function formatInteger(n: number): string {
|
|
98
|
-
return n.toLocaleString();
|
|
99
|
-
}
|
package/src/utils/logger.ts
DELETED
package/src/utils/outcome.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
// [LAW:types-are-the-program] The provider-outcome vocabulary: a data fetch
|
|
2
|
-
// either produced a value, found that the domain genuinely has none (no
|
|
3
|
-
// upstream configured, no transcript yet), or failed to get an answer at all
|
|
4
|
-
// (timeout, spawn error, unreadable file). Before this type, the third state
|
|
5
|
-
// was unrepresentable — failure had to wear a real value's clothes (0, "",
|
|
6
|
-
// a basename) and the lie flowed all the way to the rendered bar.
|
|
7
|
-
//
|
|
8
|
-
// [LAW:dataflow-not-control-flow] Failure is a value that flows to a
|
|
9
|
-
// boundary, not a swallowed branch. Producers classify once, where the
|
|
10
|
-
// command semantics are known; consumers fold once, at the edge where the
|
|
11
|
-
// log effect and the payload mapping live.
|
|
12
|
-
export type Outcome<T> =
|
|
13
|
-
| { readonly kind: "ok"; readonly value: T }
|
|
14
|
-
| { readonly kind: "absent" }
|
|
15
|
-
| { readonly kind: "failed"; readonly reason: string };
|
|
16
|
-
|
|
17
|
-
export function ok<T>(value: T): Outcome<T> {
|
|
18
|
-
return { kind: "ok", value };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export const ABSENT: Outcome<never> = { kind: "absent" };
|
|
22
|
-
|
|
23
|
-
export function failed(reason: string): Outcome<never> {
|
|
24
|
-
return { kind: "failed", reason };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// [LAW:dataflow-not-control-flow] Total fold for consumers that only need a
|
|
28
|
-
// value-or-fallback view (e.g. var-system's typed projection). Both non-ok
|
|
29
|
-
// arms collapse to the fallback; consumers that must distinguish absent from
|
|
30
|
-
// failed (the logging boundaries) match on `kind` instead.
|
|
31
|
-
export function orElse<T>(outcome: Outcome<T> | undefined, fallback: T): T {
|
|
32
|
-
return outcome?.kind === "ok" ? outcome.value : fallback;
|
|
33
|
-
}
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
// [LAW:single-enforcer] All hookData schema validation flows through
|
|
2
|
-
// validateHookData. One trust boundary, one check.
|
|
3
|
-
//
|
|
4
|
-
// [LAW:dataflow-not-control-flow] Every check runs unconditionally. Results
|
|
5
|
-
// accumulate into a ValidationReport — callers decide what to do with them.
|
|
6
|
-
// No early exits, no control-flow branches that skip checks.
|
|
7
|
-
|
|
8
|
-
import type { ClaudeHookData } from "./claude";
|
|
9
|
-
|
|
10
|
-
export interface ValidationReport {
|
|
11
|
-
// Required fields that were absent or had wrong types.
|
|
12
|
-
missingRequired: string[];
|
|
13
|
-
typeMismatches: Array<{ path: string; expected: string; got: string }>;
|
|
14
|
-
// Top-level keys not in the known schema — Anthropic may have added new fields.
|
|
15
|
-
unknownTopLevelFields: string[];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Top-level keys Anthropic sends (plus hook_event_name, which cc-candybar adds).
|
|
19
|
-
// Adding a new Anthropic field here suppresses the "unknown field" log for it.
|
|
20
|
-
const KNOWN_TOP_LEVEL = new Set([
|
|
21
|
-
"hook_event_name", // cc-candybar internal
|
|
22
|
-
"session_id",
|
|
23
|
-
"session_name",
|
|
24
|
-
"transcript_path",
|
|
25
|
-
"cwd",
|
|
26
|
-
"model",
|
|
27
|
-
"workspace",
|
|
28
|
-
"version",
|
|
29
|
-
"output_style",
|
|
30
|
-
"cost",
|
|
31
|
-
"context_window",
|
|
32
|
-
"exceeds_200k_tokens",
|
|
33
|
-
"effort",
|
|
34
|
-
"thinking",
|
|
35
|
-
"rate_limits",
|
|
36
|
-
"vim",
|
|
37
|
-
"agent",
|
|
38
|
-
"worktree",
|
|
39
|
-
]);
|
|
40
|
-
|
|
41
|
-
// Required fields: [dot-separated path, expected typeof result]
|
|
42
|
-
// "object" means non-null, non-array object. Checked in declaration order.
|
|
43
|
-
const REQUIRED_FIELDS: Array<
|
|
44
|
-
[string, "string" | "number" | "boolean" | "object"]
|
|
45
|
-
> = [
|
|
46
|
-
["session_id", "string"],
|
|
47
|
-
["transcript_path", "string"],
|
|
48
|
-
["cwd", "string"],
|
|
49
|
-
["model", "object"],
|
|
50
|
-
["model.id", "string"],
|
|
51
|
-
["model.display_name", "string"],
|
|
52
|
-
["workspace", "object"],
|
|
53
|
-
["workspace.current_dir", "string"],
|
|
54
|
-
["workspace.project_dir", "string"],
|
|
55
|
-
];
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Validate raw hookData received over the wire against the known Anthropic schema.
|
|
59
|
-
*
|
|
60
|
-
* Returns the data typed as ClaudeHookData alongside a ValidationReport.
|
|
61
|
-
* Never throws — divergences are reported, not thrown. The daemon decides
|
|
62
|
-
* how to surface them (dlog warn/info).
|
|
63
|
-
*
|
|
64
|
-
* [LAW:no-defensive-null-guards] Validation at the trust boundary is correct.
|
|
65
|
-
* Everywhere else in the codebase, hookData fields are used without guards
|
|
66
|
-
* because this boundary guarantees their presence.
|
|
67
|
-
*/
|
|
68
|
-
export function validateHookData(raw: unknown): {
|
|
69
|
-
data: ClaudeHookData;
|
|
70
|
-
report: ValidationReport;
|
|
71
|
-
} {
|
|
72
|
-
const report: ValidationReport = {
|
|
73
|
-
missingRequired: [],
|
|
74
|
-
typeMismatches: [],
|
|
75
|
-
unknownTopLevelFields: [],
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const obj: Record<string, unknown> =
|
|
79
|
-
raw !== null && typeof raw === "object" && !Array.isArray(raw)
|
|
80
|
-
? (raw as Record<string, unknown>)
|
|
81
|
-
: {};
|
|
82
|
-
|
|
83
|
-
for (const [path, expectedType] of REQUIRED_FIELDS) {
|
|
84
|
-
const value = resolvePath(obj, path);
|
|
85
|
-
if (value === undefined || value === null) {
|
|
86
|
-
report.missingRequired.push(path);
|
|
87
|
-
} else {
|
|
88
|
-
const actualType = Array.isArray(value) ? "array" : typeof value;
|
|
89
|
-
const mismatch =
|
|
90
|
-
expectedType === "object"
|
|
91
|
-
? actualType !== "object"
|
|
92
|
-
: actualType !== expectedType;
|
|
93
|
-
if (mismatch) {
|
|
94
|
-
report.typeMismatches.push({
|
|
95
|
-
path,
|
|
96
|
-
expected: expectedType,
|
|
97
|
-
got: actualType,
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
for (const key of Object.keys(obj)) {
|
|
104
|
-
if (!KNOWN_TOP_LEVEL.has(key)) {
|
|
105
|
-
report.unknownTopLevelFields.push(key);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return { data: raw as ClaudeHookData, report };
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function resolvePath(obj: Record<string, unknown>, dotPath: string): unknown {
|
|
113
|
-
let cur: unknown = obj;
|
|
114
|
-
for (const key of dotPath.split(".")) {
|
|
115
|
-
if (
|
|
116
|
-
cur === null ||
|
|
117
|
-
cur === undefined ||
|
|
118
|
-
typeof cur !== "object" ||
|
|
119
|
-
Array.isArray(cur)
|
|
120
|
-
) {
|
|
121
|
-
return undefined;
|
|
122
|
-
}
|
|
123
|
-
cur = (cur as Record<string, unknown>)[key];
|
|
124
|
-
}
|
|
125
|
-
return cur;
|
|
126
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
// [LAW:one-source-of-truth] A keyed in-flight coalescer: while a computation
|
|
2
|
-
// for a given key is running, every concurrent caller for that same key shares
|
|
3
|
-
// the ONE in-flight promise instead of starting its own. The fs gate
|
|
4
|
-
// (transcript-fs.ts) bounds how many fs ops run at once; it does NOT dedupe the
|
|
5
|
-
// work — K concurrent renders each still launch their own (now-bounded)
|
|
6
|
-
// whole-tree scan. This is the missing piece: K renders trigger ONE scan, not
|
|
7
|
-
// K. The gate makes that one scan's cost bounded; this makes there be one.
|
|
8
|
-
//
|
|
9
|
-
// [LAW:types-are-the-program] Coalescing is expressed by ROUTING through one
|
|
10
|
-
// owner of "is this key already computing", not by a guard scattered at each
|
|
11
|
-
// callsite. The selection is dataflow — `inflight.get(key) ?? start(...)` —
|
|
12
|
-
// both arms yield a `Promise<T>`, so the operation (return the shared promise)
|
|
13
|
-
// always runs; only the value varies. There is no `if (alreadyRunning) return`
|
|
14
|
-
// branch that skips work.
|
|
15
|
-
//
|
|
16
|
-
// Scope of sharing is exactly the in-flight WINDOW: the entry is removed when
|
|
17
|
-
// the promise settles (success OR failure), so this is a coalescer, never a
|
|
18
|
-
// cache. A fresh call after completion starts a new computation — staleness is
|
|
19
|
-
// impossible because nothing is retained past settle. Result caching, when
|
|
20
|
-
// wanted, is a separate concern owned by the caller (the disk/LRU caches).
|
|
21
|
-
//
|
|
22
|
-
// This also dissolves the render-timeout orphaning problem
|
|
23
|
-
// (brandon-daemon-memory-leak-gn4.3): a render that abandons its await (the
|
|
24
|
-
// daemon's 200ms response timeout fires) does not cancel or duplicate the
|
|
25
|
-
// shared computation — there is only ever one scan in flight per key, so the
|
|
26
|
-
// timed-out render leaves behind the single canonical computation that the next
|
|
27
|
-
// render coalesces onto. A timeout therefore adds zero new fs work.
|
|
28
|
-
export class SingleFlight {
|
|
29
|
-
private readonly inflight = new Map<string, Promise<unknown>>();
|
|
30
|
-
|
|
31
|
-
run<T>(key: string, factory: () => Promise<T>): Promise<T> {
|
|
32
|
-
return (
|
|
33
|
-
(this.inflight.get(key) as Promise<T> | undefined) ??
|
|
34
|
-
this.start(key, factory)
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
private start<T>(key: string, factory: () => Promise<T>): Promise<T> {
|
|
39
|
-
const promise = factory();
|
|
40
|
-
this.inflight.set(key, promise);
|
|
41
|
-
// Deregister on settle. The identity check guards the (impossible-by-key
|
|
42
|
-
// but cheap-to-prove) case where a newer promise has replaced this one:
|
|
43
|
-
// only the promise that registered itself clears itself.
|
|
44
|
-
const deregister = (): void => {
|
|
45
|
-
if (this.inflight.get(key) === promise) this.inflight.delete(key);
|
|
46
|
-
};
|
|
47
|
-
promise.then(deregister, deregister);
|
|
48
|
-
return promise;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Number of computations currently in flight — a read-only observability
|
|
52
|
-
// surface for tests asserting the coalescing contract (one in-flight entry
|
|
53
|
-
// per key). Never read as control flow.
|
|
54
|
-
get size(): number {
|
|
55
|
-
return this.inflight.size;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
// [LAW:single-enforcer] Terminal width has exactly one authoritative source:
|
|
2
|
-
// the live client's shell context (env / ioctl), captured at the wire boundary
|
|
3
|
-
// and threaded through as request data. This module is a pure resolver from
|
|
4
|
-
// (caller-supplied hint, ambient env, stderr TTY) to "width with reserve
|
|
5
|
-
// applied, or null." Subprocess-based fallbacks belong at the wire boundary,
|
|
6
|
-
// not here. stderr (not stdout) is the TTY-side fallback: when this resolver
|
|
7
|
-
// runs in a hook context, stdout is the captured statusline pipe while stderr
|
|
8
|
-
// stays attached to the parent terminal.
|
|
9
|
-
//
|
|
10
|
-
// [LAW:dataflow-not-control-flow] The function always runs the same code path.
|
|
11
|
-
// Variability lives in the inputs (hint set or not, env set or not, stderr a
|
|
12
|
-
// TTY or not), never in whether work runs.
|
|
13
|
-
|
|
14
|
-
// @info Reserves columns for Claude Code's statusline left gutter. Claude Code
|
|
15
|
-
// prints each statusline row inset by a fixed left margin, so a row rendered at
|
|
16
|
-
// the full raw column count would be shifted right past the terminal edge and
|
|
17
|
-
// soft-wrap its trailing cells. The reserve is exactly that left margin — NOT a
|
|
18
|
-
// right-side overlay budget. The version/autoupdate/"Remote Control" hints render
|
|
19
|
-
// on their own right-aligned lines below the bar and never overlap a statusline
|
|
20
|
-
// row, so they cost zero usable width here. Verified empirically against a live
|
|
21
|
-
// Claude Code statusline (the older value of 45 was a guess for a right-side
|
|
22
|
-
// overlay that does not sit on the statusline row); re-measure if Claude Code's
|
|
23
|
-
// statusline gutter changes.
|
|
24
|
-
const RESERVED_CHARS = 2;
|
|
25
|
-
|
|
26
|
-
// [LAW:single-enforcer] The canonical raw-cols → usable-cols transform.
|
|
27
|
-
// Every consumer that needs to honor Claude Code's statusline gutter routes
|
|
28
|
-
// through here; there is no parallel `cols - N` math anywhere. Exposed so callers
|
|
29
|
-
// that already have a raw width (e.g. the daemon's wire-fallback path,
|
|
30
|
-
// the demo reading process.stdout.columns) can apply the reserve without
|
|
31
|
-
// re-entering the env/stderr resolution chain in getTerminalWidth.
|
|
32
|
-
export function applyClaudeCodeReserve(rawCols: number): number {
|
|
33
|
-
return Math.max(1, rawCols - RESERVED_CHARS);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function getTerminalWidth(termColsHint?: number): number | null {
|
|
37
|
-
if (termColsHint && termColsHint > 0)
|
|
38
|
-
return applyClaudeCodeReserve(termColsHint);
|
|
39
|
-
|
|
40
|
-
const envColumns = process.env.COLUMNS;
|
|
41
|
-
if (envColumns) {
|
|
42
|
-
const parsed = parseInt(envColumns, 10);
|
|
43
|
-
if (!isNaN(parsed) && parsed > 0) return applyClaudeCodeReserve(parsed);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (process.stderr.columns && process.stderr.columns > 0) {
|
|
47
|
-
return applyClaudeCodeReserve(process.stderr.columns);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return null;
|
|
51
|
-
}
|
package/src/utils/terminal.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export const ESC = String.fromCharCode(27);
|
|
2
|
-
const ANSI_REGEX = new RegExp(`${ESC}\\[[0-9;]*m`, "g");
|
|
3
|
-
export const ANSI_SPLIT = new RegExp(`(${ESC}\\[[0-9;]*m)`);
|
|
4
|
-
|
|
5
|
-
export function stripAnsi(str: string): string {
|
|
6
|
-
return str.replace(ANSI_REGEX, "");
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function visibleLength(str: string): number {
|
|
10
|
-
return stripAnsi(str).length;
|
|
11
|
-
}
|