@promptctl/cc-candybar 1.10.0 → 1.12.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@promptctl/cc-candybar",
3
- "version": "1.10.0",
3
+ "version": "1.12.0",
4
4
  "description": "Statusline renderer for Claude Code — a JSON5-configurable DSL with daemon-cached data sources, byte-clean palette-aware composition, and OSC8 click verbs.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",
@@ -91,10 +91,10 @@
91
91
  "mobx": "^6.15.0"
92
92
  },
93
93
  "optionalDependencies": {
94
- "@promptctl/cc-candybar-darwin-arm64": "1.10.0",
95
- "@promptctl/cc-candybar-darwin-x64": "1.10.0",
96
- "@promptctl/cc-candybar-linux-x64": "1.10.0",
97
- "@promptctl/cc-candybar-linux-arm64": "1.10.0"
94
+ "@promptctl/cc-candybar-darwin-arm64": "1.12.0",
95
+ "@promptctl/cc-candybar-darwin-x64": "1.12.0",
96
+ "@promptctl/cc-candybar-linux-x64": "1.12.0",
97
+ "@promptctl/cc-candybar-linux-arm64": "1.12.0"
98
98
  },
99
99
  "pnpm": {
100
100
  "supportedArchitectures": {
@@ -40,6 +40,20 @@
40
40
  "type": "integer",
41
41
  "minimum": 0,
42
42
  "maximum": 16
43
+ },
44
+ "charset": {
45
+ "enum": [
46
+ "unicode",
47
+ "ascii"
48
+ ]
49
+ },
50
+ "colorCompatibility": {
51
+ "enum": [
52
+ "truecolor",
53
+ "256",
54
+ "ansi",
55
+ "none"
56
+ ]
43
57
  }
44
58
  },
45
59
  "additionalProperties": false
@@ -14,7 +14,11 @@
14
14
  // references it here. The dependency is one-way (this file → action.ts), never
15
15
  // the reverse, so that shape can be lifted out without a cycle.
16
16
  import type { ActionDecl } from "./action.js";
17
- import type { StripStyle } from "../themes/policy.js";
17
+ import type {
18
+ Charset,
19
+ ColorCompatibility,
20
+ StripStyle,
21
+ } from "../themes/policy.js";
18
22
 
19
23
  // [LAW:types-are-the-program] Three stages, three names.
20
24
  //
@@ -204,6 +208,24 @@ export interface Globals {
204
208
  // [config-only] The daemon resolves `globals.padding ?? 1` into
205
209
  // renderOpts.padding; no SessionState/click half.
206
210
  readonly padding?: number;
211
+
212
+ // The legacy display.charset knob: which glyph vocabulary the strip joiners
213
+ // render with. Default "unicode" (current behavior — rich-js's powerline
214
+ // caps, U+E0Bx). "ascii" swaps the caps for single-column ASCII glyphs so
215
+ // terminals/fonts without powerline glyphs render cleanly instead of tofu.
216
+ // Orthogonal to `style`: style picks the joiner shape, charset the glyphs.
217
+ // [config-only] The daemon resolves `globals.charset ?? "unicode"` into
218
+ // renderOpts.charset; no SessionState/click half.
219
+ readonly charset?: Charset;
220
+
221
+ // The legacy display.colorCompatibility knob: the color depth rich-js
222
+ // downsamples output to. Default "truecolor" (current behavior — NOT the
223
+ // legacy "auto" default, which would change rendering for existing users).
224
+ // The type excludes "auto" entirely: the daemon is detached, so env
225
+ // detection would read the wrong terminal — see COLOR_COMPATIBILITIES.
226
+ // [config-only] The daemon resolves `globals.colorCompatibility ??
227
+ // "truecolor"` into renderOpts.colorCompatibility; no SessionState/click half.
228
+ readonly colorCompatibility?: ColorCompatibility;
207
229
  }
208
230
 
209
231
  // [LAW:one-type-per-behavior] One discriminated union covers every source
@@ -4,19 +4,54 @@
4
4
  // add a key to GLOBALS_SCHEMA and Globals; the engine does the rest.
5
5
 
6
6
  import { type Globals } from "../dsl-types.js";
