@pablozaiden/terminatui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.devcontainer/devcontainer.json +19 -0
- package/.devcontainer/install-prerequisites.sh +49 -0
- package/.github/workflows/copilot-setup-steps.yml +32 -0
- package/.github/workflows/pull-request.yml +27 -0
- package/.github/workflows/release-npm-package.yml +78 -0
- package/LICENSE +21 -0
- package/README.md +524 -0
- package/examples/tui-app/commands/greet.ts +75 -0
- package/examples/tui-app/commands/index.ts +3 -0
- package/examples/tui-app/commands/math.ts +114 -0
- package/examples/tui-app/commands/status.ts +75 -0
- package/examples/tui-app/index.ts +34 -0
- package/guides/01-hello-world.md +96 -0
- package/guides/02-adding-options.md +103 -0
- package/guides/03-multiple-commands.md +163 -0
- package/guides/04-subcommands.md +206 -0
- package/guides/05-interactive-tui.md +194 -0
- package/guides/06-config-validation.md +264 -0
- package/guides/07-async-cancellation.md +388 -0
- package/guides/08-complete-application.md +673 -0
- package/guides/README.md +74 -0
- package/package.json +32 -0
- package/src/__tests__/application.test.ts +425 -0
- package/src/__tests__/buildCliCommand.test.ts +125 -0
- package/src/__tests__/builtins.test.ts +133 -0
- package/src/__tests__/colors.test.ts +127 -0
- package/src/__tests__/command.test.ts +157 -0
- package/src/__tests__/commandClass.test.ts +130 -0
- package/src/__tests__/context.test.ts +97 -0
- package/src/__tests__/help.test.ts +412 -0
- package/src/__tests__/parser.test.ts +268 -0
- package/src/__tests__/registry.test.ts +195 -0
- package/src/__tests__/registryNew.test.ts +160 -0
- package/src/__tests__/schemaToFields.test.ts +176 -0
- package/src/__tests__/table.test.ts +146 -0
- package/src/__tests__/tui.test.ts +26 -0
- package/src/builtins/help.ts +85 -0
- package/src/builtins/index.ts +4 -0
- package/src/builtins/settings.ts +106 -0
- package/src/builtins/version.ts +72 -0
- package/src/cli/help.ts +174 -0
- package/src/cli/index.ts +3 -0
- package/src/cli/output/colors.ts +74 -0
- package/src/cli/output/index.ts +2 -0
- package/src/cli/output/table.ts +141 -0
- package/src/cli/parser.ts +241 -0
- package/src/commands/help.ts +50 -0
- package/src/commands/index.ts +1 -0
- package/src/components/index.ts +147 -0
- package/src/core/application.ts +461 -0
- package/src/core/command.ts +269 -0
- package/src/core/context.ts +112 -0
- package/src/core/help.ts +214 -0
- package/src/core/index.ts +15 -0
- package/src/core/logger.ts +164 -0
- package/src/core/registry.ts +140 -0
- package/src/hooks/index.ts +131 -0
- package/src/index.ts +137 -0
- package/src/registry/commandRegistry.ts +77 -0
- package/src/registry/index.ts +1 -0
- package/src/tui/TuiApp.tsx +582 -0
- package/src/tui/TuiApplication.tsx +230 -0
- package/src/tui/app.ts +29 -0
- package/src/tui/components/ActionButton.tsx +36 -0
- package/src/tui/components/CliModal.tsx +81 -0
- package/src/tui/components/CommandSelector.tsx +159 -0
- package/src/tui/components/ConfigForm.tsx +148 -0
- package/src/tui/components/EditorModal.tsx +177 -0
- package/src/tui/components/FieldRow.tsx +30 -0
- package/src/tui/components/Header.tsx +31 -0
- package/src/tui/components/JsonHighlight.tsx +128 -0
- package/src/tui/components/LogsPanel.tsx +86 -0
- package/src/tui/components/ResultsPanel.tsx +93 -0
- package/src/tui/components/StatusBar.tsx +59 -0
- package/src/tui/components/index.ts +13 -0
- package/src/tui/components/types.ts +30 -0
- package/src/tui/context/KeyboardContext.tsx +118 -0
- package/src/tui/context/index.ts +7 -0
- package/src/tui/hooks/index.ts +35 -0
- package/src/tui/hooks/useClipboard.ts +66 -0
- package/src/tui/hooks/useCommandExecutor.ts +131 -0
- package/src/tui/hooks/useConfigState.ts +171 -0
- package/src/tui/hooks/useKeyboardHandler.ts +91 -0
- package/src/tui/hooks/useLogStream.ts +96 -0
- package/src/tui/hooks/useSpinner.ts +46 -0
- package/src/tui/index.ts +65 -0
- package/src/tui/theme.ts +21 -0
- package/src/tui/utils/buildCliCommand.ts +90 -0
- package/src/tui/utils/index.ts +13 -0
- package/src/tui/utils/parameterPersistence.ts +96 -0
- package/src/tui/utils/schemaToFields.ts +144 -0
- package/src/types/command.ts +103 -0
- package/src/types/execution.ts +11 -0
- package/src/types/index.ts +1 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { test, expect, describe } from "bun:test";
|
|
2
|
+
import { createCommandRegistry } from "../registry/commandRegistry.ts";
|
|
3
|
+
import { defineCommand } from "../types/command.ts";
|
|
4
|
+
|
|
5
|
+
describe("createCommandRegistry", () => {
|
|
6
|
+
test("registers a command", () => {
|
|
7
|
+
const registry = createCommandRegistry();
|
|
8
|
+
const cmd = defineCommand({
|
|
9
|
+
name: "test",
|
|
10
|
+
description: "Test command",
|
|
11
|
+
execute: () => {},
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
registry.register(cmd);
|
|
15
|
+
|
|
16
|
+
expect(registry.has("test")).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("retrieves command by name", () => {
|
|
20
|
+
const registry = createCommandRegistry();
|
|
21
|
+
const cmd = defineCommand({
|
|
22
|
+
name: "greet",
|
|
23
|
+
description: "Greet command",
|
|
24
|
+
execute: () => {},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
registry.register(cmd);
|
|
28
|
+
|
|
29
|
+
expect(registry.get("greet")).toBe(cmd);
|
|
30
|
+
});
|
|
31
|
+
|
|
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
|
+
test("returns undefined for unknown command", () => {
|
|
48
|
+
const registry = createCommandRegistry();
|
|
49
|
+
expect(registry.get("unknown")).toBeUndefined();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("lists all commands", () => {
|
|
53
|
+
const registry = createCommandRegistry();
|
|
54
|
+
const cmd1 = defineCommand({
|
|
55
|
+
name: "a",
|
|
56
|
+
description: "A",
|
|
57
|
+
execute: () => {},
|
|
58
|
+
});
|
|
59
|
+
const cmd2 = defineCommand({
|
|
60
|
+
name: "b",
|
|
61
|
+
description: "B",
|
|
62
|
+
execute: () => {},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
registry.register(cmd1);
|
|
66
|
+
registry.register(cmd2);
|
|
67
|
+
|
|
68
|
+
const commands = registry.list();
|
|
69
|
+
expect(commands).toContain(cmd1);
|
|
70
|
+
expect(commands).toContain(cmd2);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("throws on duplicate registration", () => {
|
|
74
|
+
const registry = createCommandRegistry();
|
|
75
|
+
const cmd = defineCommand({
|
|
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
|
+
|
|
129
|
+
registry.register(cmd);
|
|
130
|
+
|
|
131
|
+
expect(registry.has("c")).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("getNames returns command names", () => {
|
|
135
|
+
const registry = createCommandRegistry();
|
|
136
|
+
const cmdA = defineCommand({
|
|
137
|
+
name: "a",
|
|
138
|
+
description: "A",
|
|
139
|
+
execute: () => {},
|
|
140
|
+
});
|
|
141
|
+
const cmdB = defineCommand({
|
|
142
|
+
name: "b",
|
|
143
|
+
description: "B",
|
|
144
|
+
execute: () => {},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
registry.register(cmdA);
|
|
148
|
+
registry.register(cmdB);
|
|
149
|
+
|
|
150
|
+
const names = registry.getNames();
|
|
151
|
+
expect(names).toContain("a");
|
|
152
|
+
expect(names).toContain("b");
|
|
153
|
+
});
|
|
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
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach } from "bun:test";
|
|
2
|
+
import { CommandRegistry } from "../core/registry.ts";
|
|
3
|
+
import { Command } from "../core/command.ts";
|
|
4
|
+
import type { AppContext } from "../core/context.ts";
|
|
5
|
+
import type { OptionSchema } from "../types/command.ts";
|
|
6
|
+
|
|
7
|
+
// Test command implementations
|
|
8
|
+
class TestCommand extends Command<OptionSchema> {
|
|
9
|
+
constructor(
|
|
10
|
+
public readonly name: string,
|
|
11
|
+
public readonly description: string = "Test command"
|
|
12
|
+
) {
|
|
13
|
+
super();
|
|
14
|
+
}
|
|
15
|
+
readonly options = {};
|
|
16
|
+
|
|
17
|
+
override async execute(_ctx: AppContext): Promise<void> {}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("CommandRegistry (new)", () => {
|
|
21
|
+
let registry: CommandRegistry;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
registry = new CommandRegistry();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("register", () => {
|
|
28
|
+
test("registers a command", () => {
|
|
29
|
+
const cmd = new TestCommand("test");
|
|
30
|
+
registry.register(cmd);
|
|
31
|
+
expect(registry.has("test")).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("throws on duplicate registration", () => {
|
|
35
|
+
const cmd = new TestCommand("test");
|
|
36
|
+
registry.register(cmd);
|
|
37
|
+
expect(() => registry.register(cmd)).toThrow(
|
|
38
|
+
"Command 'test' is already registered"
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("registerAll", () => {
|
|
44
|
+
test("registers multiple commands", () => {
|
|
45
|
+
const cmd1 = new TestCommand("cmd1");
|
|
46
|
+
const cmd2 = new TestCommand("cmd2");
|
|
47
|
+
registry.registerAll([cmd1, cmd2]);
|
|
48
|
+
expect(registry.has("cmd1")).toBe(true);
|
|
49
|
+
expect(registry.has("cmd2")).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("get", () => {
|
|
54
|
+
test("returns command by name", () => {
|
|
55
|
+
const cmd = new TestCommand("test");
|
|
56
|
+
registry.register(cmd);
|
|
57
|
+
expect(registry.get("test")).toBe(cmd);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("returns undefined for unknown command", () => {
|
|
61
|
+
expect(registry.get("unknown")).toBeUndefined();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("has", () => {
|
|
66
|
+
test("returns true for registered command", () => {
|
|
67
|
+
registry.register(new TestCommand("test"));
|
|
68
|
+
expect(registry.has("test")).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("returns false for unknown command", () => {
|
|
72
|
+
expect(registry.has("unknown")).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("list", () => {
|
|
77
|
+
test("returns empty array for empty registry", () => {
|
|
78
|
+
expect(registry.list()).toEqual([]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("returns all registered commands", () => {
|
|
82
|
+
const cmd1 = new TestCommand("cmd1");
|
|
83
|
+
const cmd2 = new TestCommand("cmd2");
|
|
84
|
+
registry.registerAll([cmd1, cmd2]);
|
|
85
|
+
expect(registry.list()).toContain(cmd1);
|
|
86
|
+
expect(registry.list()).toContain(cmd2);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("names", () => {
|
|
91
|
+
test("returns command names", () => {
|
|
92
|
+
registry.register(new TestCommand("cmd1"));
|
|
93
|
+
registry.register(new TestCommand("cmd2"));
|
|
94
|
+
expect(registry.names()).toContain("cmd1");
|
|
95
|
+
expect(registry.names()).toContain("cmd2");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("resolve", () => {
|
|
100
|
+
test("returns undefined for empty path", () => {
|
|
101
|
+
const result = registry.resolve([]);
|
|
102
|
+
expect(result.command).toBeUndefined();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("resolves single command", () => {
|
|
106
|
+
const cmd = new TestCommand("test");
|
|
107
|
+
registry.register(cmd);
|
|
108
|
+
const result = registry.resolve(["test"]);
|
|
109
|
+
expect(result.command).toBe(cmd);
|
|
110
|
+
expect(result.resolvedPath).toEqual(["test"]);
|
|
111
|
+
expect(result.remainingPath).toEqual([]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("returns undefined for unknown command", () => {
|
|
115
|
+
const result = registry.resolve(["unknown"]);
|
|
116
|
+
expect(result.command).toBeUndefined();
|
|
117
|
+
expect(result.remainingPath).toEqual(["unknown"]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("resolves nested subcommands", () => {
|
|
121
|
+
const subCmd = new TestCommand("sub");
|
|
122
|
+
const cmd = new TestCommand("parent");
|
|
123
|
+
cmd.subCommands = [subCmd];
|
|
124
|
+
registry.register(cmd);
|
|
125
|
+
|
|
126
|
+
const result = registry.resolve(["parent", "sub"]);
|
|
127
|
+
expect(result.command).toBe(subCmd);
|
|
128
|
+
expect(result.resolvedPath).toEqual(["parent", "sub"]);
|
|
129
|
+
expect(result.remainingPath).toEqual([]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("returns remaining path for unresolved parts", () => {
|
|
133
|
+
const cmd = new TestCommand("parent");
|
|
134
|
+
registry.register(cmd);
|
|
135
|
+
|
|
136
|
+
const result = registry.resolve(["parent", "unknown"]);
|
|
137
|
+
expect(result.command).toBe(cmd);
|
|
138
|
+
expect(result.resolvedPath).toEqual(["parent"]);
|
|
139
|
+
expect(result.remainingPath).toEqual(["unknown"]);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("clear", () => {
|
|
144
|
+
test("clears all commands", () => {
|
|
145
|
+
registry.register(new TestCommand("test"));
|
|
146
|
+
registry.clear();
|
|
147
|
+
expect(registry.size).toBe(0);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("size", () => {
|
|
152
|
+
test("returns number of registered commands", () => {
|
|
153
|
+
expect(registry.size).toBe(0);
|
|
154
|
+
registry.register(new TestCommand("cmd1"));
|
|
155
|
+
expect(registry.size).toBe(1);
|
|
156
|
+
registry.register(new TestCommand("cmd2"));
|
|
157
|
+
expect(registry.size).toBe(2);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { schemaToFieldConfigs, getFieldDisplayValue } from "../tui/utils/schemaToFields.ts";
|
|
3
|
+
import type { OptionSchema } from "../types/command.ts";
|
|
4
|
+
|
|
5
|
+
describe("schemaToFieldConfigs", () => {
|
|
6
|
+
test("converts string type to text field", () => {
|
|
7
|
+
const schema: OptionSchema = {
|
|
8
|
+
name: {
|
|
9
|
+
type: "string",
|
|
10
|
+
description: "User name",
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const fields = schemaToFieldConfigs(schema);
|
|
15
|
+
expect(fields).toHaveLength(1);
|
|
16
|
+
expect(fields[0]?.key).toBe("name");
|
|
17
|
+
expect(fields[0]?.type).toBe("text");
|
|
18
|
+
expect(fields[0]?.label).toBe("Name");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("converts string with enum to enum field", () => {
|
|
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
|
+
});
|
|
35
|
+
|
|
36
|
+
test("converts boolean type", () => {
|
|
37
|
+
const schema: OptionSchema = {
|
|
38
|
+
verbose: {
|
|
39
|
+
type: "boolean",
|
|
40
|
+
description: "Verbose output",
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const fields = schemaToFieldConfigs(schema);
|
|
45
|
+
expect(fields[0]?.type).toBe("boolean");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("converts number type", () => {
|
|
49
|
+
const schema: OptionSchema = {
|
|
50
|
+
count: {
|
|
51
|
+
type: "number",
|
|
52
|
+
description: "Count",
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const fields = schemaToFieldConfigs(schema);
|
|
57
|
+
expect(fields[0]?.type).toBe("number");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("converts array type to text", () => {
|
|
61
|
+
const schema: OptionSchema = {
|
|
62
|
+
files: {
|
|
63
|
+
type: "array",
|
|
64
|
+
description: "Files to process",
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const fields = schemaToFieldConfigs(schema);
|
|
69
|
+
expect(fields[0]?.type).toBe("text");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("uses label from schema if provided", () => {
|
|
73
|
+
const schema: OptionSchema = {
|
|
74
|
+
repoPath: {
|
|
75
|
+
type: "string",
|
|
76
|
+
description: "Repository path",
|
|
77
|
+
label: "Repository",
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const fields = schemaToFieldConfigs(schema);
|
|
82
|
+
expect(fields[0]?.label).toBe("Repository");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("sorts by order", () => {
|
|
86
|
+
const schema: OptionSchema = {
|
|
87
|
+
third: { type: "string", description: "Third", order: 3 },
|
|
88
|
+
first: { type: "string", description: "First", order: 1 },
|
|
89
|
+
second: { type: "string", description: "Second", order: 2 },
|
|
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
|
+
});
|
|
108
|
+
|
|
109
|
+
test("includes placeholder from schema", () => {
|
|
110
|
+
const schema: OptionSchema = {
|
|
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");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("includes group from schema", () => {
|
|
123
|
+
const schema: OptionSchema = {
|
|
124
|
+
name: {
|
|
125
|
+
type: "string",
|
|
126
|
+
description: "Name",
|
|
127
|
+
group: "Basic",
|
|
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
|
+
});
|
|
142
|
+
|
|
143
|
+
test("displays enum option name", () => {
|
|
144
|
+
const field = {
|
|
145
|
+
key: "color",
|
|
146
|
+
label: "Color",
|
|
147
|
+
type: "enum" as const,
|
|
148
|
+
options: [
|
|
149
|
+
{ name: "Red", value: "red" },
|
|
150
|
+
{ name: "Green", value: "green" },
|
|
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
|
+
});
|
|
163
|
+
|
|
164
|
+
test("truncates long values", () => {
|
|
165
|
+
const field = { key: "desc", label: "Description", type: "text" as const };
|
|
166
|
+
const longValue = "a".repeat(100);
|
|
167
|
+
const result = getFieldDisplayValue(longValue, field);
|
|
168
|
+
expect(result.length).toBe(60);
|
|
169
|
+
expect(result.endsWith("...")).toBe(true);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("displays short values as-is", () => {
|
|
173
|
+
const field = { key: "name", label: "Name", type: "text" as const };
|
|
174
|
+
expect(getFieldDisplayValue("hello", field)).toBe("hello");
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { test, expect, describe } from "bun:test";
|
|
2
|
+
import { table, keyValueList, bulletList, numberedList } from "../cli/output/table.ts";
|
|
3
|
+
|
|
4
|
+
describe("table", () => {
|
|
5
|
+
test("creates table with data and columns", () => {
|
|
6
|
+
const data = [
|
|
7
|
+
{ name: "Alice", age: 30 },
|
|
8
|
+
{ name: "Bob", age: 25 },
|
|
9
|
+
];
|
|
10
|
+
const result = table(data, { columns: ["name", "age"] });
|
|
11
|
+
|
|
12
|
+
expect(result).toContain("name");
|
|
13
|
+
expect(result).toContain("Alice");
|
|
14
|
+
expect(result).toContain("Bob");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("handles empty data", () => {
|
|
18
|
+
const result = table([]);
|
|
19
|
+
expect(result).toBe("");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("handles single row", () => {
|
|
23
|
+
const data = [{ col: "value" }];
|
|
24
|
+
const result = table(data);
|
|
25
|
+
expect(result).toContain("value");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("handles multiple columns", () => {
|
|
29
|
+
const data = [{ a: "1", b: "2", c: "3" }];
|
|
30
|
+
const result = table(data);
|
|
31
|
+
expect(result).toContain("1");
|
|
32
|
+
expect(result).toContain("2");
|
|
33
|
+
expect(result).toContain("3");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("handles custom column config", () => {
|
|
37
|
+
const data = [{ value: 123 }];
|
|
38
|
+
const result = table(data, {
|
|
39
|
+
columns: [{ key: "value", header: "Amount" }],
|
|
40
|
+
});
|
|
41
|
+
expect(result).toContain("Amount");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("handles custom formatter", () => {
|
|
45
|
+
const data = [{ price: 100 }];
|
|
46
|
+
const result = table(data, {
|
|
47
|
+
columns: [
|
|
48
|
+
{
|
|
49
|
+
key: "price",
|
|
50
|
+
formatter: (v) => `$${v}`,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
});
|
|
54
|
+
expect(result).toContain("$100");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("hides headers when showHeaders is false", () => {
|
|
58
|
+
const data = [{ name: "Test" }];
|
|
59
|
+
const result = table(data, { showHeaders: false });
|
|
60
|
+
expect(result).not.toContain("---");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("handles undefined values", () => {
|
|
64
|
+
const data = [{ a: undefined }];
|
|
65
|
+
const result = table(data);
|
|
66
|
+
expect(typeof result).toBe("string");
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("keyValueList", () => {
|
|
71
|
+
test("formats key-value pairs", () => {
|
|
72
|
+
const data = { name: "Test", version: "1.0" };
|
|
73
|
+
const result = keyValueList(data);
|
|
74
|
+
expect(result).toContain("name");
|
|
75
|
+
expect(result).toContain("Test");
|
|
76
|
+
expect(result).toContain("version");
|
|
77
|
+
expect(result).toContain("1.0");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("handles empty object", () => {
|
|
81
|
+
const result = keyValueList({});
|
|
82
|
+
expect(result).toBe("");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("handles custom separator", () => {
|
|
86
|
+
const data = { key: "value" };
|
|
87
|
+
const result = keyValueList(data, { separator: " =" });
|
|
88
|
+
expect(result).toContain("=");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("bulletList", () => {
|
|
93
|
+
test("formats bullet list", () => {
|
|
94
|
+
const items = ["one", "two", "three"];
|
|
95
|
+
const result = bulletList(items);
|
|
96
|
+
expect(result).toContain("•");
|
|
97
|
+
expect(result).toContain("one");
|
|
98
|
+
expect(result).toContain("two");
|
|
99
|
+
expect(result).toContain("three");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("handles empty array", () => {
|
|
103
|
+
const result = bulletList([]);
|
|
104
|
+
expect(result).toBe("");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("handles custom bullet", () => {
|
|
108
|
+
const items = ["item"];
|
|
109
|
+
const result = bulletList(items, { bullet: "-" });
|
|
110
|
+
expect(result).toContain("-");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("handles indent", () => {
|
|
114
|
+
const items = ["item"];
|
|
115
|
+
const result = bulletList(items, { indent: 2 });
|
|
116
|
+
expect(result.startsWith(" ")).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("numberedList", () => {
|
|
121
|
+
test("formats numbered list", () => {
|
|
122
|
+
const items = ["first", "second"];
|
|
123
|
+
const result = numberedList(items);
|
|
124
|
+
expect(result).toContain("1.");
|
|
125
|
+
expect(result).toContain("2.");
|
|
126
|
+
expect(result).toContain("first");
|
|
127
|
+
expect(result).toContain("second");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("handles empty array", () => {
|
|
131
|
+
const result = numberedList([]);
|
|
132
|
+
expect(result).toBe("");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("handles custom start number", () => {
|
|
136
|
+
const items = ["item"];
|
|
137
|
+
const result = numberedList(items, { start: 5 });
|
|
138
|
+
expect(result).toContain("5.");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("handles indent", () => {
|
|
142
|
+
const items = ["item"];
|
|
143
|
+
const result = numberedList(items, { indent: 2 });
|
|
144
|
+
expect(result.startsWith(" ")).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
KeyboardPriority,
|
|
4
|
+
} from "../tui/index.ts";
|
|
5
|
+
|
|
6
|
+
describe("TUI", () => {
|
|
7
|
+
describe("KeyboardPriority", () => {
|
|
8
|
+
test("Modal has highest priority", () => {
|
|
9
|
+
expect(KeyboardPriority.Modal).toBe(100);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("Focused has medium priority", () => {
|
|
13
|
+
expect(KeyboardPriority.Focused).toBe(50);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("Global has lowest priority", () => {
|
|
17
|
+
expect(KeyboardPriority.Global).toBe(0);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("priorities are correctly ordered", () => {
|
|
21
|
+
expect(KeyboardPriority.Modal).toBeGreaterThan(KeyboardPriority.Focused);
|
|
22
|
+
expect(KeyboardPriority.Focused).toBeGreaterThan(KeyboardPriority.Global);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|