@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,66 +1,45 @@
|
|
|
1
1
|
import { test, expect, describe } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { CommandRegistry } from "../core/registry.ts";
|
|
3
|
+
import { Command } from "../core/command.ts";
|
|
4
|
+
import type { OptionSchema } from "../types/command.ts";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
name: "test",
|
|
10
|
-
description: "Test command",
|
|
11
|
-
execute: () => {},
|
|
12
|
-
});
|
|
6
|
+
class SimpleCommand extends Command<OptionSchema> {
|
|
7
|
+
readonly name: string;
|
|
8
|
+
readonly description: string;
|
|
9
|
+
readonly options = {} as const;
|
|
13
10
|
|
|
14
|
-
|
|
11
|
+
constructor(name: string, description = "") {
|
|
12
|
+
super();
|
|
13
|
+
this.name = name;
|
|
14
|
+
this.description = description;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
override async execute(): Promise<void> {}
|
|
18
|
+
}
|
|
15
19
|
|
|
20
|
+
describe("CommandRegistry", () => {
|
|
21
|
+
test("registers a command", () => {
|
|
22
|
+
const registry = new CommandRegistry();
|
|
23
|
+
registry.register(new SimpleCommand("test", "Test command"));
|
|
16
24
|
expect(registry.has("test")).toBe(true);
|
|
17
25
|
});
|
|
18
26
|
|
|
19
27
|
test("retrieves command by name", () => {
|
|
20
|
-
const registry =
|
|
21
|
-
const cmd =
|
|
22
|
-
name: "greet",
|
|
23
|
-
description: "Greet command",
|
|
24
|
-
execute: () => {},
|
|
25
|
-
});
|
|
26
|
-
|
|
28
|
+
const registry = new CommandRegistry();
|
|
29
|
+
const cmd = new SimpleCommand("greet", "Greet command");
|
|
27
30
|
registry.register(cmd);
|
|
28
|
-
|
|
29
31
|
expect(registry.get("greet")).toBe(cmd);
|
|
30
32
|
});
|
|
31
33
|
|
|
32
|
-
test("resolves command by alias", () => {
|
|
33
|
-
const registry = createCommandRegistry();
|
|
34
|
-
const cmd = defineCommand({
|
|
35
|
-
name: "list",
|
|
36
|
-
description: "List command",
|
|
37
|
-
aliases: ["ls", "l"],
|
|
38
|
-
execute: () => {},
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
registry.register(cmd);
|
|
42
|
-
|
|
43
|
-
expect(registry.resolve("ls")).toBe(cmd);
|
|
44
|
-
expect(registry.resolve("l")).toBe(cmd);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
34
|
test("returns undefined for unknown command", () => {
|
|
48
|
-
const registry =
|
|
35
|
+
const registry = new CommandRegistry();
|
|
49
36
|
expect(registry.get("unknown")).toBeUndefined();
|
|
50
37
|
});
|
|
51
38
|
|
|
52
39
|
test("lists all commands", () => {
|
|
53
|
-
const registry =
|
|
54
|
-
const cmd1 =
|
|
55
|
-
|
|
56
|
-
description: "A",
|
|
57
|
-
execute: () => {},
|
|
58
|
-
});
|
|
59
|
-
const cmd2 = defineCommand({
|
|
60
|
-
name: "b",
|
|
61
|
-
description: "B",
|
|
62
|
-
execute: () => {},
|
|
63
|
-
});
|
|
40
|
+
const registry = new CommandRegistry();
|
|
41
|
+
const cmd1 = new SimpleCommand("a", "A");
|
|
42
|
+
const cmd2 = new SimpleCommand("b", "B");
|
|
64
43
|
|
|
65
44
|
registry.register(cmd1);
|
|
66
45
|
registry.register(cmd2);
|
|
@@ -71,125 +50,19 @@ describe("createCommandRegistry", () => {
|
|
|
71
50
|
});
|
|
72
51
|
|
|
73
52
|
test("throws on duplicate registration", () => {
|
|
74
|
-
const registry =
|
|
75
|
-
const cmd =
|
|
76
|
-
name: "dup",
|
|
77
|
-
description: "Duplicate",
|
|
78
|
-
execute: () => {},
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
registry.register(cmd);
|
|
82
|
-
|
|
83
|
-
expect(() => registry.register(cmd)).toThrow();
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test("throws on alias conflict", () => {
|
|
87
|
-
const registry = createCommandRegistry();
|
|
88
|
-
const cmd1 = defineCommand({
|
|
89
|
-
name: "first",
|
|
90
|
-
description: "First",
|
|
91
|
-
aliases: ["f"],
|
|
92
|
-
execute: () => {},
|
|
93
|
-
});
|
|
94
|
-
const cmd2 = defineCommand({
|
|
95
|
-
name: "second",
|
|
96
|
-
description: "Second",
|
|
97
|
-
aliases: ["f"],
|
|
98
|
-
execute: () => {},
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
registry.register(cmd1);
|
|
102
|
-
|
|
103
|
-
expect(() => registry.register(cmd2)).toThrow();
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
test("has method checks existence by name", () => {
|
|
107
|
-
const registry = createCommandRegistry();
|
|
108
|
-
const cmd = defineCommand({
|
|
109
|
-
name: "exists",
|
|
110
|
-
description: "Exists",
|
|
111
|
-
execute: () => {},
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
registry.register(cmd);
|
|
115
|
-
|
|
116
|
-
expect(registry.has("exists")).toBe(true);
|
|
117
|
-
expect(registry.has("notexists")).toBe(false);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test("has method checks existence by alias", () => {
|
|
121
|
-
const registry = createCommandRegistry();
|
|
122
|
-
const cmd = defineCommand({
|
|
123
|
-
name: "cmd",
|
|
124
|
-
description: "Cmd",
|
|
125
|
-
aliases: ["c"],
|
|
126
|
-
execute: () => {},
|
|
127
|
-
});
|
|
128
|
-
|
|
53
|
+
const registry = new CommandRegistry();
|
|
54
|
+
const cmd = new SimpleCommand("dup", "Duplicate");
|
|
129
55
|
registry.register(cmd);
|
|
130
|
-
|
|
131
|
-
expect(registry.has("c")).toBe(true);
|
|
56
|
+
expect(() => registry.register(cmd)).toThrow(/already registered/i);
|
|
132
57
|
});
|
|
133
58
|
|
|
134
|
-
test("
|
|
135
|
-
const registry =
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
description: "A",
|
|
139
|
-
execute: () => {},
|
|
140
|
-
});
|
|
141
|
-
const cmdB = defineCommand({
|
|
142
|
-
name: "b",
|
|
143
|
-
description: "B",
|
|
144
|
-
execute: () => {},
|
|
145
|
-
});
|
|
59
|
+
test("names returns command names", () => {
|
|
60
|
+
const registry = new CommandRegistry();
|
|
61
|
+
registry.register(new SimpleCommand("a", "A"));
|
|
62
|
+
registry.register(new SimpleCommand("b", "B"));
|
|
146
63
|
|
|
147
|
-
registry.
|
|
148
|
-
registry.register(cmdB);
|
|
149
|
-
|
|
150
|
-
const names = registry.getNames();
|
|
64
|
+
const names = registry.names();
|
|
151
65
|
expect(names).toContain("a");
|
|
152
66
|
expect(names).toContain("b");
|
|
153
67
|
});
|
|
154
|
-
|
|
155
|
-
test("getCommandMap returns command map", () => {
|
|
156
|
-
const registry = createCommandRegistry();
|
|
157
|
-
const cmdA = defineCommand({
|
|
158
|
-
name: "a",
|
|
159
|
-
description: "A",
|
|
160
|
-
execute: () => {},
|
|
161
|
-
});
|
|
162
|
-
const cmdB = defineCommand({
|
|
163
|
-
name: "b",
|
|
164
|
-
description: "B",
|
|
165
|
-
execute: () => {},
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
registry.register(cmdA);
|
|
169
|
-
registry.register(cmdB);
|
|
170
|
-
|
|
171
|
-
const map = registry.getCommandMap();
|
|
172
|
-
expect(map["a"]).toBe(cmdA);
|
|
173
|
-
expect(map["b"]).toBe(cmdB);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
test("resolve returns undefined for non-existent command", () => {
|
|
177
|
-
const registry = createCommandRegistry();
|
|
178
|
-
expect(registry.resolve("nonexistent")).toBeUndefined();
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
test("get does not resolve aliases", () => {
|
|
182
|
-
const registry = createCommandRegistry();
|
|
183
|
-
const cmd = defineCommand({
|
|
184
|
-
name: "list",
|
|
185
|
-
description: "List command",
|
|
186
|
-
aliases: ["ls"],
|
|
187
|
-
execute: () => {},
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
registry.register(cmd);
|
|
191
|
-
|
|
192
|
-
expect(registry.get("ls")).toBeUndefined();
|
|
193
|
-
expect(registry.resolve("ls")).toBe(cmd);
|
|
194
|
-
});
|
|
195
68
|
});
|
|
@@ -1,176 +1,93 @@
|
|
|
1
|
-
import { describe,
|
|
2
|
-
import {
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { getFieldDisplayValue, schemaToFieldConfigs } from "../tui/utils/schemaToFields.ts";
|
|
3
3
|
import type { OptionSchema } from "../types/command.ts";
|
|
4
4
|
|
|
5
|
-
describe("
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
5
|
+
describe("schemaToFields", () => {
|
|
6
|
+
describe("schemaToFieldConfigs", () => {
|
|
7
|
+
test("maps option schema to field configs", () => {
|
|
8
|
+
const schema: OptionSchema = {
|
|
9
|
+
name: { type: "string", description: "User name" },
|
|
10
|
+
color: {
|
|
11
|
+
type: "string",
|
|
12
|
+
description: "Color choice",
|
|
13
|
+
enum: ["red", "green", "blue"],
|
|
14
|
+
},
|
|
15
|
+
verbose: { type: "boolean", description: "Verbose output" },
|
|
16
|
+
count: { type: "number", description: "Count" },
|
|
17
|
+
files: { type: "array", description: "Files to process" },
|
|
18
|
+
repoPath: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "Repository path",
|
|
21
|
+
label: "Repository",
|
|
22
|
+
},
|
|
23
|
+
hidden: { type: "string", description: "Hidden", tuiHidden: true },
|
|
24
|
+
path: { type: "string", description: "Path", placeholder: "Enter path here" },
|
|
25
|
+
grouped: { type: "string", description: "Grouped", group: "Basic" },
|
|
26
|
+
};
|
|
20
27
|
|
|
21
|
-
|
|
22
|
-
const schema: OptionSchema = {
|
|
23
|
-
color: {
|
|
24
|
-
type: "string",
|
|
25
|
-
description: "Color choice",
|
|
26
|
-
enum: ["red", "green", "blue"],
|
|
27
|
-
},
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const fields = schemaToFieldConfigs(schema);
|
|
31
|
-
expect(fields[0]?.type).toBe("enum");
|
|
32
|
-
expect(fields[0]?.options).toHaveLength(3);
|
|
33
|
-
expect(fields[0]?.options?.[0]?.name).toBe("red");
|
|
34
|
-
});
|
|
28
|
+
const fields = schemaToFieldConfigs(schema);
|
|
35
29
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
type: "boolean",
|
|
40
|
-
description: "Verbose output",
|
|
41
|
-
},
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const fields = schemaToFieldConfigs(schema);
|
|
45
|
-
expect(fields[0]?.type).toBe("boolean");
|
|
46
|
-
});
|
|
30
|
+
// Basic mappings
|
|
31
|
+
const name = fields.find((f) => f.key === "name");
|
|
32
|
+
expect(name).toMatchObject({ type: "text", label: "Name" });
|
|
47
33
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
type: "number",
|
|
52
|
-
description: "Count",
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const fields = schemaToFieldConfigs(schema);
|
|
57
|
-
expect(fields[0]?.type).toBe("number");
|
|
58
|
-
});
|
|
34
|
+
const color = fields.find((f) => f.key === "color");
|
|
35
|
+
expect(color?.type).toBe("enum");
|
|
36
|
+
expect(color?.options?.length).toBe(3);
|
|
59
37
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
type: "array",
|
|
64
|
-
description: "Files to process",
|
|
65
|
-
},
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const fields = schemaToFieldConfigs(schema);
|
|
69
|
-
expect(fields[0]?.type).toBe("text");
|
|
70
|
-
});
|
|
38
|
+
expect(fields.find((f) => f.key === "verbose")?.type).toBe("boolean");
|
|
39
|
+
expect(fields.find((f) => f.key === "count")?.type).toBe("number");
|
|
40
|
+
expect(fields.find((f) => f.key === "files")?.type).toBe("text");
|
|
71
41
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
label: "Repository",
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const fields = schemaToFieldConfigs(schema);
|
|
82
|
-
expect(fields[0]?.label).toBe("Repository");
|
|
42
|
+
// Decorations
|
|
43
|
+
expect(fields.find((f) => f.key === "repoPath")?.label).toBe("Repository");
|
|
44
|
+
expect(fields.some((f) => f.key === "hidden")).toBe(false);
|
|
45
|
+
expect(fields.find((f) => f.key === "path")?.placeholder).toBe("Enter path here");
|
|
46
|
+
expect(fields.find((f) => f.key === "grouped")?.group).toBe("Basic");
|
|
83
47
|
});
|
|
84
48
|
|
|
85
49
|
test("sorts by order", () => {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const fields = schemaToFieldConfigs(schema);
|
|
93
|
-
expect(fields[0]?.key).toBe("first");
|
|
94
|
-
expect(fields[1]?.key).toBe("second");
|
|
95
|
-
expect(fields[2]?.key).toBe("third");
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test("excludes tuiHidden fields", () => {
|
|
99
|
-
const schema: OptionSchema = {
|
|
100
|
-
visible: { type: "string", description: "Visible" },
|
|
101
|
-
hidden: { type: "string", description: "Hidden", tuiHidden: true },
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
const fields = schemaToFieldConfigs(schema);
|
|
105
|
-
expect(fields).toHaveLength(1);
|
|
106
|
-
expect(fields[0]?.key).toBe("visible");
|
|
107
|
-
});
|
|
50
|
+
const schema: OptionSchema = {
|
|
51
|
+
third: { type: "string", description: "Third", order: 3 },
|
|
52
|
+
first: { type: "string", description: "First", order: 1 },
|
|
53
|
+
second: { type: "string", description: "Second", order: 2 },
|
|
54
|
+
};
|
|
108
55
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
path: {
|
|
112
|
-
type: "string",
|
|
113
|
-
description: "Path",
|
|
114
|
-
placeholder: "Enter path here",
|
|
115
|
-
},
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
const fields = schemaToFieldConfigs(schema);
|
|
119
|
-
expect(fields[0]?.placeholder).toBe("Enter path here");
|
|
56
|
+
const fields = schemaToFieldConfigs(schema);
|
|
57
|
+
expect(fields.map((f) => f.key)).toEqual(["first", "second", "third"]);
|
|
120
58
|
});
|
|
59
|
+
});
|
|
121
60
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const fields = schemaToFieldConfigs(schema);
|
|
132
|
-
expect(fields[0]?.group).toBe("Basic");
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
describe("getFieldDisplayValue", () => {
|
|
137
|
-
test("displays boolean as True/False", () => {
|
|
138
|
-
const field = { key: "enabled", label: "Enabled", type: "boolean" as const };
|
|
139
|
-
expect(getFieldDisplayValue(true, field)).toBe("True");
|
|
140
|
-
expect(getFieldDisplayValue(false, field)).toBe("False");
|
|
141
|
-
});
|
|
61
|
+
describe("getFieldDisplayValue", () => {
|
|
62
|
+
test("formats values for display", () => {
|
|
63
|
+
expect(getFieldDisplayValue(true, { key: "enabled", label: "Enabled", type: "boolean" })).toBe(
|
|
64
|
+
"True"
|
|
65
|
+
);
|
|
66
|
+
expect(getFieldDisplayValue(false, { key: "enabled", label: "Enabled", type: "boolean" })).toBe(
|
|
67
|
+
"False"
|
|
68
|
+
);
|
|
142
69
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
expect(getFieldDisplayValue("red", field)).toBe("Red");
|
|
154
|
-
expect(getFieldDisplayValue("green", field)).toBe("Green");
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test("displays (empty) for empty strings", () => {
|
|
158
|
-
const field = { key: "name", label: "Name", type: "text" as const };
|
|
159
|
-
expect(getFieldDisplayValue("", field)).toBe("(empty)");
|
|
160
|
-
expect(getFieldDisplayValue(null, field)).toBe("(empty)");
|
|
161
|
-
expect(getFieldDisplayValue(undefined, field)).toBe("(empty)");
|
|
162
|
-
});
|
|
70
|
+
const enumField = {
|
|
71
|
+
key: "color",
|
|
72
|
+
label: "Color",
|
|
73
|
+
type: "enum" as const,
|
|
74
|
+
options: [
|
|
75
|
+
{ name: "Red", value: "red" },
|
|
76
|
+
{ name: "Green", value: "green" },
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
expect(getFieldDisplayValue("red", enumField)).toBe("Red");
|
|
163
80
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
expect(result.endsWith("...")).toBe(true);
|
|
170
|
-
});
|
|
81
|
+
const textField = { key: "name", label: "Name", type: "text" as const };
|
|
82
|
+
expect(getFieldDisplayValue("", textField)).toBe("(empty)");
|
|
83
|
+
expect(getFieldDisplayValue(null, textField)).toBe("(empty)");
|
|
84
|
+
expect(getFieldDisplayValue(undefined, textField)).toBe("(empty)");
|
|
85
|
+
expect(getFieldDisplayValue("hello", textField)).toBe("hello");
|
|
171
86
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
87
|
+
const longValue = "a".repeat(100);
|
|
88
|
+
const longResult = getFieldDisplayValue(longValue, { key: "desc", label: "Description", type: "text" });
|
|
89
|
+
expect(longResult.length).toBe(60);
|
|
90
|
+
expect(longResult.endsWith("...")).toBe(true);
|
|
175
91
|
});
|
|
92
|
+
});
|
|
176
93
|
});
|
package/src/builtins/help.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Command, type AnyCommand } from "../core/command.ts";
|
|
2
|
-
import type { AppContext } from "../core/context.ts";
|
|
3
2
|
import { generateCommandHelp, generateAppHelp } from "../core/help.ts";
|
|
3
|
+
import { KNOWN_COMMANDS } from "../core/knownCommands.ts";
|
|
4
4
|
import type { OptionSchema } from "../types/command.ts";
|
|
5
|
+
import { GLOBAL_OPTIONS_SCHEMA } from "../core/application.ts";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Built-in help command that is auto-injected as a subcommand into all commands.
|
|
@@ -11,8 +12,9 @@ import type { OptionSchema } from "../types/command.ts";
|
|
|
11
12
|
* be instantiated directly.
|
|
12
13
|
*/
|
|
13
14
|
export class HelpCommand extends Command<OptionSchema> {
|
|
14
|
-
readonly name =
|
|
15
|
+
readonly name = KNOWN_COMMANDS.help;
|
|
15
16
|
readonly description = "Show help for this command";
|
|
17
|
+
override readonly tuiHidden = true;
|
|
16
18
|
readonly options = {} as const;
|
|
17
19
|
|
|
18
20
|
private parentCommand: AnyCommand | null = null;
|
|
@@ -40,7 +42,7 @@ export class HelpCommand extends Command<OptionSchema> {
|
|
|
40
42
|
return false;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
override async execute(
|
|
45
|
+
override async execute(): Promise<void> {
|
|
44
46
|
let helpText: string;
|
|
45
47
|
|
|
46
48
|
if (this.parentCommand) {
|
|
@@ -48,12 +50,18 @@ export class HelpCommand extends Command<OptionSchema> {
|
|
|
48
50
|
helpText = generateCommandHelp(this.parentCommand, {
|
|
49
51
|
appName: this.appName,
|
|
50
52
|
version: this.appVersion,
|
|
53
|
+
globalOptionsSchema: GLOBAL_OPTIONS_SCHEMA,
|
|
51
54
|
});
|
|
52
55
|
} else {
|
|
53
56
|
// Show help for the entire application
|
|
54
|
-
|
|
57
|
+
const visibleCommands = this.allCommands.filter(
|
|
58
|
+
(cmd) => !cmd.tuiHidden || cmd.supportsCli()
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
helpText = generateAppHelp(visibleCommands, {
|
|
55
62
|
appName: this.appName,
|
|
56
63
|
version: this.appVersion,
|
|
64
|
+
globalOptionsSchema: GLOBAL_OPTIONS_SCHEMA,
|
|
57
65
|
});
|
|
58
66
|
}
|
|
59
67
|
|
package/src/builtins/settings.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Command } from "../core/command.ts";
|
|
2
|
-
import
|
|
2
|
+
import { AppContext } from "../core/context.ts";
|
|
3
3
|
import { LogLevel } from "../core/logger.ts";
|
|
4
4
|
import type { OptionSchema, OptionValues } from "../types/command.ts";
|
|
5
5
|
import type { CommandResult } from "../core/command.ts";
|
|
6
|
+
import { getEnumKeys } from "../tui/utils/getEnumKeys.ts";
|
|
7
|
+
import { KNOWN_COMMANDS } from "../core/knownCommands.ts";
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Options schema for the settings command.
|
|
@@ -12,7 +14,7 @@ const settingsOptions = {
|
|
|
12
14
|
type: "string",
|
|
13
15
|
description: "Minimum log level to emit",
|
|
14
16
|
default: "info",
|
|
15
|
-
enum:
|
|
17
|
+
enum: getEnumKeys(LogLevel) as (keyof typeof LogLevel)[],
|
|
16
18
|
label: "Log Level",
|
|
17
19
|
order: 1,
|
|
18
20
|
},
|
|
@@ -35,28 +37,6 @@ interface SettingsConfig {
|
|
|
35
37
|
detailedLogs: boolean;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
/**
|
|
39
|
-
* Map of string log level names to LogLevel enum values.
|
|
40
|
-
*/
|
|
41
|
-
const logLevelMap: Record<string, LogLevel> = {
|
|
42
|
-
silly: LogLevel.Silly,
|
|
43
|
-
trace: LogLevel.Trace,
|
|
44
|
-
debug: LogLevel.Debug,
|
|
45
|
-
info: LogLevel.Info,
|
|
46
|
-
warn: LogLevel.Warn,
|
|
47
|
-
error: LogLevel.Error,
|
|
48
|
-
fatal: LogLevel.Fatal,
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Parse a string log level to the LogLevel enum.
|
|
53
|
-
*/
|
|
54
|
-
function parseLogLevel(value?: string): LogLevel {
|
|
55
|
-
if (!value) return LogLevel.Info;
|
|
56
|
-
const level = logLevelMap[value.toLowerCase()];
|
|
57
|
-
return level ?? LogLevel.Info;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
40
|
/**
|
|
61
41
|
* Built-in settings command for configuring logging.
|
|
62
42
|
*
|
|
@@ -69,32 +49,38 @@ function parseLogLevel(value?: string): LogLevel {
|
|
|
69
49
|
* In TUI mode, this command provides a UI for configuring these settings.
|
|
70
50
|
*/
|
|
71
51
|
export class SettingsCommand extends Command<typeof settingsOptions, SettingsConfig> {
|
|
72
|
-
|
|
52
|
+
override supportsCli(): boolean {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
readonly name = KNOWN_COMMANDS.settings;
|
|
73
57
|
override readonly displayName = "Settings";
|
|
58
|
+
override readonly tuiHidden = true;
|
|
74
59
|
readonly description = "Configure logging level and output format";
|
|
75
60
|
readonly options = settingsOptions;
|
|
76
61
|
|
|
77
62
|
override readonly actionLabel = "Save Settings";
|
|
78
63
|
override readonly immediateExecution = false;
|
|
79
64
|
|
|
80
|
-
override buildConfig(
|
|
81
|
-
const
|
|
65
|
+
override buildConfig(opts: SettingsOptions): SettingsConfig {
|
|
66
|
+
const logLevelStr = opts["log-level"];
|
|
67
|
+
const logLevel = LogLevel[logLevelStr as keyof typeof LogLevel] ?? LogLevel.info;
|
|
82
68
|
const detailedLogs = Boolean(opts["detailed-logs"]);
|
|
83
69
|
|
|
84
70
|
return { logLevel, detailedLogs };
|
|
85
71
|
}
|
|
86
72
|
|
|
87
|
-
override async execute(
|
|
88
|
-
this.applySettings(
|
|
73
|
+
override async execute(config: SettingsConfig): Promise<CommandResult> {
|
|
74
|
+
this.applySettings(config);
|
|
89
75
|
return {
|
|
90
76
|
success: true,
|
|
91
77
|
message: `Logging set to ${LogLevel[config.logLevel]}${config.detailedLogs ? " with detailed format" : ""}`,
|
|
92
78
|
};
|
|
93
79
|
}
|
|
94
80
|
|
|
95
|
-
private applySettings(
|
|
96
|
-
|
|
97
|
-
|
|
81
|
+
private applySettings(config: SettingsConfig): void {
|
|
82
|
+
AppContext.current.logger.setMinLevel(config.logLevel);
|
|
83
|
+
AppContext.current.logger.setDetailed(config.detailedLogs);
|
|
98
84
|
}
|
|
99
85
|
}
|
|
100
86
|
|
package/src/builtins/version.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { Command } from "../core/command.ts";
|
|
2
|
-
import type { AppContext } from "../core/context.ts";
|
|
3
2
|
import { colors } from "../cli/output/colors.ts";
|
|
4
3
|
import type { OptionSchema } from "../types/command.ts";
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Configuration for version command.
|
|
8
7
|
*/
|
|
9
|
-
|
|
8
|
+
interface VersionConfig {
|
|
10
9
|
/** Application name */
|
|
11
10
|
appName: string;
|
|
12
11
|
/** Application version (e.g., "1.0.0") */
|
|
@@ -19,7 +18,7 @@ export interface VersionConfig {
|
|
|
19
18
|
* Format version string with optional commit hash.
|
|
20
19
|
* If commitHash is empty or undefined, shows "(dev)".
|
|
21
20
|
*/
|
|
22
|
-
|
|
21
|
+
function formatVersion(version: string, commitHash?: string): string {
|
|
23
22
|
const hashPart = commitHash && commitHash.length > 0
|
|
24
23
|
? commitHash.substring(0, 7)
|
|
25
24
|
: "(dev)";
|
|
@@ -33,6 +32,7 @@ export function formatVersion(version: string, commitHash?: string): string {
|
|
|
33
32
|
export class VersionCommand extends Command<OptionSchema> {
|
|
34
33
|
readonly name = "version";
|
|
35
34
|
readonly description = "Show version information";
|
|
35
|
+
override readonly tuiHidden = true;
|
|
36
36
|
readonly options = {} as const;
|
|
37
37
|
readonly aliases = ["--version", "-v"];
|
|
38
38
|
|
|
@@ -54,7 +54,7 @@ export class VersionCommand extends Command<OptionSchema> {
|
|
|
54
54
|
return formatVersion(this.appVersion, this.commitHash);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
override async execute(
|
|
57
|
+
override async execute(): Promise<void> {
|
|
58
58
|
const versionDisplay = this.getFormattedVersion();
|
|
59
59
|
console.log(`${colors.bold(this.appName)} ${colors.dim(`v${versionDisplay}`)}`);
|
|
60
60
|
}
|
package/src/cli/output/colors.ts
CHANGED
|
@@ -23,7 +23,7 @@ const BG_BLUE = "\x1b[44m";
|
|
|
23
23
|
/**
|
|
24
24
|
* Check if terminal supports colors
|
|
25
25
|
*/
|
|
26
|
-
|
|
26
|
+
function supportsColors(): boolean {
|
|
27
27
|
if (typeof process === "undefined") return false;
|
|
28
28
|
if (process.env["NO_COLOR"]) return false;
|
|
29
29
|
if (process.env["FORCE_COLOR"]) return true;
|