@pablozaiden/terminatui 0.2.0 → 0.3.0-beta-1
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/AGENTS.md +14 -2
- package/CLAUDE.md +1 -0
- package/README.md +64 -43
- package/bun.lock +85 -0
- package/examples/tui-app/commands/config/app/get.ts +6 -10
- package/examples/tui-app/commands/config/app/index.ts +2 -6
- package/examples/tui-app/commands/config/app/set.ts +23 -13
- package/examples/tui-app/commands/config/index.ts +2 -6
- package/examples/tui-app/commands/config/user/get.ts +6 -10
- package/examples/tui-app/commands/config/user/index.ts +2 -6
- package/examples/tui-app/commands/config/user/set.ts +6 -10
- package/examples/tui-app/commands/greet.ts +13 -11
- package/examples/tui-app/commands/math.ts +5 -9
- package/examples/tui-app/commands/status.ts +21 -12
- package/examples/tui-app/index.ts +6 -3
- package/guides/01-hello-world.md +7 -2
- package/guides/02-adding-options.md +2 -2
- package/guides/03-multiple-commands.md +6 -8
- package/guides/04-subcommands.md +8 -8
- package/guides/05-interactive-tui.md +45 -30
- package/guides/06-config-validation.md +4 -12
- package/guides/07-async-cancellation.md +14 -16
- package/guides/08-complete-application.md +12 -42
- package/guides/README.md +7 -3
- package/package.json +4 -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__/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 +4 -4
- 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/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 +135 -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 +115 -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 +70 -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 +160 -0
- package/src/tui/screens/ErrorScreen.tsx +58 -0
- package/src/tui/screens/ResultsScreen.tsx +60 -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/examples/tui-app/commands/index.ts +0 -4
- 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/index.ts +0 -137
- 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
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
type OptionSchema,
|
|
5
|
-
type OptionValues,
|
|
6
|
-
type CommandResult
|
|
7
|
-
} from "../../../../../src/index.ts";
|
|
1
|
+
import { Command, type CommandResult } from "../../../../../src/core/command";
|
|
2
|
+
import { AppContext } from "../../../../../src/core/context";
|
|
3
|
+
import type { OptionSchema, OptionValues } from "../../../../../src/types/command";
|
|
8
4
|
|
|
9
5
|
const options = {
|
|
10
6
|
key: {
|
|
@@ -31,7 +27,7 @@ export class UserGetCommand extends Command<typeof options> {
|
|
|
31
27
|
{ command: "config user get --key email", description: "Get user email" },
|
|
32
28
|
];
|
|
33
29
|
|
|
34
|
-
override async execute(
|
|
30
|
+
override async execute(opts: OptionValues<typeof options>): Promise<CommandResult> {
|
|
35
31
|
// Simulated user config store
|
|
36
32
|
const userConfig: Record<string, string> = {
|
|
37
33
|
name: "John Doe",
|
|
@@ -43,14 +39,14 @@ export class UserGetCommand extends Command<typeof options> {
|
|
|
43
39
|
const value = userConfig[opts.key];
|
|
44
40
|
|
|
45
41
|
if (value === undefined) {
|
|
46
|
-
|
|
42
|
+
AppContext.current.logger.warn(`Key "${opts.key}" not found in user configuration`);
|
|
47
43
|
return {
|
|
48
44
|
success: false,
|
|
49
45
|
message: `Key "${opts.key}" not found`,
|
|
50
46
|
};
|
|
51
47
|
}
|
|
52
48
|
|
|
53
|
-
|
|
49
|
+
AppContext.current.logger.info(`Retrieved user.${opts.key} = ${value}`);
|
|
54
50
|
return {
|
|
55
51
|
success: true,
|
|
56
52
|
data: { key: opts.key, value },
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Command,
|
|
3
|
-
type AppContext,
|
|
4
|
-
type CommandResult
|
|
5
|
-
} from "../../../../../src/index.ts";
|
|
1
|
+
import { Command, type CommandResult } from "../../../../../src/core/command.ts";
|
|
6
2
|
import { UserGetCommand } from "./get.ts";
|
|
7
3
|
import { UserSetCommand } from "./set.ts";
|
|
8
4
|
|
|
@@ -17,7 +13,7 @@ export class UserConfigCommand extends Command {
|
|
|
17
13
|
new UserSetCommand(),
|
|
18
14
|
];
|
|
19
15
|
|
|
20
|
-
override execute(
|
|
16
|
+
override execute(): CommandResult {
|
|
21
17
|
console.log("Use 'config user <command>' for user configuration.");
|
|
22
18
|
console.log("Available: get, set");
|
|
23
19
|
return { success: true };
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
type OptionSchema,
|
|
5
|
-
type OptionValues,
|
|
6
|
-
type CommandResult
|
|
7
|
-
} from "../../../../../src/index.ts";
|
|
1
|
+
import { Command, type CommandResult } from "../../../../../src/core/command";
|
|
2
|
+
import { AppContext } from "../../../../../src/core/context";
|
|
3
|
+
import type { OptionSchema, OptionValues } from "../../../../../src/types/command";
|
|
8
4
|
|
|
9
5
|
const options = {
|
|
10
6
|
key: {
|
|
@@ -40,13 +36,13 @@ export class UserSetCommand extends Command<typeof options> {
|
|
|
40
36
|
{ command: "config user set --key theme --value light", description: "Set theme" },
|
|
41
37
|
];
|
|
42
38
|
|
|
43
|
-
override async execute(
|
|
44
|
-
|
|
39
|
+
override async execute(opts: OptionValues<typeof options>): Promise<CommandResult> {
|
|
40
|
+
AppContext.current.logger.info(`Setting user.${opts.key} = "${opts.value}"`);
|
|
45
41
|
|
|
46
42
|
// Simulate setting the value
|
|
47
43
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
48
44
|
|
|
49
|
-
|
|
45
|
+
AppContext.current.logger.info(`Successfully updated user configuration`);
|
|
50
46
|
return {
|
|
51
47
|
success: true,
|
|
52
48
|
data: { key: opts.key, value: opts.value },
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
type CommandResult
|
|
7
|
-
} from "../../../src/index.ts";
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { Command, type CommandResult } from "../../../src/core/command";
|
|
3
|
+
import { AppContext } from "../../../src/core/context";
|
|
4
|
+
import type { OptionSchema, OptionValues } from "../../../src/types/command";
|
|
5
|
+
import { JsonHighlight } from "../../../src/tui/components/JsonHighlight.tsx";
|
|
8
6
|
|
|
9
7
|
const greetOptions = {
|
|
10
8
|
name: {
|
|
@@ -20,7 +18,7 @@ const greetOptions = {
|
|
|
20
18
|
type: "boolean",
|
|
21
19
|
description: "Use uppercase",
|
|
22
20
|
alias: "l",
|
|
23
|
-
default:
|
|
21
|
+
default: true,
|
|
24
22
|
label: "Loud Mode",
|
|
25
23
|
order: 2,
|
|
26
24
|
group: "Options",
|
|
@@ -48,16 +46,20 @@ export class GreetCommand extends Command<typeof greetOptions> {
|
|
|
48
46
|
{ command: "greet --name World --loud --times 3", description: "Loud greeting 3 times" },
|
|
49
47
|
];
|
|
50
48
|
|
|
51
|
-
override async execute(
|
|
49
|
+
override async execute(opts: OptionValues<typeof greetOptions>): Promise<CommandResult> {
|
|
52
50
|
const greeting = this.createGreeting(opts);
|
|
53
|
-
|
|
51
|
+
AppContext.current.logger.trace(greeting);
|
|
54
52
|
return {
|
|
55
53
|
success: true,
|
|
56
|
-
data: { greeting, timestamp: new Date().toISOString() },
|
|
54
|
+
data: { greeting, timestamp: new Date().toISOString(), meta: { loud: opts.loud, times: opts.times } },
|
|
57
55
|
message: greeting,
|
|
58
56
|
};
|
|
59
57
|
}
|
|
60
58
|
|
|
59
|
+
override renderResult(result: CommandResult): ReactNode {
|
|
60
|
+
return JsonHighlight({ value: result.data });
|
|
61
|
+
}
|
|
62
|
+
|
|
61
63
|
override getClipboardContent(result: CommandResult): string | undefined {
|
|
62
64
|
const data = result.data as { greeting?: string } | undefined;
|
|
63
65
|
return data?.greeting;
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
type OptionSchema,
|
|
5
|
-
type OptionValues,
|
|
6
|
-
type CommandResult
|
|
7
|
-
} from "../../../src/index.ts";
|
|
1
|
+
import { Command, type CommandResult } from "../../../src/core/command";
|
|
2
|
+
import { AppContext } from "../../../src/core/context";
|
|
3
|
+
import type { OptionSchema, OptionValues } from "../../../src/types/command";
|
|
8
4
|
|
|
9
5
|
const mathOptions = {
|
|
10
6
|
operation: {
|
|
@@ -50,10 +46,10 @@ export class MathCommand extends Command<typeof mathOptions> {
|
|
|
50
46
|
|
|
51
47
|
override readonly actionLabel = "Calculate";
|
|
52
48
|
|
|
53
|
-
override async execute(
|
|
49
|
+
override async execute(opts: OptionValues<typeof mathOptions>): Promise<CommandResult> {
|
|
54
50
|
const result = this.calculate(opts);
|
|
55
51
|
if (!result.success) {
|
|
56
|
-
|
|
52
|
+
AppContext.current.logger.error(result.message || "Calculation failed");
|
|
57
53
|
}
|
|
58
54
|
return result;
|
|
59
55
|
}
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
type OptionSchema,
|
|
5
|
-
type OptionValues,
|
|
6
|
-
type CommandResult
|
|
7
|
-
} from "../../../src/index.ts";
|
|
1
|
+
import { Command, type CommandExecutionContext, type CommandResult } from "../../../src/core/command";
|
|
2
|
+
import { AppContext } from "../../../src/core/context";
|
|
3
|
+
import type { OptionSchema, OptionValues } from "../../../src/types/command";
|
|
8
4
|
|
|
9
5
|
const statusOptions = {
|
|
10
6
|
detailed: {
|
|
@@ -26,9 +22,9 @@ export class StatusCommand extends Command<typeof statusOptions> {
|
|
|
26
22
|
override readonly actionLabel = "Check Status";
|
|
27
23
|
override readonly immediateExecution = true; // No required fields
|
|
28
24
|
|
|
29
|
-
override async execute(
|
|
30
|
-
const result = await this.getStatus(opts);
|
|
31
|
-
|
|
25
|
+
override async execute(opts: OptionValues<typeof statusOptions>, execCtx : CommandExecutionContext): Promise<CommandResult> {
|
|
26
|
+
const result = await this.getStatus(opts, execCtx);
|
|
27
|
+
AppContext.current.logger.info(result.message || "Status check complete");
|
|
32
28
|
return result;
|
|
33
29
|
}
|
|
34
30
|
|
|
@@ -45,11 +41,24 @@ export class StatusCommand extends Command<typeof statusOptions> {
|
|
|
45
41
|
].join("\n");
|
|
46
42
|
}
|
|
47
43
|
|
|
48
|
-
private async getStatus(opts: OptionValues<typeof statusOptions
|
|
44
|
+
private async getStatus(opts: OptionValues<typeof statusOptions>, execCtx: CommandExecutionContext): Promise<CommandResult> {
|
|
49
45
|
const detailed = opts.detailed as boolean;
|
|
50
46
|
|
|
51
47
|
// Simulate some async work
|
|
52
|
-
await new Promise(
|
|
48
|
+
await new Promise(resolve => {
|
|
49
|
+
let count = 0;
|
|
50
|
+
let interval = setInterval(() => {
|
|
51
|
+
count++;
|
|
52
|
+
AppContext.current.logger.info(`Applying configuration... (${count}/5)`);
|
|
53
|
+
if (count >= 5 || execCtx.signal.aborted) {
|
|
54
|
+
if (count < 5) {
|
|
55
|
+
AppContext.current.logger.warn("Status check aborted");
|
|
56
|
+
}
|
|
57
|
+
clearInterval(interval);
|
|
58
|
+
resolve(undefined);
|
|
59
|
+
}
|
|
60
|
+
}, 1000);
|
|
61
|
+
});
|
|
53
62
|
|
|
54
63
|
const memMB = Math.round(process.memoryUsage().heapUsed / 1024 / 1024);
|
|
55
64
|
const uptimeSec = Math.round(process.uptime());
|
|
@@ -12,8 +12,11 @@
|
|
|
12
12
|
* bun examples/tui-app/index.ts greet --name "World" --loud
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { TuiApplication } from "../../src/
|
|
16
|
-
import {
|
|
15
|
+
import { TuiApplication } from "../../src/tui/TuiApplication.tsx";
|
|
16
|
+
import { ConfigCommand } from "./commands/config/index.ts";
|
|
17
|
+
import { GreetCommand } from "./commands/greet.ts";
|
|
18
|
+
import { MathCommand } from "./commands/math.ts";
|
|
19
|
+
import { StatusCommand } from "./commands/status.ts";
|
|
17
20
|
|
|
18
21
|
class ExampleApp extends TuiApplication {
|
|
19
22
|
constructor() {
|
|
@@ -32,4 +35,4 @@ class ExampleApp extends TuiApplication {
|
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
// Run the app
|
|
35
|
-
await new ExampleApp().run(
|
|
38
|
+
await new ExampleApp().run();
|
package/guides/01-hello-world.md
CHANGED
|
@@ -28,7 +28,12 @@ bun add @pablozaiden/terminatui
|
|
|
28
28
|
Create `src/greet.ts`:
|
|
29
29
|
|
|
30
30
|
```typescript
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
Command,
|
|
33
|
+
type OptionSchema,
|
|
34
|
+
type CommandResult,
|
|
35
|
+
type CommandExecutionContext,
|
|
36
|
+
} from "@pablozaiden/terminatui";
|
|
32
37
|
|
|
33
38
|
const options = {
|
|
34
39
|
name: {
|
|
@@ -43,7 +48,7 @@ export class GreetCommand extends Command<typeof options> {
|
|
|
43
48
|
readonly description = "Greet someone";
|
|
44
49
|
readonly options = options;
|
|
45
50
|
|
|
46
|
-
execute(
|
|
51
|
+
execute(config: { name: string }, _execCtx: CommandExecutionContext): CommandResult {
|
|
47
52
|
console.log(`Hello, ${config.name}!`);
|
|
48
53
|
return { success: true };
|
|
49
54
|
}
|
|
@@ -19,7 +19,7 @@ myapp greet --name Alice --loud
|
|
|
19
19
|
Update `src/greet.ts`:
|
|
20
20
|
|
|
21
21
|
```typescript
|
|
22
|
-
import { Command, type
|
|
22
|
+
import { Command, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
|
|
23
23
|
|
|
24
24
|
const options = {
|
|
25
25
|
name: {
|
|
@@ -40,7 +40,7 @@ export class GreetCommand extends Command<typeof options> {
|
|
|
40
40
|
readonly description = "Greet someone";
|
|
41
41
|
readonly options = options;
|
|
42
42
|
|
|
43
|
-
execute(
|
|
43
|
+
execute(config: { name: string; loud: boolean }): CommandResult {
|
|
44
44
|
const message = `Hello, ${config.name}!`;
|
|
45
45
|
console.log(config.loud ? message.toUpperCase() : message);
|
|
46
46
|
return { success: true };
|
|
@@ -19,8 +19,7 @@ fileutil count --dir ./src --ext .ts
|
|
|
19
19
|
Create `src/commands/list.ts`:
|
|
20
20
|
|
|
21
21
|
```typescript
|
|
22
|
-
import {
|
|
23
|
-
import { Command, type AppContext, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
|
|
22
|
+
import { Command, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
|
|
24
23
|
|
|
25
24
|
const options = {
|
|
26
25
|
dir: {
|
|
@@ -36,9 +35,9 @@ export class ListCommand extends Command<typeof options> {
|
|
|
36
35
|
readonly description = "List files in a directory";
|
|
37
36
|
readonly options = options;
|
|
38
37
|
|
|
39
|
-
execute(
|
|
38
|
+
async execute(config: { dir: string }): Promise<CommandResult> {
|
|
40
39
|
try {
|
|
41
|
-
const files =
|
|
40
|
+
const files = await Array.fromAsync(new Bun.Glob("*").scan({ cwd: config.dir, onlyFiles: false }));
|
|
42
41
|
console.log(`Files in ${config.dir}:`);
|
|
43
42
|
files.forEach((file) => console.log(` ${file}`));
|
|
44
43
|
return { success: true, message: `Found ${files.length} files` };
|
|
@@ -54,8 +53,7 @@ export class ListCommand extends Command<typeof options> {
|
|
|
54
53
|
Create `src/commands/count.ts`:
|
|
55
54
|
|
|
56
55
|
```typescript
|
|
57
|
-
import {
|
|
58
|
-
import { Command, type AppContext, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
|
|
56
|
+
import { Command, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
|
|
59
57
|
|
|
60
58
|
const options = {
|
|
61
59
|
dir: {
|
|
@@ -76,9 +74,9 @@ export class CountCommand extends Command<typeof options> {
|
|
|
76
74
|
readonly description = "Count files in a directory";
|
|
77
75
|
readonly options = options;
|
|
78
76
|
|
|
79
|
-
execute(
|
|
77
|
+
async execute(config: { dir: string; ext?: string }): Promise<CommandResult> {
|
|
80
78
|
try {
|
|
81
|
-
let files =
|
|
79
|
+
let files = await Array.fromAsync(new Bun.Glob("*").scan({ cwd: config.dir, onlyFiles: false }));
|
|
82
80
|
|
|
83
81
|
if (config.ext) {
|
|
84
82
|
files = files.filter((f) => f.endsWith(config.ext!));
|
package/guides/04-subcommands.md
CHANGED
|
@@ -17,7 +17,7 @@ dbctl db status
|
|
|
17
17
|
Create `src/commands/db/migrate.ts`:
|
|
18
18
|
|
|
19
19
|
```typescript
|
|
20
|
-
import { Command, type
|
|
20
|
+
import { Command, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
|
|
21
21
|
|
|
22
22
|
const options = {
|
|
23
23
|
target: {
|
|
@@ -37,7 +37,7 @@ export class MigrateCommand extends Command<typeof options> {
|
|
|
37
37
|
readonly description = "Run database migrations";
|
|
38
38
|
readonly options = options;
|
|
39
39
|
|
|
40
|
-
execute(
|
|
40
|
+
execute(config: { target: string; dry: boolean }): CommandResult {
|
|
41
41
|
ctx.logger.info(`Migrating to: ${config.target}`);
|
|
42
42
|
|
|
43
43
|
if (config.dry) {
|
|
@@ -55,7 +55,7 @@ export class MigrateCommand extends Command<typeof options> {
|
|
|
55
55
|
Create `src/commands/db/seed.ts`:
|
|
56
56
|
|
|
57
57
|
```typescript
|
|
58
|
-
import { Command, type
|
|
58
|
+
import { Command, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
|
|
59
59
|
|
|
60
60
|
const options = {
|
|
61
61
|
file: {
|
|
@@ -71,7 +71,7 @@ export class SeedCommand extends Command<typeof options> {
|
|
|
71
71
|
readonly description = "Seed the database with data";
|
|
72
72
|
readonly options = options;
|
|
73
73
|
|
|
74
|
-
execute(
|
|
74
|
+
execute(config: { file: string }): CommandResult {
|
|
75
75
|
ctx.logger.info(`Seeding from: ${config.file}`);
|
|
76
76
|
console.log(`Loading seed data from ${config.file}...`);
|
|
77
77
|
console.log("Database seeded successfully!");
|
|
@@ -83,14 +83,14 @@ export class SeedCommand extends Command<typeof options> {
|
|
|
83
83
|
Create `src/commands/db/status.ts`:
|
|
84
84
|
|
|
85
85
|
```typescript
|
|
86
|
-
import { Command, type
|
|
86
|
+
import { Command, type CommandResult } from "@pablozaiden/terminatui";
|
|
87
87
|
|
|
88
88
|
export class StatusCommand extends Command {
|
|
89
89
|
readonly name = "status";
|
|
90
90
|
readonly description = "Show database status";
|
|
91
91
|
readonly options = {};
|
|
92
92
|
|
|
93
|
-
execute(
|
|
93
|
+
execute(): CommandResult {
|
|
94
94
|
console.log("Database Status:");
|
|
95
95
|
console.log(" Connected: Yes");
|
|
96
96
|
console.log(" Version: 1.2.3");
|
|
@@ -105,7 +105,7 @@ export class StatusCommand extends Command {
|
|
|
105
105
|
Create `src/commands/db/index.ts`:
|
|
106
106
|
|
|
107
107
|
```typescript
|
|
108
|
-
import { Command, type
|
|
108
|
+
import { Command, type CommandResult } from "@pablozaiden/terminatui";
|
|
109
109
|
import { MigrateCommand } from "./migrate";
|
|
110
110
|
import { SeedCommand } from "./seed";
|
|
111
111
|
import { StatusCommand } from "./status";
|
|
@@ -123,7 +123,7 @@ export class DbCommand extends Command {
|
|
|
123
123
|
];
|
|
124
124
|
|
|
125
125
|
// Parent command can have its own execute (optional)
|
|
126
|
-
execute(
|
|
126
|
+
execute(): CommandResult {
|
|
127
127
|
console.log("Use 'dbctl db <command>' for database operations.");
|
|
128
128
|
console.log("Available: migrate, seed, status");
|
|
129
129
|
return { success: true };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Guide 5: Interactive TUI
|
|
1
|
+
# Guide 5: Interactive TUI
|
|
2
2
|
|
|
3
3
|
Add an auto-generated Terminal User Interface to your CLI.
|
|
4
4
|
|
|
@@ -8,20 +8,34 @@ A task runner with both CLI and interactive TUI modes:
|
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
# CLI mode
|
|
11
|
-
taskr run --task build --env production
|
|
11
|
+
taskr --mode cli run --task build --env production
|
|
12
12
|
|
|
13
|
-
# TUI mode
|
|
14
|
-
|
|
13
|
+
# TUI mode
|
|
14
|
+
# (if your app's default mode is a TUI mode)
|
|
15
|
+
taskr
|
|
16
|
+
|
|
17
|
+
# Force TUI mode
|
|
18
|
+
taskr --mode opentui
|
|
19
|
+
# or
|
|
20
|
+
taskr --mode ink
|
|
21
|
+
|
|
22
|
+
# Force CLI mode
|
|
23
|
+
taskr --mode cli
|
|
15
24
|
```
|
|
16
25
|
|
|
17
|
-
|
|
26
|
+
Only the selected mode (`--mode`) or your app's default mode controls whether the app runs in CLI or TUI.
|
|
18
27
|
|
|
19
28
|
## Step 1: Create the Command with TUI Metadata
|
|
20
29
|
|
|
21
30
|
Create `src/commands/run.ts`:
|
|
22
31
|
|
|
23
32
|
```typescript
|
|
24
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
Command,
|
|
35
|
+
type OptionSchema,
|
|
36
|
+
type CommandResult,
|
|
37
|
+
type CommandExecutionContext,
|
|
38
|
+
} from "@pablozaiden/terminatui";
|
|
25
39
|
|
|
26
40
|
const options = {
|
|
27
41
|
task: {
|
|
@@ -70,11 +84,9 @@ export class RunCommand extends Command<typeof options, RunConfig> {
|
|
|
70
84
|
override readonly displayName = "Run Task";
|
|
71
85
|
override readonly actionLabel = "Start Task";
|
|
72
86
|
|
|
73
|
-
async execute(
|
|
74
|
-
ctx.logger.info(`Starting task: ${config.task}`);
|
|
75
|
-
|
|
87
|
+
async execute(config: RunConfig, _execCtx: CommandExecutionContext): Promise<CommandResult> {
|
|
76
88
|
if (config.verbose) {
|
|
77
|
-
|
|
89
|
+
console.debug(`Environment: ${config.env}`);
|
|
78
90
|
}
|
|
79
91
|
|
|
80
92
|
// Simulate task execution
|
|
@@ -82,10 +94,10 @@ export class RunCommand extends Command<typeof options, RunConfig> {
|
|
|
82
94
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
83
95
|
console.log("Task completed!");
|
|
84
96
|
|
|
85
|
-
return {
|
|
86
|
-
success: true,
|
|
97
|
+
return {
|
|
98
|
+
success: true,
|
|
87
99
|
data: { task: config.task, env: config.env },
|
|
88
|
-
message: `Task ${config.task} completed successfully
|
|
100
|
+
message: `Task ${config.task} completed successfully`,
|
|
89
101
|
};
|
|
90
102
|
}
|
|
91
103
|
}
|
|
@@ -100,13 +112,16 @@ import { TuiApplication } from "@pablozaiden/terminatui";
|
|
|
100
112
|
import { RunCommand } from "./commands/run";
|
|
101
113
|
|
|
102
114
|
class TaskRunnerApp extends TuiApplication {
|
|
115
|
+
// Default is CLI; each app decides.
|
|
116
|
+
protected override defaultMode = "opentui" as const;
|
|
117
|
+
|
|
103
118
|
constructor() {
|
|
104
119
|
super({
|
|
105
120
|
name: "taskr",
|
|
106
|
-
displayName: "🚀 Task Runner",
|
|
121
|
+
displayName: "🚀 Task Runner", // Shown in TUI header
|
|
107
122
|
version: "1.0.0",
|
|
108
123
|
commands: [new RunCommand()],
|
|
109
|
-
enableTui: true,
|
|
124
|
+
enableTui: true, // Default: true
|
|
110
125
|
});
|
|
111
126
|
}
|
|
112
127
|
}
|
|
@@ -116,26 +131,26 @@ await new TaskRunnerApp().run();
|
|
|
116
131
|
|
|
117
132
|
## Step 3: Test Both Modes
|
|
118
133
|
|
|
119
|
-
**CLI Mode** (
|
|
134
|
+
**CLI Mode** (forced):
|
|
120
135
|
|
|
121
136
|
```bash
|
|
122
|
-
bun src/index.ts run --task build --env production
|
|
137
|
+
bun src/index.ts --mode cli run --task build --env production
|
|
123
138
|
# Running build in production...
|
|
124
139
|
# Task completed!
|
|
125
140
|
```
|
|
126
141
|
|
|
127
|
-
**TUI Mode** (
|
|
142
|
+
**TUI Mode** (forced):
|
|
128
143
|
|
|
129
144
|
```bash
|
|
130
|
-
bun src/index.ts
|
|
145
|
+
bun src/index.ts --mode opentui
|
|
131
146
|
```
|
|
132
147
|
|
|
133
148
|
This opens an interactive interface:
|
|
134
149
|
- Use ↑/↓ to navigate fields
|
|
135
150
|
- Press Enter to edit a field
|
|
151
|
+
- Navigate to "CLI Command" button and press Enter to see the CLI command
|
|
136
152
|
- Press Enter on "Start Task" to run
|
|
137
153
|
- Press Esc to go back
|
|
138
|
-
- Press C to see the CLI command
|
|
139
154
|
|
|
140
155
|
## TUI Metadata Reference
|
|
141
156
|
|
|
@@ -145,13 +160,13 @@ Add these properties to your options for TUI customization:
|
|
|
145
160
|
{
|
|
146
161
|
type: "string",
|
|
147
162
|
description: "...",
|
|
148
|
-
|
|
163
|
+
|
|
149
164
|
// TUI-specific
|
|
150
|
-
label: "Display Label",
|
|
151
|
-
order: 1,
|
|
152
|
-
group: "Settings",
|
|
153
|
-
placeholder: "Enter...",
|
|
154
|
-
tuiHidden: false,
|
|
165
|
+
label: "Display Label", // Custom field label
|
|
166
|
+
order: 1, // Field sort order
|
|
167
|
+
group: "Settings", // Group heading
|
|
168
|
+
placeholder: "Enter...", // Placeholder text
|
|
169
|
+
tuiHidden: false, // Hide from TUI (still in CLI)
|
|
155
170
|
}
|
|
156
171
|
```
|
|
157
172
|
|
|
@@ -161,10 +176,10 @@ Add these properties to your options for TUI customization:
|
|
|
161
176
|
class MyCommand extends Command {
|
|
162
177
|
// Display name in command selector
|
|
163
178
|
override readonly displayName = "My Command";
|
|
164
|
-
|
|
179
|
+
|
|
165
180
|
// Button text (default: "Run")
|
|
166
181
|
override readonly actionLabel = "Execute";
|
|
167
|
-
|
|
182
|
+
|
|
168
183
|
// Skip config screen, run immediately
|
|
169
184
|
override readonly immediateExecution = false;
|
|
170
185
|
}
|
|
@@ -175,9 +190,8 @@ class MyCommand extends Command {
|
|
|
175
190
|
| Key | Action |
|
|
176
191
|
|-----|--------|
|
|
177
192
|
| ↑/↓ | Navigate fields |
|
|
178
|
-
| Enter | Edit field / Run |
|
|
193
|
+
| Enter | Edit field / Press button / Run |
|
|
179
194
|
| Tab | Cycle focus |
|
|
180
|
-
| C | Show CLI command |
|
|
181
195
|
| L | Toggle logs |
|
|
182
196
|
| Ctrl+Y | Copy to clipboard |
|
|
183
197
|
| Esc | Back / Cancel |
|
|
@@ -185,6 +199,7 @@ class MyCommand extends Command {
|
|
|
185
199
|
## What You Learned
|
|
186
200
|
|
|
187
201
|
- Use `TuiApplication` instead of `Application`
|
|
202
|
+
- Use `--mode` (or app default mode) to control CLI vs TUI
|
|
188
203
|
- Add TUI metadata to options (label, order, group)
|
|
189
204
|
- Customize with `displayName` and `actionLabel`
|
|
190
205
|
- Both CLI and TUI work with the same command
|
|
@@ -16,11 +16,9 @@ Create `src/commands/deploy.ts`:
|
|
|
16
16
|
|
|
17
17
|
```typescript
|
|
18
18
|
import path from "node:path";
|
|
19
|
-
import { existsSync } from "node:fs";
|
|
20
19
|
import {
|
|
21
20
|
Command,
|
|
22
21
|
ConfigValidationError,
|
|
23
|
-
type AppContext,
|
|
24
22
|
type OptionSchema,
|
|
25
23
|
type OptionValues,
|
|
26
24
|
type CommandResult
|
|
@@ -84,10 +82,7 @@ export class DeployCommand extends Command<typeof options, DeployConfig> {
|
|
|
84
82
|
* Transform and validate raw options into DeployConfig.
|
|
85
83
|
* Runs before execute() - errors here show helpful messages.
|
|
86
84
|
*/
|
|
87
|
-
override buildConfig(
|
|
88
|
-
_ctx: AppContext,
|
|
89
|
-
opts: OptionValues<typeof options>
|
|
90
|
-
): DeployConfig {
|
|
85
|
+
override async buildConfig(opts: OptionValues<typeof options>): Promise<DeployConfig> {
|
|
91
86
|
// 1. Validate app path exists
|
|
92
87
|
const appRaw = opts["app"] as string | undefined;
|
|
93
88
|
if (!appRaw) {
|
|
@@ -98,7 +93,7 @@ export class DeployCommand extends Command<typeof options, DeployConfig> {
|
|
|
98
93
|
}
|
|
99
94
|
|
|
100
95
|
const appPath = path.resolve(appRaw);
|
|
101
|
-
if (!
|
|
96
|
+
if (!(await Bun.file(appPath).exists())) {
|
|
102
97
|
throw new ConfigValidationError(
|
|
103
98
|
`Application path does not exist: ${appPath}`,
|
|
104
99
|
"app"
|
|
@@ -157,11 +152,8 @@ export class DeployCommand extends Command<typeof options, DeployConfig> {
|
|
|
157
152
|
* Execute with fully validated DeployConfig.
|
|
158
153
|
* No need to validate here - buildConfig already did it!
|
|
159
154
|
*/
|
|
160
|
-
async execute(
|
|
161
|
-
|
|
162
|
-
ctx.logger.debug(`Path: ${config.appPath}`);
|
|
163
|
-
ctx.logger.debug(`Replicas: ${config.replicas}`);
|
|
164
|
-
ctx.logger.debug(`URL: ${config.envConfig.url}`);
|
|
155
|
+
async execute(config: DeployConfig): Promise<CommandResult> {
|
|
156
|
+
console.log(`Deploying ${config.appName} to ${config.environment}`);
|
|
165
157
|
|
|
166
158
|
if (config.dryRun) {
|
|
167
159
|
console.log("DRY RUN - Would deploy:");
|