@pablozaiden/terminatui 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 (95) hide show
  1. package/.devcontainer/devcontainer.json +19 -0
  2. package/.devcontainer/install-prerequisites.sh +49 -0
  3. package/.github/workflows/copilot-setup-steps.yml +32 -0
  4. package/.github/workflows/pull-request.yml +27 -0
  5. package/.github/workflows/release-npm-package.yml +78 -0
  6. package/LICENSE +21 -0
  7. package/README.md +524 -0
  8. package/examples/tui-app/commands/greet.ts +75 -0
  9. package/examples/tui-app/commands/index.ts +3 -0
  10. package/examples/tui-app/commands/math.ts +114 -0
  11. package/examples/tui-app/commands/status.ts +75 -0
  12. package/examples/tui-app/index.ts +34 -0
  13. package/guides/01-hello-world.md +96 -0
  14. package/guides/02-adding-options.md +103 -0
  15. package/guides/03-multiple-commands.md +163 -0
  16. package/guides/04-subcommands.md +206 -0
  17. package/guides/05-interactive-tui.md +194 -0
  18. package/guides/06-config-validation.md +264 -0
  19. package/guides/07-async-cancellation.md +388 -0
  20. package/guides/08-complete-application.md +673 -0
  21. package/guides/README.md +74 -0
  22. package/package.json +32 -0
  23. package/src/__tests__/application.test.ts +425 -0
  24. package/src/__tests__/buildCliCommand.test.ts +125 -0
  25. package/src/__tests__/builtins.test.ts +133 -0
  26. package/src/__tests__/colors.test.ts +127 -0
  27. package/src/__tests__/command.test.ts +157 -0
  28. package/src/__tests__/commandClass.test.ts +130 -0
  29. package/src/__tests__/context.test.ts +97 -0
  30. package/src/__tests__/help.test.ts +412 -0
  31. package/src/__tests__/parser.test.ts +268 -0
  32. package/src/__tests__/registry.test.ts +195 -0
  33. package/src/__tests__/registryNew.test.ts +160 -0
  34. package/src/__tests__/schemaToFields.test.ts +176 -0
  35. package/src/__tests__/table.test.ts +146 -0
  36. package/src/__tests__/tui.test.ts +26 -0
  37. package/src/builtins/help.ts +85 -0
  38. package/src/builtins/index.ts +4 -0
  39. package/src/builtins/settings.ts +106 -0
  40. package/src/builtins/version.ts +72 -0
  41. package/src/cli/help.ts +174 -0
  42. package/src/cli/index.ts +3 -0
  43. package/src/cli/output/colors.ts +74 -0
  44. package/src/cli/output/index.ts +2 -0
  45. package/src/cli/output/table.ts +141 -0
  46. package/src/cli/parser.ts +241 -0
  47. package/src/commands/help.ts +50 -0
  48. package/src/commands/index.ts +1 -0
  49. package/src/components/index.ts +147 -0
  50. package/src/core/application.ts +461 -0
  51. package/src/core/command.ts +269 -0
  52. package/src/core/context.ts +112 -0
  53. package/src/core/help.ts +214 -0
  54. package/src/core/index.ts +15 -0
  55. package/src/core/logger.ts +164 -0
  56. package/src/core/registry.ts +140 -0
  57. package/src/hooks/index.ts +131 -0
  58. package/src/index.ts +137 -0
  59. package/src/registry/commandRegistry.ts +77 -0
  60. package/src/registry/index.ts +1 -0
  61. package/src/tui/TuiApp.tsx +582 -0
  62. package/src/tui/TuiApplication.tsx +230 -0
  63. package/src/tui/app.ts +29 -0
  64. package/src/tui/components/ActionButton.tsx +36 -0
  65. package/src/tui/components/CliModal.tsx +81 -0
  66. package/src/tui/components/CommandSelector.tsx +159 -0
  67. package/src/tui/components/ConfigForm.tsx +148 -0
  68. package/src/tui/components/EditorModal.tsx +177 -0
  69. package/src/tui/components/FieldRow.tsx +30 -0
  70. package/src/tui/components/Header.tsx +31 -0
  71. package/src/tui/components/JsonHighlight.tsx +128 -0
  72. package/src/tui/components/LogsPanel.tsx +86 -0
  73. package/src/tui/components/ResultsPanel.tsx +93 -0
  74. package/src/tui/components/StatusBar.tsx +59 -0
  75. package/src/tui/components/index.ts +13 -0
  76. package/src/tui/components/types.ts +30 -0
  77. package/src/tui/context/KeyboardContext.tsx +118 -0
  78. package/src/tui/context/index.ts +7 -0
  79. package/src/tui/hooks/index.ts +35 -0
  80. package/src/tui/hooks/useClipboard.ts +66 -0
  81. package/src/tui/hooks/useCommandExecutor.ts +131 -0
  82. package/src/tui/hooks/useConfigState.ts +171 -0
  83. package/src/tui/hooks/useKeyboardHandler.ts +91 -0
  84. package/src/tui/hooks/useLogStream.ts +96 -0
  85. package/src/tui/hooks/useSpinner.ts +46 -0
  86. package/src/tui/index.ts +65 -0
  87. package/src/tui/theme.ts +21 -0
  88. package/src/tui/utils/buildCliCommand.ts +90 -0
  89. package/src/tui/utils/index.ts +13 -0
  90. package/src/tui/utils/parameterPersistence.ts +96 -0
  91. package/src/tui/utils/schemaToFields.ts +144 -0
  92. package/src/types/command.ts +103 -0
  93. package/src/types/execution.ts +11 -0
  94. package/src/types/index.ts +1 -0
  95. package/tsconfig.json +25 -0
