@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.
Files changed (181) hide show
  1. package/README.md +64 -43
  2. package/package.json +11 -8
  3. package/src/__tests__/application.test.ts +87 -68
  4. package/src/__tests__/buildCliCommand.test.ts +99 -119
  5. package/src/__tests__/builtins.test.ts +27 -75
  6. package/src/__tests__/command.test.ts +100 -131
  7. package/src/__tests__/configOnChange.test.ts +63 -0
  8. package/src/__tests__/context.test.ts +1 -26
  9. package/src/__tests__/helpCore.test.ts +227 -0
  10. package/src/__tests__/parser.test.ts +98 -244
  11. package/src/__tests__/registry.test.ts +33 -160
  12. package/src/__tests__/schemaToFields.test.ts +75 -158
  13. package/src/builtins/help.ts +12 -4
  14. package/src/builtins/settings.ts +18 -32
  15. package/src/builtins/version.ts +3 -3
  16. package/src/cli/output/colors.ts +1 -1
  17. package/src/cli/parser.ts +26 -95
  18. package/src/core/application.ts +192 -110
  19. package/src/core/command.ts +26 -9
  20. package/src/core/context.ts +31 -20
  21. package/src/core/help.ts +24 -18
  22. package/src/core/knownCommands.ts +13 -0
  23. package/src/core/logger.ts +39 -42
  24. package/src/core/registry.ts +5 -12
  25. package/src/index.ts +22 -137
  26. package/src/tui/TuiApplication.tsx +63 -120
  27. package/src/tui/TuiRoot.tsx +135 -0
  28. package/src/tui/adapters/factory.ts +19 -0
  29. package/src/tui/adapters/ink/InkRenderer.tsx +139 -0
  30. package/src/tui/adapters/ink/components/Button.tsx +12 -0
  31. package/src/tui/adapters/ink/components/Code.tsx +6 -0
  32. package/src/tui/adapters/ink/components/CodeHighlight.tsx +6 -0
  33. package/src/tui/adapters/ink/components/Container.tsx +5 -0
  34. package/src/tui/adapters/ink/components/Field.tsx +12 -0
  35. package/src/tui/adapters/ink/components/Label.tsx +24 -0
  36. package/src/tui/adapters/ink/components/MenuButton.tsx +12 -0
  37. package/src/tui/adapters/ink/components/MenuItem.tsx +17 -0
  38. package/src/tui/adapters/ink/components/Overlay.tsx +5 -0
  39. package/src/tui/adapters/ink/components/Panel.tsx +15 -0
  40. package/src/tui/adapters/ink/components/ScrollView.tsx +5 -0
  41. package/src/tui/adapters/ink/components/Select.tsx +44 -0
  42. package/src/tui/adapters/ink/components/Spacer.tsx +15 -0
  43. package/src/tui/adapters/ink/components/Spinner.tsx +5 -0
  44. package/src/tui/adapters/ink/components/TextInput.tsx +22 -0
  45. package/src/tui/adapters/ink/components/Value.tsx +7 -0
  46. package/src/tui/adapters/ink/keyboard.ts +97 -0
  47. package/src/tui/adapters/ink/utils.ts +16 -0
  48. package/src/tui/adapters/opentui/OpenTuiRenderer.tsx +119 -0
  49. package/src/tui/adapters/opentui/components/Button.tsx +13 -0
  50. package/src/tui/adapters/opentui/components/Code.tsx +12 -0
  51. package/src/tui/adapters/opentui/components/CodeHighlight.tsx +24 -0
  52. package/src/tui/adapters/opentui/components/Container.tsx +56 -0
  53. package/src/tui/adapters/opentui/components/Field.tsx +18 -0
  54. package/src/tui/adapters/opentui/components/Label.tsx +15 -0
  55. package/src/tui/adapters/opentui/components/MenuButton.tsx +14 -0
  56. package/src/tui/adapters/opentui/components/MenuItem.tsx +29 -0
  57. package/src/tui/adapters/opentui/components/Overlay.tsx +21 -0
  58. package/src/tui/adapters/opentui/components/Panel.tsx +78 -0
  59. package/src/tui/adapters/opentui/components/ScrollView.tsx +85 -0
  60. package/src/tui/adapters/opentui/components/Select.tsx +59 -0
  61. package/src/tui/adapters/opentui/components/Spacer.tsx +5 -0
  62. package/src/tui/adapters/opentui/components/Spinner.tsx +12 -0
  63. package/src/tui/adapters/opentui/components/TextInput.tsx +13 -0
  64. package/src/tui/adapters/opentui/components/Value.tsx +13 -0
  65. package/src/tui/{hooks → adapters/opentui/hooks}/useSpinner.ts +2 -11
  66. package/src/tui/adapters/opentui/keyboard.ts +61 -0
  67. package/src/tui/adapters/types.ts +71 -0
  68. package/src/tui/components/ActionButton.tsx +0 -36
  69. package/src/tui/components/CommandSelector.tsx +45 -92
  70. package/src/tui/components/ConfigForm.tsx +68 -42
  71. package/src/tui/components/FieldRow.tsx +0 -30
  72. package/src/tui/components/Header.tsx +14 -13
  73. package/src/tui/components/JsonHighlight.tsx +10 -17
  74. package/src/tui/components/ModalBase.tsx +38 -0
  75. package/src/tui/components/ResultsPanel.tsx +27 -36
  76. package/src/tui/components/StatusBar.tsx +24 -39
  77. package/src/tui/components/logColors.ts +12 -0
  78. package/src/tui/context/ClipboardContext.tsx +87 -0
  79. package/src/tui/context/ExecutorContext.tsx +139 -0
  80. package/src/tui/context/KeyboardContext.tsx +85 -71
  81. package/src/tui/context/LogsContext.tsx +35 -0
  82. package/src/tui/context/NavigationContext.tsx +194 -0
  83. package/src/tui/context/RendererContext.tsx +20 -0
  84. package/src/tui/context/TuiAppContext.tsx +58 -0
  85. package/src/tui/hooks/useActiveKeyHandler.ts +75 -0
  86. package/src/tui/hooks/useBackHandler.ts +34 -0
  87. package/src/tui/hooks/useClipboard.ts +40 -25
  88. package/src/tui/hooks/useClipboardProvider.ts +42 -0
  89. package/src/tui/hooks/useGlobalKeyHandler.ts +54 -0
  90. package/src/tui/modals/CliModal.tsx +82 -0
  91. package/src/tui/modals/EditorModal.tsx +207 -0
  92. package/src/tui/modals/LogsModal.tsx +98 -0
  93. package/src/tui/registry.ts +102 -0
  94. package/src/tui/screens/CommandSelectScreen.tsx +162 -0
  95. package/src/tui/screens/ConfigScreen.tsx +165 -0
  96. package/src/tui/screens/ErrorScreen.tsx +58 -0
  97. package/src/tui/screens/ResultsScreen.tsx +68 -0
  98. package/src/tui/screens/RunningScreen.tsx +72 -0
  99. package/src/tui/screens/ScreenBase.ts +6 -0
  100. package/src/tui/semantic/Button.tsx +7 -0
  101. package/src/tui/semantic/Code.tsx +7 -0
  102. package/src/tui/semantic/CodeHighlight.tsx +7 -0
  103. package/src/tui/semantic/Container.tsx +7 -0
  104. package/src/tui/semantic/Field.tsx +7 -0
  105. package/src/tui/semantic/Label.tsx +7 -0
  106. package/src/tui/semantic/MenuButton.tsx +7 -0
  107. package/src/tui/semantic/MenuItem.tsx +7 -0
  108. package/src/tui/semantic/Overlay.tsx +7 -0
  109. package/src/tui/semantic/Panel.tsx +7 -0
  110. package/src/tui/semantic/ScrollView.tsx +9 -0
  111. package/src/tui/semantic/Select.tsx +7 -0
  112. package/src/tui/semantic/Spacer.tsx +7 -0
  113. package/src/tui/semantic/Spinner.tsx +7 -0
  114. package/src/tui/semantic/TextInput.tsx +7 -0
  115. package/src/tui/semantic/Value.tsx +7 -0
  116. package/src/tui/semantic/types.ts +195 -0
  117. package/src/tui/theme.ts +25 -14
  118. package/src/tui/utils/buildCliCommand.ts +1 -0
  119. package/src/tui/utils/getEnumKeys.ts +3 -0
  120. package/src/tui/utils/parameterPersistence.ts +1 -0
  121. package/src/types/command.ts +0 -60
  122. package/.devcontainer/devcontainer.json +0 -19
  123. package/.devcontainer/install-prerequisites.sh +0 -49
  124. package/.github/workflows/copilot-setup-steps.yml +0 -32
  125. package/.github/workflows/pull-request.yml +0 -27
  126. package/.github/workflows/release-npm-package.yml +0 -81
  127. package/AGENTS.md +0 -31
  128. package/bun.lock +0 -236
  129. package/examples/tui-app/commands/config/app/get.ts +0 -66
  130. package/examples/tui-app/commands/config/app/index.ts +0 -27
  131. package/examples/tui-app/commands/config/app/set.ts +0 -86
  132. package/examples/tui-app/commands/config/index.ts +0 -32
  133. package/examples/tui-app/commands/config/user/get.ts +0 -65
  134. package/examples/tui-app/commands/config/user/index.ts +0 -27
  135. package/examples/tui-app/commands/config/user/set.ts +0 -61
  136. package/examples/tui-app/commands/greet.ts +0 -76
  137. package/examples/tui-app/commands/index.ts +0 -4
  138. package/examples/tui-app/commands/math.ts +0 -115
  139. package/examples/tui-app/commands/status.ts +0 -77
  140. package/examples/tui-app/index.ts +0 -35
  141. package/guides/01-hello-world.md +0 -96
  142. package/guides/02-adding-options.md +0 -103
  143. package/guides/03-multiple-commands.md +0 -163
  144. package/guides/04-subcommands.md +0 -206
  145. package/guides/05-interactive-tui.md +0 -194
  146. package/guides/06-config-validation.md +0 -264
  147. package/guides/07-async-cancellation.md +0 -336
  148. package/guides/08-complete-application.md +0 -537
  149. package/guides/README.md +0 -74
  150. package/src/__tests__/colors.test.ts +0 -127
  151. package/src/__tests__/commandClass.test.ts +0 -130
  152. package/src/__tests__/help.test.ts +0 -412
  153. package/src/__tests__/registryNew.test.ts +0 -160
  154. package/src/__tests__/table.test.ts +0 -146
  155. package/src/__tests__/tui.test.ts +0 -26
  156. package/src/builtins/index.ts +0 -4
  157. package/src/cli/help.ts +0 -174
  158. package/src/cli/index.ts +0 -3
  159. package/src/cli/output/index.ts +0 -2
  160. package/src/cli/output/table.ts +0 -141
  161. package/src/commands/help.ts +0 -50
  162. package/src/commands/index.ts +0 -1
  163. package/src/components/index.ts +0 -147
  164. package/src/core/index.ts +0 -15
  165. package/src/hooks/index.ts +0 -131
  166. package/src/registry/commandRegistry.ts +0 -77
  167. package/src/registry/index.ts +0 -1
  168. package/src/tui/TuiApp.tsx +0 -619
  169. package/src/tui/app.ts +0 -29
  170. package/src/tui/components/CliModal.tsx +0 -81
  171. package/src/tui/components/EditorModal.tsx +0 -177
  172. package/src/tui/components/LogsPanel.tsx +0 -86
  173. package/src/tui/components/index.ts +0 -13
  174. package/src/tui/context/index.ts +0 -7
  175. package/src/tui/hooks/index.ts +0 -35
  176. package/src/tui/hooks/useKeyboardHandler.ts +0 -91
  177. package/src/tui/hooks/useLogStream.ts +0 -96
  178. package/src/tui/index.ts +0 -65
  179. package/src/tui/utils/index.ts +0 -13
  180. package/src/types/index.ts +0 -1
  181. package/tsconfig.json +0 -25
