@pablozaiden/terminatui 0.2.0 → 0.3.0-beta-1

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 (175) hide show
  1. package/AGENTS.md +14 -2
  2. package/CLAUDE.md +1 -0
  3. package/README.md +64 -43
  4. package/bun.lock +85 -0
  5. package/examples/tui-app/commands/config/app/get.ts +6 -10
  6. package/examples/tui-app/commands/config/app/index.ts +2 -6
  7. package/examples/tui-app/commands/config/app/set.ts +23 -13
  8. package/examples/tui-app/commands/config/index.ts +2 -6
  9. package/examples/tui-app/commands/config/user/get.ts +6 -10
  10. package/examples/tui-app/commands/config/user/index.ts +2 -6
  11. package/examples/tui-app/commands/config/user/set.ts +6 -10
  12. package/examples/tui-app/commands/greet.ts +13 -11
  13. package/examples/tui-app/commands/math.ts +5 -9
  14. package/examples/tui-app/commands/status.ts +21 -12
  15. package/examples/tui-app/index.ts +6 -3
  16. package/guides/01-hello-world.md +7 -2
  17. package/guides/02-adding-options.md +2 -2
  18. package/guides/03-multiple-commands.md +6 -8
  19. package/guides/04-subcommands.md +8 -8
  20. package/guides/05-interactive-tui.md +45 -30
  21. package/guides/06-config-validation.md +4 -12
  22. package/guides/07-async-cancellation.md +14 -16
  23. package/guides/08-complete-application.md +12 -42
  24. package/guides/README.md +7 -3
  25. package/package.json +4 -8
  26. package/src/__tests__/application.test.ts +87 -68
  27. package/src/__tests__/buildCliCommand.test.ts +99 -119
  28. package/src/__tests__/builtins.test.ts +27 -75
  29. package/src/__tests__/command.test.ts +100 -131
  30. package/src/__tests__/context.test.ts +1 -26
  31. package/src/__tests__/helpCore.test.ts +227 -0
  32. package/src/__tests__/parser.test.ts +98 -244
  33. package/src/__tests__/registry.test.ts +33 -160
  34. package/src/__tests__/schemaToFields.test.ts +75 -158
  35. package/src/builtins/help.ts +12 -4
  36. package/src/builtins/settings.ts +18 -32
  37. package/src/builtins/version.ts +4 -4
  38. package/src/cli/output/colors.ts +1 -1
  39. package/src/cli/parser.ts +26 -95
  40. package/src/core/application.ts +192 -110
  41. package/src/core/command.ts +26 -9
  42. package/src/core/context.ts +31 -20
  43. package/src/core/help.ts +24 -18
  44. package/src/core/knownCommands.ts +13 -0
  45. package/src/core/logger.ts +39 -42
  46. package/src/core/registry.ts +5 -12
  47. package/src/tui/TuiApplication.tsx +63 -120
  48. package/src/tui/TuiRoot.tsx +135 -0
  49. package/src/tui/adapters/factory.ts +19 -0
  50. package/src/tui/adapters/ink/InkRenderer.tsx +135 -0
  51. package/src/tui/adapters/ink/components/Button.tsx +12 -0
  52. package/src/tui/adapters/ink/components/Code.tsx +6 -0
  53. package/src/tui/adapters/ink/components/CodeHighlight.tsx +6 -0
  54. package/src/tui/adapters/ink/components/Container.tsx +5 -0
  55. package/src/tui/adapters/ink/components/Field.tsx +12 -0
  56. package/src/tui/adapters/ink/components/Label.tsx +24 -0
  57. package/src/tui/adapters/ink/components/MenuButton.tsx +12 -0
  58. package/src/tui/adapters/ink/components/MenuItem.tsx +17 -0
  59. package/src/tui/adapters/ink/components/Overlay.tsx +5 -0
  60. package/src/tui/adapters/ink/components/Panel.tsx +15 -0
  61. package/src/tui/adapters/ink/components/ScrollView.tsx +5 -0
  62. package/src/tui/adapters/ink/components/Select.tsx +44 -0
  63. package/src/tui/adapters/ink/components/Spacer.tsx +15 -0
  64. package/src/tui/adapters/ink/components/Spinner.tsx +5 -0
  65. package/src/tui/adapters/ink/components/TextInput.tsx +22 -0
  66. package/src/tui/adapters/ink/components/Value.tsx +7 -0
  67. package/src/tui/adapters/ink/keyboard.ts +97 -0
  68. package/src/tui/adapters/ink/utils.ts +16 -0
  69. package/src/tui/adapters/opentui/OpenTuiRenderer.tsx +115 -0
  70. package/src/tui/adapters/opentui/components/Button.tsx +13 -0
  71. package/src/tui/adapters/opentui/components/Code.tsx +12 -0
  72. package/src/tui/adapters/opentui/components/CodeHighlight.tsx +24 -0
  73. package/src/tui/adapters/opentui/components/Container.tsx +56 -0
  74. package/src/tui/adapters/opentui/components/Field.tsx +18 -0
  75. package/src/tui/adapters/opentui/components/Label.tsx +15 -0
  76. package/src/tui/adapters/opentui/components/MenuButton.tsx +14 -0
  77. package/src/tui/adapters/opentui/components/MenuItem.tsx +29 -0
  78. package/src/tui/adapters/opentui/components/Overlay.tsx +21 -0
  79. package/src/tui/adapters/opentui/components/Panel.tsx +78 -0
  80. package/src/tui/adapters/opentui/components/ScrollView.tsx +85 -0
  81. package/src/tui/adapters/opentui/components/Select.tsx +59 -0
  82. package/src/tui/adapters/opentui/components/Spacer.tsx +5 -0
  83. package/src/tui/adapters/opentui/components/Spinner.tsx +12 -0
  84. package/src/tui/adapters/opentui/components/TextInput.tsx +13 -0
  85. package/src/tui/adapters/opentui/components/Value.tsx +13 -0
  86. package/src/tui/{hooks → adapters/opentui/hooks}/useSpinner.ts +2 -11
  87. package/src/tui/adapters/opentui/keyboard.ts +61 -0
  88. package/src/tui/adapters/types.ts +70 -0
  89. package/src/tui/components/ActionButton.tsx +0 -36
  90. package/src/tui/components/CommandSelector.tsx +45 -92
  91. package/src/tui/components/ConfigForm.tsx +68 -42
  92. package/src/tui/components/FieldRow.tsx +0 -30
  93. package/src/tui/components/Header.tsx +14 -13
  94. package/src/tui/components/JsonHighlight.tsx +10 -17
  95. package/src/tui/components/ModalBase.tsx +38 -0
  96. package/src/tui/components/ResultsPanel.tsx +27 -36
  97. package/src/tui/components/StatusBar.tsx +24 -39
  98. package/src/tui/components/logColors.ts +12 -0
  99. package/src/tui/context/ClipboardContext.tsx +87 -0
  100. package/src/tui/context/ExecutorContext.tsx +139 -0
  101. package/src/tui/context/KeyboardContext.tsx +85 -71
  102. package/src/tui/context/LogsContext.tsx +35 -0
  103. package/src/tui/context/NavigationContext.tsx +194 -0
  104. package/src/tui/context/RendererContext.tsx +20 -0
  105. package/src/tui/context/TuiAppContext.tsx +58 -0
  106. package/src/tui/hooks/useActiveKeyHandler.ts +75 -0
  107. package/src/tui/hooks/useBackHandler.ts +34 -0
  108. package/src/tui/hooks/useClipboard.ts +40 -25
  109. package/src/tui/hooks/useClipboardProvider.ts +42 -0
  110. package/src/tui/hooks/useGlobalKeyHandler.ts +54 -0
  111. package/src/tui/modals/CliModal.tsx +82 -0
  112. package/src/tui/modals/EditorModal.tsx +207 -0
  113. package/src/tui/modals/LogsModal.tsx +98 -0
  114. package/src/tui/registry.ts +102 -0
  115. package/src/tui/screens/CommandSelectScreen.tsx +162 -0
  116. package/src/tui/screens/ConfigScreen.tsx +160 -0
  117. package/src/tui/screens/ErrorScreen.tsx +58 -0
  118. package/src/tui/screens/ResultsScreen.tsx +60 -0
  119. package/src/tui/screens/RunningScreen.tsx +72 -0
  120. package/src/tui/screens/ScreenBase.ts +6 -0
  121. package/src/tui/semantic/Button.tsx +7 -0
  122. package/src/tui/semantic/Code.tsx +7 -0
  123. package/src/tui/semantic/CodeHighlight.tsx +7 -0
  124. package/src/tui/semantic/Container.tsx +7 -0
  125. package/src/tui/semantic/Field.tsx +7 -0
  126. package/src/tui/semantic/Label.tsx +7 -0
  127. package/src/tui/semantic/MenuButton.tsx +7 -0
  128. package/src/tui/semantic/MenuItem.tsx +7 -0
  129. package/src/tui/semantic/Overlay.tsx +7 -0
  130. package/src/tui/semantic/Panel.tsx +7 -0
  131. package/src/tui/semantic/ScrollView.tsx +9 -0
  132. package/src/tui/semantic/Select.tsx +7 -0
  133. package/src/tui/semantic/Spacer.tsx +7 -0
  134. package/src/tui/semantic/Spinner.tsx +7 -0
  135. package/src/tui/semantic/TextInput.tsx +7 -0
  136. package/src/tui/semantic/Value.tsx +7 -0
  137. package/src/tui/semantic/types.ts +195 -0
  138. package/src/tui/theme.ts +25 -14
  139. package/src/tui/utils/buildCliCommand.ts +1 -0
  140. package/src/tui/utils/getEnumKeys.ts +3 -0
  141. package/src/tui/utils/parameterPersistence.ts +1 -0
  142. package/src/types/command.ts +0 -60
  143. package/examples/tui-app/commands/index.ts +0 -4
  144. package/src/__tests__/colors.test.ts +0 -127
  145. package/src/__tests__/commandClass.test.ts +0 -130
  146. package/src/__tests__/help.test.ts +0 -412
  147. package/src/__tests__/registryNew.test.ts +0 -160
  148. package/src/__tests__/table.test.ts +0 -146
  149. package/src/__tests__/tui.test.ts +0 -26
  150. package/src/builtins/index.ts +0 -4
  151. package/src/cli/help.ts +0 -174
  152. package/src/cli/index.ts +0 -3
  153. package/src/cli/output/index.ts +0 -2
  154. package/src/cli/output/table.ts +0 -141
  155. package/src/commands/help.ts +0 -50
  156. package/src/commands/index.ts +0 -1
  157. package/src/components/index.ts +0 -147
  158. package/src/core/index.ts +0 -15
  159. package/src/hooks/index.ts +0 -131
  160. package/src/index.ts +0 -137
  161. package/src/registry/commandRegistry.ts +0 -77
  162. package/src/registry/index.ts +0 -1
  163. package/src/tui/TuiApp.tsx +0 -619
  164. package/src/tui/app.ts +0 -29
  165. package/src/tui/components/CliModal.tsx +0 -81
  166. package/src/tui/components/EditorModal.tsx +0 -177
  167. package/src/tui/components/LogsPanel.tsx +0 -86
  168. package/src/tui/components/index.ts +0 -13
  169. package/src/tui/context/index.ts +0 -7
  170. package/src/tui/hooks/index.ts +0 -35
  171. package/src/tui/hooks/useKeyboardHandler.ts +0 -91
  172. package/src/tui/hooks/useLogStream.ts +0 -96
  173. package/src/tui/index.ts +0 -65
  174. package/src/tui/utils/index.ts +0 -13
  175. package/src/types/index.ts +0 -1