7
- import { STRIP_STYLES } from "../../themes/policy.js";
7
+ import {
8
+ CHARSETS,
9
+ COLOR_COMPATIBILITIES,
10
+ STRIP_STYLES,
11
+ type ColorCompatibility,
12
+ } from "../../themes/policy.js";
8
13
  import {
9
14
  optionalBooleanSpec,
15
+ optionalEnum,
10
16
  optionalEnumSpec,
11
17
  optionalIntSpec,
12
18
  optionalStringSpec,
13
19
  paletteSpec,
14
20
  record,
15
21
  recordJson,
22
+ type FieldSpec,
16
23
  type JsonNode,
17
24
  type RecordSchema,
18
25
  type ValidateCtx,
19
26
  } from "./validate-core.js";
27
+ import { findKeyLine } from "./diagnostics.js";
28
+
29
+ // [LAW:types-are-the-program] Closed enum like `charset`, plus one
30
+ // migration-pointing rejection: "auto" was the LEGACY default, so migrating
31
+ // configs will carry it — but the daemon runs detached, so rich-js env
32
+ // detection would downsample against the daemon's terminal, not the client's.
33
+ // Rather than ship that silent lie [LAW:no-silent-failure], "auto" is outside
34
+ // the ColorCompatibility domain and gets an error that says why (same species
35
+ // as the removed-layout migration errors in layout.ts). The json emit and the
36
+ // membership check derive from the same COLOR_COMPATIBILITIES literal.
37
+ const colorCompatibilitySpec: FieldSpec<ColorCompatibility> = {
38
+ required: false,
39
+ json: { enum: [...COLOR_COMPATIBILITIES] },
40
+ parse: (ctx, path, field, raw) => {
41
+ if (raw[field] === "auto") {
42
+ ctx.issues.push({
43
+ path: `${path}.${field}`,
44
+ message:
45
+ `${path}.${field}: "auto" is not supported — the render daemon runs detached, ` +
46
+ `so terminal detection would read the daemon's environment, not your terminal's. ` +
47
+ `Pick an explicit depth: ${COLOR_COMPATIBILITIES.join(", ")}`,
48
+ line: findKeyLine(ctx.source, [...path.split("."), field]),
49
+ });
50
+ return undefined;
51
+ }
52
+ return optionalEnum(ctx, path, raw, field, COLOR_COMPATIBILITIES);
53
+ },
54
+ };
20
55
 
21
56
  const GLOBALS_SCHEMA: RecordSchema<Globals> = {
22
57
  noun: "globals key",
@@ -37,6 +72,12 @@ const GLOBALS_SCHEMA: RecordSchema<Globals> = {
37
72
  // [LAW:no-silent-failure] — an absurd value is a loud load error, not a
38
73
  // silently-huge render.
39
74
  padding: optionalIntSpec({ min: 0, max: 16 }),
75
+ // [LAW:types-are-the-program] Closed enum like `style`: the joiner glyph
76
+ // vocabularies pickJoiner can render — validates by membership, emits a
77
+ // JSON-Schema `enum` from the same CHARSETS literal.
78
+ charset: optionalEnumSpec(CHARSETS),
79
+ // Closed enum with a bespoke "auto" rejection — see colorCompatibilitySpec.
80
+ colorCompatibility: colorCompatibilitySpec,
40
81
  },
41
82
  };
42
83
 
@@ -46,10 +46,14 @@ import {
46
46
  } from "../themes/index.js";
47
47
  import {
48
48
  renderStripCells,
49
+ DEFAULT_CHARSET,
50
+ DEFAULT_COLOR_COMPATIBILITY,
49
51
  DEFAULT_PADDING,
50
52
  DEFAULT_TERMINAL_WIDTH,
51
53
  DEFAULT_WRAP,
52
54
  type BuildLineOptions,
55
+ type Charset,
56
+ type ColorCompatibility,
53
57
  } from "../render/strip.js";
54
58
  import { applyClaudeCodeReserve } from "../utils/terminal-width.js";
55
59
  import type { RichText } from "@promptctl/rich-js";
