@runa-cmd/help 0.1.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.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +279 -0
  3. package/dist/ansi/codes.d.ts +33 -0
  4. package/dist/ansi/codes.d.ts.map +1 -0
  5. package/dist/ansi/codes.js +54 -0
  6. package/dist/ansi/codes.js.map +1 -0
  7. package/dist/ansi/detect.d.ts +36 -0
  8. package/dist/ansi/detect.d.ts.map +1 -0
  9. package/dist/ansi/detect.js +82 -0
  10. package/dist/ansi/detect.js.map +1 -0
  11. package/dist/ansi/downsample.d.ts +22 -0
  12. package/dist/ansi/downsample.d.ts.map +1 -0
  13. package/dist/ansi/downsample.js +126 -0
  14. package/dist/ansi/downsample.js.map +1 -0
  15. package/dist/index.d.ts +26 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +23 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/plugin.d.ts +18 -0
  20. package/dist/plugin.d.ts.map +1 -0
  21. package/dist/plugin.js +63 -0
  22. package/dist/plugin.js.map +1 -0
  23. package/dist/primitives/box.d.ts +23 -0
  24. package/dist/primitives/box.d.ts.map +1 -0
  25. package/dist/primitives/box.js +76 -0
  26. package/dist/primitives/box.js.map +1 -0
  27. package/dist/primitives/columns.d.ts +15 -0
  28. package/dist/primitives/columns.d.ts.map +1 -0
  29. package/dist/primitives/columns.js +37 -0
  30. package/dist/primitives/columns.js.map +1 -0
  31. package/dist/primitives/join.d.ts +32 -0
  32. package/dist/primitives/join.d.ts.map +1 -0
  33. package/dist/primitives/join.js +89 -0
  34. package/dist/primitives/join.js.map +1 -0
  35. package/dist/primitives/list.d.ts +22 -0
  36. package/dist/primitives/list.d.ts.map +1 -0
  37. package/dist/primitives/list.js +25 -0
  38. package/dist/primitives/list.js.map +1 -0
  39. package/dist/primitives/styled.d.ts +28 -0
  40. package/dist/primitives/styled.d.ts.map +1 -0
  41. package/dist/primitives/styled.js +59 -0
  42. package/dist/primitives/styled.js.map +1 -0
  43. package/dist/primitives/table.d.ts +19 -0
  44. package/dist/primitives/table.d.ts.map +1 -0
  45. package/dist/primitives/table.js +50 -0
  46. package/dist/primitives/table.js.map +1 -0
  47. package/dist/primitives/wrap.d.ts +10 -0
  48. package/dist/primitives/wrap.d.ts.map +1 -0
  49. package/dist/primitives/wrap.js +69 -0
  50. package/dist/primitives/wrap.js.map +1 -0
  51. package/dist/render.d.ts +8 -0
  52. package/dist/render.d.ts.map +1 -0
  53. package/dist/render.js +219 -0
  54. package/dist/render.js.map +1 -0
  55. package/dist/theme.d.ts +12 -0
  56. package/dist/theme.d.ts.map +1 -0
  57. package/dist/theme.js +45 -0
  58. package/dist/theme.js.map +1 -0
  59. package/dist/types.d.ts +60 -0
  60. package/dist/types.d.ts.map +1 -0
  61. package/dist/types.js +2 -0
  62. package/dist/types.js.map +1 -0
  63. package/package.json +63 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Runa CLI Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,279 @@
