@pablozaiden/terminatui 0.2.0 → 0.3.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/README.md +64 -43
- package/package.json +11 -8
- package/src/__tests__/application.test.ts +87 -68
- package/src/__tests__/buildCliCommand.test.ts +99 -119
- package/src/__tests__/builtins.test.ts +27 -75
- package/src/__tests__/command.test.ts +100 -131
- package/src/__tests__/configOnChange.test.ts +63 -0
- package/src/__tests__/context.test.ts +1 -26
- package/src/__tests__/helpCore.test.ts +227 -0
- package/src/__tests__/parser.test.ts +98 -244
- package/src/__tests__/registry.test.ts +33 -160
- package/src/__tests__/schemaToFields.test.ts +75 -158
- package/src/builtins/help.ts +12 -4
- package/src/builtins/settings.ts +18 -32
- package/src/builtins/version.ts +3 -3
- package/src/cli/output/colors.ts +1 -1
- package/src/cli/parser.ts +26 -95
- package/src/core/application.ts +192 -110
- package/src/core/command.ts +26 -9
- package/src/core/context.ts +31 -20
- package/src/core/help.ts +24 -18
- package/src/core/knownCommands.ts +13 -0
- package/src/core/logger.ts +39 -42
- package/src/core/registry.ts +5 -12
- package/src/index.ts +22 -137
- package/src/tui/TuiApplication.tsx +63 -120
- package/src/tui/TuiRoot.tsx +135 -0
- package/src/tui/adapters/factory.ts +19 -0
- package/src/tui/adapters/ink/InkRenderer.tsx +139 -0
- package/src/tui/adapters/ink/components/Button.tsx +12 -0
- package/src/tui/adapters/ink/components/Code.tsx +6 -0
- package/src/tui/adapters/ink/components/CodeHighlight.tsx +6 -0
- package/src/tui/adapters/ink/components/Container.tsx +5 -0
- package/src/tui/adapters/ink/components/Field.tsx +12 -0
- package/src/tui/adapters/ink/components/Label.tsx +24 -0
- package/src/tui/adapters/ink/components/MenuButton.tsx +12 -0
- package/src/tui/adapters/ink/components/MenuItem.tsx +17 -0
- package/src/tui/adapters/ink/components/Overlay.tsx +5 -0
- package/src/tui/adapters/ink/components/Panel.tsx +15 -0
- package/src/tui/adapters/ink/components/ScrollView.tsx +5 -0
- package/src/tui/adapters/ink/components/Select.tsx +44 -0
- package/src/tui/adapters/ink/components/Spacer.tsx +15 -0
- package/src/tui/adapters/ink/components/Spinner.tsx +5 -0
- package/src/tui/adapters/ink/components/TextInput.tsx +22 -0
- package/src/tui/adapters/ink/components/Value.tsx +7 -0
- package/src/tui/adapters/ink/keyboard.ts +97 -0
- package/src/tui/adapters/ink/utils.ts +16 -0
- package/src/tui/adapters/opentui/OpenTuiRenderer.tsx +119 -0
- package/src/tui/adapters/opentui/components/Button.tsx +13 -0
- package/src/tui/adapters/opentui/components/Code.tsx +12 -0
- package/src/tui/adapters/opentui/components/CodeHighlight.tsx +24 -0
- package/src/tui/adapters/opentui/components/Container.tsx +56 -0
- package/src/tui/adapters/opentui/components/Field.tsx +18 -0
- package/src/tui/adapters/opentui/components/Label.tsx +15 -0
- package/src/tui/adapters/opentui/components/MenuButton.tsx +14 -0
- package/src/tui/adapters/opentui/components/MenuItem.tsx +29 -0
- package/src/tui/adapters/opentui/components/Overlay.tsx +21 -0
- package/src/tui/adapters/opentui/components/Panel.tsx +78 -0
- package/src/tui/adapters/opentui/components/ScrollView.tsx +85 -0
- package/src/tui/adapters/opentui/components/Select.tsx +59 -0
- package/src/tui/adapters/opentui/components/Spacer.tsx +5 -0
- package/src/tui/adapters/opentui/components/Spinner.tsx +12 -0
- package/src/tui/adapters/opentui/components/TextInput.tsx +13 -0
- package/src/tui/adapters/opentui/components/Value.tsx +13 -0
- package/src/tui/{hooks → adapters/opentui/hooks}/useSpinner.ts +2 -11
- package/src/tui/adapters/opentui/keyboard.ts +61 -0
- package/src/tui/adapters/types.ts +71 -0
- package/src/tui/components/ActionButton.tsx +0 -36
- package/src/tui/components/CommandSelector.tsx +45 -92
- package/src/tui/components/ConfigForm.tsx +68 -42
- package/src/tui/components/FieldRow.tsx +0 -30
- package/src/tui/components/Header.tsx +14 -13
- package/src/tui/components/JsonHighlight.tsx +10 -17
- package/src/tui/components/ModalBase.tsx +38 -0
- package/src/tui/components/ResultsPanel.tsx +27 -36
- package/src/tui/components/StatusBar.tsx +24 -39
- package/src/tui/components/logColors.ts +12 -0
- package/src/tui/context/ClipboardContext.tsx +87 -0
- package/src/tui/context/ExecutorContext.tsx +139 -0
- package/src/tui/context/KeyboardContext.tsx +85 -71
- package/src/tui/context/LogsContext.tsx +35 -0
- package/src/tui/context/NavigationContext.tsx +194 -0
- package/src/tui/context/RendererContext.tsx +20 -0
- package/src/tui/context/TuiAppContext.tsx +58 -0
- package/src/tui/hooks/useActiveKeyHandler.ts +75 -0
- package/src/tui/hooks/useBackHandler.ts +34 -0
- package/src/tui/hooks/useClipboard.ts +40 -25
- package/src/tui/hooks/useClipboardProvider.ts +42 -0
- package/src/tui/hooks/useGlobalKeyHandler.ts +54 -0
- package/src/tui/modals/CliModal.tsx +82 -0
- package/src/tui/modals/EditorModal.tsx +207 -0
- package/src/tui/modals/LogsModal.tsx +98 -0
- package/src/tui/registry.ts +102 -0
- package/src/tui/screens/CommandSelectScreen.tsx +162 -0
- package/src/tui/screens/ConfigScreen.tsx +165 -0
- package/src/tui/screens/ErrorScreen.tsx +58 -0
- package/src/tui/screens/ResultsScreen.tsx +68 -0
- package/src/tui/screens/RunningScreen.tsx +72 -0
- package/src/tui/screens/ScreenBase.ts +6 -0
- package/src/tui/semantic/Button.tsx +7 -0
- package/src/tui/semantic/Code.tsx +7 -0
- package/src/tui/semantic/CodeHighlight.tsx +7 -0
- package/src/tui/semantic/Container.tsx +7 -0
- package/src/tui/semantic/Field.tsx +7 -0
- package/src/tui/semantic/Label.tsx +7 -0
- package/src/tui/semantic/MenuButton.tsx +7 -0
- package/src/tui/semantic/MenuItem.tsx +7 -0
- package/src/tui/semantic/Overlay.tsx +7 -0
- package/src/tui/semantic/Panel.tsx +7 -0
- package/src/tui/semantic/ScrollView.tsx +9 -0
- package/src/tui/semantic/Select.tsx +7 -0
- package/src/tui/semantic/Spacer.tsx +7 -0
- package/src/tui/semantic/Spinner.tsx +7 -0
- package/src/tui/semantic/TextInput.tsx +7 -0
- package/src/tui/semantic/Value.tsx +7 -0
- package/src/tui/semantic/types.ts +195 -0
- package/src/tui/theme.ts +25 -14
- package/src/tui/utils/buildCliCommand.ts +1 -0
- package/src/tui/utils/getEnumKeys.ts +3 -0
- package/src/tui/utils/parameterPersistence.ts +1 -0
- package/src/types/command.ts +0 -60
- package/.devcontainer/devcontainer.json +0 -19
- package/.devcontainer/install-prerequisites.sh +0 -49
- package/.github/workflows/copilot-setup-steps.yml +0 -32
- package/.github/workflows/pull-request.yml +0 -27
- package/.github/workflows/release-npm-package.yml +0 -81
- package/AGENTS.md +0 -31
- package/bun.lock +0 -236
- package/examples/tui-app/commands/config/app/get.ts +0 -66
- package/examples/tui-app/commands/config/app/index.ts +0 -27
- package/examples/tui-app/commands/config/app/set.ts +0 -86
- package/examples/tui-app/commands/config/index.ts +0 -32
- package/examples/tui-app/commands/config/user/get.ts +0 -65
- package/examples/tui-app/commands/config/user/index.ts +0 -27
- package/examples/tui-app/commands/config/user/set.ts +0 -61
- package/examples/tui-app/commands/greet.ts +0 -76
- package/examples/tui-app/commands/index.ts +0 -4
- package/examples/tui-app/commands/math.ts +0 -115
- package/examples/tui-app/commands/status.ts +0 -77
- package/examples/tui-app/index.ts +0 -35
- package/guides/01-hello-world.md +0 -96
- package/guides/02-adding-options.md +0 -103
- package/guides/03-multiple-commands.md +0 -163
- package/guides/04-subcommands.md +0 -206
- package/guides/05-interactive-tui.md +0 -194
- package/guides/06-config-validation.md +0 -264
- package/guides/07-async-cancellation.md +0 -336
- package/guides/08-complete-application.md +0 -537
- package/guides/README.md +0 -74
- package/src/__tests__/colors.test.ts +0 -127
- package/src/__tests__/commandClass.test.ts +0 -130
- package/src/__tests__/help.test.ts +0 -412
- package/src/__tests__/registryNew.test.ts +0 -160
- package/src/__tests__/table.test.ts +0 -146
- package/src/__tests__/tui.test.ts +0 -26
- package/src/builtins/index.ts +0 -4
- package/src/cli/help.ts +0 -174
- package/src/cli/index.ts +0 -3
- package/src/cli/output/index.ts +0 -2
- package/src/cli/output/table.ts +0 -141
- package/src/commands/help.ts +0 -50
- package/src/commands/index.ts +0 -1
- package/src/components/index.ts +0 -147
- package/src/core/index.ts +0 -15
- package/src/hooks/index.ts +0 -131
- package/src/registry/commandRegistry.ts +0 -77
- package/src/registry/index.ts +0 -1
- package/src/tui/TuiApp.tsx +0 -619
- package/src/tui/app.ts +0 -29
- package/src/tui/components/CliModal.tsx +0 -81
- package/src/tui/components/EditorModal.tsx +0 -177
- package/src/tui/components/LogsPanel.tsx +0 -86
- package/src/tui/components/index.ts +0 -13
- package/src/tui/context/index.ts +0 -7
- package/src/tui/hooks/index.ts +0 -35
- package/src/tui/hooks/useKeyboardHandler.ts +0 -91
- package/src/tui/hooks/useLogStream.ts +0 -96
- package/src/tui/index.ts +0 -65
- package/src/tui/utils/index.ts +0 -13
- package/src/types/index.ts +0 -1
- package/tsconfig.json +0 -25
package/src/core/command.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
import type { AppContext } from "./context.ts";
|
|
3
2
|
import type { OptionSchema, OptionValues } from "../types/command.ts";
|
|
4
3
|
|
|
5
4
|
/**
|
|
@@ -93,7 +92,7 @@ export type AnyCommand = Command<any, any>;
|
|
|
93
92
|
* description = "Run the application";
|
|
94
93
|
* options = runOptions;
|
|
95
94
|
*
|
|
96
|
-
* async buildConfig(
|
|
95
|
+
* async buildConfig(opts: OptionValues<typeof runOptions>): Promise<RunConfig> {
|
|
97
96
|
* const repoPath = path.resolve(opts.repo);
|
|
98
97
|
* if (!existsSync(repoPath)) {
|
|
99
98
|
* throw new ConfigValidationError(`Repository not found: ${repoPath}`, "repo");
|
|
@@ -101,7 +100,7 @@ export type AnyCommand = Command<any, any>;
|
|
|
101
100
|
* return { repoPath, iterations: parseInt(opts.iterations) };
|
|
102
101
|
* }
|
|
103
102
|
*
|
|
104
|
-
* async execute(
|
|
103
|
+
* async execute(config: RunConfig) {
|
|
105
104
|
* // config is already validated
|
|
106
105
|
* return { success: true, data: result };
|
|
107
106
|
* }
|
|
@@ -141,6 +140,9 @@ export abstract class Command<
|
|
|
141
140
|
/** Whether this command runs immediately without config screen (like "check") */
|
|
142
141
|
immediateExecution?: boolean;
|
|
143
142
|
|
|
143
|
+
/** If true, this command should not appear in the TUI command list. */
|
|
144
|
+
tuiHidden?: boolean;
|
|
145
|
+
|
|
144
146
|
/**
|
|
145
147
|
* Build and validate a configuration object from parsed options.
|
|
146
148
|
*
|
|
@@ -153,24 +155,23 @@ export abstract class Command<
|
|
|
153
155
|
* @throws ConfigValidationError if validation fails
|
|
154
156
|
* @returns The validated configuration object
|
|
155
157
|
*/
|
|
156
|
-
buildConfig?(
|
|
158
|
+
buildConfig?(opts: OptionValues<TOptions>): Promise<TConfig> | TConfig;
|
|
157
159
|
|
|
158
160
|
/**
|
|
159
161
|
* Execute the command.
|
|
160
162
|
* The framework will call this method for both CLI and TUI modes.
|
|
161
163
|
*
|
|
162
|
-
* @param ctx - Application context
|
|
163
164
|
* @param config - The configuration object (from buildConfig, or raw options if buildConfig is not implemented)
|
|
164
165
|
* @param execCtx - Execution context with abort signal for cancellation support
|
|
165
166
|
* @returns Optional result for display in TUI results panel
|
|
166
167
|
*/
|
|
167
|
-
abstract execute(
|
|
168
|
+
abstract execute(config: TConfig, execCtx?: CommandExecutionContext): Promise<CommandResult | void> | CommandResult | void;
|
|
168
169
|
|
|
169
170
|
/**
|
|
170
171
|
* Called before buildConfig. Use for early validation, resource acquisition, etc.
|
|
171
172
|
* If this throws, buildConfig and execute will not be called but afterExecute will still run.
|
|
172
173
|
*/
|
|
173
|
-
beforeExecute?(
|
|
174
|
+
beforeExecute?(opts: OptionValues<TOptions>): Promise<void> | void;
|
|
174
175
|
|
|
175
176
|
/**
|
|
176
177
|
* Called after execute, even if execute threw an error.
|
|
@@ -178,7 +179,6 @@ export abstract class Command<
|
|
|
178
179
|
* @param error The error thrown by beforeExecute, buildConfig, or execute, if any
|
|
179
180
|
*/
|
|
180
181
|
afterExecute?(
|
|
181
|
-
ctx: AppContext,
|
|
182
182
|
opts: OptionValues<TOptions>,
|
|
183
183
|
error?: Error
|
|
184
184
|
): Promise<void> | void;
|
|
@@ -250,7 +250,24 @@ export abstract class Command<
|
|
|
250
250
|
* Called by the framework during registration.
|
|
251
251
|
*/
|
|
252
252
|
validate(): void {
|
|
253
|
-
|
|
253
|
+
this.validateSubCommands();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private validateSubCommands(): void {
|
|
257
|
+
if (!this.subCommands || this.subCommands.length === 0) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const names = new Set<string>();
|
|
262
|
+
for (const subCommand of this.subCommands) {
|
|
263
|
+
if (names.has(subCommand.name)) {
|
|
264
|
+
throw new Error(
|
|
265
|
+
`Duplicate subcommand '${subCommand.name}' under '${this.name}'`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
names.add(subCommand.name);
|
|
269
|
+
subCommand.validate();
|
|
270
|
+
}
|
|
254
271
|
}
|
|
255
272
|
|
|
256
273
|
/**
|
package/src/core/context.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Logger, createLogger, type LoggerConfig } from "./logger.ts";
|
|
1
|
+
import { type Logger, createLogger, type LoggerConfig, type LogEvent } from "./logger.ts";
|
|
2
|
+
import { appendFileSync } from "fs";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Application configuration stored in context.
|
|
@@ -8,10 +9,14 @@ export interface AppConfig {
|
|
|
8
9
|
name: string;
|
|
9
10
|
/** Application version */
|
|
10
11
|
version: string;
|
|
12
|
+
/** Log target */
|
|
13
|
+
logTarget?: LogTarget[];
|
|
11
14
|
/** Additional configuration values */
|
|
12
15
|
[key: string]: unknown;
|
|
13
16
|
}
|
|
14
17
|
|
|
18
|
+
export type LogTarget = "memory" | "file" | "none";
|
|
19
|
+
|
|
15
20
|
/**
|
|
16
21
|
* AppContext is the central container for application-wide services and state.
|
|
17
22
|
* It holds the logger, configuration, and a generic service registry.
|
|
@@ -22,6 +27,7 @@ export interface AppConfig {
|
|
|
22
27
|
export class AppContext {
|
|
23
28
|
private static _current: AppContext | null = null;
|
|
24
29
|
private readonly services = new Map<string, unknown>();
|
|
30
|
+
private readonly startTime = Date.now();
|
|
25
31
|
|
|
26
32
|
/** The application logger */
|
|
27
33
|
public readonly logger: Logger;
|
|
@@ -29,9 +35,27 @@ export class AppContext {
|
|
|
29
35
|
/** The application configuration */
|
|
30
36
|
public readonly config: AppConfig;
|
|
31
37
|
|
|
38
|
+
public logTarget: LogTarget[] = [];
|
|
39
|
+
|
|
40
|
+
public readonly logHistory: LogEvent[] = [];
|
|
41
|
+
|
|
32
42
|
constructor(config: AppConfig, loggerConfig?: LoggerConfig) {
|
|
33
43
|
this.config = config;
|
|
34
44
|
this.logger = createLogger(loggerConfig);
|
|
45
|
+
this.logTarget = config.logTarget ?? ["none"];
|
|
46
|
+
|
|
47
|
+
this.logger.onLogEvent((event) => {
|
|
48
|
+
if (this.logTarget.includes("memory")) {
|
|
49
|
+
this.logHistory.push(event);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (this.logTarget.includes("file")) {
|
|
53
|
+
const logFileName = `${this.config.name}-${this.startTime}.log`;
|
|
54
|
+
const logLine = event.message + "\n";
|
|
55
|
+
|
|
56
|
+
appendFileSync(logFileName, logLine);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
35
59
|
}
|
|
36
60
|
|
|
37
61
|
/**
|
|
@@ -40,35 +64,22 @@ export class AppContext {
|
|
|
40
64
|
*/
|
|
41
65
|
static get current(): AppContext {
|
|
42
66
|
if (!AppContext._current) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"Ensure Application.run() has been called."
|
|
46
|
-
);
|
|
67
|
+
// return a fake context to avoid optional chaining everywhere
|
|
68
|
+
return new AppContext({ name: "unknown", version: "0.0.0" });
|
|
47
69
|
}
|
|
48
70
|
return AppContext._current;
|
|
49
71
|
}
|
|
50
72
|
|
|
51
|
-
/**
|
|
52
|
-
* Check if a current context exists.
|
|
53
|
-
*/
|
|
54
|
-
static hasCurrent(): boolean {
|
|
55
|
-
return AppContext._current !== null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
73
|
/**
|
|
59
74
|
* Set the current application context.
|
|
60
75
|
* Called internally by Application.
|
|
61
76
|
*/
|
|
62
77
|
static setCurrent(context: AppContext): void {
|
|
63
|
-
|
|
64
|
-
|
|
78
|
+
if (!context) {
|
|
79
|
+
throw new Error("Cannot set null or undefined context");
|
|
80
|
+
}
|
|
65
81
|
|
|
66
|
-
|
|
67
|
-
* Clear the current context.
|
|
68
|
-
* Useful for testing.
|
|
69
|
-
*/
|
|
70
|
-
static clearCurrent(): void {
|
|
71
|
-
AppContext._current = null;
|
|
82
|
+
AppContext._current = context;
|
|
72
83
|
}
|
|
73
84
|
|
|
74
85
|
/**
|
package/src/core/help.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface HelpOptions {
|
|
|
12
12
|
version?: string;
|
|
13
13
|
/** Command path leading to this command (e.g., ["app", "remote", "add"]) */
|
|
14
14
|
commandPath?: string[];
|
|
15
|
+
/** Global options schema for this app */
|
|
16
|
+
globalOptionsSchema?: Record<string, OptionDef>;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
/**
|
|
@@ -59,38 +61,34 @@ export function formatSubCommands(command: AnyCommand): string {
|
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
/**
|
|
62
|
-
* Format options
|
|
64
|
+
* Format an options schema into a help section.
|
|
63
65
|
*/
|
|
64
|
-
export function
|
|
65
|
-
if (
|
|
66
|
+
export function formatOptionSchema(title: string, schema: Record<string, OptionDef>): string {
|
|
67
|
+
if (Object.keys(schema).length === 0) return "";
|
|
66
68
|
|
|
67
|
-
const entries = Object.entries(
|
|
68
|
-
const def = defUntyped as OptionDef;
|
|
69
|
+
const entries = Object.entries(schema).map(([name, def]) => {
|
|
69
70
|
const alias = def.alias ? `-${def.alias}, ` : " ";
|
|
70
|
-
const flag = `${alias}--${name}`;
|
|
71
71
|
const required = def.required ? colors.red(" (required)") : "";
|
|
72
72
|
const defaultVal =
|
|
73
73
|
def.default !== undefined ? colors.dim(` [default: ${def.default}]`) : "";
|
|
74
74
|
const enumVals = def.enum ? colors.dim(` [${def.enum.join(" | ")}]`) : "";
|
|
75
|
-
|
|
75
|
+
|
|
76
|
+
const noVariant = def.type === "boolean" ? `, --no-${name}` : "";
|
|
77
|
+
const flag = `${alias}--${name}${noVariant}`;
|
|
78
|
+
const typeHint = def.type === "boolean" ? "" : colors.dim(` <${def.type}>`);
|
|
76
79
|
|
|
77
80
|
return ` ${colors.yellow(flag)}${typeHint}${required}\n ${def.description}${enumVals}${defaultVal}`;
|
|
78
81
|
});
|
|
79
82
|
|
|
80
|
-
return [colors.bold("
|
|
83
|
+
return [colors.bold(title + ":"), ...entries].join("\n");
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
/**
|
|
84
|
-
* Format
|
|
87
|
+
* Format options list.
|
|
85
88
|
*/
|
|
86
|
-
export function
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
` ${colors.yellow(" --detailed-logs")}\n Include timestamp and level prefix in log output`,
|
|
90
|
-
` ${colors.yellow(" --no-detailed-logs")}\n Disable detailed log format`,
|
|
91
|
-
];
|
|
92
|
-
|
|
93
|
-
return [colors.bold("Global Options:"), ...entries].join("\n");
|
|
89
|
+
export function formatOptions(command: AnyCommand): string {
|
|
90
|
+
if (!command.options || Object.keys(command.options).length === 0) return "";
|
|
91
|
+
return formatOptionSchema("Options", command.options as Record<string, OptionDef>);
|
|
94
92
|
}
|
|
95
93
|
|
|
96
94
|
/**
|
|
@@ -154,7 +152,9 @@ export function generateCommandHelp(command: AnyCommand, options: HelpOptions =
|
|
|
154
152
|
}
|
|
155
153
|
|
|
156
154
|
// Global options (available on all commands)
|
|
157
|
-
|
|
155
|
+
if (options.globalOptionsSchema && Object.keys(options.globalOptionsSchema).length > 0) {
|
|
156
|
+
sections.push(`\n${formatOptionSchema("Global Options", options.globalOptionsSchema)}`);
|
|
157
|
+
}
|
|
158
158
|
|
|
159
159
|
// Examples
|
|
160
160
|
const examplesSection = formatExamples(command);
|
|
@@ -191,6 +191,12 @@ export function generateAppHelp(commands: AnyCommand[], options: HelpOptions = {
|
|
|
191
191
|
// Usage
|
|
192
192
|
sections.push(`${colors.bold("Usage:")}\n ${appName} [command] [options]\n`);
|
|
193
193
|
|
|
194
|
+
// Global options
|
|
195
|
+
if (options.globalOptionsSchema && Object.keys(options.globalOptionsSchema).length > 0) {
|
|
196
|
+
sections.push(formatOptionSchema("Global Options", options.globalOptionsSchema));
|
|
197
|
+
sections.push("");
|
|
198
|
+
}
|
|
199
|
+
|
|
194
200
|
// Commands
|
|
195
201
|
if (commands.length > 0) {
|
|
196
202
|
const entries = commands.map((cmd) => {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const KNOWN_COMMANDS = {
|
|
2
|
+
help: "help",
|
|
3
|
+
settings: "settings",
|
|
4
|
+
version: "version",
|
|
5
|
+
} as const;
|
|
6
|
+
|
|
7
|
+
export type KnownCommandName = (typeof KNOWN_COMMANDS)[keyof typeof KNOWN_COMMANDS];
|
|
8
|
+
|
|
9
|
+
export const RESERVED_TOP_LEVEL_COMMAND_NAMES = new Set<KnownCommandName>([
|
|
10
|
+
KNOWN_COMMANDS.help,
|
|
11
|
+
KNOWN_COMMANDS.settings,
|
|
12
|
+
KNOWN_COMMANDS.version,
|
|
13
|
+
]);
|
package/src/core/logger.ts
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
import { EventEmitter } from "events";
|
|
2
|
-
import { Logger as TsLogger } from "tslog";
|
|
2
|
+
import { Logger as TsLogger, type IMeta } from "tslog";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Log levels from least to most severe.
|
|
6
6
|
*/
|
|
7
7
|
export enum LogLevel {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
silly = 0,
|
|
9
|
+
trace = 1,
|
|
10
|
+
debug = 2,
|
|
11
|
+
info = 3,
|
|
12
|
+
warn = 4,
|
|
13
|
+
error = 5,
|
|
14
|
+
fatal = 6,
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Event emitted when a log message is written.
|
|
19
19
|
*/
|
|
20
20
|
export interface LogEvent {
|
|
21
|
+
rawMessage: string;
|
|
21
22
|
message: string;
|
|
22
23
|
level: LogLevel;
|
|
23
24
|
timestamp: Date;
|
|
@@ -42,14 +43,12 @@ export interface LoggerConfig {
|
|
|
42
43
|
export class Logger {
|
|
43
44
|
private tsLogger: TsLogger<unknown>;
|
|
44
45
|
private readonly eventEmitter = new EventEmitter();
|
|
45
|
-
private tuiMode: boolean;
|
|
46
46
|
private detailed: boolean;
|
|
47
47
|
private minLevel: LogLevel;
|
|
48
48
|
|
|
49
49
|
constructor(config: LoggerConfig = {}) {
|
|
50
|
-
this.tuiMode = config.tuiMode ?? false;
|
|
51
50
|
this.detailed = config.detailed ?? false;
|
|
52
|
-
this.minLevel = config.minLevel ?? LogLevel.
|
|
51
|
+
this.minLevel = config.minLevel ?? LogLevel.info;
|
|
53
52
|
|
|
54
53
|
this.tsLogger = this.createTsLogger(this.minLevel);
|
|
55
54
|
}
|
|
@@ -63,27 +62,19 @@ export class Logger {
|
|
|
63
62
|
logMetaMarkup: string,
|
|
64
63
|
logArgs: unknown[],
|
|
65
64
|
logErrors: string[],
|
|
66
|
-
logMeta
|
|
65
|
+
logMeta?: IMeta
|
|
67
66
|
) => {
|
|
68
67
|
const baseLine = `${logMetaMarkup}${(logArgs as string[]).join(" ")}${logErrors.join("")}`;
|
|
69
68
|
const simpleLine = `${(logArgs as string[]).join(" ")}${logErrors.join("")}`;
|
|
70
|
-
const
|
|
71
|
-
const levelFromMeta =
|
|
72
|
-
typeof meta?.["logLevelId"] === "number"
|
|
73
|
-
? (meta["logLevelId"] as LogLevel)
|
|
74
|
-
: LogLevel.Info;
|
|
75
|
-
|
|
69
|
+
const level = logMeta?.logLevelId as LogLevel ?? LogLevel.info;
|
|
76
70
|
const output = this.detailed ? baseLine : simpleLine;
|
|
77
71
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
} else {
|
|
85
|
-
process.stderr.write(output + "\n");
|
|
86
|
-
}
|
|
72
|
+
this.eventEmitter.emit("log", {
|
|
73
|
+
message: output,
|
|
74
|
+
rawMessage: simpleLine,
|
|
75
|
+
level: level,
|
|
76
|
+
timestamp: new Date(),
|
|
77
|
+
} satisfies LogEvent);
|
|
87
78
|
},
|
|
88
79
|
},
|
|
89
80
|
});
|
|
@@ -97,13 +88,6 @@ export class Logger {
|
|
|
97
88
|
return () => this.eventEmitter.off("log", listener);
|
|
98
89
|
}
|
|
99
90
|
|
|
100
|
-
/**
|
|
101
|
-
* Enable or disable TUI mode.
|
|
102
|
-
*/
|
|
103
|
-
setTuiMode(enabled: boolean): void {
|
|
104
|
-
this.tuiMode = enabled;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
91
|
/**
|
|
108
92
|
* Enable or disable detailed log format.
|
|
109
93
|
*/
|
|
@@ -127,35 +111,48 @@ export class Logger {
|
|
|
127
111
|
}
|
|
128
112
|
|
|
129
113
|
// Logging methods
|
|
130
|
-
|
|
131
|
-
this.tsLogger.silly(...args);
|
|
114
|
+
silly(...args: unknown[]): void {
|
|
115
|
+
this.tsLogger.silly(...asStringArray(args));
|
|
132
116
|
}
|
|
133
117
|
|
|
134
118
|
trace(...args: unknown[]): void {
|
|
135
|
-
this.tsLogger.trace(...args);
|
|
119
|
+
this.tsLogger.trace(...asStringArray(args));
|
|
136
120
|
}
|
|
137
121
|
|
|
138
122
|
debug(...args: unknown[]): void {
|
|
139
|
-
this.tsLogger.debug(...args);
|
|
123
|
+
this.tsLogger.debug(...asStringArray(args));
|
|
140
124
|
}
|
|
141
125
|
|
|
142
126
|
info(...args: unknown[]): void {
|
|
143
|
-
this.tsLogger.info(...args);
|
|
127
|
+
this.tsLogger.info(...asStringArray(args));
|
|
144
128
|
}
|
|
145
129
|
|
|
146
130
|
warn(...args: unknown[]): void {
|
|
147
|
-
this.tsLogger.warn(...args);
|
|
131
|
+
this.tsLogger.warn(...asStringArray(args));
|
|
148
132
|
}
|
|
149
133
|
|
|
150
134
|
error(...args: unknown[]): void {
|
|
151
|
-
this.tsLogger.error(...args);
|
|
135
|
+
this.tsLogger.error(...asStringArray(args));
|
|
152
136
|
}
|
|
153
137
|
|
|
154
138
|
fatal(...args: unknown[]): void {
|
|
155
|
-
this.tsLogger.fatal(...args);
|
|
139
|
+
this.tsLogger.fatal(...asStringArray(args));
|
|
156
140
|
}
|
|
157
141
|
}
|
|
158
142
|
|
|
143
|
+
function asStringArray(args: unknown[]): string[] {
|
|
144
|
+
return args.map(arg => {
|
|
145
|
+
if (typeof arg === "string") {
|
|
146
|
+
return arg;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
return JSON.stringify(arg);
|
|
150
|
+
} catch {
|
|
151
|
+
return String(arg);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
159
156
|
/**
|
|
160
157
|
* Create a new logger instance with the given configuration.
|
|
161
158
|
*/
|
package/src/core/registry.ts
CHANGED
|
@@ -72,7 +72,11 @@ export class CommandRegistry {
|
|
|
72
72
|
* @param path Array of command names forming the path
|
|
73
73
|
* @returns Object with resolved command, remaining path, and full path
|
|
74
74
|
*/
|
|
75
|
-
resolve(path: string[]):
|
|
75
|
+
resolve(path: string[]): {
|
|
76
|
+
command: AnyCommand | undefined;
|
|
77
|
+
remainingPath: string[];
|
|
78
|
+
resolvedPath: string[];
|
|
79
|
+
} {
|
|
76
80
|
if (path.length === 0) {
|
|
77
81
|
return { command: undefined, remainingPath: [], resolvedPath: [] };
|
|
78
82
|
}
|
|
@@ -127,14 +131,3 @@ export class CommandRegistry {
|
|
|
127
131
|
}
|
|
128
132
|
}
|
|
129
133
|
|
|
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
|
-
}
|
package/src/index.ts
CHANGED
|
@@ -1,137 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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";
|
|
1
|
+
export * from "./builtins/help.ts";
|
|
2
|
+
export * from "./builtins/settings.ts";
|
|
3
|
+
export * from "./builtins/version.ts";
|
|
4
|
+
|
|
5
|
+
export * from "./cli/parser.ts";
|
|
6
|
+
export * from "./cli/output/colors.ts";
|
|
7
|
+
|
|
8
|
+
export * from "./core/application.ts";
|
|
9
|
+
export * from "./core/command.ts";
|
|
10
|
+
export * from "./core/context.ts";
|
|
11
|
+
export * from "./core/help.ts";
|
|
12
|
+
export * from "./core/knownCommands.ts";
|
|
13
|
+
export * from "./core/logger.ts";
|
|
14
|
+
export * from "./core/registry.ts";
|
|
15
|
+
|
|
16
|
+
export * from "./tui/TuiApplication.tsx";
|
|
17
|
+
export * from "./tui/TuiRoot.tsx";
|
|
18
|
+
export * from "./tui/registry.ts";
|
|
19
|
+
export * from "./tui/theme.ts";
|
|
20
|
+
export * from "./types/command.ts";
|
|
21
|
+
|
|
22
|
+
export * from "./tui/components/JsonHighlight.tsx";
|