@opensip-cli/cli-ui 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 (80) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +8 -0
  3. package/README.md +31 -0
  4. package/dist/banner.d.ts +70 -0
  5. package/dist/banner.d.ts.map +1 -0
  6. package/dist/banner.js +203 -0
  7. package/dist/banner.js.map +1 -0
  8. package/dist/clock.d.ts +28 -0
  9. package/dist/clock.d.ts.map +1 -0
  10. package/dist/clock.js +45 -0
  11. package/dist/clock.js.map +1 -0
  12. package/dist/error-message.d.ts +12 -0
  13. package/dist/error-message.d.ts.map +1 -0
  14. package/dist/error-message.js +13 -0
  15. package/dist/error-message.js.map +1 -0
  16. package/dist/fit-table-format.d.ts +35 -0
  17. package/dist/fit-table-format.d.ts.map +1 -0
  18. package/dist/fit-table-format.js +48 -0
  19. package/dist/fit-table-format.js.map +1 -0
  20. package/dist/format-duration.d.ts +13 -0
  21. package/dist/format-duration.d.ts.map +1 -0
  22. package/dist/format-duration.js +23 -0
  23. package/dist/format-duration.js.map +1 -0
  24. package/dist/index.d.ts +33 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +32 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/live-progress.d.ts +57 -0
  29. package/dist/live-progress.d.ts.map +1 -0
  30. package/dist/live-progress.js +106 -0
  31. package/dist/live-progress.js.map +1 -0
  32. package/dist/progress-event.d.ts +66 -0
  33. package/dist/progress-event.d.ts.map +1 -0
  34. package/dist/progress-event.js +21 -0
  35. package/dist/progress-event.js.map +1 -0
  36. package/dist/project-header.d.ts +31 -0
  37. package/dist/project-header.d.ts.map +1 -0
  38. package/dist/project-header.js +38 -0
  39. package/dist/project-header.js.map +1 -0
  40. package/dist/render-to-ink.d.ts +15 -0
  41. package/dist/render-to-ink.d.ts.map +1 -0
  42. package/dist/render-to-ink.js +104 -0
  43. package/dist/render-to-ink.js.map +1 -0
  44. package/dist/render-to-text.d.ts +24 -0
  45. package/dist/render-to-text.d.ts.map +1 -0
  46. package/dist/render-to-text.js +86 -0
  47. package/dist/render-to-text.js.map +1 -0
  48. package/dist/run-footer-hints.d.ts +32 -0
  49. package/dist/run-footer-hints.d.ts.map +1 -0
  50. package/dist/run-footer-hints.js +33 -0
  51. package/dist/run-footer-hints.js.map +1 -0
  52. package/dist/run-header.d.ts +25 -0
  53. package/dist/run-header.d.ts.map +1 -0
  54. package/dist/run-header.js +19 -0
  55. package/dist/run-header.js.map +1 -0
  56. package/dist/run-summary.d.ts +48 -0
  57. package/dist/run-summary.d.ts.map +1 -0
  58. package/dist/run-summary.js +56 -0
  59. package/dist/run-summary.js.map +1 -0
  60. package/dist/run-timing-provider.d.ts +59 -0
  61. package/dist/run-timing-provider.d.ts.map +1 -0
  62. package/dist/run-timing-provider.js +52 -0
  63. package/dist/run-timing-provider.js.map +1 -0
  64. package/dist/spinner.d.ts +29 -0
  65. package/dist/spinner.d.ts.map +1 -0
  66. package/dist/spinner.js +44 -0
  67. package/dist/spinner.js.map +1 -0
  68. package/dist/theme.d.ts +42 -0
  69. package/dist/theme.d.ts.map +1 -0
  70. package/dist/theme.js +96 -0
  71. package/dist/theme.js.map +1 -0
  72. package/dist/verbose-detail.d.ts +43 -0
  73. package/dist/verbose-detail.d.ts.map +1 -0
  74. package/dist/verbose-detail.js +104 -0
  75. package/dist/verbose-detail.js.map +1 -0
  76. package/dist/view-model.d.ts +132 -0
  77. package/dist/view-model.d.ts.map +1 -0
  78. package/dist/view-model.js +71 -0
  79. package/dist/view-model.js.map +1 -0
  80. package/package.json +52 -0