@@ -0,0 +1,7 @@
1
+ import type { MenuItemProps } from "./types.ts";
2
+ import { useRenderer } from "../context/RendererContext.tsx";
3
+
4
+ export function MenuItem(props: MenuItemProps) {
5
+ const renderer = useRenderer();
6
+ return renderer.components.MenuItem(props);
7
+ }
@@ -0,0 +1,7 @@
1
+ import type { OverlayProps } from "./types.ts";
2
+ import { useRenderer } from "../context/RendererContext.tsx";
3
+
4
+ export function Overlay(props: OverlayProps) {
5
+ const renderer = useRenderer();
6
+ return renderer.components.Overlay(props);
7
+ }
@@ -0,0 +1,7 @@
1
+ import type { PanelProps } from "./types.ts";
2
+ import { useRenderer } from "../context/RendererContext.tsx";
3
+
4
+ export function Panel(props: PanelProps) {
5
+ const renderer = useRenderer();
6
+ return renderer.components.Panel(props);
7
+ }
@@ -0,0 +1,9 @@
1
+ import type { ScrollViewProps, ScrollViewRef } from "./types.ts";
2
+ import { useRenderer } from "../context/RendererContext.tsx";
3
+
4
+ export function ScrollView(props: ScrollViewProps) {
5
+ const renderer = useRenderer();
6
+ return renderer.components.ScrollView(props);
7
+ }
8
+
9
+ export type { ScrollViewRef };
@@ -0,0 +1,7 @@
1
+ import type { SelectProps } from "./types.ts";
2
+ import { useRenderer } from "../context/RendererContext.tsx";
3
+
4
+ export function Select(props: SelectProps) {
5
+ const renderer = useRenderer();
6
+ return renderer.components.Select(props);
7
+ }
@@ -0,0 +1,7 @@
1
+ import type { SpacerProps } from "./types.ts";
2
+ import { useRenderer } from "../context/RendererContext.tsx";
3
+
4
+ export function Spacer(props: SpacerProps) {
5
+ const renderer = useRenderer();
6
+ return renderer.components.Spacer(props);
7
+ }
@@ -0,0 +1,7 @@
1
+ import type { SpinnerProps } from "./types.ts";
2
+ import { useRenderer } from "../context/RendererContext.tsx";
3
+
4
+ export function Spinner(props: SpinnerProps) {
5
+ const renderer = useRenderer();
6
+ return renderer.components.Spinner(props);
7
+ }
@@ -0,0 +1,7 @@
1
+ import type { TextInputProps } from "./types.ts";
2
+ import { useRenderer } from "../context/RendererContext.tsx";
3
+
4
+ export function TextInput(props: TextInputProps) {
5
+ const renderer = useRenderer();
6
+ return renderer.components.TextInput(props);
7
+ }
@@ -0,0 +1,7 @@
1
+ import type { ValueProps } from "./types.ts";
2
+ import { useRenderer } from "../context/RendererContext.tsx";
3
+
4
+ export function Value(props: ValueProps) {
5
+ const renderer = useRenderer();
6
+ return renderer.components.Value(props);
7
+ }
@@ -0,0 +1,195 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ export type Align = "flex-start" | "center" | "flex-end" | "stretch";
4
+ export type Justify = "flex-start" | "center" | "flex-end" | "space-between" | "space-around" | "space-evenly";
5
+ export type FlexDirection = "row" | "column";
6
+
7
+ export interface Spacing {
8
+ top?: number;
9
+ right?: number;
10
+ bottom?: number;
11
+ left?: number;
12
+ }
13
+
14
+ export interface ThemeConfig {
15
+ colors: {
16
+ background: string;
17
+ panelBackground: string;
18
+ overlay: string;
19
+
20
+ text: string;
21
+ mutedText: string;
22
+ inverseText: string;
23
+
24
+ border: string;
25
+ focusBorder: string;
26
+
27
+ primary: string;
28
+ primaryText: string;
29
+
30
+ success: string;
31
+ warning: string;
32
+ error: string;
33
+
34
+ value: string;
35
+ code: string;
36
+
37
+ selectionBackground: string;
38
+ selectionText: string;
39
+ };
40
+ }
41
+
42
+ export type SemanticColor = keyof ThemeConfig["colors"];
43
+
44
+ export interface LayoutProps {
45
+ flex?: number;
46
+ width?: number | string;
47
+ height?: number | string;
48
+
49
+ flexDirection?: FlexDirection;
50
+ alignItems?: Align;
51
+ justifyContent?: Justify;
52
+
53
+ gap?: number;
54
+ padding?: number | Spacing;
55
+
56
+ /** When set, should prevent the node from shrinking in flex layouts. */
57
+ noShrink?: boolean;
58
+ }
59
+
60
+ export type PanelSurface = "panel" | "overlay";
61
+
62
+ export interface PanelProps extends LayoutProps {
63
+ title?: string;
64
+ focused?: boolean;
65
+ border?: boolean;
66
+ surface?: PanelSurface;
67
+
68
+ /** Renderer-level compact styling (e.g. no default padding). */
69
+ dense?: boolean;
70
+
71
+ children?: ReactNode;
72
+ }
73
+
74
+ export interface ContainerProps extends LayoutProps {
75
+ children?: ReactNode;
76
+ }
77
+
78
+ export interface ScrollViewRef {
79
+ scrollToTop: () => void;
80
+ scrollToBottom: () => void;
81
+ scrollToIndex: (index: number) => void;
82
+ }
83
+
84
+ export interface ScrollViewProps extends LayoutProps {
85
+ axis?: "vertical" | "horizontal" | "both";
86
+ stickyToEnd?: boolean;
87
+ focused?: boolean;
88
+ scrollRef?: (ref: ScrollViewRef | null) => void;
89
+ children?: ReactNode;
90
+ }
91
+
92
+ export interface OverlayProps {
93
+ zIndex?: number;
94
+ top?: number | string;
95
+ left?: number | string;
96
+ right?: number | string;
97
+ bottom?: number | string;
98
+ width?: number | string;
99
+ height?: number | string;
100
+ children?: ReactNode;
101
+ }
102
+
103
+ export interface SpacerProps {
104
+ size: number;
105
+ axis?: "horizontal" | "vertical";
106
+ }
107
+
108
+ export interface SpinnerProps {
109
+ active: boolean;
110
+ }
111
+
112
+ export interface LabelProps {
113
+ color?: SemanticColor;
114
+ bold?: boolean;
115
+ italic?: boolean;
116
+ wrap?: boolean;
117
+ children: ReactNode;
118
+ }
119
+
120
+ export interface ValueProps {
121
+ color?: SemanticColor;
122
+ truncate?: boolean;
123
+ children: ReactNode;
124
+ }
125
+
126
+ export interface CodeProps {
127
+ color?: SemanticColor;
128
+ children: string;
129
+ }
130
+
131
+ export type CodeTokenType =
132
+ | "punctuation"
133
+ | "string"
134
+ | "number"
135
+ | "boolean"
136
+ | "null"
137
+ | "key"
138
+ | "unknown";
139
+
140
+ export interface CodeToken {
141
+ type: CodeTokenType;
142
+ value: string;
143
+ }
144
+
145
+ export interface CodeHighlightProps {
146
+ tokens: CodeToken[];
147
+ }
148
+
149
+ export interface FieldProps {
150
+ label: string;
151
+ value: ReactNode;
152
+ selected?: boolean;
153
+ onActivate?: () => void;
154
+ }
155
+
156
+ export interface TextInputProps {
157
+ value: string;
158
+ placeholder?: string;
159
+ focused?: boolean;
160
+ onChange: (value: string) => void;
161
+ onSubmit?: () => void;
162
+ }
163
+
164
+ export interface SelectOption {
165
+ label: string;
166
+ value: string;
167
+ }
168
+
169
+ export interface SelectProps {
170
+ options: SelectOption[];
171
+ value: string;
172
+ focused?: boolean;
173
+ onChange: (value: string) => void;
174
+ onSubmit?: () => void;
175
+ }
176
+
177
+ export interface ButtonProps {
178
+ label: string;
179
+ selected?: boolean;
180
+ onActivate?: () => void;
181
+ }
182
+
183
+ export interface MenuButtonProps {
184
+ label: string;
185
+ selected?: boolean;
186
+ onActivate?: () => void;
187
+ }
188
+
189
+ export interface MenuItemProps {
190
+ label: string;
191
+ description?: string;
192
+ suffix?: string;
193
+ selected?: boolean;
194
+ onActivate?: () => void;
195
+ }
package/src/tui/theme.ts CHANGED
@@ -1,21 +1,32 @@
1
1
  /**
2
- * Default TUI theme colors.
2
+ * Default TUI theme.
3
+ *
4
+ * This is intentionally semantic: UI code should reference meanings
5
+ * (e.g. "error", "mutedText", "selectionBackground") rather than hardcoding colors.
3
6
  */
