@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,164 @@
1
+ import { EventEmitter } from "events";
2
+ import { Logger as TsLogger } from "tslog";
3
+
4
+ /**
5
+ * Log levels from least to most severe.
6
+ */
7
+ export enum LogLevel {
8
+ Silly = 0,
9
+ Trace = 1,
10
+ Debug = 2,
11
+ Info = 3,
12
+ Warn = 4,
13
+ Error = 5,
14
+ Fatal = 6,
15
+ }
16
+
17
+ /**
18
+ * Event emitted when a log message is written.
19
+ */
20
+ export interface LogEvent {
21
+ message: string;
22
+ level: LogLevel;
23
+ timestamp: Date;
24
+ }
25
+
26
+ /**
27
+ * Logger configuration options.
28
+ */
29
+ export interface LoggerConfig {
30
+ /** Minimum log level to output */
31
+ minLevel?: LogLevel;
32
+ /** Whether to use detailed format (with timestamp/level) */
33
+ detailed?: boolean;
34
+ /** Whether to route logs to TUI event emitter instead of stderr */
35
+ tuiMode?: boolean;
36
+ }
37
+
38
+ /**
39
+ * Logger class that wraps tslog and supports TUI mode.
40
+ * Can be instantiated multiple times for different contexts.
41
+ */
42
+ export class Logger {
43
+ private tsLogger: TsLogger<unknown>;
44
+ private readonly eventEmitter = new EventEmitter();
45
+ private tuiMode: boolean;
46
+ private detailed: boolean;
47
+ private minLevel: LogLevel;
48
+
49
+ constructor(config: LoggerConfig = {}) {
50
+ this.tuiMode = config.tuiMode ?? false;
51
+ this.detailed = config.detailed ?? false;
52
+ this.minLevel = config.minLevel ?? LogLevel.Info;
53
+
54
+ this.tsLogger = this.createTsLogger(this.minLevel);
55
+ }
56
+
57
+ private createTsLogger(minLevel: LogLevel): TsLogger<unknown> {
58
+ return new TsLogger({
59
+ type: "pretty",
60
+ minLevel,
61
+ overwrite: {
62
+ transportFormatted: (
63
+ logMetaMarkup: string,
64
+ logArgs: unknown[],
65
+ logErrors: string[],
66
+ logMeta: unknown
67
+ ) => {
68
+ const baseLine = `${logMetaMarkup}${(logArgs as string[]).join(" ")}${logErrors.join("")}`;
69
+ const simpleLine = `${(logArgs as string[]).join(" ")}${logErrors.join("")}`;
70
+ const meta = logMeta as Record<string, unknown>;
71
+ const levelFromMeta =
72
+ typeof meta?.["logLevelId"] === "number"
73
+ ? (meta["logLevelId"] as LogLevel)
74
+ : LogLevel.Info;
75
+
76
+ const output = this.detailed ? baseLine : simpleLine;
77
+
78
+ if (this.tuiMode) {
79
+ this.eventEmitter.emit("log", {
80
+ message: output,
81
+ level: levelFromMeta,
82
+ timestamp: new Date(),
83
+ } satisfies LogEvent);
84
+ } else {
85
+ process.stderr.write(output + "\n");
86
+ }
87
+ },
88
+ },
89
+ });
90
+ }
91
+
92
+ /**
93
+ * Subscribe to log events (for TUI mode).
94
+ */
95
+ onLogEvent(listener: (event: LogEvent) => void): () => void {
96
+ this.eventEmitter.on("log", listener);
97
+ return () => this.eventEmitter.off("log", listener);
98
+ }
99
+
100
+ /**
101
+ * Enable or disable TUI mode.
102
+ */
103
+ setTuiMode(enabled: boolean): void {
104
+ this.tuiMode = enabled;
105
+ }
106
+
107
+ /**
108
+ * Enable or disable detailed log format.
109
+ */
110
+ setDetailed(enabled: boolean): void {
111
+ this.detailed = enabled;
112
+ }
113
+
114
+ /**
115
+ * Set the minimum log level.
116
+ */
117
+ setMinLevel(level: LogLevel): void {
118
+ this.minLevel = level;
119
+ this.tsLogger = this.createTsLogger(level);
120
+ }
121
+
122
+ /**
123
+ * Get the current minimum log level.
124
+ */
125
+ getMinLevel(): LogLevel {
126
+ return this.minLevel;
127
+ }
128
+
129
+ // Logging methods
130
+ silly(...args: unknown[]): void {
131
+ this.tsLogger.silly(...args);
132
+ }
133
+
134
+ trace(...args: unknown[]): void {
135
+ this.tsLogger.trace(...args);
136
+ }
137
+
138
+ debug(...args: unknown[]): void {
139
+ this.tsLogger.debug(...args);
140
+ }
141
+
142
+ info(...args: unknown[]): void {
143
+ this.tsLogger.info(...args);
144
+ }
145
+
146
+ warn(...args: unknown[]): void {
147
+ this.tsLogger.warn(...args);
148
+ }
149
+
150
+ error(...args: unknown[]): void {
151
+ this.tsLogger.error(...args);
152
+ }
153
+
154
+ fatal(...args: unknown[]): void {
155
+ this.tsLogger.fatal(...args);
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Create a new logger instance with the given configuration.
161
+ */
162
+ export function createLogger(config: LoggerConfig = {}): Logger {
163
+ return new Logger(config);
164
+ }
@@ -0,0 +1,140 @@
1
+ import { type AnyCommand } from "./command.ts";
2
+
3
+ /**
4
+ * Registry for managing commands.
5
+ * Provides registration, lookup, and resolution of command paths.
6
+ */
7
+ export class CommandRegistry {
8
+ private readonly commands = new Map<string, AnyCommand>();
9
+
10
+ /**
11
+ * Register a command.
12
+ * @param command The command to register
13
+ * @throws If a command with the same name is already registered
14
+ */
15
+ register(command: AnyCommand): void {
16
+ command.validate();
17
+
18
+ if (this.commands.has(command.name)) {
19
+ throw new Error(`Command '${command.name}' is already registered`);
20
+ }
21
+
22
+ this.commands.set(command.name, command);
23
+ }
24
+
25
+ /**
26
+ * Register multiple commands.
27
+ * @param commands Array of commands to register
28
+ */
29
+ registerAll(commands: AnyCommand[]): void {
30
+ for (const command of commands) {
31
+ this.register(command);
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Get a command by name.
37
+ * @param name Command name
38
+ * @returns The command or undefined if not found
39
+ */
40
+ get(name: string): AnyCommand | undefined {
41
+ return this.commands.get(name);
42
+ }
43
+
44
+ /**
45
+ * Check if a command is registered.
46
+ * @param name Command name
47
+ */
48
+ has(name: string): boolean {
49
+ return this.commands.has(name);
50
+ }
51
+
52
+ /**
53
+ * Get all registered commands.
54
+ * @returns Array of all commands
55
+ */
56
+ list(): AnyCommand[] {
57
+ return Array.from(this.commands.values());
58
+ }
59
+
60
+ /**
61
+ * Get command names.
62
+ * @returns Array of command names
63
+ */
64
+ names(): string[] {
65
+ return Array.from(this.commands.keys());
66
+ }
67
+
68
+ /**
69
+ * Resolve a command path to a command.
70
+ * Supports nested subcommands like ["run", "check", "help"].
71
+ *
72
+ * @param path Array of command names forming the path
73
+ * @returns Object with resolved command, remaining path, and full path
74
+ */
75
+ resolve(path: string[]): ResolveResult {
76
+ if (path.length === 0) {
77
+ return { command: undefined, remainingPath: [], resolvedPath: [] };
78
+ }
79
+
80
+ const [first, ...rest] = path;
81
+ if (!first) {
82
+ return { command: undefined, remainingPath: path, resolvedPath: [] };
83
+ }
84
+
85
+ const command = this.get(first);
86
+
87
+ if (!command) {
88
+ return { command: undefined, remainingPath: path, resolvedPath: [] };
89
+ }
90
+
91
+ // Resolve nested subcommands
92
+ let current = command;
93
+ const resolvedPath: string[] = [first];
94
+ let remainingPath: string[] = rest;
95
+
96
+ while (remainingPath.length > 0) {
97
+ const nextName = remainingPath[0];
98
+ if (!nextName) break;
99
+
100
+ const subCommand = current.getSubCommand(nextName);
101
+
102
+ if (!subCommand) {
103
+ break;
104
+ }
105
+
106
+ current = subCommand;
107
+ resolvedPath.push(nextName);
108
+ remainingPath = remainingPath.slice(1);
109
+ }
110
+
111
+ return { command: current, remainingPath, resolvedPath };
112
+ }
113
+
114
+ /**
115
+ * Clear all registered commands.
116
+ * Useful for testing.
117
+ */
118
+ clear(): void {
119
+ this.commands.clear();
120
+ }
121
+
122
+ /**
123
+ * Get the number of registered commands.
124
+ */
125
+ get size(): number {
126
+ return this.commands.size;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Result of resolving a command path.
132
+ */
133
+ export interface ResolveResult {
134
+ /** The resolved command, or undefined if not found */
135
+ command: AnyCommand | undefined;
136
+ /** Path elements that couldn't be resolved (remaining after last matched command) */
137
+ remainingPath: string[];
138
+ /** Path elements that were successfully resolved */
139
+ resolvedPath: string[];
140
+ }
@@ -0,0 +1,131 @@
1
+ import { useState, useCallback } from "react";
2
+ import type { Command, OptionSchema, OptionValues } from "../types/command.ts";
3
+
4
+ /**
5
+ * Hook for command execution
6
+ */
7
+ export function useCommand<T extends OptionSchema>(command: Command<T>) {
8
+ const [isExecuting, setIsExecuting] = useState(false);
9
+ const [error, setError] = useState<Error | null>(null);
10
+
11
+ const execute = useCallback(
12
+ async (options: OptionValues<T>) => {
13
+ setIsExecuting(true);
14
+ setError(null);
15
+
16
+ try {
17
+ await command.execute({
18
+ options,
19
+ args: [],
20
+ commandPath: [command.name],
21
+ });
22
+ } catch (err) {
23
+ setError(err instanceof Error ? err : new Error(String(err)));
24
+ } finally {
25
+ setIsExecuting(false);
26
+ }
27
+ },
28
+ [command]
29
+ );
30
+
31
+ return { execute, isExecuting, error };
32
+ }
33
+
34
+ /**
35
+ * Hook for managing options state
36
+ */
37
+ export function useOptions<T extends OptionSchema>(
38
+ schema: T,
39
+ initialValues?: Partial<OptionValues<T>>
40
+ ) {
41
+ const [values, setValues] = useState<OptionValues<T>>(() => {
42
+ const defaults: Record<string, unknown> = {};
43
+ for (const [key, def] of Object.entries(schema)) {
44
+ defaults[key] = initialValues?.[key as keyof T] ?? def.default;
45
+ }
46
+ return defaults as OptionValues<T>;
47
+ });
48
+
49
+ const setValue = useCallback(
50
+ <K extends keyof T>(key: K, value: OptionValues<T>[K]) => {
51
+ setValues((prev) => ({ ...prev, [key]: value }));
52
+ },
53
+ []
54
+ );
55
+
56
+ const reset = useCallback(() => {
57
+ const defaults: Record<string, unknown> = {};
58
+ for (const [key, def] of Object.entries(schema)) {
59
+ defaults[key] = def.default;
60
+ }
61
+ setValues(defaults as OptionValues<T>);
62
+ }, [schema]);
63
+
64
+ return { values, setValue, setValues, reset };
65
+ }
66
+
67
+ /**
68
+ * Hook for navigation between views
69
+ */
70
+ export function useNavigation(views: string[], initialView?: string) {
71
+ const [currentView, setCurrentView] = useState(initialView ?? views[0] ?? "");
72
+ const [history, setHistory] = useState<string[]>([]);
73
+
74
+ const navigate = useCallback((view: string) => {
75
+ setHistory((prev) => [...prev, currentView]);
76
+ setCurrentView(view);
77
+ }, [currentView]);
78
+
79
+ const goBack = useCallback(() => {
80
+ const prev = history[history.length - 1];
81
+ if (prev) {
82
+ setHistory((h) => h.slice(0, -1));
83
+ setCurrentView(prev);
84
+ }
85
+ }, [history]);
86
+
87
+ const canGoBack = history.length > 0;
88
+
89
+ return { currentView, navigate, goBack, canGoBack };
90
+ }
91
+
92
+ /**
93
+ * Hook for modal state
94
+ */
95
+ export function useModal(initialOpen = false) {
96
+ const [isOpen, setIsOpen] = useState(initialOpen);
97
+
98
+ const open = useCallback(() => setIsOpen(true), []);
99
+ const close = useCallback(() => setIsOpen(false), []);
100
+ const toggle = useCallback(() => setIsOpen((prev) => !prev), []);
101
+
102
+ return { isOpen, open, close, toggle };
103
+ }
104
+
105
+ /**
106
+ * Hook for async operations
107
+ */
108
+ export function useAsync<T>(asyncFn: () => Promise<T>) {
109
+ const [data, setData] = useState<T | null>(null);
110
+ const [isLoading, setIsLoading] = useState(false);
111
+ const [error, setError] = useState<Error | null>(null);
112
+
113
+ const execute = useCallback(async () => {
114
+ setIsLoading(true);
115
+ setError(null);
116
+
117
+ try {
118
+ const result = await asyncFn();
119
+ setData(result);
120
+ return result;
121
+ } catch (err) {
122
+ const error = err instanceof Error ? err : new Error(String(err));
123
+ setError(error);
124
+ throw error;
125
+ } finally {
126
+ setIsLoading(false);
127
+ }
128
+ }, [asyncFn]);
129
+
130
+ return { data, isLoading, error, execute };
131
+ }
package/src/index.ts ADDED
@@ -0,0 +1,137 @@
1
+ // Core - New OOP Architecture
2
+ export {
3
+ Application,
4
+ AppContext,
5
+ Command,
6
+ CommandRegistry,
7
+ ConfigValidationError,
8
+ AbortError,
9
+ Logger,
10
+ createLogger,
11
+ LogLevel,
12
+ generateCommandHelp,
13
+ generateAppHelp,
14
+ } from "./core/index.ts";
15
+ export type {
16
+ ApplicationConfig,
17
+ ApplicationHooks,
18
+ GlobalOptions,
19
+ AppConfig,
20
+ AnyCommand,
21
+ CommandExample,
22
+ CommandResult,
23
+ CommandExecutionContext,
24
+ ResolveResult,
25
+ LoggerConfig,
26
+ LogEvent,
27
+ HelpOptions,
28
+ } from "./core/index.ts";
29
+
30
+ // Execution Mode
31
+ export { ExecutionMode } from "./types/execution.ts";
32
+
33
+ // Built-in Commands (new)
34
+ export { HelpCommand, VersionCommand, SettingsCommand, formatVersion } from "./builtins/index.ts";
35
+ export type { VersionConfig } from "./builtins/index.ts";
36
+
37
+ // TUI Framework (new)
38
+ export {
39
+ TuiApp,
40
+ TuiApplication,
41
+ Theme,
42
+ KeyboardProvider,
43
+ KeyboardPriority,
44
+ useKeyboardHandler,
45
+ useClipboard,
46
+ useSpinner,
47
+ useConfigState,
48
+ useCommandExecutor,
49
+ useLogStream,
50
+ LogLevel as TuiLogLevel,
51
+ FieldRow,
52
+ ActionButton,
53
+ Header,
54
+ StatusBar,
55
+ LogsPanel,
56
+ ResultsPanel,
57
+ ConfigForm,
58
+ EditorModal,
59
+ CliModal,
60
+ CommandSelector,
61
+ JsonHighlight,
62
+ schemaToFieldConfigs,
63
+ groupFieldConfigs,
64
+ getFieldDisplayValue,
65
+ buildCliCommand,
66
+ } from "./tui/index.ts";
67
+ export type {
68
+ TuiApplicationConfig,
69
+ ThemeColors,
70
+ KeyboardEvent as TuiKeyboardEvent,
71
+ KeyboardHandler,
72
+ UseClipboardResult,
73
+ UseSpinnerResult,
74
+ UseConfigStateOptions,
75
+ UseConfigStateResult,
76
+ UseCommandExecutorResult,
77
+ LogEntry,
78
+ LogEvent as TuiLogEvent,
79
+ LogSource,
80
+ UseLogStreamResult,
81
+ FieldType,
82
+ FieldOption,
83
+ FieldConfig,
84
+ } from "./tui/index.ts";
85
+
86
+ // Types (legacy, for backwards compatibility)
87
+ export { defineCommand, defineTuiCommand } from "./types/command.ts";
88
+ export type {
89
+ Command as LegacyCommand,
90
+ TuiCommand,
91
+ OptionDef,
92
+ OptionSchema,
93
+ OptionValues,
94
+ CommandContext,
95
+ CommandExecutor,
96
+ } from "./types/command.ts";
97
+
98
+ // CLI Parser
99
+ export {
100
+ parseCliArgs,
101
+ extractCommandChain,
102
+ schemaToParseArgsOptions,
103
+ parseOptionValues,
104
+ validateOptions,
105
+ } from "./cli/parser.ts";
106
+ export type { ParseResult, ParseError } from "./cli/parser.ts";
107
+
108
+ // Registry (legacy)
109
+ export { createCommandRegistry } from "./registry/commandRegistry.ts";
110
+ export type { CommandRegistry as LegacyCommandRegistry } from "./registry/commandRegistry.ts";
111
+
112
+ // Built-in Commands (legacy)
113
+ export { createHelpCommand } from "./commands/help.ts";
114
+
115
+ // CLI Output
116
+ export { colors, supportsColors } from "./cli/output/colors.ts";
117
+ export { table, keyValueList, bulletList, numberedList } from "./cli/output/table.ts";
118
+
119
+ // Help Generation (legacy)
120
+ export {
121
+ generateHelp,
122
+ formatCommands,
123
+ formatOptions as formatOptionsLegacy,
124
+ formatUsage as formatUsageLegacy,
125
+ formatExamples as formatExamplesLegacy,
126
+ getCommandSummary,
127
+ } from "./cli/help.ts";
128
+
129
+ // TUI
130
+ export { createApp } from "./tui/app.ts";
131
+ export type { AppConfig as TuiAppConfig, AppState } from "./tui/app.ts";
132
+
133
+ // Components
134
+ export { Box, Text, Input, Select, Button, Modal, Spinner } from "./components/index.ts";
135
+
136
+ // Hooks
137
+ export { useCommand, useOptions, useNavigation, useModal, useAsync } from "./hooks/index.ts";
@@ -0,0 +1,77 @@
1
+ import type { Command, OptionSchema } from "../types/command.ts";
2
+
3
+ /**
4
+ * Command registry for managing commands
5
+ */
6
+ export interface CommandRegistry<T extends OptionSchema = OptionSchema> {
7
+ register(command: Command<T>): void;
8
+ get(name: string): Command<T> | undefined;
9
+ resolve(nameOrAlias: string): Command<T> | undefined;
10
+ has(nameOrAlias: string): boolean;
11
+ list(): Command<T>[];
12
+ getNames(): string[];
13
+ getCommandMap(): Record<string, Command<T>>;
14
+ }
15
+
16
+ /**
17
+ * Create a command registry
18
+ */
19
+ export function createCommandRegistry<
20
+ T extends OptionSchema = OptionSchema,
21
+ >(): CommandRegistry<T> {
22
+ const commands = new Map<string, Command<T>>();
23
+ const aliases = new Map<string, string>();
24
+
25
+ return {
26
+ register(command: Command<T>): void {
27
+ if (commands.has(command.name)) {
28
+ throw new Error(`Command "${command.name}" is already registered`);
29
+ }
30
+
31
+ commands.set(command.name, command);
32
+
33
+ if (command.aliases) {
34
+ for (const alias of command.aliases) {
35
+ if (aliases.has(alias) || commands.has(alias)) {
36
+ throw new Error(`Alias "${alias}" conflicts with existing command or alias`);
37
+ }
38
+ aliases.set(alias, command.name);
39
+ }
40
+ }
41
+ },
42
+
43
+ get(name: string): Command<T> | undefined {
44
+ return commands.get(name);
45
+ },
46
+
47
+ resolve(nameOrAlias: string): Command<T> | undefined {
48
+ // Try direct name first
49
+ const cmd = commands.get(nameOrAlias);
50
+ if (cmd) return cmd;
51
+
52
+ // Try alias
53
+ const resolvedName = aliases.get(nameOrAlias);
54
+ if (resolvedName) {
55
+ return commands.get(resolvedName);
56
+ }
57
+
58
+ return undefined;
59
+ },
60
+
61
+ has(nameOrAlias: string): boolean {
62
+ return commands.has(nameOrAlias) || aliases.has(nameOrAlias);
63
+ },
64
+
65
+ list(): Command<T>[] {
66
+ return Array.from(commands.values());
67
+ },
68
+
69
+ getNames(): string[] {
70
+ return Array.from(commands.keys());
71
+ },
72
+
73
+ getCommandMap(): Record<string, Command<T>> {
74
+ return Object.fromEntries(commands);
75
+ },
76
+ };
77
+ }
@@ -0,0 +1 @@
1
+ export * from "./commandRegistry.ts";