@@ -780,6 +784,19 @@ async function handleRequest(req: Request): Promise<HandledRequest> {
780
784
  // reserve) derives from. [LAW:one-source-of-truth]
781
785
  renderOpts.padding =
782
786
  entry.state.config.globals.padding ?? DEFAULT_PADDING;
787
+ // [config-only] globals.charset, same shape as autoWrap/padding: the
788
+ // config global over the base floor is the whole resolution — the ONE
789
+ // home of the glyph-vocabulary choice pickJoiner derives from.
790
+ // [LAW:one-source-of-truth]
791
+ renderOpts.charset =
792
+ entry.state.config.globals.charset ?? DEFAULT_CHARSET;
793
+ // [config-only] globals.colorCompatibility, same shape as charset:
794
+ // the config global over the base floor is the whole resolution —
795
+ // the ONE home of the color-depth choice rich-js downsamples to.
796
+ // [LAW:one-source-of-truth]
797
+ renderOpts.colorCompatibility =
798
+ entry.state.config.globals.colorCompatibility ??
799
+ DEFAULT_COLOR_COMPATIBILITY;
783
800
  // [LAW:single-enforcer] renderDsl internally calls
784
801
  // `registry.applyInput(payload)` as its first step (see step 1 in
785
802
  // src/dsl/render.ts). The daemon must not pre-apply — doing so
@@ -884,7 +901,12 @@ async function handleRequest(req: Request): Promise<HandledRequest> {
884
901
  compiled: dbgEntry.compiled,
885
902
  lastRenderBySegment:
886
903
  req.what === "segments"
887
- ? serializeSegmentCells(dbgEntry.lastRenderCellsBySegment)
904
+ ? serializeSegmentCells(
905
+ dbgEntry.lastRenderCellsBySegment,
906
+ dbgEntry.config.globals.charset ?? DEFAULT_CHARSET,
907
+ dbgEntry.config.globals.colorCompatibility ??
908
+ DEFAULT_COLOR_COMPATIBILITY,
909
+ )
888
910
  : EMPTY_RENDER_MAP,
889
911
  };
890
912
  return stay({ ok: true, debug: buildDebugSnapshot(req.what, dbgState) });
@@ -1069,9 +1091,10 @@ const verbCtx = { sessionState, dlog };
1069
1091
  // is rendered standalone (wrap doesn't apply to a one-segment projection).
1070
1092
  const RENDER_OPTS_BASE = {
1071
1093
  style: "powerline" as const,
1072
- colorCompatibility: "truecolor" as const,
1094
+ colorCompatibility: DEFAULT_COLOR_COMPATIBILITY,
1073
1095
  wrap: DEFAULT_WRAP,
1074
1096
  padding: DEFAULT_PADDING,
1097
+ charset: DEFAULT_CHARSET,
1075
1098
  };
1076
1099
  const DEBUG_RENDER_OPTS: BuildLineOptions = {
1077
1100
  ...RENDER_OPTS_BASE,
@@ -1083,12 +1106,27 @@ const DEBUG_RENDER_OPTS: BuildLineOptions = {
1083
1106
  // the DaemonDslState type requires the field.
1084
1107
  const EMPTY_RENDER_MAP = new Map<string, string>();
1085
1108
 
1109
+ // [LAW:one-source-of-truth] The joiner glyph vocabulary and the color depth
1110
+ // are serialization-time choices, and both are config-only (no SessionState
1111
+ // half) — so the faithful values are fully derivable from the sampled entry's
1112
+ // config, unlike style, whose live session-over-config resolution needs a
1113
+ // session a debug request doesn't carry. The caller threads the
1114
+ // entry-resolved values; this serializer never re-defaults them.
1086
1115
  function serializeSegmentCells(
1087
1116
  cells: ReadonlyMap<string, readonly RichText[]>,
1117
+ charset: Charset,
1118
+ colorCompatibility: ColorCompatibility,
1088
1119
  ): Map<string, string> {
1089
1120
  const out = new Map<string, string>();
1090
1121
  for (const [name, segCells] of cells) {
1091
- out.set(name, renderStripCells(segCells, DEBUG_RENDER_OPTS));
1122
+ out.set(
1123
+ name,
1124
+ renderStripCells(segCells, {
1125
+ ...DEBUG_RENDER_OPTS,
1126
+ charset,
1127
+ colorCompatibility,
1128
+ }),
1129
+ );
1092
1130
  }
1093
1131
  return out;
1094
1132
  }
package/src/demo/dsl.ts CHANGED
@@ -32,6 +32,8 @@ import { listResolvablePaletteNames } from "../themes/policy.js";
32
32
  import { effectiveThemeName, resolverForThemeName } from "../themes/index.js";
33
33
  import { registerDslConfig, renderDsl } from "../dsl/render.js";
34
34
  import {
35
+ DEFAULT_CHARSET,
36
+ DEFAULT_COLOR_COMPATIBILITY,
35
37
  DEFAULT_PADDING,
36
38
  DEFAULT_TERMINAL_WIDTH,
37
39
  DEFAULT_WRAP,
@@ -107,7 +109,10 @@ try {
107
109
  basePalette,
108
110
  {
109
111
  style: "powerline",
110
- colorCompatibility: "truecolor",
112
+ // Same resolution the daemon applies: the config global over the
113
+ // truecolor default floor.
114
+ colorCompatibility:
115
+ config.globals.colorCompatibility ?? DEFAULT_COLOR_COMPATIBILITY,
111
116
  // [LAW:one-source-of-truth] Demo applies the same Claude-Code-UI
112
117
  // reserve the daemon does so demo output matches the bytes a real
113
118
  // statusline would emit at the same terminal width.
@@ -118,6 +123,7 @@ try {
118
123
  // default-on floor.
119
124
  wrap: config.globals.autoWrap ?? DEFAULT_WRAP,
120
125
  padding: config.globals.padding ?? DEFAULT_PADDING,
126
+ charset: config.globals.charset ?? DEFAULT_CHARSET,
121
127
  },
122
128
  );
123
129
  process.stdout.write(` ${line}\n`);
@@ -8,9 +8,14 @@ import {
8
8
  FlexStrip,
9
9
  renderToString,
10
10
  type Joiner,
11
- type ColorSystemSpec,
11
+ type PowerlineJoinerOptions,
12
+ type CapsuleJoinerOptions,
12
13
  } from "@promptctl/rich-js";
13
- import type { StripStyle } from "../themes/policy.js";
14
+ import type {
15
+ Charset,
16
+ ColorCompatibility,
17
+ StripStyle,
18
+ } from "../themes/policy.js";
14
19
 
15
20
  export interface RenderedSegmentLike {
16
21
  type: string;
@@ -19,11 +24,11 @@ export interface RenderedSegmentLike {
19
24
  fgHex?: string;
20
25
  }
21
26
 
22
- // [LAW:one-source-of-truth] `StripStyle` and its value list live in
23
- // themes/policy.ts (the style-identifier policy module, importable by the
27
+ // [LAW:one-source-of-truth] `StripStyle`/`Charset` and their value lists live
28
+ // in themes/policy.ts (the render-identifier policy module, importable by the
24
29
  // option-source machinery without a render→template-engine cycle). Re-exported
25
- // here so render-layer consumers can keep importing it from the strip module.
26
- export type { StripStyle };
30
+ // here so render-layer consumers can keep importing them from the strip module.
31
+ export type { Charset, ColorCompatibility, StripStyle };
27
32
 
28
33
  // [LAW:one-source-of-truth] Raw terminal cols we assume when the wire
29
34
  // didn't give us one (older client, env-stripped spawn). RAW — not
@@ -43,9 +48,27 @@ export const DEFAULT_WRAP = true;
43
48
  // (`globals.padding ?? DEFAULT_PADDING`) derives from this constant.
44
49
  export const DEFAULT_PADDING = 1;
45
50
 
51
+ // [LAW:one-source-of-truth] The one statement of the globals.charset default
52
+ // (powerline unicode glyphs — current behavior, matching the legacy
53
+ // display.charset). Every resolver of the config global
54
+ // (`globals.charset ?? DEFAULT_CHARSET`) derives from this constant.
55
+ export const DEFAULT_CHARSET: Charset = "unicode";
56
+
57
+ // [LAW:one-source-of-truth] The one statement of the globals.colorCompatibility
58
+ // default (truecolor — the CURRENT pinned value, deliberately NOT the legacy
59
+ // "auto" default, which would change rendering for existing users). Every
60
+ // resolver of the config global (`globals.colorCompatibility ??
61
+ // DEFAULT_COLOR_COMPATIBILITY`) derives from this constant.
62
+ export const DEFAULT_COLOR_COMPATIBILITY: ColorCompatibility = "truecolor";
63
+
46
64
  export interface BuildLineOptions {
47
65
  style: StripStyle;
48
- colorCompatibility: ColorSystemSpec;
66
+ // [LAW:types-are-the-program] Narrower than rich-js ColorSystemSpec on
67
+ // purpose: the four explicit depths only. "auto"/null never reach a render —
68
+ // the daemon is detached, so env detection would read the wrong terminal;
69
+ // the loader rejects "auto" at the trust boundary (see COLOR_COMPATIBILITIES
70
+ // in themes/policy.ts), and every construction site states a resolved depth.
71
+ colorCompatibility: ColorCompatibility;
49
72
  separator?: string;
50
73
  // [LAW:types-are-the-program] Every render carries a width. Required (not
51
74
  // optional) so callers cannot silently drop the wire's value.
@@ -65,20 +88,51 @@ export interface BuildLineOptions {
65
88
  // site states the resolved value; the cell builders derive from it and
66
89
  // never re-default.
67
90
  padding: number;
91
+ // [LAW:one-source-of-truth] Which glyph vocabulary the joiners render with
92
+ // (globals.charset, default "unicode" — the legacy display.charset).
93
+ // Required for the same reason as padding: the resolved value reaches every
94
+ // render explicitly; pickJoiner derives from it and never re-defaults.
95
+ charset: Charset;
68
96
  }
69
97
 
70
- function pickJoiner(style: StripStyle, separator?: string): Joiner {
98
+ // [LAW:dataflow-not-control-flow] Charset variability lives in these VALUES,
99
+ // not in branches: per style, each charset names the joiner-construction
100
+ // options. The unicode entries are empty on purpose — rich-js owns its
101
+ // canonical powerline glyphs (U+E0B0 / U+E0B6+U+E0B4) and restating them here
102
+ // would be a second source that could drift [LAW:one-source-of-truth]. The
103
+ // ascii glyphs are DELIBERATELY single-column (same display width as the
104
+ // unicode caps) so stripChromeCols stays charset-invariant; the
105
+ // measured-chrome pin in test/picker-pagination.test.ts checks that for every
106
+ // style × charset. Widen a glyph and that pin fails loudly.
107
+ const POWERLINE_GLYPHS: Record<Charset, PowerlineJoinerOptions> = {
108
+ unicode: {},
109
+ ascii: { glyph: ">" },
110
+ };
111
+ const CAPSULE_GLYPHS: Record<Charset, CapsuleJoinerOptions> = {
112
+ unicode: {},
113
+ ascii: { left: "(", right: ")" },
114
+ };
115
+
116
+ function pickJoiner(
117
+ style: StripStyle,
118
+ charset: Charset,
119
+ separator?: string,
120
+ ): Joiner {
71
121
  // [LAW:dataflow-not-control-flow] joiner choice is data-driven; one arm per
72
- // shape. [LAW:types-are-the-program] Total over StripStyle the `never`
122
+ // shape. Style picks the joiner CLASS, charset indexes the glyph options fed
123
+ // to it — the two dimensions stay orthogonal (any style renders under either
124
+ // charset). Plain takes no charset lookup: its separator is already user
125
+ // data (globals.default_separator) and its default (" | ") is ASCII-safe.
126
+ // [LAW:types-are-the-program] Total over StripStyle — the `never`
73
127
  // default makes adding a STRIP_STYLES member a compile error here until it
74
128
  // gets a joiner, so the picker's domain can never offer an unrenderable shape.
75
129
  switch (style) {
76
130
  case "capsule":
77
- return new CapsuleJoiner();
131
+ return new CapsuleJoiner(CAPSULE_GLYPHS[charset]);
78
132
  case "plain":
79
133
  return new PlainJoiner(separator !== undefined ? { separator } : {});
80
134
  case "powerline":
81
- return new PowerlineJoiner();
135
+ return new PowerlineJoiner(POWERLINE_GLYPHS[charset]);
82
136
  default: {
83
137
  const _exhaustive: never = style;
84
138
  return _exhaustive;
@@ -96,10 +150,13 @@ function pickJoiner(style: StripStyle, separator?: string): Joiner {
96
150
  // [LAW:dataflow-not-control-flow] / [LAW:types-are-the-program] Total over
97
151
  // StripStyle — the `never` default makes adding a STRIP_STYLES member a compile
98
152
  // error here until its chrome is declared, the same guard pickJoiner carries. The
99
- // numbers are the default cap glyphs pickJoiner constructs: powerline appends ONE
100
- // trailing separator (U+E0B0, 1 col); capsule brackets BOTH edges (U+E0B6 +
101
- // U+E0B4, 2 cols); plain has no caps. test/picker-pagination.test.ts measures the
102
- // real rendered chrome against these so the declaration cannot drift from rich-js.
153
+ // numbers are the cap glyphs pickJoiner constructs: powerline appends ONE
154
+ // trailing separator (1 col); capsule brackets BOTH edges (2 cols); plain has no
155
+ // caps. Charset does NOT change these both glyph vocabularies use
156
+ // single-column caps by construction (see the glyph tables above), which is why
157
+ // this stays total over StripStyle alone. test/picker-pagination.test.ts
158
+ // measures the real rendered chrome against these for every style × charset so
159
+ // the declaration cannot drift from rich-js or from the ascii glyph choice.
103
160
  export function stripChromeCols(style: StripStyle): number {
104
161
  switch (style) {
105
162
  case "powerline":
@@ -144,7 +201,7 @@ export function renderStripCells(
144
201
  options: BuildLineOptions,
145
202
  ): string {
146
203
  if (cells.length === 0) return "";
147
- const joiner = pickJoiner(options.style, options.separator);
204
+ const joiner = pickJoiner(options.style, options.charset, options.separator);
148
205
  if (options.wrap && Number.isFinite(options.width)) {
149
206
  const flex = new FlexStrip([...cells], { joiner });
150
207
  const out = renderToString(flex, {
@@ -5,7 +5,7 @@
5
5
  // PaletteResolver, no ColorRgba, no hex. The semantic/anchor knowledge
6
6
  // (which tokens keep their hue) stays in rich-js (ANCHORED_ROOTS), not here.
7
7
 
8
- import { listThemePalettes } from "@promptctl/rich-js";
8
+ import { listThemePalettes, type ColorSystemSpec } from "@promptctl/rich-js";
9
9
 
10
10
  // --- Theme name aliasing ---
11
11
 
@@ -79,3 +79,45 @@ export function effectiveStripStyle(
79
79
  const chosen = sessionStyle ?? globalsStyle ?? "powerline";
80
80
  return isStripStyle(chosen) ? chosen : "powerline";
81
81
  }
82
+
83
+ // --- Joiner charset identifiers ---
84
+
85
+ // [LAW:one-source-of-truth][LAW:types-are-the-program] The single canonical set
86
+ // of glyph vocabularies the strip joiners can render with (the legacy
87
+ // display.charset). Same species as STRIP_STYLES — a closed render-vocabulary
88
+ // enum hosted in this leaf policy module so the config loader (validation +
89
+ // JSON-schema emit) and the render layer (glyph dispatch) both derive from one
90
+ // literal without a config↔render cycle [LAW:one-way-deps]. "ascii" swaps the
91
+ // powerline-private-use cap glyphs (U+E0Bx — tofu without a Nerd Font) for
92
+ // plain-ASCII equivalents; it is orthogonal to StripStyle: style picks the
93
+ // joiner SHAPE, charset picks the glyph VALUES fed to it.
94
+ // [config-only] Unlike STRIP_STYLES there is no SessionState/click half, so no
95
+ // narrowing guard or effective* resolver — the config global over the default
96
+ // is the whole resolution.
97
+ export const CHARSETS = ["unicode", "ascii"] as const;
98
+ export type Charset = (typeof CHARSETS)[number];
99
+
100
+ // --- Color-depth identifiers ---
101
+
102
+ // [LAW:one-source-of-truth][LAW:types-are-the-program] The single canonical set
103
+ // of color depths a config can pin (the legacy display.colorCompatibility).
104
+ // Same species as CHARSETS: a closed render-vocabulary enum hosted in this leaf
105
+ // policy module so the config loader (validation + JSON-schema emit) and the
106
+ // render layer both derive from one literal without a config↔render cycle
107
+ // [LAW:one-way-deps]. `satisfies` ties every member to rich-js's
108
+ // ColorSystemSpec at compile time WITHOUT widening the derived union — if
109
+ // rich-js renames a depth, this literal fails to compile rather than drifting.
110
+ //
111
+ // Deliberately NARROWER than ColorSystemSpec: "auto" (and null) are excluded.
112
+ // The daemon is long-lived and detached, so its process env is NOT the client
113
+ // terminal's — rich-js env detection would silently downsample against the
114
+ // wrong terminal [LAW:no-silent-failure]. Honoring "auto" needs a client
115
+ // capability hint over the wire (the termCols pattern); until that lands, the
116
+ // loader rejects "auto" with a pointer instead of shipping a lie.
117
+ export const COLOR_COMPATIBILITIES = [
118
+ "truecolor",
119
+ "256",
120
+ "ansi",
121
+ "none",
122
+ ] as const satisfies readonly ColorSystemSpec[];
123
+ export type ColorCompatibility = (typeof COLOR_COMPATIBILITIES)[number];