4
- export const Theme = {
5
- background: "#0b0c10",
7
+ export const SemanticColors = {
8
+ background: "#0f1117",
9
+ panelBackground: "#0f1117",
10
+ overlay: "#101218",
11
+
12
+ text: "#d6dde6",
13
+ mutedText: "#666666",
14
+ inverseText: "#0b0c10",
15
+
6
16
  border: "#2c2f36",
7
- borderFocused: "#5da9e9",
8
- borderSelected: "#61afef",
9
- label: "#c0cad6",
10
- value: "#98c379",
11
- actionButton: "#a0e8af",
12
- header: "#a8b3c1",
13
- statusText: "#d6dde6",
14
- overlay: "#0e1117",
15
- overlayTitle: "#e5c07b",
16
- error: "#f78888",
17
+ focusBorder: "#5da9e9",
18
+
19
+ primary: "#61afef",
20
+ primaryText: "#0b0c10",
21
+
17
22
  success: "#98c379",
18
23
  warning: "#f5c542",
24
+ error: "#f78888",
25
+
26
+ value: "#98c379",
27
+ code: "#c0cad6",
28
+
29
+ selectionBackground: "#61afef",
30
+ selectionText: "#0b0c10",
19
31
  } as const;
20
32
 
21
- export type ThemeColors = typeof Theme;
@@ -79,6 +79,7 @@ export function buildCliCommand<T extends OptionSchema>(
79
79
  }
80
80
  }