@@ -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(ctx: AppContext, opts: OptionValues<typeof runOptions>): Promise<RunConfig> {
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(ctx: AppContext, config: RunConfig) {
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?(ctx: AppContext, opts: OptionValues<TOptions>): Promise<TConfig> | TConfig;
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(ctx: AppContext, config: TConfig, execCtx?: CommandExecutionContext): Promise<CommandResult | void> | CommandResult | void;
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?(ctx: AppContext, opts: OptionValues<TOptions>): Promise<void> | void;
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
- // No validation needed - execute is abstract and required
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
  /**
@@ -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
- throw new Error(
44
- "AppContext.current accessed before initialization. " +
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
- AppContext._current = context;
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 list.
64
+ * Format an options schema into a help section.
63
65
  */
64
- export function formatOptions(command: AnyCommand): string {
65
- if (!command.options || Object.keys(command.options).length === 0) return "";
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(command.options).map(([name, defUntyped]) => {
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
- const typeHint = colors.dim(` <${def.type}>`);
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("Options:"), ...entries].join("\n");
83
+ return [colors.bold(title + ":"), ...entries].join("\n");
81
84
  }
82
85
 
83
86
  /**
84
- * Format global options section (available on all commands).
87
+ * Format options list.
85
88
  */
86
- export function formatGlobalOptions(): string {
87
- const entries = [
88
- ` ${colors.yellow(" --log-level")}${colors.dim(" <string>")}\n Set minimum log level${colors.dim(" [silly | trace | debug | info | warn | error | fatal]")}`,
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
- sections.push(`\n${formatGlobalOptions()}`);
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
+ ]);
@@ -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
- Silly = 0,
9
- Trace = 1,
10
- Debug = 2,
11
- Info = 3,
12
- Warn = 4,
13
- Error = 5,
14
- Fatal = 6,
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.Info;
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: unknown
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 meta = logMeta as Record<string, unknown>;
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
- 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
- }
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
- silly(...args: unknown[]): void {
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
  */
@@ -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[]): ResolveResult {
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
- // 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";
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";