1
+ # @runa-cmd/help
2
+
3
+ Themeable help output for Runa CLIs with composable layout primitives.
4
+
5
+ Three layers of customization: zero-config defaults, theme colors, or build your own layout with primitives.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @runa-cmd/help
11
+ ```
12
+
13
+ > Peer dependencies: `@runa-cmd/core` and `zod ^4.0.0`.
14
+
15
+ ## Quick Start
16
+
17
+ ```ts
18
+ import { defineCLI } from '@runa-cmd/core';
19
+ import { helpPlugin } from '@runa-cmd/help';
20
+
21
+ const cli = defineCLI({
22
+ meta: { name: 'my-tool', version: '1.0.0', description: 'My CLI tool' },
23
+ commands: { deploy, init },
24
+ plugins: [helpPlugin()],
25
+ });
26
+
27
+ cli.run();
28
+ ```
29
+
30
+ ```
31
+ $ my-tool --help
32
+ $ my-tool deploy --help
33
+ ```
34
+
35
+ The plugin adds a `--help` / `-h` global flag. When triggered, it renders formatted help and exits.
36
+
37
+ ## Three Layers
38
+
39
+ ### Layer 1: Zero Config
40
+
41
+ Just add the plugin — sensible defaults with auto-detected terminal colors:
42
+
43
+ ```ts
44
+ plugins: [helpPlugin()]
45
+ ```
46
+
47
+ ### Layer 2: Custom Theme
48
+
49
+ Override any of the 6 color slots:
50
+
51
+ ```ts
52
+ plugins: [
53
+ helpPlugin({
54
+ theme: {
55
+ primary: '#00BFFF', // Headings, command names
56
+ secondary: '#FF6B6B', // Flags, options
57
+ muted: 'dim', // Descriptions, defaults
58
+ error: '#FF0000', // Error messages
59
+ warning: '#FFAA00', // Deprecation notices
60
+ success: '#00FF00', // Success indicators
61
+ },
62
+ }),
63
+ ]
64
+ ```
65
+
66
+ Colors are automatically downsampled to match terminal capabilities: truecolor, 256-color, 16-color, or no color (`NO_COLOR` respected).
67
+
68
+ ### Layer 3: Custom Renderer
69
+
70
+ Full control — use the layout primitives to build your own help output:
71
+
72
+ ```ts
73
+ import { helpPlugin, box, styled, table, join, bold, fg } from '@runa-cmd/help';
74
+
75
+ plugins: [
76
+ helpPlugin({
77
+ render({ schema, theme, termWidth }) {
78
+ return join.vertical([
79
+ box({
80
+ content: `${bold(schema.meta.name)} v${schema.meta.version}`,
81
+ border: 'rounded',
82
+ borderColor: theme.primary,
83
+ }),
84
+ styled(schema.meta.description ?? '', { color: theme.muted }),
85
+ table(
86
+ schema.commands.map((cmd) => [
87
+ styled(cmd.name, { bold: true, color: theme.primary }),
88
+ styled(cmd.description, { color: theme.muted }),
89
+ ]),
90
+ { headers: ['Command', 'Description'] },
91
+ ),
92
+ ]);
93
+ },
94
+ }),
95
+ ]
96
+ ```
97
+
98
+ ## Layout Primitives
99
+
100
+ All primitives return a `Block` (a plain string, possibly with ANSI codes). They're composable — the output of one can be the input to another.
101
+
102
+ ### `box(options)`
103
+
104
+ Renders a bordered container with word wrapping.
105
+
106
+ ```ts
107
+ import { box } from '@runa-cmd/help';
108
+
109
+ box({
110
+ content: 'Hello from a box!',
111
+ border: 'rounded', // 'single' | 'double' | 'rounded' | 'none'
112
+ borderColor: '#7D56F4', // Hex or named ANSI color
113
+ padding: [1, 2], // [vertical, horizontal] or [top, right, bottom, left]
114
+ width: 40, // Fixed width (auto if omitted)
115
+ });
116
+ ```
117
+
118
+ ### `styled(text, options)`
119
+
120
+ Applies text formatting. Does not word-wrap.
121
+
122
+ ```ts
123
+ import { styled } from '@runa-cmd/help';
124
+
125
+ styled('Important!', {
126
+ bold: true,
127
+ italic: true,
128
+ color: '#FF6B6B',
129
+ padding: [0, 1],
130
+ });
131
+ ```
132
+
133
+ ### `columns(blocks, options)`
134
+
135
+ Distributes blocks evenly across terminal width:
136
+
137
+ ```ts
138
+ import { columns, styled } from '@runa-cmd/help';
139
+
140
+ columns([
141
+ styled('Left column'),
142
+ styled('Right column'),
143
+ ], { gap: 4 });
144
+ ```
145
+
146
+ ### `table(rows, options)`
147
+
148
+ Renders a 2D array as aligned columns with automatic width sizing:
149
+
150
+ ```ts
151
+ import { table, styled } from '@runa-cmd/help';
152
+
153
+ table([
154
+ [styled('--verbose', { bold: true }), 'Enable verbose output'],
155
+ [styled('--env', { bold: true }), 'Target environment'],
156
+ ], {
157
+ padding: [0, 2],
158
+ headers: ['Flag', 'Description'],
159
+ });
160
+ ```
161
+
162
+ ### `list(items, options)`
163
+
164
+ Renders a bulleted list:
165
+
166
+ ```ts
167
+ import { list } from '@runa-cmd/help';
168
+
169
+ list(['First item', 'Second item', 'Third item'], {
170
+ bullet: '>', // Default: '•'
171
+ indent: 4, // Default: 2
172
+ });
173
+ ```
174
+
175
+ ### `join.vertical(blocks, options)` / `join.horizontal(blocks, options)`
176
+
177
+ Composes blocks together:
178
+
179
+ ```ts
180
+ import { join, box, styled } from '@runa-cmd/help';
181
+
182
+ // Stack vertically with gap
183
+ join.vertical([
184
+ styled('Header', { bold: true }),
185
+ styled('Body text'),
186
+ styled('Footer', { dim: true }),
187
+ ], { gap: 1 });
188
+
189
+ // Place side by side
190
+ join.horizontal([
191
+ box({ content: 'Left', border: 'single' }),
192
+ box({ content: 'Right', border: 'single' }),
193
+ ], { gap: 2, align: 'top' }); // 'top' | 'center' | 'bottom'
194
+ ```
195
+
196
+ ## ANSI Utilities
197
+
198
+ Low-level text formatting functions:
199
+
200
+ ```ts
201
+ import {
202
+ bold, dim, italic, underline, // Text decorators
203
+ fg, reset, strip, // Color and cleanup
204
+ visibleWidth, // Width without ANSI codes
205
+ downsampleColor, // Hex → terminal-appropriate ANSI
206
+ detectColorDepth, // Terminal color capability
207
+ detectTermWidth, // Terminal width (default 80)
208
+ getRenderContext, // Lazy-detected { colorDepth, termWidth }
209
+ resetRenderContext, // Clear cached detection (for tests)
210
+ } from '@runa-cmd/help';
211
+ ```
212
+
213
+ ### Color Downsampling
214
+
215
+ Colors automatically adapt to terminal capabilities:
216
+
217
+ ```ts
218
+ import { downsampleColor } from '@runa-cmd/help';
219
+
220
+ downsampleColor('#7D56F4', 'truecolor'); // '\x1b[38;2;125;86;244m'
221
+ downsampleColor('#7D56F4', '256'); // '\x1b[38;5;99m'
222
+ downsampleColor('#7D56F4', '16'); // '\x1b[35m' (magenta)
223
+ downsampleColor('#7D56F4', 'none'); // '' (no color)
224
+ ```
225
+
226
+ Respects `NO_COLOR`, `FORCE_COLOR`, and `COLORTERM` environment variables.
227
+
228
+ ## Theme
229
+
230
+ The default theme:
231
+
232
+ ```ts
233
+ import { defaultTheme, resolveTheme } from '@runa-cmd/help';
234
+
235
+ // defaultTheme = {
236
+ // primary: '#7D56F4', // Lipgloss purple
237
+ // secondary: '#FF6B6B',
238
+ // muted: 'dim',
239
+ // error: '#FF0000',
240
+ // warning: '#FFAA00',
241
+ // success: '#00FF00',
242
+ // }
243
+
244
+ // Resolve a partial override to ANSI-ready strings:
245
+ const resolved = resolveTheme({ primary: '#00BFFF' }, getRenderContext());
246
+ // resolved.primary = '\x1b[38;2;0;191;255m' (on truecolor terminals)
247
+ ```
248
+
249
+ ## Default Renderer
250
+
251
+ The built-in renderer (`defaultRenderer`) produces output with these sections (each skipped if empty):
252
+
253
+ 1. **Header** — Name, version, description
254
+ 2. **Usage** — `name [command] [options]`
255
+ 3. **Arguments** — Positional args table
256
+ 4. **Options** — Named options table, grouped by `group` metadata
257
+ 5. **Commands** — Subcommands table
258
+
259
+ The renderer is subcommand-aware: `my-tool deploy --help` shows help for the `deploy` command specifically.
260
+
261
+ ## Types
262
+
263
+ ```ts
264
+ import type {
265
+ HelpPluginOptions,
266
+ RenderFnCtx,
267
+ Theme, ResolvedTheme,
268
+ Block,
269
+ RenderContext, ColorDepth,
270
+ BoxOptions, StyledOptions, ColumnsOptions,
271
+ TableOptions, ListOptions,
272
+ VerticalJoinOptions, HorizontalJoinOptions, HorizontalAlign,
273
+ BorderStyle,
274
+ } from '@runa-cmd/help';
275
+ ```
276
+
277
+ ## License
278
+
279
+ MIT
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @runa-cmd/help — ANSI escape code builders
3
+ *
4
+ * Low-level helpers for applying text styles via ANSI escape sequences.
5
+ * These produce raw escape strings — color downsampling is handled by downsample.ts.
6
+ */
7
+ /** ANSI reset — clears all styles */
8
+ export declare const reset = "\u001B[0m";
9
+ /** Apply bold style */
10
+ export declare function bold(text: string): string;
11
+ /** Apply dim style */
12
+ export declare function dim(text: string): string;
13
+ /** Apply italic style */
14
+ export declare function italic(text: string): string;
15
+ /** Apply underline style */
16
+ export declare function underline(text: string): string;
17
+ /**
18
+ * Apply foreground color.
19
+ * Accepts a pre-computed ANSI escape string (from downsample.ts).
20
+ * If empty string (no color mode), returns text unchanged.
21
+ */
22
+ export declare function fg(text: string, ansiColor: string): string;
23
+ /**
24
+ * Strip all ANSI escape codes from a string.
25
+ * Useful for raw mode testing and width calculations.
26
+ */
27
+ export declare function strip(text: string): string;
28
+ /**
29
+ * Get the visible width of a string (excluding ANSI codes).
30
+ * Note: does NOT handle Unicode wide characters (CJK, emoji) — deferred to future package.
31
+ */
32
+ export declare function visibleWidth(text: string): number;
33
+ //# sourceMappingURL=codes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codes.d.ts","sourceRoot":"","sources":["../../src/ansi/codes.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,qCAAqC;AACrC,eAAO,MAAM,KAAK,cAAa,CAAC;AAEhC,uBAAuB;AACvB,wBAAgB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEzC;AAED,sBAAsB;AACtB,wBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAExC;AAED,yBAAyB;AACzB,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE3C;AAED,4BAA4B;AAC5B,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED;;;;GAIG;AACH,wBAAgB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAG1D;AAQD;;;GAGG;AACH,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1C;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEjD"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @runa-cmd/help — ANSI escape code builders
3
+ *
4
+ * Low-level helpers for applying text styles via ANSI escape sequences.
5
+ * These produce raw escape strings — color downsampling is handled by downsample.ts.
6
+ */
7
+ const ESC = '\x1b[';
8
+ /** ANSI reset — clears all styles */
9
+ export const reset = `${ESC}0m`;
10
+ /** Apply bold style */
11
+ export function bold(text) {
12
+ return `${ESC}1m${text}${reset}`;
13
+ }
14
+ /** Apply dim style */
15
+ export function dim(text) {
16
+ return `${ESC}2m${text}${reset}`;
17
+ }
18
+ /** Apply italic style */
19
+ export function italic(text) {
20
+ return `${ESC}3m${text}${reset}`;
21
+ }
22
+ /** Apply underline style */
23
+ export function underline(text) {
24
+ return `${ESC}4m${text}${reset}`;
25
+ }
26
+ /**
27
+ * Apply foreground color.
28
+ * Accepts a pre-computed ANSI escape string (from downsample.ts).
29
+ * If empty string (no color mode), returns text unchanged.
30
+ */
31
+ export function fg(text, ansiColor) {
32
+ if (!ansiColor)
33
+ return text;
34
+ return `${ansiColor}${text}${reset}`;
35
+ }
36
+ // ─── ANSI stripping regex ───────────────────────────────────
37
+ // Matches all ANSI escape sequences (CSI sequences + OSC sequences)
38
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape detection requires control characters
39
+ const ANSI_RE = /\x1b\[[0-9;]*m/g;
40
+ /**
41
+ * Strip all ANSI escape codes from a string.
42
+ * Useful for raw mode testing and width calculations.
43
+ */
44
+ export function strip(text) {
45
+ return text.replace(ANSI_RE, '');
46
+ }
47
+ /**
48
+ * Get the visible width of a string (excluding ANSI codes).
49
+ * Note: does NOT handle Unicode wide characters (CJK, emoji) — deferred to future package.
50
+ */
51
+ export function visibleWidth(text) {
52
+ return strip(text).length;
53
+ }
54
+ //# sourceMappingURL=codes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codes.js","sourceRoot":"","sources":["../../src/ansi/codes.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,GAAG,GAAG,OAAO,CAAC;AAEpB,qCAAqC;AACrC,MAAM,CAAC,MAAM,KAAK,GAAG,GAAG,GAAG,IAAI,CAAC;AAEhC,uBAAuB;AACvB,MAAM,UAAU,IAAI,CAAC,IAAY;IAChC,OAAO,GAAG,GAAG,KAAK,IAAI,GAAG,KAAK,EAAE,CAAC;AAClC,CAAC;AAED,sBAAsB;AACtB,MAAM,UAAU,GAAG,CAAC,IAAY;IAC/B,OAAO,GAAG,GAAG,KAAK,IAAI,GAAG,KAAK,EAAE,CAAC;AAClC,CAAC;AAED,yBAAyB;AACzB,MAAM,UAAU,MAAM,CAAC,IAAY;IAClC,OAAO,GAAG,GAAG,KAAK,IAAI,GAAG,KAAK,EAAE,CAAC;AAClC,CAAC;AAED,4BAA4B;AAC5B,MAAM,UAAU,SAAS,CAAC,IAAY;IACrC,OAAO,GAAG,GAAG,KAAK,IAAI,GAAG,KAAK,EAAE,CAAC;AAClC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,EAAE,CAAC,IAAY,EAAE,SAAiB;IACjD,IAAI,CAAC,SAAS;QAAE,OAAO,IAAI,CAAC;IAC5B,OAAO,GAAG,SAAS,GAAG,IAAI,GAAG,KAAK,EAAE,CAAC;AACtC,CAAC;AAED,+DAA+D;AAE/D,oEAAoE;AACpE,6GAA6G;AAC7G,MAAM,OAAO,GAAG,iBAAiB,CAAC;AAElC;;;GAGG;AACH,MAAM,UAAU,KAAK,CAAC,IAAY;IACjC,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;AAClC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY;IACxC,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC;AAC3B,CAAC"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @runa-cmd/help — Terminal detection
3
+ *
4
+ * Detects terminal color depth and width from environment variables
5
+ * and process.stdout capabilities.
6
+ */
7
+ import type { ColorDepth, RenderContext } from '../types.js';
8
+ /**
9
+ * Detect terminal color depth from env vars and stdout capabilities.
10
+ *
11
+ * Priority order:
12
+ * 1. NO_COLOR → 'none'
13
+ * 2. FORCE_COLOR → minimum '16'
14
+ * 3. COLORTERM=truecolor|24bit → 'truecolor'
15
+ * 4. TERM=xterm-256color → '256'
16
+ * 5. process.stdout.hasColors(256) → '256'
17
+ * 6. process.stdout.hasColors(16) → '16'
18
+ * 7. fallback → 'none'
19
+ */
20
+ export declare function detectColorDepth(): ColorDepth;
21
+ /**
22
+ * Detect terminal width in columns.
23
+ * Falls back to 80 if unavailable (e.g. piped output).
24
+ */
25
+ export declare function detectTermWidth(): number;
26
+ /**
27
+ * Get the current RenderContext (lazy singleton — auto-detected once).
28
+ * All primitives use this internally when no explicit ctx is passed.
29
+ */
30
+ export declare function getRenderContext(): RenderContext;
31
+ /**
32
+ * Reset the cached RenderContext.
33
+ * Used in tests to force re-detection.
34
+ */
35
+ export declare function resetRenderContext(): void;
36
+ //# sourceMappingURL=detect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detect.d.ts","sourceRoot":"","sources":["../../src/ansi/detect.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE7D;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,IAAI,UAAU,CAyC7C;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAMD;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,aAAa,CAQhD;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Detect terminal color depth from env vars and stdout capabilities.
3
+ *
4
+ * Priority order:
5
+ * 1. NO_COLOR → 'none'
6
+ * 2. FORCE_COLOR → minimum '16'
7
+ * 3. COLORTERM=truecolor|24bit → 'truecolor'
8
+ * 4. TERM=xterm-256color → '256'
9
+ * 5. process.stdout.hasColors(256) → '256'
10
+ * 6. process.stdout.hasColors(16) → '16'
11
+ * 7. fallback → 'none'
12
+ */
13
+ export function detectColorDepth() {
14
+ const env = process.env;
15
+ // NO_COLOR takes absolute priority (https://no-color.org/)
16
+ if (env.NO_COLOR !== undefined) {
17
+ return 'none';
18
+ }
19
+ // FORCE_COLOR forces color output (e.g. in CI or piped contexts)
20
+ const forceColor = env.FORCE_COLOR;
21
+ if (forceColor !== undefined) {
22
+ if (forceColor === '3')
23
+ return 'truecolor';
24
+ if (forceColor === '2')
25
+ return '256';
26
+ return '16';
27
+ }
28
+ // Non-TTY (piped) → no color unless FORCE_COLOR was set (handled above)
29
+ if (!process.stdout.isTTY) {
30
+ return 'none';
31
+ }
32
+ // COLORTERM for truecolor detection
33
+ const colorTerm = env.COLORTERM;
34
+ if (colorTerm === 'truecolor' || colorTerm === '24bit') {
35
+ return 'truecolor';
36
+ }
37
+ // TERM for 256 color detection
38
+ const term = env.TERM;
39
+ if (term?.includes('256color')) {
40
+ return '256';
41
+ }
42
+ // process.stdout.hasColors() as additional signal
43
+ if (typeof process.stdout.hasColors === 'function') {
44
+ if (process.stdout.hasColors(16_777_216))
45
+ return 'truecolor';
46
+ if (process.stdout.hasColors(256))
47
+ return '256';
48
+ if (process.stdout.hasColors(16))
49
+ return '16';
50
+ }
51
+ return 'none';
52
+ }
53
+ /**
54
+ * Detect terminal width in columns.
55
+ * Falls back to 80 if unavailable (e.g. piped output).
56
+ */
57
+ export function detectTermWidth() {
58
+ return process.stdout.columns ?? 80;
59
+ }
60
+ // ─── Lazy Singleton ─────────────────────────────────────────
61
+ let _cached;
62
+ /**
63
+ * Get the current RenderContext (lazy singleton — auto-detected once).
64
+ * All primitives use this internally when no explicit ctx is passed.
65
+ */
66
+ export function getRenderContext() {
67
+ if (!_cached) {
68
+ _cached = {
69
+ colorDepth: detectColorDepth(),
70
+ termWidth: detectTermWidth(),
71
+ };
72
+ }
73
+ return _cached;
74
+ }
75
+ /**
76
+ * Reset the cached RenderContext.
77
+ * Used in tests to force re-detection.
78
+ */
79
+ export function resetRenderContext() {
80
+ _cached = undefined;
81
+ }
82
+ //# sourceMappingURL=detect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detect.js","sourceRoot":"","sources":["../../src/ansi/detect.ts"],"names":[],"mappings":"AAQA;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,gBAAgB;IAC/B,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC;IAExB,2DAA2D;IAC3D,IAAI,GAAG,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QAChC,OAAO,MAAM,CAAC;IACf,CAAC;IAED,iEAAiE;IACjE,MAAM,UAAU,GAAG,GAAG,CAAC,WAAW,CAAC;IACnC,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;QAC9B,IAAI,UAAU,KAAK,GAAG;YAAE,OAAO,WAAW,CAAC;QAC3C,IAAI,UAAU,KAAK,GAAG;YAAE,OAAO,KAAK,CAAC;QACrC,OAAO,IAAI,CAAC;IACb,CAAC;IAED,wEAAwE;IACxE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QAC3B,OAAO,MAAM,CAAC;IACf,CAAC;IAED,oCAAoC;IACpC,MAAM,SAAS,GAAG,GAAG,CAAC,SAAS,CAAC;IAChC,IAAI,SAAS,KAAK,WAAW,IAAI,SAAS,KAAK,OAAO,EAAE,CAAC;QACxD,OAAO,WAAW,CAAC;IACpB,CAAC;IAED,+BAA+B;IAC/B,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC;IACtB,IAAI,IAAI,EAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;QAChC,OAAO,KAAK,CAAC;IACd,CAAC;IAED,kDAAkD;IAClD,IAAI,OAAO,OAAO,CAAC,MAAM,CAAC,SAAS,KAAK,UAAU,EAAE,CAAC;QACpD,IAAI,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC;YAAE,OAAO,WAAW,CAAC;QAC7D,IAAI,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;QAChD,IAAI,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC;YAAE,OAAO,IAAI,CAAC;IAC/C,CAAC;IAED,OAAO,MAAM,CAAC;AACf,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe;IAC9B,OAAO,OAAO,CAAC,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC;AACrC,CAAC;AAED,+DAA+D;AAE/D,IAAI,OAAkC,CAAC;AAEvC;;;GAGG;AACH,MAAM,UAAU,gBAAgB;IAC/B,IAAI,CAAC,OAAO,EAAE,CAAC;QACd,OAAO,GAAG;YACT,UAAU,EAAE,gBAAgB,EAAE;YAC9B,SAAS,EAAE,eAAe,EAAE;SAC5B,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC;AAChB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB;IACjC,OAAO,GAAG,SAAS,CAAC;AACrB,CAAC"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @runa-cmd/help — Color downsampling
3
+ *
4
+ * Converts hex colors (#RRGGBB) to the appropriate ANSI escape sequence
5
+ * based on the terminal's color depth. Named attributes (e.g. 'dim')
6
+ * pass through unchanged.
7
+ *
8
+ * ANSI 256 palette: 6×6×6 RGB cube (indices 16-231) + 24 grayscale (232-255).
9
+ * Computed programmatically at module load, not hardcoded.
10
+ */
11
+ import type { ColorDepth } from '../types.js';
12
+ /**
13
+ * Downsample a color to an ANSI escape string based on terminal color depth.
14
+ *
15
+ * - Hex colors (#RRGGBB) → converted to nearest ANSI code for the depth
16
+ * - Named attributes ('dim', 'red', etc.) → passed through as ANSI escape
17
+ * - If depth is 'none' → returns '' (empty, no color)
18
+ *
19
+ * Returns the ANSI escape STRING (without reset) — caller wraps text with it.
20
+ */
21
+ export declare function downsampleColor(color: string, depth: ColorDepth): string;
22
+ //# sourceMappingURL=downsample.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"downsample.d.ts","sourceRoot":"","sources":["../../src/ansi/downsample.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AA8G9C;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,MAAM,CAiCxE"}
@@ -0,0 +1,126 @@
1
+ // ─── Hex Parsing ────────────────────────────────────────────
2
+ const HEX_RE = /^#([0-9a-f]{6})$/i;
3
+ function parseHex(hex) {
4
+ const match = HEX_RE.exec(hex);
5
+ if (!match)
6
+ return null;
7
+ const val = Number.parseInt(match[1], 16);
8
+ return [(val >> 16) & 0xff, (val >> 8) & 0xff, val & 0xff];
9
+ }
10
+ const PALETTE_256 = [];
11
+ // Standard 16 colors (indices 0-15)
12
+ const STANDARD_16 = [
13
+ [0, 0, 0], // 0: black
14
+ [128, 0, 0], // 1: red
15
+ [0, 128, 0], // 2: green
16
+ [128, 128, 0], // 3: yellow
17
+ [0, 0, 128], // 4: blue
18
+ [128, 0, 128], // 5: magenta
19
+ [0, 128, 128], // 6: cyan
20
+ [192, 192, 192], // 7: white
21
+ [128, 128, 128], // 8: bright black (gray)
22
+ [255, 0, 0], // 9: bright red
23
+ [0, 255, 0], // 10: bright green
24
+ [255, 255, 0], // 11: bright yellow
25
+ [0, 0, 255], // 12: bright blue
26
+ [255, 0, 255], // 13: bright magenta
27
+ [0, 255, 255], // 14: bright cyan
28
+ [255, 255, 255], // 15: bright white
29
+ ];
30
+ // Build standard 16
31
+ for (let i = 0; i < 16; i++) {
32
+ const [r, g, b] = STANDARD_16[i];
33
+ PALETTE_256.push({ r, g, b, index: i });
34
+ }
35
+ // 6×6×6 RGB cube (indices 16-231)
36
+ const CUBE_STEPS = [0, 95, 135, 175, 215, 255];
37
+ for (let ri = 0; ri < 6; ri++) {
38
+ for (let gi = 0; gi < 6; gi++) {
39
+ for (let bi = 0; bi < 6; bi++) {
40
+ PALETTE_256.push({
41
+ r: CUBE_STEPS[ri],
42
+ g: CUBE_STEPS[gi],
43
+ b: CUBE_STEPS[bi],
44
+ index: 16 + ri * 36 + gi * 6 + bi,
45
+ });
46
+ }
47
+ }
48
+ }
49
+ // Grayscale ramp (indices 232-255)
50
+ for (let i = 0; i < 24; i++) {
51
+ const v = 8 + i * 10;
52
+ PALETTE_256.push({ r: v, g: v, b: v, index: 232 + i });
53
+ }
54
+ // ─── Nearest Color Search ───────────────────────────────────
55
+ function nearestIndex(r, g, b, palette) {
56
+ let bestDist = Number.POSITIVE_INFINITY;
57
+ let bestIndex = 0;
58
+ for (const entry of palette) {
59
+ const dr = r - entry.r;
60
+ const dg = g - entry.g;
61
+ const db = b - entry.b;
62
+ const dist = dr * dr + dg * dg + db * db;
63
+ if (dist < bestDist) {
64
+ bestDist = dist;
65
+ bestIndex = entry.index;
66
+ }
67
+ }
68
+ return bestIndex;
69
+ }
70
+ // ─── Named ANSI Attributes ─────────────────────────────────
71
+ const ESC = '\x1b[';
72
+ const NAMED_ATTRS = {
73
+ bold: `${ESC}1m`,
74
+ dim: `${ESC}2m`,
75
+ italic: `${ESC}3m`,
76
+ underline: `${ESC}4m`,
77
+ red: `${ESC}31m`,
78
+ green: `${ESC}32m`,
79
+ yellow: `${ESC}33m`,
80
+ blue: `${ESC}34m`,
81
+ magenta: `${ESC}35m`,
82
+ cyan: `${ESC}36m`,
83
+ white: `${ESC}37m`,
84
+ };
85
+ // ─── Public API ─────────────────────────────────────────────
86
+ /**
87
+ * Downsample a color to an ANSI escape string based on terminal color depth.
88
+ *
89
+ * - Hex colors (#RRGGBB) → converted to nearest ANSI code for the depth
90
+ * - Named attributes ('dim', 'red', etc.) → passed through as ANSI escape
91
+ * - If depth is 'none' → returns '' (empty, no color)
92
+ *
93
+ * Returns the ANSI escape STRING (without reset) — caller wraps text with it.
94
+ */
95
+ export function downsampleColor(color, depth) {
96
+ if (depth === 'none')
97
+ return '';
98
+ // Named attribute passthrough
99
+ const named = NAMED_ATTRS[color];
100
+ if (named)
101
+ return named;
102
+ // Parse hex
103
+ const rgb = parseHex(color);
104
+ if (!rgb)
105
+ return '';
106
+ const [r, g, b] = rgb;
107
+ switch (depth) {
108
+ case 'truecolor':
109
+ return `${ESC}38;2;${r};${g};${b}m`;
110
+ case '256': {
111
+ // Search full 256 palette (16 + cube + grayscale)
112
+ const idx = nearestIndex(r, g, b, PALETTE_256);
113
+ return `${ESC}38;5;${idx}m`;
114
+ }
115
+ case '16': {
116
+ // Search only the standard 16 colors
117
+ const idx = nearestIndex(r, g, b, PALETTE_256.slice(0, 16));
118
+ if (idx >= 8) {
119
+ // Bright colors: use 90-97 range
120
+ return `${ESC}${90 + (idx - 8)}m`;
121
+ }
122
+ return `${ESC}${30 + idx}m`;
123
+ }
124
+ }
125
+ }
126
+ //# sourceMappingURL=downsample.js.map