@@ -0,0 +1,44 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Spinner — animated progress indicator. Renders one braille frame per tick
4
+ * plus an optional `completed/total (pct%)` suffix.
5
+ *
6
+ * Two modes:
7
+ * - Inside a `<ClockProvider>`: consumes the provider's tick via
8
+ * `useClock()`. Multiple spinners share the same timer.
9
+ * - Outside a provider (default): owns its own interval via `useTick()`.
10
+ * Convenient for single-spinner screens where setting up a provider
11
+ * is unnecessary.
12
+ */
13
+ import { Text } from 'ink';
14
+ import { useClock, useTick } from './clock.js';
15
+ import { useTheme } from './theme.js';
16
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
17
+ /** Spinner frame keyed off the surrounding `<ClockProvider>`. */
18
+ export function useSpinner() {
19
+ const tick = useClock();
20
+ return SPINNER_FRAMES[tick % SPINNER_FRAMES.length] ?? '';
21
+ }
22
+ /** Spinner frame from a self-owned interval — provider-free. */
23
+ export function useStandaloneSpinner(intervalMs) {
24
+ const tick = useTick(intervalMs);
25
+ return SPINNER_FRAMES[tick % SPINNER_FRAMES.length] ?? '';
26
+ }
27
+ export function Spinner({ total, completed, label = 'Running...', standalone = false, }) {
28
+ return standalone ? (_jsx(SpinnerStandalone, { total: total, completed: completed, label: label })) : (_jsx(SpinnerCtx, { total: total, completed: completed, label: label }));
29
+ }
30
+ function SpinnerCtx({ total, completed, label, }) {
31
+ const theme = useTheme();
32
+ const frame = useSpinner();
33
+ return (_jsx(SpinnerLine, { frame: frame, brand: theme.brand, total: total, completed: completed, label: label }));
34
+ }
35
+ function SpinnerStandalone({ total, completed, label, }) {
36
+ const theme = useTheme();
37
+ const frame = useStandaloneSpinner();
38
+ return (_jsx(SpinnerLine, { frame: frame, brand: theme.brand, total: total, completed: completed, label: label }));
39
+ }
40
+ function SpinnerLine({ frame, brand, total, completed, label, }) {
41
+ const pct = total > 0 ? Math.round((completed / total) * 100) : 0;
42
+ return (_jsxs(Text, { children: [_jsx(Text, { color: brand, children: frame }), " ", label, total > 0 ? (_jsxs(Text, { children: [' ', completed, "/", total, " (", pct, "%)"] })) : null] }));
43
+ }
44
+ //# sourceMappingURL=spinner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spinner.js","sourceRoot":"","sources":["../src/spinner.tsx"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAG3B,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;AAE1E,iEAAiE;AACjE,MAAM,UAAU,UAAU;IACxB,MAAM,IAAI,GAAG,QAAQ,EAAE,CAAC;IACxB,OAAO,cAAc,CAAC,IAAI,GAAG,cAAc,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;AAC5D,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,oBAAoB,CAAC,UAAmB;IACtD,MAAM,IAAI,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IACjC,OAAO,cAAc,CAAC,IAAI,GAAG,cAAc,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;AAC5D,CAAC;AAcD,MAAM,UAAU,OAAO,CAAC,EACtB,KAAK,EACL,SAAS,EACT,KAAK,GAAG,YAAY,EACpB,UAAU,GAAG,KAAK,GACL;IACb,OAAO,UAAU,CAAC,CAAC,CAAC,CAClB,KAAC,iBAAiB,IAAC,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,GAAI,CACxE,CAAC,CAAC,CAAC,CACF,KAAC,UAAU,IAAC,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,GAAI,CACjE,CAAC;AACJ,CAAC;AAED,SAAS,UAAU,CAAC,EAClB,KAAK,EACL,SAAS,EACT,KAAK,GAKN;IACC,MAAM,KAAK,GAAG,QAAQ,EAAE,CAAC;IACzB,MAAM,KAAK,GAAG,UAAU,EAAE,CAAC;IAC3B,OAAO,CACL,KAAC,WAAW,IACV,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,KAAK,CAAC,KAAK,EAClB,KAAK,EAAE,KAAK,EACZ,SAAS,EAAE,SAAS,EACpB,KAAK,EAAE,KAAK,GACZ,CACH,CAAC;AACJ,CAAC;AAED,SAAS,iBAAiB,CAAC,EACzB,KAAK,EACL,SAAS,EACT,KAAK,GAKN;IACC,MAAM,KAAK,GAAG,QAAQ,EAAE,CAAC;IACzB,MAAM,KAAK,GAAG,oBAAoB,EAAE,CAAC;IACrC,OAAO,CACL,KAAC,WAAW,IACV,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,KAAK,CAAC,KAAK,EAClB,KAAK,EAAE,KAAK,EACZ,SAAS,EAAE,SAAS,EACpB,KAAK,EAAE,KAAK,GACZ,CACH,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAAC,EACnB,KAAK,EACL,KAAK,EACL,KAAK,EACL,SAAS,EACT,KAAK,GAON;IACC,MAAM,GAAG,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,SAAS,GAAG,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAClE,OAAO,CACL,MAAC,IAAI,eACH,KAAC,IAAI,IAAC,KAAK,EAAE,KAAK,YAAG,KAAK,GAAQ,OAAE,KAAK,EACxC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CACX,MAAC,IAAI,eACF,GAAG,EACH,SAAS,OAAG,KAAK,QAAI,GAAG,UACpB,CACR,CAAC,CAAC,CAAC,IAAI,IACH,CACR,CAAC;AACJ,CAAC"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Theme system for Ink UI components.
3
+ *
4
+ * Provides color tokens, terminal capability detection, and a React context
5
+ * so every component can access the theme via useTheme().
6
+ */
7
+ import React from 'react';
8
+ export interface Theme {
9
+ /** OpenSIP brand color — warm amber */
10
+ readonly brand: string;
11
+ readonly success: string;
12
+ readonly error: string;
13
+ readonly warning: string;
14
+ readonly info: string;
15
+ readonly muted: string;
16
+ /** Score color thresholds */
17
+ readonly scoreHigh: string;
18
+ readonly scoreMid: string;
19
+ readonly scoreLow: string;
20
+ /** Check status colors */
21
+ readonly statusPass: string;
22
+ readonly statusFail: string;
23
+ readonly statusTimeout: string;
24
+ /** Whether color output is enabled */
25
+ readonly colorsEnabled: boolean;
26
+ }
27
+ export declare const DEFAULT_THEME: Theme;
28
+ export interface TerminalCapabilities {
29
+ readonly isTTY: boolean;
30
+ readonly supportsColor: boolean;
31
+ readonly supports256Color: boolean;
32
+ readonly supportsTrueColor: boolean;
33
+ }
34
+ export declare function detectTerminalCapabilities(): TerminalCapabilities;
35
+ export declare const ThemeContext: React.Context<Theme>;
36
+ export interface ThemeProviderProps {
37
+ readonly theme?: Theme;
38
+ readonly children: React.ReactNode;
39
+ }
40
+ export declare function ThemeProvider({ theme, children }: ThemeProviderProps): React.ReactElement;
41
+ export declare function useTheme(): Theme;
42
+ //# sourceMappingURL=theme.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAM1B,MAAM,WAAW,KAAK;IACpB,uCAAuC;IACvC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAEvB,6BAA6B;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAE1B,0BAA0B;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAE/B,sCAAsC;IACtC,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;CACjC;AAMD,eAAO,MAAM,aAAa,EAAE,KAiB3B,CAAC;AA4BF,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;IACnC,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;CACrC;AAED,wBAAgB,0BAA0B,IAAI,oBAAoB,CAgCjE;AAMD,eAAO,MAAM,YAAY,sBAA4C,CAAC;AAEtE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CACpC;AAED,wBAAgB,aAAa,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,kBAAkB,GAAG,KAAK,CAAC,YAAY,CAezF;AAED,wBAAgB,QAAQ,IAAI,KAAK,CAEhC"}
package/dist/theme.js ADDED
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Theme system for Ink UI components.
3
+ *
4
+ * Provides color tokens, terminal capability detection, and a React context
5
+ * so every component can access the theme via useTheme().
6
+ */
7
+ import React from 'react';
8
+ // ---------------------------------------------------------------------------
9
+ // Default theme
10
+ // ---------------------------------------------------------------------------
11
+ export const DEFAULT_THEME = {
12
+ brand: '#C8956C',
13
+ success: 'green',
14
+ error: 'red',
15
+ warning: 'yellow',
16
+ info: 'cyan',
17
+ muted: 'gray',
18
+ scoreHigh: 'green',
19
+ scoreMid: 'yellow',
20
+ scoreLow: 'red',
21
+ statusPass: 'green',
22
+ statusFail: 'red',
23
+ statusTimeout: 'yellow',
24
+ colorsEnabled: true,
25
+ };
26
+ // In a no-color theme, all color tokens are zeroed out: Ink does not read
27
+ // `colorsEnabled` itself, so handing it any non-empty color string (`'red'`,
28
+ // `'#C8956C'`) would still emit ANSI escapes even with `NO_COLOR=1`. Empty
29
+ // strings cause Ink's `<Text color={...}>` to no-op, honoring the NO_COLOR
30
+ // convention without forcing every component to guard `theme.colorsEnabled`
31
+ // at every call site.
32
+ const NO_COLOR_THEME = {
33
+ brand: '',
34
+ success: '',
35
+ error: '',
36
+ warning: '',
37
+ info: '',
38
+ muted: '',
39
+ scoreHigh: '',
40
+ scoreMid: '',
41
+ scoreLow: '',
42
+ statusPass: '',
43
+ statusFail: '',
44
+ statusTimeout: '',
45
+ colorsEnabled: false,
46
+ };
47
+ export function detectTerminalCapabilities() {
48
+ const isTTY = !!process.stdout.isTTY;
49
+ const noColor = !!process.env.NO_COLOR;
50
+ const colorTerm = process.env.COLORTERM ?? '';
51
+ const termProgram = process.env.TERM_PROGRAM ?? '';
52
+ const term = process.env.TERM ?? '';
53
+ if (noColor) {
54
+ return { isTTY, supportsColor: false, supports256Color: false, supportsTrueColor: false };
55
+ }
56
+ const envSuggestsTrueColor = colorTerm === 'truecolor' ||
57
+ colorTerm === '24bit' ||
58
+ termProgram === 'iTerm.app' ||
59
+ termProgram === 'WezTerm' ||
60
+ termProgram === 'Hyper';
61
+ const envSuggests256Color = envSuggestsTrueColor || term.includes('256color') || termProgram === 'Apple_Terminal';
62
+ // All capability flags MUST be gated on `isTTY`. When stdout is piped to a
63
+ // file or CI log, ANSI truecolor escapes still leak in if callers inspect
64
+ // `supports256Color` / `supportsTrueColor` to decide whether to emit hex
65
+ // color values — even though `supportsColor` itself is correctly false.
66
+ // Treating capability as a single coherent gate (`isTTY && env signal`)
67
+ // prevents the exported surface from contradicting itself.
68
+ const supportsTrueColor = isTTY && envSuggestsTrueColor;
69
+ const supports256Color = isTTY && envSuggests256Color;
70
+ const supportsColor = isTTY && (envSuggests256Color || term !== 'dumb');
71
+ return { isTTY, supportsColor, supports256Color, supportsTrueColor };
72
+ }
73
+ // ---------------------------------------------------------------------------
74
+ // React context + provider + hook
75
+ // ---------------------------------------------------------------------------
76
+ export const ThemeContext = React.createContext(DEFAULT_THEME);
77
+ export function ThemeProvider({ theme, children }) {
78
+ // Memoize the resolved theme so the context value reference is stable
79
+ // across re-renders. Ink's ClockProvider tick re-renders ThemeProvider
80
+ // on every frame; without useMemo, every render would re-read
81
+ // process.env, allocate a new `resolved` object, and force every
82
+ // `useTheme()` subscriber (Banner, Spinner, RunHeader, …) to re-render
83
+ // unnecessarily. Capability detection reads stable process state, so
84
+ // recomputing only when `theme` changes is correct.
85
+ const resolved = React.useMemo(() => {
86
+ if (theme)
87
+ return theme;
88
+ const caps = detectTerminalCapabilities();
89
+ return caps.supportsColor ? DEFAULT_THEME : NO_COLOR_THEME;
90
+ }, [theme]);
91
+ return React.createElement(ThemeContext.Provider, { value: resolved }, children);
92
+ }
93
+ export function useTheme() {
94
+ return React.useContext(ThemeContext);
95
+ }
96
+ //# sourceMappingURL=theme.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.js","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Shared verbose-detail view producers (ADR-0021).
3
+ *
4
+ * `--verbose` is an output-currency concern: a tool's verbose detail body is
5
+ * carried as data on its `*DoneResult` and rendered ONCE through these
6
+ * producers, so the detail is identical in a TTY (`renderToInk`) and a pipe
7
+ * (`renderToText`). They replace the three hand-copied "Use --verbose…" hints
8
+ * and the fitness-local `FindingsBlock` Ink component.
9
+ *
10
+ * cli-ui must never import `@opensip-cli/contracts` (keystone boundary), so
11
+ * these take plain structural inputs ({@link FindingGroupView}) — the cli
12
+ * `resultToView` seam passes the contracts `FindingGroup` *values* straight in
13
+ * (the shapes are structurally identical; a type-compat test pins that).
14
+ *
15
+ * Pure data + types: no `ink`/`react` imports (same rule as view-model.ts).
16
+ */
17
+ import { type HintItem, type ViewNode } from './view-model.js';
18
+ /** One displayed finding (structural twin of contracts' `FindingLine`). */
19
+ export interface FindingLineView {
20
+ readonly severity: 'error' | 'warning';
21
+ readonly message: string;
22
+ readonly location?: string;
23
+ readonly suggestion?: string;
24
+ }
25
+ /** A verbose findings block (structural twin of contracts' `FindingGroup`). */
26
+ export interface FindingGroupView {
27
+ readonly title: string;
28
+ readonly error?: string;
29
+ readonly errorCount: number;
30
+ readonly warningCount: number;
31
+ readonly findings: readonly FindingLineView[];
32
+ }
33
+ /** The single canonical "Use --verbose…" hint item. Tools that compose their
34
+ * own footer (e.g. graph, which also shows a report hint) reuse THIS item so
35
+ * the string is never re-typed. */
36
+ export declare const VERBOSE_DETAIL_HINT: HintItem;
37
+ /** The canonical next-step hint shown when a run was NOT verbose. */
38
+ export declare function viewVerboseHint(): ViewNode;
39
+ /** Render a line-oriented verbose body verbatim (graph's catalog/findings dump). */
40
+ export declare function viewVerboseLines(lines: readonly string[]): ViewNode;
41
+ /** Render a verbose findings body — header + one block per group. */
42
+ export declare function viewFindingsGroups(groups: readonly FindingGroupView[]): ViewNode;
43
+ //# sourceMappingURL=verbose-detail.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verbose-detail.d.ts","sourceRoot":"","sources":["../src/verbose-detail.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,EAAe,KAAK,QAAQ,EAAa,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAKvF,2EAA2E;AAC3E,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,+EAA+E;AAC/E,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAC;CAC/C;AAED;;oCAEoC;AACpC,eAAO,MAAM,mBAAmB,EAAE,QAGjC,CAAC;AAEF,qEAAqE;AACrE,wBAAgB,eAAe,IAAI,QAAQ,CAE1C;AAED,oFAAoF;AACpF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,QAAQ,CAEnE;AAkED,qEAAqE;AACrE,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,gBAAgB,EAAE,GAAG,QAAQ,CAchF"}
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Shared verbose-detail view producers (ADR-0021).
3
+ *
4
+ * `--verbose` is an output-currency concern: a tool's verbose detail body is
5
+ * carried as data on its `*DoneResult` and rendered ONCE through these
6
+ * producers, so the detail is identical in a TTY (`renderToInk`) and a pipe
7
+ * (`renderToText`). They replace the three hand-copied "Use --verbose…" hints
8
+ * and the fitness-local `FindingsBlock` Ink component.
9
+ *
10
+ * cli-ui must never import `@opensip-cli/contracts` (keystone boundary), so
11
+ * these take plain structural inputs ({@link FindingGroupView}) — the cli
12
+ * `resultToView` seam passes the contracts `FindingGroup` *values* straight in
13
+ * (the shapes are structurally identical; a type-compat test pins that).
14
+ *
15
+ * Pure data + types: no `ink`/`react` imports (same rule as view-model.ts).
16
+ */
17
+ import { viewFooterHints } from './run-footer-hints.js';
18
+ import { group, line } from './view-model.js';
19
+ /** Per-check cap on rendered findings — mirrors the prior FindingsBlock limit. */
20
+ const DEFAULT_VIOLATIONS_PER_GROUP = 25;
21
+ /** The single canonical "Use --verbose…" hint item. Tools that compose their
22
+ * own footer (e.g. graph, which also shows a report hint) reuse THIS item so
23
+ * the string is never re-typed. */
24
+ export const VERBOSE_DETAIL_HINT = {
25
+ text: 'Use --verbose for detailed results',
26
+ bold: ['--verbose'],
27
+ };
28
+ /** The canonical next-step hint shown when a run was NOT verbose. */
29
+ export function viewVerboseHint() {
30
+ return viewFooterHints([VERBOSE_DETAIL_HINT]);
31
+ }
32
+ /** Render a line-oriented verbose body verbatim (graph's catalog/findings dump). */
33
+ export function viewVerboseLines(lines) {
34
+ return group(lines.map((l) => line([{ text: l }])));
35
+ }
36
+ /** Render one finding row: ` error <message> <location>` + optional suggestion. */
37
+ function findingNode(f) {
38
+ const spans = [
39
+ { text: ' ' },
40
+ {
41
+ text: f.severity === 'error' ? 'error' : 'warn',
42
+ tone: f.severity === 'error' ? 'error' : 'warning',
43
+ },
44
+ { text: ' ' },
45
+ { text: f.message },
46
+ ];
47
+ if (f.location !== undefined && f.location !== '') {
48
+ spans.push({ text: ' ' }, { text: f.location, dim: true });
49
+ }
50
+ const rows = [line(spans)];
51
+ if (f.suggestion !== undefined && f.suggestion !== '') {
52
+ rows.push(line([{ text: ' ' }, { text: f.suggestion, dim: true }]));
53
+ }
54
+ return group(rows);
55
+ }
56
+ /** Render one findings group: a check title + count, an optional error line, the
57
+ * capped findings, and a "+N more" line when truncated. */
58
+ function groupNode(g) {
59
+ const count = g.errorCount + g.warningCount + (g.error === undefined ? 0 : 1);
60
+ const visible = g.findings.slice(0, DEFAULT_VIOLATIONS_PER_GROUP);
61
+ const hidden = Math.max(0, g.findings.length - visible.length);
62
+ const children = [
63
+ line([
64
+ { text: g.title, tone: 'brand' },
65
+ { text: ` (${String(count)})`, dim: true },
66
+ ]),
67
+ ];
68
+ if (g.error !== undefined) {
69
+ children.push(line([
70
+ { text: ' ' },
71
+ { text: 'error', tone: 'error' },
72
+ { text: ' ' },
73
+ { text: g.error },
74
+ ]));
75
+ }
76
+ for (const f of visible)
77
+ children.push(findingNode(f));
78
+ if (hidden > 0) {
79
+ children.push(line([
80
+ { text: ' … ' },
81
+ { text: `${String(hidden)} more hidden (use ` },
82
+ { text: '--json', bold: true },
83
+ { text: ' or ' },
84
+ { text: 'opensip report', bold: true },
85
+ { text: ' for all)' },
86
+ ], true));
87
+ }
88
+ children.push({ kind: 'spacer' });
89
+ return group(children, 2);
90
+ }
91
+ /** Render a verbose findings body — header + one block per group. */
92
+ export function viewFindingsGroups(groups) {
93
+ const total = groups.reduce((sum, g) => sum + g.errorCount + g.warningCount + (g.error === undefined ? 0 : 1), 0);
94
+ const children = [
95
+ line([
96
+ { text: 'Findings', bold: true },
97
+ { text: ` (${String(total)}):`, dim: true },
98
+ ]),
99
+ { kind: 'spacer' },
100
+ ...groups.map((g) => groupNode(g)),
101
+ ];
102
+ return group(children, 2);
103
+ }
104
+ //# sourceMappingURL=verbose-detail.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verbose-detail.js","sourceRoot":"","sources":["../src/verbose-detail.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,132 @@
1
+ /**
2
+ * View-model — the renderer-agnostic vocabulary that decouples *what* a
3
+ * command shows from *how* it is rendered.
4
+ *
5
+ * A `ViewNode` describes output as a tree of line-oriented blocks. Two
6
+ * interpreters consume the same tree: `renderToInk` (TTY, themed) and
7
+ * `renderToText` (pipe/CI, zero ANSI). Because each piece of output is
8
+ * expressed once as a `ViewNode`, the interactive and non-interactive
9
+ * forms cannot structurally drift — there is no second definition to fall
10
+ * out of sync. This generalizes the existing `formatProjectHeader` +
11
+ * `ProjectHeader` pattern (format once, render twice) to all CLI output.
12
+ *
13
+ * Deliberately NOT a general layout engine: there is no flexbox, no
14
+ * multi-column flow beyond the simple `table` node. Keep it line-oriented;
15
+ * if a node needs richer layout, that is a signal to reconsider rather
16
+ * than to grow this vocabulary.
17
+ *
18
+ * This module is pure data + types. It must stay free of `ink`/`react`
19
+ * imports so `renderToText` (and its consumers on the piped path) never
20
+ * pull React into the process.
21
+ */
22
+ /**
23
+ * Semantic color intent. The Ink interpreter maps each tone onto a
24
+ * `DEFAULT_THEME` token; the text interpreter ignores tones entirely. The
25
+ * theme remains the single source of color truth — producers never name
26
+ * raw colors.
27
+ */
28
+ export type Tone = 'brand' | 'success' | 'error' | 'warning' | 'info' | 'muted' | 'default';
29
+ /** An inline run of text within a line, optionally toned, bold, and/or dimmed. */
30
+ export interface Span {
31
+ readonly text: string;
32
+ readonly tone?: Tone;
33
+ readonly bold?: boolean;
34
+ /** Render this span muted (Ink `dimColor`). Dropped by the text interpreter. */
35
+ readonly dim?: boolean;
36
+ }
37
+ /** A single hint in a footer strip: its text and which substrings to bold. */
38
+ export interface HintItem {
39
+ readonly text: string;
40
+ readonly bold?: readonly string[];
41
+ }
42
+ /**
43
+ * Block-level output node. The interpreters switch on `kind`; every kind
44
+ * is total in both interpreters (no node may render in one and throw in
45
+ * the other).
46
+ */
47
+ export type ViewNode =
48
+ /** One line of styled spans. `dim` renders the whole line muted. */
49
+ {
50
+ readonly kind: 'line';
51
+ readonly spans: readonly Span[];
52
+ readonly dim?: boolean;
53
+ }
54
+ /** A section heading (e.g. `== Findings ==`). */
55
+ | {
56
+ readonly kind: 'heading';
57
+ readonly text: string;
58
+ readonly tone?: Tone;
59
+ }
60
+ /** A block of `label: value` pairs, one per line. */
61
+ | {
62
+ readonly kind: 'keyValues';
63
+ readonly pairs: readonly {
64
+ readonly label: string;
65
+ readonly value: string;
66
+ }[];
67
+ }
68
+ /**
69
+ * A column-aligned table. Each row is a list of cell spans — one span per
70
+ * column (cell N styles column N). Both interpreters pad every cell to its
71
+ * column's width (the max of the header and all cells in that column), so the
72
+ * columns line up in a TTY and a pipe alike. `align` is per-column
73
+ * (`'right'` pads on the left — for numeric/duration columns); default left.
74
+ */
75
+ | {
76
+ readonly kind: 'table';
77
+ readonly columns: readonly string[];
78
+ readonly rows: readonly (readonly Span[])[];
79
+ readonly align?: readonly ('left' | 'right')[];
80
+ /** When false, suppress the header row (a bare aligned grid). Default true. */
81
+ readonly showHeader?: boolean;
82
+ }
83
+ /** A next-step hint strip, ` | `-joined, with bolded flag substrings. */
84
+ | {
85
+ readonly kind: 'hints';
86
+ readonly items: readonly HintItem[];
87
+ }
88
+ /** A horizontal rule. */
89
+ | {
90
+ readonly kind: 'separator';
91
+ }
92
+ /** A single blank line. */
93
+ | {
94
+ readonly kind: 'spacer';
95
+ }
96
+ /** A container; children render in order, optionally indented. */
97
+ | {
98
+ readonly kind: 'group';
99
+ readonly children: readonly ViewNode[];
100
+ readonly indent?: number;
101
+ };
102
+ /** A line from plain text (single default-toned span). */
103
+ export declare function text(value: string): Span;
104
+ /** A line node from spans (or a single string). */
105
+ export declare function line(spans: readonly Span[] | string, dim?: boolean): ViewNode;
106
+ /** A group node from children, optionally indented. */
107
+ export declare function group(children: readonly ViewNode[], indent?: number): ViewNode;
108
+ /**
109
+ * Per-column display widths for a `table` node — the max of each column's header
110
+ * and all its cells. Shared by both interpreters so the TTY and pipe forms pad
111
+ * identically. Pure (no react), so `render-to-text` can use it too.
112
+ */
113
+ export declare function tableColumnWidths(columns: readonly string[], rows: readonly (readonly Span[])[]): number[];
114
+ /** Pad `value` to `width`; `'right'` pads on the left (numeric/duration columns). */
115
+ export declare function padTableCell(value: string, width: number, align: 'left' | 'right'): string;
116
+ /** Per-column spec for {@link viewTable}: a header label and optional alignment. */
117
+ export interface TableColumnSpec {
118
+ readonly header: string;
119
+ readonly align?: 'left' | 'right';
120
+ }
121
+ /**
122
+ * Build a column-aligned `table` node. Columns may be plain header strings or
123
+ * {@link TableColumnSpec}s (to right-align numeric columns). Each row is one
124
+ * span per column (cell N styles column N); the interpreters pad every cell to
125
+ * its column width so the grid lines up in a TTY and a pipe. This is the shared,
126
+ * reusable table primitive — producers (history, session replay, per-unit
127
+ * results) build one of these instead of hand-padding lines.
128
+ */
129
+ export declare function viewTable(columns: readonly (string | TableColumnSpec)[], rows: readonly (readonly Span[])[], opts?: {
130
+ readonly showHeader?: boolean;
131
+ }): ViewNode;
132
+ //# sourceMappingURL=view-model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"view-model.d.ts","sourceRoot":"","sources":["../src/view-model.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * View-model — the renderer-agnostic vocabulary that decouples *what* a
3
+ * command shows from *how* it is rendered.
4
+ *
5
+ * A `ViewNode` describes output as a tree of line-oriented blocks. Two
6
+ * interpreters consume the same tree: `renderToInk` (TTY, themed) and
7
+ * `renderToText` (pipe/CI, zero ANSI). Because each piece of output is
8
+ * expressed once as a `ViewNode`, the interactive and non-interactive
9
+ * forms cannot structurally drift — there is no second definition to fall
10
+ * out of sync. This generalizes the existing `formatProjectHeader` +
11
+ * `ProjectHeader` pattern (format once, render twice) to all CLI output.
12
+ *
13
+ * Deliberately NOT a general layout engine: there is no flexbox, no
14
+ * multi-column flow beyond the simple `table` node. Keep it line-oriented;
15
+ * if a node needs richer layout, that is a signal to reconsider rather
16
+ * than to grow this vocabulary.
17
+ *
18
+ * This module is pure data + types. It must stay free of `ink`/`react`
19
+ * imports so `renderToText` (and its consumers on the piped path) never
20
+ * pull React into the process.
21
+ */
22
+ // ---------------------------------------------------------------------------
23
+ // Constructor helpers — keep producers terse without hiding the data shape.
24
+ // ---------------------------------------------------------------------------
25
+ /** A line from plain text (single default-toned span). */
26
+ export function text(value) {
27
+ return { text: value };
28
+ }
29
+ /** A line node from spans (or a single string). */
30
+ export function line(spans, dim) {
31
+ const resolved = typeof spans === 'string' ? [text(spans)] : spans;
32
+ return dim === undefined
33
+ ? { kind: 'line', spans: resolved }
34
+ : { kind: 'line', spans: resolved, dim };
35
+ }
36
+ /** A group node from children, optionally indented. */
37
+ export function group(children, indent) {
38
+ return indent === undefined ? { kind: 'group', children } : { kind: 'group', children, indent };
39
+ }
40
+ /**
41
+ * Per-column display widths for a `table` node — the max of each column's header
42
+ * and all its cells. Shared by both interpreters so the TTY and pipe forms pad
43
+ * identically. Pure (no react), so `render-to-text` can use it too.
44
+ */
45
+ export function tableColumnWidths(columns, rows) {
46
+ const colCount = Math.max(columns.length, ...rows.map((r) => r.length), 0);
47
+ return Array.from({ length: colCount }, (_, i) => Math.max(columns[i]?.length ?? 0, ...rows.map((r) => r[i]?.text.length ?? 0), 0));
48
+ }
49
+ /** Pad `value` to `width`; `'right'` pads on the left (numeric/duration columns). */
50
+ export function padTableCell(value, width, align) {
51
+ return align === 'right' ? value.padStart(width) : value.padEnd(width);
52
+ }
53
+ /**
54
+ * Build a column-aligned `table` node. Columns may be plain header strings or
55
+ * {@link TableColumnSpec}s (to right-align numeric columns). Each row is one
56
+ * span per column (cell N styles column N); the interpreters pad every cell to
57
+ * its column width so the grid lines up in a TTY and a pipe. This is the shared,
58
+ * reusable table primitive — producers (history, session replay, per-unit
59
+ * results) build one of these instead of hand-padding lines.
60
+ */
61
+ export function viewTable(columns, rows, opts) {
62
+ const specs = columns.map((c) => (typeof c === 'string' ? { header: c } : c));
63
+ return {
64
+ kind: 'table',
65
+ columns: specs.map((s) => s.header),
66
+ rows,
67
+ align: specs.map((s) => s.align ?? 'left'),
68
+ ...(opts?.showHeader === undefined ? {} : { showHeader: opts.showHeader }),
69
+ };
70
+ }
71
+ //# sourceMappingURL=view-model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"view-model.js","sourceRoot":"","sources":["../src/view-model.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@opensip-cli/cli-ui",
3
+ "version": "0.1.0",
4
+ "license": "Apache-2.0",
5
+ "description": "Shared Ink/React presentational primitives for OpenSIP CLI CLI tools — banner, spinner, run header, theme. Used by every tool that ships an Ink live view.",
6
+ "keywords": [
7
+ "opensip-cli",
8
+ "static-analysis",
9
+ "code-quality",
10
+ "cli",
11
+ "terminal",
12
+ "ink",
13
+ "react"
14
+ ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/opensip-ai/opensip-cli.git",
18
+ "directory": "packages/cli-ui"
19
+ },
20
+ "homepage": "https://github.com/opensip-ai/opensip-cli",
21
+ "bugs": {
22
+ "url": "https://github.com/opensip-ai/opensip-cli/issues"
23
+ },
24
+ "type": "module",
25
+ "main": "./dist/index.js",
26
+ "types": "./dist/index.d.ts",
27
+ "exports": {
28
+ ".": "./dist/index.js"
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "LICENSE",
33
+ "NOTICE"
34
+ ],
35
+ "dependencies": {
36
+ "ink": "^7.0.5",
37
+ "react": "^19.2.7"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^24.13.2",
41
+ "@types/react": "^19.2.17",
42
+ "ink-testing-library": "^4.0.0",
43
+ "typescript": "~6.0.3",
44
+ "vitest": "^4.1.8"
45
+ },
46
+ "scripts": {
47
+ "build": "tsc",
48
+ "test": "vitest run --passWithNoTests",
49
+ "typecheck": "tsc --noEmit",
50
+ "clean": "rm -rf dist"
51
+ }
52
+ }