@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.
- package/.devcontainer/devcontainer.json +19 -0
- package/.devcontainer/install-prerequisites.sh +49 -0
- package/.github/workflows/copilot-setup-steps.yml +32 -0
- package/.github/workflows/pull-request.yml +27 -0
- package/.github/workflows/release-npm-package.yml +78 -0
- package/LICENSE +21 -0
- package/README.md +524 -0
- package/examples/tui-app/commands/greet.ts +75 -0
- package/examples/tui-app/commands/index.ts +3 -0
- package/examples/tui-app/commands/math.ts +114 -0
- package/examples/tui-app/commands/status.ts +75 -0
- package/examples/tui-app/index.ts +34 -0
- package/guides/01-hello-world.md +96 -0
- package/guides/02-adding-options.md +103 -0
- package/guides/03-multiple-commands.md +163 -0
- package/guides/04-subcommands.md +206 -0
- package/guides/05-interactive-tui.md +194 -0
- package/guides/06-config-validation.md +264 -0
- package/guides/07-async-cancellation.md +388 -0
- package/guides/08-complete-application.md +673 -0
- package/guides/README.md +74 -0
- package/package.json +32 -0
- package/src/__tests__/application.test.ts +425 -0
- package/src/__tests__/buildCliCommand.test.ts +125 -0
- package/src/__tests__/builtins.test.ts +133 -0
- package/src/__tests__/colors.test.ts +127 -0
- package/src/__tests__/command.test.ts +157 -0
- package/src/__tests__/commandClass.test.ts +130 -0
- package/src/__tests__/context.test.ts +97 -0
- package/src/__tests__/help.test.ts +412 -0
- package/src/__tests__/parser.test.ts +268 -0
- package/src/__tests__/registry.test.ts +195 -0
- package/src/__tests__/registryNew.test.ts +160 -0
- package/src/__tests__/schemaToFields.test.ts +176 -0
- package/src/__tests__/table.test.ts +146 -0
- package/src/__tests__/tui.test.ts +26 -0
- package/src/builtins/help.ts +85 -0
- package/src/builtins/index.ts +4 -0
- package/src/builtins/settings.ts +106 -0
- package/src/builtins/version.ts +72 -0
- package/src/cli/help.ts +174 -0
- package/src/cli/index.ts +3 -0
- package/src/cli/output/colors.ts +74 -0
- package/src/cli/output/index.ts +2 -0
- package/src/cli/output/table.ts +141 -0
- package/src/cli/parser.ts +241 -0
- package/src/commands/help.ts +50 -0
- package/src/commands/index.ts +1 -0
- package/src/components/index.ts +147 -0
- package/src/core/application.ts +461 -0
- package/src/core/command.ts +269 -0
- package/src/core/context.ts +112 -0
- package/src/core/help.ts +214 -0
- package/src/core/index.ts +15 -0
- package/src/core/logger.ts +164 -0
- package/src/core/registry.ts +140 -0
- package/src/hooks/index.ts +131 -0
- package/src/index.ts +137 -0
- package/src/registry/commandRegistry.ts +77 -0
- package/src/registry/index.ts +1 -0
- package/src/tui/TuiApp.tsx +582 -0
- package/src/tui/TuiApplication.tsx +230 -0
- package/src/tui/app.ts +29 -0
- package/src/tui/components/ActionButton.tsx +36 -0
- package/src/tui/components/CliModal.tsx +81 -0
- package/src/tui/components/CommandSelector.tsx +159 -0
- package/src/tui/components/ConfigForm.tsx +148 -0
- package/src/tui/components/EditorModal.tsx +177 -0
- package/src/tui/components/FieldRow.tsx +30 -0
- package/src/tui/components/Header.tsx +31 -0
- package/src/tui/components/JsonHighlight.tsx +128 -0
- package/src/tui/components/LogsPanel.tsx +86 -0
- package/src/tui/components/ResultsPanel.tsx +93 -0
- package/src/tui/components/StatusBar.tsx +59 -0
- package/src/tui/components/index.ts +13 -0
- package/src/tui/components/types.ts +30 -0
- package/src/tui/context/KeyboardContext.tsx +118 -0
- package/src/tui/context/index.ts +7 -0
- package/src/tui/hooks/index.ts +35 -0
- package/src/tui/hooks/useClipboard.ts +66 -0
- package/src/tui/hooks/useCommandExecutor.ts +131 -0
- package/src/tui/hooks/useConfigState.ts +171 -0
- package/src/tui/hooks/useKeyboardHandler.ts +91 -0
- package/src/tui/hooks/useLogStream.ts +96 -0
- package/src/tui/hooks/useSpinner.ts +46 -0
- package/src/tui/index.ts +65 -0
- package/src/tui/theme.ts +21 -0
- package/src/tui/utils/buildCliCommand.ts +90 -0
- package/src/tui/utils/index.ts +13 -0
- package/src/tui/utils/parameterPersistence.ts +96 -0
- package/src/tui/utils/schemaToFields.ts +144 -0
- package/src/types/command.ts +103 -0
- package/src/types/execution.ts +11 -0
- package/src/types/index.ts +1 -0
- 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";
|