81
81
 
82
+ parts.push("--mode", "cli");
82
83
  return parts.join(" ");
83
84
  }
84
85
 
@@ -0,0 +1,3 @@
1
+ export function getEnumKeys<T extends Record<string, string | number>>(enumObj: T): string[] {
2
+ return Object.keys(enumObj).filter((key) => Number.isNaN(Number(key)));
3
+ }
@@ -47,6 +47,7 @@ export function loadPersistedParameters(
47
47
  return JSON.parse(content) as Record<string, unknown>;
48
48
  }
49
49
  } catch (error) {
50
+
50
51
  // Silently ignore errors - just return empty object
51
52
  console.error(`Failed to load persisted parameters: ${error}`);
52
53
  }
@@ -41,63 +41,3 @@ export type OptionValues<T extends OptionSchema> = {
41
41
  : unknown;
42
42
  };
43
43
 
44
- /**
45
- * Context passed to command executors
46
- */
47
- export interface CommandContext<T extends OptionSchema = OptionSchema> {
48
- options: OptionValues<T>;
49
- args: string[];
50
- commandPath: string[];
51
- }
52
-
53
- /**
54
- * Command executor function
55
- */
56
- export type CommandExecutor<T extends OptionSchema = OptionSchema> = (
57
- ctx: CommandContext<T>
58
- ) => void | Promise<void>;
59
-
60
- /**
61
- * Command definition
62
- */
63
- export interface Command<
64
- T extends OptionSchema = OptionSchema,
65
- R = void,
66
- > {
67
- name: string;
68
- description: string;
69
- aliases?: string[];
70
- hidden?: boolean;
71
- options?: T;
72
- subcommands?: Record<string, Command>;
73
- examples?: Array<{ command: string; description: string }>;
74
- execute: (ctx: CommandContext<T>) => R | Promise<R>;
75
- beforeExecute?: (ctx: CommandContext<T>) => void | Promise<void>;
76
- afterExecute?: (ctx: CommandContext<T>) => void | Promise<void>;
77
- }
78
-
79
- /**
80
- * TUI command with a render function
81
- */
82
- export interface TuiCommand<T extends OptionSchema = OptionSchema>
83
- extends Omit<Command<T>, "execute"> {
84
- render: (ctx: CommandContext<T>) => React.ReactNode;
85
- }
86
-
87
- /**
88
- * Define a CLI command
89
- */
90
- export function defineCommand<T extends OptionSchema = OptionSchema>(
91
- config: Command<T>
92
- ): Command<T> {
93
- return config;
94
- }
95
-
96
- /**
97
- * Define a TUI command
98
- */
99
- export function defineTuiCommand<T extends OptionSchema = OptionSchema>(
100
- config: TuiCommand<T>
101
- ): TuiCommand<T> {
102
- return config;
103
- }
@@ -1,4 +0,0 @@
1
- export { GreetCommand } from "./greet.ts";
2
- export { MathCommand } from "./math.ts";
3
- export { StatusCommand } from "./status.ts";
4
- export { ConfigCommand } from "./config/index.ts";
@@ -1,127 +0,0 @@
1
- import { test, expect, describe } from "bun:test";
2
- import { colors, supportsColors } from "../cli/output/colors.ts";
3
-
4
- describe("colors", () => {
5
- describe("basic colors", () => {
6
- test("red wraps text", () => {
7
- const result = colors.red("test");
8
- expect(result).toContain("test");
9
- });
10
-
11
- test("green wraps text", () => {
12
- const result = colors.green("test");
13
- expect(result).toContain("test");
14
- });
15
-
16
- test("blue wraps text", () => {
17
- const result = colors.blue("test");
18
- expect(result).toContain("test");
19
- });
20
-
21
- test("yellow wraps text", () => {
22
- const result = colors.yellow("test");
23
- expect(result).toContain("test");
24
- });
25
-
26
- test("cyan wraps text", () => {
27
- const result = colors.cyan("test");
28
- expect(result).toContain("test");
29
- });
30
-
31
- test("gray wraps text", () => {
32
- const result = colors.gray("test");
33
- expect(result).toContain("test");
34
- });
35
- });
36
-
37
- describe("styles", () => {
38
- test("bold wraps text", () => {
39
- const result = colors.bold("test");
40
- expect(result).toContain("test");
41
- });
42
-
43
- test("dim wraps text", () => {
44
- const result = colors.dim("test");
45
- expect(result).toContain("test");
46
- });
47
-
48
- test("italic wraps text", () => {
49
- const result = colors.italic("test");
50
- expect(result).toContain("test");
51
- });
52
-
53
- test("underline wraps text", () => {
54
- const result = colors.underline("test");
55
- expect(result).toContain("test");
56
- });
57
-
58
- test("strikethrough wraps text", () => {
59
- const result = colors.strikethrough("test");
60
- expect(result).toContain("test");
61
- });
62
- });
63
-
64
- describe("semantic colors", () => {
65
- test("success includes checkmark and message", () => {
66
- const result = colors.success("done");
67
- expect(result).toContain("done");
68
- expect(result).toContain("✓");
69
- });
70
-
71
- test("error includes message", () => {
72
- const result = colors.error("failed");
73
- expect(result).toContain("failed");
74
- });
75
-
76
- test("warning includes message", () => {
77
- const result = colors.warning("caution");
78
- expect(result).toContain("caution");
79
- });
80
-
81
- test("info includes message", () => {
82
- const result = colors.info("note");
83
- expect(result).toContain("note");
84
- });
85
- });
86
-
87
- describe("chaining", () => {
88
- test("can combine bold and red", () => {
89
- const result = colors.bold(colors.red("test"));
90
- expect(result).toContain("test");
91
- });
92
-
93
- test("can combine dim and italic", () => {
94
- const result = colors.dim(colors.italic("test"));
95
- expect(result).toContain("test");
96
- });
97
- });
98
-
99
- describe("edge cases", () => {
100
- test("handles empty string", () => {
101
- const result = colors.red("");
102
- expect(typeof result).toBe("string");
103
- });
104
-
105
- test("handles string with newlines", () => {
106
- const result = colors.blue("line1\nline2");
107
- expect(result).toContain("line1");
108
- expect(result).toContain("line2");
109
- });
110
-
111
- test("handles string with special characters", () => {
112
- const result = colors.green("test © ® ™");
113
- expect(result).toContain("©");
114
- });
115
- });
116
- });
117
-
118
- describe("supportsColors", () => {
119
- test("is a function", () => {
120
- expect(typeof supportsColors).toBe("function");
121
- });
122
-
123
- test("returns a boolean", () => {
124
- const result = supportsColors();
125
- expect(typeof result).toBe("boolean");
126
- });
127
- });
@@ -1,130 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
- import { Command, type CommandResult } from "../core/command.ts";
3
- import type { AppContext } from "../core/context.ts";
4
- import type { OptionSchema, OptionValues } from "../types/command.ts";
5
-
6
- // Test command with options
7
- class TestCommand extends Command<{ name: { type: "string"; description: string } }> {
8
- readonly name = "test";
9
- readonly description = "A test command";
10
- readonly options = {
11
- name: { type: "string" as const, description: "Name option" },
12
- };
13
-
14
- executedWith: OptionValues<typeof this.options> | null = null;
15
-
16
- override async execute(
17
- _ctx: AppContext,
18
- opts: OptionValues<typeof this.options>
19
- ): Promise<CommandResult> {
20
- this.executedWith = opts;
21
- return { success: true, message: "Executed" };
22
- }
23
- }
24
-
25
- // Simple command without options
26
- class SimpleCommand extends Command<OptionSchema> {
27
- readonly name = "simple";
28
- readonly description = "A simple command";
29
- readonly options = {};
30
-
31
- executed = false;
32
-
33
- override async execute(_ctx: AppContext): Promise<CommandResult> {
34
- this.executed = true;
35
- return { success: true, message: "Done" };
36
- }
37
- }
38
-
39
- describe("Command", () => {
40
- describe("core properties", () => {
41
- test("has name", () => {
42
- const cmd = new TestCommand();
43
- expect(cmd.name).toBe("test");
44
- });
45
-
46
- test("has description", () => {
47
- const cmd = new TestCommand();
48
- expect(cmd.description).toBe("A test command");
49
- });
50
-
51
- test("has options", () => {
52
- const cmd = new TestCommand();
53
- expect(cmd.options).toEqual({
54
- name: { type: "string", description: "Name option" },
55
- });
56
- });
57
- });
58
-
59
- describe("optional metadata", () => {
60
- test("subCommands defaults to undefined", () => {
61
- const cmd = new TestCommand();
62
- expect(cmd.subCommands).toBeUndefined();
63
- });
64
-
65
- test("examples defaults to undefined", () => {
66
- const cmd = new TestCommand();
67
- expect(cmd.examples).toBeUndefined();
68
- });
69
-
70
- test("longDescription defaults to undefined", () => {
71
- const cmd = new TestCommand();
72
- expect(cmd.longDescription).toBeUndefined();
73
- });
74
- });
75
-
76
- describe("supportsCli", () => {
77
- test("returns true for command with execute", () => {
78
- const cmd = new TestCommand();
79
- expect(cmd.supportsCli()).toBe(true);
80
- });
81
- });
82
-
83
- describe("supportsTui", () => {
84
- test("returns true for command with execute", () => {
85
- const cmd = new TestCommand();
86
- expect(cmd.supportsTui()).toBe(true);
87
- });
88
- });
89
-
90
- describe("both modes", () => {
91
- test("command supports both CLI and TUI", () => {
92
- const cmd = new TestCommand();
93
- expect(cmd.supportsCli()).toBe(true);
94
- expect(cmd.supportsTui()).toBe(true);
95
- });
96
- });
97
-
98
- describe("validate", () => {
99
- test("passes for command with execute", () => {
100
- const cmd = new TestCommand();
101
- expect(() => cmd.validate()).not.toThrow();
102
- });
103
- });
104
-
105
- describe("subcommands", () => {
106
- test("hasSubCommands returns false when no subcommands", () => {
107
- const cmd = new TestCommand();
108
- expect(cmd.hasSubCommands()).toBe(false);
109
- });
110
-
111
- test("hasSubCommands returns true when subcommands exist", () => {
112
- const cmd = new TestCommand();
113
- cmd.subCommands = [new SimpleCommand()];
114
- expect(cmd.hasSubCommands()).toBe(true);
115
- });
116
-
117
- test("getSubCommand finds subcommand by name", () => {
118
- const cmd = new TestCommand();
119
- const subCmd = new SimpleCommand();
120
- cmd.subCommands = [subCmd];
121
- expect(cmd.getSubCommand("simple")).toBe(subCmd);
122
- });
123
-
124
- test("getSubCommand returns undefined for unknown name", () => {
125
- const cmd = new TestCommand();
126
- cmd.subCommands = [new SimpleCommand()];
127
- expect(cmd.getSubCommand("unknown")).toBeUndefined();
128
- });
129
- });
130
- });