@@ -0,0 +1,96 @@
1
+ import { useState, useEffect, useCallback } from "react";
2
+
3
+ /**
4
+ * Log levels for display styling.
5
+ */
6
+ export enum LogLevel {
7
+ Silly = "silly",
8
+ Trace = "trace",
9
+ Debug = "debug",
10
+ Info = "info",
11
+ Warn = "warn",
12
+ Error = "error",
13
+ Fatal = "fatal",
14
+ }
15
+
16
+ /**
17
+ * Log entry for display.
18
+ */
19
+ export interface LogEntry {
20
+ timestamp: Date;
21
+ level: LogLevel;
22
+ message: string;
23
+ }
24
+
25
+ /**
26
+ * Log event emitted by the log source.
27
+ */
28
+ export interface LogEvent {
29
+ level: LogLevel;
30
+ message: string;
31
+ }
32
+
33
+ /**
34
+ * Log source that can be subscribed to.
35
+ */
36
+ export interface LogSource {
37
+ /** Subscribe to log events, returns unsubscribe function */
38
+ subscribe: (callback: (event: LogEvent) => void) => () => void;
39
+ }
40
+
41
+ export interface UseLogStreamResult {
42
+ /** All collected log entries */
43
+ logs: LogEntry[];
44
+ /** Clear all logs */
45
+ clearLogs: () => void;
46
+ /** Add a log entry manually */
47
+ addLog: (level: LogLevel, message: string) => void;
48
+ }
49
+
50
+ /**
51
+ * Hook for subscribing to a log stream.
52
+ *
53
+ * @param source - Optional log source to subscribe to
54
+ * @returns Log stream state and functions
55
+ *
56
+ * @example
57
+ * ```tsx
58
+ * const { logs, clearLogs } = useLogStream(myLogSource);
59
+ * ```
60
+ */
61
+ export function useLogStream(source?: LogSource): UseLogStreamResult {
62
+ const [logs, setLogs] = useState<LogEntry[]>([]);
63
+
64
+ // Subscribe to log source
65
+ useEffect(() => {
66
+ if (!source) return;
67
+
68
+ const unsubscribe = source.subscribe((event: LogEvent) => {
69
+ setLogs((prev) => {
70
+ const newEntry: LogEntry = {
71
+ timestamp: new Date(),
72
+ level: event.level,
73
+ message: event.message,
74
+ };
75
+ return [...prev, newEntry];
76
+ });
77
+ });
78
+
79
+ return () => {
80
+ unsubscribe?.();
81
+ };
82
+ }, [source]);
83
+
84
+ const clearLogs = useCallback(() => {
85
+ setLogs([]);
86
+ }, []);
87
+
88
+ const addLog = useCallback((level: LogLevel, message: string) => {
89
+ setLogs((prev) => [
90
+ ...prev,
91
+ { timestamp: new Date(), level, message },
92
+ ]);
93
+ }, []);
94
+
95
+ return { logs, clearLogs, addLog };
96
+ }
@@ -0,0 +1,46 @@
1
+ import { useState, useEffect, useMemo } from "react";
2
+
3
+ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
4
+ const SPINNER_INTERVAL = 80;
5
+
6
+ export interface UseSpinnerResult {
7
+ /** Current frame index */
8
+ frameIndex: number;
9
+ /** Current spinner character */
10
+ frame: string;
11
+ }
12
+
13
+ /**
14
+ * Hook for animated spinner.
15
+ *
16
+ * @param active - Whether the spinner is active
17
+ * @returns Spinner state with current frame
18
+ */
19
+ export function useSpinner(active: boolean): UseSpinnerResult {
20
+ const [frameIndex, setFrameIndex] = useState(0);
21
+
22
+ useEffect(() => {
23
+ if (!active) {
24
+ setFrameIndex(0);
25
+ return;
26
+ }
27
+
28
+ const interval = setInterval(() => {
29
+ setFrameIndex((prev) => {
30
+ // Reset to avoid overflow
31
+ if (prev >= Number.MAX_SAFE_INTEGER / 2) {
32
+ return 0;
33
+ }
34
+ return prev + 1;
35
+ });
36
+ }, SPINNER_INTERVAL);
37
+
38
+ return () => clearInterval(interval);
39
+ }, [active]);
40
+
41
+ const frame = useMemo(() => {
42
+ return SPINNER_FRAMES[frameIndex % SPINNER_FRAMES.length]!;
43
+ }, [frameIndex]);
44
+
45
+ return { frameIndex, frame };
46
+ }
@@ -0,0 +1,65 @@
1
+ // Main TUI components
2
+ export { TuiApp } from "./TuiApp.tsx";
3
+ export { TuiApplication, type TuiApplicationConfig, type CustomField } from "./TuiApplication.tsx";
4
+
5
+ // Theme
6
+ export { Theme, type ThemeColors } from "./theme.ts";
7
+
8
+ // Context
9
+ export {
10
+ KeyboardProvider,
11
+ useKeyboardContext,
12
+ KeyboardPriority,
13
+ type KeyboardEvent,
14
+ type KeyboardHandler,
15
+ } from "./context/index.ts";
16
+
17
+ // Hooks
18
+ export {
19
+ useKeyboardHandler,
20
+ useClipboard,
21
+ useSpinner,
22
+ useConfigState,
23
+ useCommandExecutor,
24
+ useLogStream,
25
+ LogLevel,
26
+ type UseClipboardResult,
27
+ type UseSpinnerResult,
28
+ type UseConfigStateOptions,
29
+ type UseConfigStateResult,
30
+ type UseCommandExecutorResult,
31
+ type LogEntry,
32
+ type LogEvent,
33
+ type LogSource,
34
+ type UseLogStreamResult,
35
+ } from "./hooks/index.ts";
36
+
37
+ // Components
38
+ export {
39
+ FieldRow,
40
+ ActionButton,
41
+ Header,
42
+ StatusBar,
43
+ LogsPanel,
44
+ ResultsPanel,
45
+ ConfigForm,
46
+ EditorModal,
47
+ CliModal,
48
+ CommandSelector,
49
+ JsonHighlight,
50
+ type FieldType,
51
+ type FieldOption,
52
+ type FieldConfig,
53
+ type JsonHighlightProps,
54
+ } from "./components/index.ts";
55
+
56
+ // Utilities
57
+ export {
58
+ schemaToFieldConfigs,
59
+ groupFieldConfigs,
60
+ getFieldDisplayValue,
61
+ buildCliCommand,
62
+ } from "./utils/index.ts";
63
+
64
+ // Legacy export for backward compatibility
65
+ export * from "./app.ts";
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Default TUI theme colors.
3
+ */
4
+ export const Theme = {
5
+ background: "#0b0c10",
6
+ 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
+ success: "#98c379",
18
+ warning: "#f5c542",
19
+ } as const;
20
+
21
+ export type ThemeColors = typeof Theme;
@@ -0,0 +1,90 @@
1
+ import type { OptionSchema, OptionDef, OptionValues } from "../../types/command.ts";
2
+
3
+ /**
4
+ * Escape a shell argument if it contains special characters.
5
+ */
6
+ function escapeArg(arg: string): string {
7
+ if (/[^a-zA-Z0-9_\-./=]/.test(arg)) {
8
+ return `"${arg.replace(/"/g, '\\"')}"`;
9
+ }
10
+ return arg;
11
+ }
12
+
13
+ /**
14
+ * Build a CLI command string from values and schema.
15
+ *
16
+ * @param appName - The application name (e.g., "myapp")
17
+ * @param commandPath - The command path (e.g., ["run"], ["db", "migrate"])
18
+ * @param schema - The option schema
19
+ * @param values - The current option values
20
+ * @returns CLI command string
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * const cmd = buildCliCommand("myapp", ["run"], runOptions, config);
25
+ * // Returns: "myapp run --agent opencode --fixture test.json"
26
+ * ```
27
+ */
28
+ export function buildCliCommand<T extends OptionSchema>(
29
+ appName: string,
30
+ commandPath: string[],
31
+ schema: T,
32
+ values: OptionValues<T>
33
+ ): string {
34
+ const parts: string[] = [appName, ...commandPath];
35
+
36
+ for (const [key, def] of Object.entries(schema) as [keyof T & string, OptionDef][]) {
37
+ const value = values[key];
38
+ const defaultValue = def.default;
39
+
40
+ // Skip if value equals default (no need to include)
41
+ if (value === defaultValue) {
42
+ continue;
43
+ }
44
+
45
+ // Skip undefined/null values
46
+ if (value === undefined || value === null) {
47
+ continue;
48
+ }
49
+
50
+ const optionName = toKebabCase(key);
51
+
52
+ switch (def.type) {
53
+ case "boolean":
54
+ if (value === true) {
55
+ parts.push(`--${optionName}`);
56
+ } else if (value === false && defaultValue === true) {
57
+ parts.push(`--no-${optionName}`);
58
+ }
59
+ break;
60
+
61
+ case "array":
62
+ if (Array.isArray(value) && value.length > 0) {
63
+ for (const item of value) {
64
+ parts.push(`--${optionName}`, escapeArg(String(item)));
65
+ }
66
+ }
67
+ break;
68
+
69
+ case "number":
70
+ parts.push(`--${optionName}`, String(value));
71
+ break;
72
+
73
+ case "string":
74
+ default:
75
+ if (String(value).trim() !== "") {
76
+ parts.push(`--${optionName}`, escapeArg(String(value)));
77
+ }
78
+ break;
79
+ }
80
+ }
81
+
82
+ return parts.join(" ");
83
+ }
84
+
85
+ /**
86
+ * Convert camelCase to kebab-case.
87
+ */
88
+ function toKebabCase(str: string): string {
89
+ return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
90
+ }
@@ -0,0 +1,13 @@
1
+ export {
2
+ schemaToFieldConfigs,
3
+ groupFieldConfigs,
4
+ getFieldDisplayValue,
5
+ } from "./schemaToFields.ts";
6
+
7
+ export { buildCliCommand } from "./buildCliCommand.ts";
8
+
9
+ export {
10
+ loadPersistedParameters,
11
+ savePersistedParameters,
12
+ clearPersistedParameters,
13
+ } from "./parameterPersistence.ts";
@@ -0,0 +1,96 @@
1
+ import { homedir } from "os";
2
+ import { join } from "path";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "fs";
4
+
5
+ /**
6
+ * Get the directory path for storing app configuration.
7
+ * Creates the directory if it doesn't exist.
8
+ *
9
+ * @param appName - The application name
10
+ * @returns The path to the app config directory
11
+ */
12
+ function getAppConfigDir(appName: string): string {
13
+ const configDir = join(homedir(), `.${appName}`);
14
+ if (!existsSync(configDir)) {
15
+ mkdirSync(configDir, { recursive: true });
16
+ }
17
+ return configDir;
18
+ }
19
+
20
+ /**
21
+ * Get the file path for storing command parameters.
22
+ *
23
+ * @param appName - The application name
24
+ * @param commandName - The command name
25
+ * @returns The path to the parameters file
26
+ */
27
+ function getParametersFilePath(appName: string, commandName: string): string {
28
+ const configDir = getAppConfigDir(appName);
29
+ return join(configDir, `${commandName}.parameters.json`);
30
+ }
31
+
32
+ /**
33
+ * Load persisted parameters for a command.
34
+ *
35
+ * @param appName - The application name
36
+ * @param commandName - The command name
37
+ * @returns The persisted parameters, or an empty object if none exist
38
+ */
39
+ export function loadPersistedParameters(
40
+ appName: string,
41
+ commandName: string
42
+ ): Record<string, unknown> {
43
+ try {
44
+ const filePath = getParametersFilePath(appName, commandName);
45
+ if (existsSync(filePath)) {
46
+ const content = readFileSync(filePath, "utf-8");
47
+ return JSON.parse(content) as Record<string, unknown>;
48
+ }
49
+ } catch (error) {
50
+ // Silently ignore errors - just return empty object
51
+ console.error(`Failed to load persisted parameters: ${error}`);
52
+ }
53
+ return {};
54
+ }
55
+
56
+ /**
57
+ * Save parameters for a command.
58
+ *
59
+ * @param appName - The application name
60
+ * @param commandName - The command name
61
+ * @param parameters - The parameters to persist
62
+ */
63
+ export function savePersistedParameters(
64
+ appName: string,
65
+ commandName: string,
66
+ parameters: Record<string, unknown>
67
+ ): void {
68
+ try {
69
+ const filePath = getParametersFilePath(appName, commandName);
70
+ writeFileSync(filePath, JSON.stringify(parameters, null, 2), "utf-8");
71
+ } catch (error) {
72
+ // Silently ignore errors
73
+ console.error(`Failed to save persisted parameters: ${error}`);
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Clear persisted parameters for a command.
79
+ *
80
+ * @param appName - The application name
81
+ * @param commandName - The command name
82
+ */
83
+ export function clearPersistedParameters(
84
+ appName: string,
85
+ commandName: string
86
+ ): void {
87
+ try {
88
+ const filePath = getParametersFilePath(appName, commandName);
89
+ if (existsSync(filePath)) {
90
+ unlinkSync(filePath);
91
+ }
92
+ } catch (error) {
93
+ // Silently ignore errors
94
+ console.error(`Failed to clear persisted parameters: ${error}`);
95
+ }
96
+ }
@@ -0,0 +1,144 @@
1
+ import type { OptionSchema, OptionDef } from "../../types/command.ts";
2
+ import type { FieldConfig, FieldType, FieldOption } from "../components/types.ts";
3
+
4
+ /**
5
+ * Convert an option type to a field type.
6
+ */
7
+ function optionTypeToFieldType(def: OptionDef): FieldType {
8
+ // If it has enum values, it's an enum type
9
+ if (def.enum && def.enum.length > 0) {
10
+ return "enum";
11
+ }
12
+
13
+ switch (def.type) {
14
+ case "string":
15
+ return "text";
16
+ case "number":
17
+ return "number";
18
+ case "boolean":
19
+ return "boolean";
20
+ case "array":
21
+ return "text"; // Arrays are edited as comma-separated text
22
+ default:
23
+ return "text";
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Create field options from enum values.
29
+ */
30
+ function createFieldOptions(def: OptionDef): FieldOption[] | undefined {
31
+ if (!def.enum || def.enum.length === 0) {
32
+ return undefined;
33
+ }
34
+
35
+ return def.enum.map((value) => ({
36
+ name: String(value),
37
+ value,
38
+ }));
39
+ }
40
+
41
+ /**
42
+ * Create a label from a key name.
43
+ * Converts camelCase to Title Case.
44
+ */
45
+ function keyToLabel(key: string): string {
46
+ return key
47
+ .replace(/([A-Z])/g, " $1")
48
+ .replace(/^./, (str) => str.toUpperCase())
49
+ .trim();
50
+ }
51
+
52
+ /**
53
+ * Convert an option schema to field configs for TUI forms.
54
+ *
55
+ * @param schema - The option schema to convert
56
+ * @returns Array of field configs sorted by order
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * const fields = schemaToFieldConfigs(myOptions);
61
+ * // Returns FieldConfig[] ready for ConfigForm
62
+ * ```
63
+ */
64
+ export function schemaToFieldConfigs(schema: OptionSchema): FieldConfig[] {
65
+ const fields: FieldConfig[] = [];
66
+
67
+ for (const [key, def] of Object.entries(schema)) {
68
+ // Skip hidden fields
69
+ if (def.tuiHidden) {
70
+ continue;
71
+ }
72
+
73
+ const fieldConfig: FieldConfig = {
74
+ key,
75
+ label: def.label ?? keyToLabel(key),
76
+ type: optionTypeToFieldType(def),
77
+ options: createFieldOptions(def),
78
+ placeholder: def.placeholder,
79
+ group: def.group,
80
+ };
81
+
82
+ fields.push(fieldConfig);
83
+ }
84
+
85
+ // Sort by order if specified, otherwise preserve insertion order
86
+ fields.sort((a, b) => {
87
+ const orderA = schema[a.key]?.order ?? Number.MAX_SAFE_INTEGER;
88
+ const orderB = schema[b.key]?.order ?? Number.MAX_SAFE_INTEGER;
89
+ return orderA - orderB;
90
+ });
91
+
92
+ return fields;
93
+ }
94
+
95
+ /**
96
+ * Group field configs by their group property.
97
+ *
98
+ * @param fields - Field configs to group
99
+ * @returns Map of group name to field configs
100
+ */
101
+ export function groupFieldConfigs(
102
+ fields: FieldConfig[]
103
+ ): Map<string | undefined, FieldConfig[]> {
104
+ const groups = new Map<string | undefined, FieldConfig[]>();
105
+
106
+ for (const field of fields) {
107
+ const group = field.group;
108
+ if (!groups.has(group)) {
109
+ groups.set(group, []);
110
+ }
111
+ groups.get(group)!.push(field);
112
+ }
113
+
114
+ return groups;
115
+ }
116
+
117
+ /**
118
+ * Get display value for a field.
119
+ *
120
+ * @param value - The field value
121
+ * @param fieldConfig - The field configuration
122
+ * @returns Formatted display string
123
+ */
124
+ export function getFieldDisplayValue(
125
+ value: unknown,
126
+ fieldConfig: FieldConfig
127
+ ): string {
128
+ if (fieldConfig.type === "boolean") {
129
+ return value ? "True" : "False";
130
+ }
131
+
132
+ if (fieldConfig.type === "enum" && fieldConfig.options) {
133
+ const option = fieldConfig.options.find((o) => o.value === value);
134
+ return option?.name ?? String(value);
135
+ }
136
+
137
+ const strValue = String(value ?? "");
138
+ if (strValue === "") {
139
+ return "(empty)";
140
+ }
141
+
142
+ // Truncate long values
143
+ return strValue.length > 60 ? strValue.substring(0, 57) + "..." : strValue;
144
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Option definition for command-line arguments
3
+ */
4
+ export interface OptionDef {
5
+ type: "string" | "number" | "boolean" | "array";
6
+ description: string;
7
+ alias?: string;
8
+ default?: unknown;
9
+ required?: boolean;
10
+ env?: string;
11
+ enum?: readonly string[];
12
+ min?: number;
13
+ max?: number;
14
+
15
+ // TUI-specific properties
16
+ /** Display label in TUI form (defaults to key name) */
17
+ label?: string;
18
+ /** Display order in TUI form (lower = first) */
19
+ order?: number;
20
+ /** Group name for organizing fields in TUI */
21
+ group?: string;
22
+ /** Placeholder text for input fields */
23
+ placeholder?: string;
24
+ /** Hide this field from TUI (still available in CLI) */
25
+ tuiHidden?: boolean;
26
+ }
27
+
28
+ /**
29
+ * Schema defining all options for a command
30
+ */
31
+ export type OptionSchema = Record<string, OptionDef>;
32
+
33
+ /**
34
+ * Inferred option values from a schema
35
+ */
36
+ export type OptionValues<T extends OptionSchema> = {
37
+ [K in keyof T]: T[K]["type"] extends "string" ? string
38
+ : T[K]["type"] extends "number" ? number
39
+ : T[K]["type"] extends "boolean" ? boolean
40
+ : T[K]["type"] extends "array" ? string[]
41
+ : unknown;
42
+ };
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
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Execution mode determines how a command runs.
3
+ * - Cli: Command-line mode with arguments, executes and exits
4
+ * - Tui: Terminal UI mode with interactive rendering
5
+ */
6
+ export enum ExecutionMode {
7
+ /** Command-line mode: parse args, execute, exit */
8
+ Cli = "cli",
9
+ /** Terminal UI mode: interactive render loop */
10
+ Tui = "tui",
11
+ }
@@ -0,0 +1 @@
1
+ export * from "./command.ts";