@pablozaiden/terminatui 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -43
- package/package.json +11 -8
- package/src/__tests__/application.test.ts +87 -68
- package/src/__tests__/buildCliCommand.test.ts +99 -119
- package/src/__tests__/builtins.test.ts +27 -75
- package/src/__tests__/command.test.ts +100 -131
- package/src/__tests__/configOnChange.test.ts +63 -0
- package/src/__tests__/context.test.ts +1 -26
- package/src/__tests__/helpCore.test.ts +227 -0
- package/src/__tests__/parser.test.ts +98 -244
- package/src/__tests__/registry.test.ts +33 -160
- package/src/__tests__/schemaToFields.test.ts +75 -158
- package/src/builtins/help.ts +12 -4
- package/src/builtins/settings.ts +18 -32
- package/src/builtins/version.ts +3 -3
- package/src/cli/output/colors.ts +1 -1
- package/src/cli/parser.ts +26 -95
- package/src/core/application.ts +192 -110
- package/src/core/command.ts +26 -9
- package/src/core/context.ts +31 -20
- package/src/core/help.ts +24 -18
- package/src/core/knownCommands.ts +13 -0
- package/src/core/logger.ts +39 -42
- package/src/core/registry.ts +5 -12
- package/src/index.ts +22 -137
- package/src/tui/TuiApplication.tsx +63 -120
- package/src/tui/TuiRoot.tsx +135 -0
- package/src/tui/adapters/factory.ts +19 -0
- package/src/tui/adapters/ink/InkRenderer.tsx +139 -0
- package/src/tui/adapters/ink/components/Button.tsx +12 -0
- package/src/tui/adapters/ink/components/Code.tsx +6 -0
- package/src/tui/adapters/ink/components/CodeHighlight.tsx +6 -0
- package/src/tui/adapters/ink/components/Container.tsx +5 -0
- package/src/tui/adapters/ink/components/Field.tsx +12 -0
- package/src/tui/adapters/ink/components/Label.tsx +24 -0
- package/src/tui/adapters/ink/components/MenuButton.tsx +12 -0
- package/src/tui/adapters/ink/components/MenuItem.tsx +17 -0
- package/src/tui/adapters/ink/components/Overlay.tsx +5 -0
- package/src/tui/adapters/ink/components/Panel.tsx +15 -0
- package/src/tui/adapters/ink/components/ScrollView.tsx +5 -0
- package/src/tui/adapters/ink/components/Select.tsx +44 -0
- package/src/tui/adapters/ink/components/Spacer.tsx +15 -0
- package/src/tui/adapters/ink/components/Spinner.tsx +5 -0
- package/src/tui/adapters/ink/components/TextInput.tsx +22 -0
- package/src/tui/adapters/ink/components/Value.tsx +7 -0
- package/src/tui/adapters/ink/keyboard.ts +97 -0
- package/src/tui/adapters/ink/utils.ts +16 -0
- package/src/tui/adapters/opentui/OpenTuiRenderer.tsx +119 -0
- package/src/tui/adapters/opentui/components/Button.tsx +13 -0
- package/src/tui/adapters/opentui/components/Code.tsx +12 -0
- package/src/tui/adapters/opentui/components/CodeHighlight.tsx +24 -0
- package/src/tui/adapters/opentui/components/Container.tsx +56 -0
- package/src/tui/adapters/opentui/components/Field.tsx +18 -0
- package/src/tui/adapters/opentui/components/Label.tsx +15 -0
- package/src/tui/adapters/opentui/components/MenuButton.tsx +14 -0
- package/src/tui/adapters/opentui/components/MenuItem.tsx +29 -0
- package/src/tui/adapters/opentui/components/Overlay.tsx +21 -0
- package/src/tui/adapters/opentui/components/Panel.tsx +78 -0
- package/src/tui/adapters/opentui/components/ScrollView.tsx +85 -0
- package/src/tui/adapters/opentui/components/Select.tsx +59 -0
- package/src/tui/adapters/opentui/components/Spacer.tsx +5 -0
- package/src/tui/adapters/opentui/components/Spinner.tsx +12 -0
- package/src/tui/adapters/opentui/components/TextInput.tsx +13 -0
- package/src/tui/adapters/opentui/components/Value.tsx +13 -0
- package/src/tui/{hooks → adapters/opentui/hooks}/useSpinner.ts +2 -11
- package/src/tui/adapters/opentui/keyboard.ts +61 -0
- package/src/tui/adapters/types.ts +71 -0
- package/src/tui/components/ActionButton.tsx +0 -36
- package/src/tui/components/CommandSelector.tsx +45 -92
- package/src/tui/components/ConfigForm.tsx +68 -42
- package/src/tui/components/FieldRow.tsx +0 -30
- package/src/tui/components/Header.tsx +14 -13
- package/src/tui/components/JsonHighlight.tsx +10 -17
- package/src/tui/components/ModalBase.tsx +38 -0
- package/src/tui/components/ResultsPanel.tsx +27 -36
- package/src/tui/components/StatusBar.tsx +24 -39
- package/src/tui/components/logColors.ts +12 -0
- package/src/tui/context/ClipboardContext.tsx +87 -0
- package/src/tui/context/ExecutorContext.tsx +139 -0
- package/src/tui/context/KeyboardContext.tsx +85 -71
- package/src/tui/context/LogsContext.tsx +35 -0
- package/src/tui/context/NavigationContext.tsx +194 -0
- package/src/tui/context/RendererContext.tsx +20 -0
- package/src/tui/context/TuiAppContext.tsx +58 -0
- package/src/tui/hooks/useActiveKeyHandler.ts +75 -0
- package/src/tui/hooks/useBackHandler.ts +34 -0
- package/src/tui/hooks/useClipboard.ts +40 -25
- package/src/tui/hooks/useClipboardProvider.ts +42 -0
- package/src/tui/hooks/useGlobalKeyHandler.ts +54 -0
- package/src/tui/modals/CliModal.tsx +82 -0
- package/src/tui/modals/EditorModal.tsx +207 -0
- package/src/tui/modals/LogsModal.tsx +98 -0
- package/src/tui/registry.ts +102 -0
- package/src/tui/screens/CommandSelectScreen.tsx +162 -0
- package/src/tui/screens/ConfigScreen.tsx +165 -0
- package/src/tui/screens/ErrorScreen.tsx +58 -0
- package/src/tui/screens/ResultsScreen.tsx +68 -0
- package/src/tui/screens/RunningScreen.tsx +72 -0
- package/src/tui/screens/ScreenBase.ts +6 -0
- package/src/tui/semantic/Button.tsx +7 -0
- package/src/tui/semantic/Code.tsx +7 -0
- package/src/tui/semantic/CodeHighlight.tsx +7 -0
- package/src/tui/semantic/Container.tsx +7 -0
- package/src/tui/semantic/Field.tsx +7 -0
- package/src/tui/semantic/Label.tsx +7 -0
- package/src/tui/semantic/MenuButton.tsx +7 -0
- package/src/tui/semantic/MenuItem.tsx +7 -0
- package/src/tui/semantic/Overlay.tsx +7 -0
- package/src/tui/semantic/Panel.tsx +7 -0
- package/src/tui/semantic/ScrollView.tsx +9 -0
- package/src/tui/semantic/Select.tsx +7 -0
- package/src/tui/semantic/Spacer.tsx +7 -0
- package/src/tui/semantic/Spinner.tsx +7 -0
- package/src/tui/semantic/TextInput.tsx +7 -0
- package/src/tui/semantic/Value.tsx +7 -0
- package/src/tui/semantic/types.ts +195 -0
- package/src/tui/theme.ts +25 -14
- package/src/tui/utils/buildCliCommand.ts +1 -0
- package/src/tui/utils/getEnumKeys.ts +3 -0
- package/src/tui/utils/parameterPersistence.ts +1 -0
- package/src/types/command.ts +0 -60
- package/.devcontainer/devcontainer.json +0 -19
- package/.devcontainer/install-prerequisites.sh +0 -49
- package/.github/workflows/copilot-setup-steps.yml +0 -32
- package/.github/workflows/pull-request.yml +0 -27
- package/.github/workflows/release-npm-package.yml +0 -81
- package/AGENTS.md +0 -31
- package/bun.lock +0 -236
- package/examples/tui-app/commands/config/app/get.ts +0 -66
- package/examples/tui-app/commands/config/app/index.ts +0 -27
- package/examples/tui-app/commands/config/app/set.ts +0 -86
- package/examples/tui-app/commands/config/index.ts +0 -32
- package/examples/tui-app/commands/config/user/get.ts +0 -65
- package/examples/tui-app/commands/config/user/index.ts +0 -27
- package/examples/tui-app/commands/config/user/set.ts +0 -61
- package/examples/tui-app/commands/greet.ts +0 -76
- package/examples/tui-app/commands/index.ts +0 -4
- package/examples/tui-app/commands/math.ts +0 -115
- package/examples/tui-app/commands/status.ts +0 -77
- package/examples/tui-app/index.ts +0 -35
- package/guides/01-hello-world.md +0 -96
- package/guides/02-adding-options.md +0 -103
- package/guides/03-multiple-commands.md +0 -163
- package/guides/04-subcommands.md +0 -206
- package/guides/05-interactive-tui.md +0 -194
- package/guides/06-config-validation.md +0 -264
- package/guides/07-async-cancellation.md +0 -336
- package/guides/08-complete-application.md +0 -537
- package/guides/README.md +0 -74
- package/src/__tests__/colors.test.ts +0 -127
- package/src/__tests__/commandClass.test.ts +0 -130
- package/src/__tests__/help.test.ts +0 -412
- package/src/__tests__/registryNew.test.ts +0 -160
- package/src/__tests__/table.test.ts +0 -146
- package/src/__tests__/tui.test.ts +0 -26
- package/src/builtins/index.ts +0 -4
- package/src/cli/help.ts +0 -174
- package/src/cli/index.ts +0 -3
- package/src/cli/output/index.ts +0 -2
- package/src/cli/output/table.ts +0 -141
- package/src/commands/help.ts +0 -50
- package/src/commands/index.ts +0 -1
- package/src/components/index.ts +0 -147
- package/src/core/index.ts +0 -15
- package/src/hooks/index.ts +0 -131
- package/src/registry/commandRegistry.ts +0 -77
- package/src/registry/index.ts +0 -1
- package/src/tui/TuiApp.tsx +0 -619
- package/src/tui/app.ts +0 -29
- package/src/tui/components/CliModal.tsx +0 -81
- package/src/tui/components/EditorModal.tsx +0 -177
- package/src/tui/components/LogsPanel.tsx +0 -86
- package/src/tui/components/index.ts +0 -13
- package/src/tui/context/index.ts +0 -7
- package/src/tui/hooks/index.ts +0 -35
- package/src/tui/hooks/useKeyboardHandler.ts +0 -91
- package/src/tui/hooks/useLogStream.ts +0 -96
- package/src/tui/index.ts +0 -65
- package/src/tui/utils/index.ts +0 -13
- package/src/types/index.ts +0 -1
- package/tsconfig.json +0 -25
|
@@ -1,160 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,146 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,26 +0,0 @@
|
|
|
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
|
-
|
package/src/builtins/index.ts
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
export { HelpCommand, createHelpCommandForParent, createRootHelpCommand } from "./help.ts";
|
|
2
|
-
export { VersionCommand, createVersionCommand, formatVersion } from "./version.ts";
|
|
3
|
-
export { SettingsCommand, createSettingsCommand } from "./settings.ts";
|
|
4
|
-
export type { VersionConfig } from "./version.ts";
|
package/src/cli/help.ts
DELETED
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
import type { Command, OptionDef } from "../types/command.ts";
|
|
2
|
-
import { colors } from "./output/colors.ts";
|
|
3
|
-
|
|
4
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5
|
-
type AnyCommand = Command<any, any>;
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Format usage line for a command
|
|
9
|
-
*/
|
|
10
|
-
export function formatUsage(
|
|
11
|
-
command: AnyCommand,
|
|
12
|
-
appName = "cli"
|
|
13
|
-
): string {
|
|
14
|
-
const parts = [appName, command.name];
|
|
15
|
-
|
|
16
|
-
if (command.subcommands && Object.keys(command.subcommands).length > 0) {
|
|
17
|
-
parts.push("[command]");
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (command.options && Object.keys(command.options).length > 0) {
|
|
21
|
-
parts.push("[options]");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return parts.join(" ");
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Format subcommands list
|
|
29
|
-
*/
|
|
30
|
-
export function formatCommands(command: AnyCommand): string {
|
|
31
|
-
if (!command.subcommands) return "";
|
|
32
|
-
|
|
33
|
-
const entries = Object.entries(command.subcommands)
|
|
34
|
-
.filter(([, cmd]) => !cmd.hidden)
|
|
35
|
-
.map(([name, cmd]) => {
|
|
36
|
-
const aliases = cmd.aliases?.length ? ` (${cmd.aliases.join(", ")})` : "";
|
|
37
|
-
return ` ${colors.cyan(name)}${aliases} ${cmd.description}`;
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
if (entries.length === 0) return "";
|
|
41
|
-
|
|
42
|
-
return ["Commands:", ...entries].join("\n");
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Format options list
|
|
47
|
-
*/
|
|
48
|
-
export function formatOptions(command: AnyCommand): string {
|
|
49
|
-
if (!command.options) return "";
|
|
50
|
-
|
|
51
|
-
const entries = Object.entries(command.options).map(([name, defUntyped]) => {
|
|
52
|
-
const def = defUntyped as OptionDef;
|
|
53
|
-
const alias = def.alias ? `-${def.alias}, ` : " ";
|
|
54
|
-
const flag = `${alias}--${name}`;
|
|
55
|
-
const required = def.required ? colors.red("*") : "";
|
|
56
|
-
const defaultVal = def.default !== undefined ? ` (default: ${def.default})` : "";
|
|
57
|
-
const enumVals = def.enum ? ` [${def.enum.join("|")}]` : "";
|
|
58
|
-
|
|
59
|
-
return ` ${colors.yellow(flag)}${required} ${def.description}${enumVals}${defaultVal}`;
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
if (entries.length === 0) return "";
|
|
63
|
-
|
|
64
|
-
return ["Options:", ...entries].join("\n");
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Format examples list
|
|
69
|
-
*/
|
|
70
|
-
export function formatExamples(command: AnyCommand): string {
|
|
71
|
-
if (!command.examples?.length) return "";
|
|
72
|
-
|
|
73
|
-
const entries = command.examples.map(
|
|
74
|
-
(ex) => ` ${colors.dim("$")} ${ex.command}\n ${colors.dim(ex.description)}`
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
return ["Examples:", ...entries].join("\n");
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Format global options (log-level, detailed-logs)
|
|
82
|
-
*/
|
|
83
|
-
export function formatGlobalOptions(): string {
|
|
84
|
-
const entries = [
|
|
85
|
-
` ${colors.yellow("--log-level")} <level> Set log level [silly|trace|debug|info|warn|error|fatal]`,
|
|
86
|
-
` ${colors.yellow("--detailed-logs")} Enable detailed log output`,
|
|
87
|
-
` ${colors.yellow("--no-detailed-logs")} Disable detailed log output`,
|
|
88
|
-
];
|
|
89
|
-
|
|
90
|
-
return ["Global Options:", ...entries].join("\n");
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Get command summary line
|
|
95
|
-
*/
|
|
96
|
-
export function getCommandSummary(command: AnyCommand): string {
|
|
97
|
-
const aliases = command.aliases?.length ? ` (${command.aliases.join(", ")})` : "";
|
|
98
|
-
return `${command.name}${aliases}: ${command.description}`;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Generate full help text for a command
|
|
103
|
-
*/
|
|
104
|
-
export function generateHelp(
|
|
105
|
-
command: AnyCommand,
|
|
106
|
-
options: { appName?: string; version?: string } = {}
|
|
107
|
-
): string {
|
|
108
|
-
const { appName = "cli", version } = options;
|
|
109
|
-
const sections: string[] = [];
|
|
110
|
-
|
|
111
|
-
// Header
|
|
112
|
-
if (version) {
|
|
113
|
-
sections.push(`${colors.bold(appName)} ${colors.dim(`v${version}`)}`);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Description
|
|
117
|
-
sections.push(command.description);
|
|
118
|
-
|
|
119
|
-
// Usage
|
|
120
|
-
sections.push(`\n${colors.bold("Usage:")}\n ${formatUsage(command, appName)}`);
|
|
121
|
-
|
|
122
|
-
// Commands
|
|
123
|
-
const commandsSection = formatCommands(command);
|
|
124
|
-
if (commandsSection) {
|
|
125
|
-
sections.push(`\n${commandsSection}`);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Options
|
|
129
|
-
const optionsSection = formatOptions(command);
|
|
130
|
-
if (optionsSection) {
|
|
131
|
-
sections.push(`\n${optionsSection}`);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Examples
|
|
135
|
-
const examplesSection = formatExamples(command);
|
|
136
|
-
if (examplesSection) {
|
|
137
|
-
sections.push(`\n${examplesSection}`);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return sections.join("\n");
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Generate help text for a specific command (includes global options)
|
|
145
|
-
*/
|
|
146
|
-
export function generateCommandHelp(
|
|
147
|
-
command: AnyCommand,
|
|
148
|
-
appName = "cli"
|
|
149
|
-
): string {
|
|
150
|
-
const sections: string[] = [];
|
|
151
|
-
|
|
152
|
-
// Description
|
|
153
|
-
sections.push(command.description);
|
|
154
|
-
|
|
155
|
-
// Usage
|
|
156
|
-
sections.push(`\n${colors.bold("Usage:")}\n ${formatUsage(command, appName)}`);
|
|
157
|
-
|
|
158
|
-
// Options
|
|
159
|
-
const optionsSection = formatOptions(command);
|
|
160
|
-
if (optionsSection) {
|
|
161
|
-
sections.push(`\n${optionsSection}`);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Global Options
|
|
165
|
-
sections.push(`\n${formatGlobalOptions()}`);
|
|
166
|
-
|
|
167
|
-
// Examples
|
|
168
|
-
const examplesSection = formatExamples(command);
|
|
169
|
-
if (examplesSection) {
|
|
170
|
-
sections.push(`\n${examplesSection}`);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return sections.join("\n");
|
|
174
|
-
}
|
package/src/cli/index.ts
DELETED
package/src/cli/output/index.ts
DELETED
package/src/cli/output/table.ts
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Column configuration for tables
|
|
3
|
-
*/
|
|
4
|
-
export interface ColumnConfig {
|
|
5
|
-
key: string;
|
|
6
|
-
header?: string;
|
|
7
|
-
width?: number;
|
|
8
|
-
align?: "left" | "right" | "center";
|
|
9
|
-
formatter?: (value: unknown) => string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface TableOptions {
|
|
13
|
-
columns?: (string | ColumnConfig)[];
|
|
14
|
-
showHeaders?: boolean;
|
|
15
|
-
border?: boolean;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Create a formatted table string
|
|
20
|
-
*/
|
|
21
|
-
export function table<T extends Record<string, unknown>>(
|
|
22
|
-
data: T[],
|
|
23
|
-
options: TableOptions = {}
|
|
24
|
-
): string {
|
|
25
|
-
if (data.length === 0) return "";
|
|
26
|
-
|
|
27
|
-
const { showHeaders = true } = options;
|
|
28
|
-
|
|
29
|
-
// Determine columns
|
|
30
|
-
const columns: ColumnConfig[] = options.columns
|
|
31
|
-
? options.columns.map((col) =>
|
|
32
|
-
typeof col === "string" ? { key: col, header: col } : col
|
|
33
|
-
)
|
|
34
|
-
: Object.keys(data[0] ?? {}).map((key) => ({ key, header: key }));
|
|
35
|
-
|
|
36
|
-
// Calculate column widths
|
|
37
|
-
const widths = columns.map((col) => {
|
|
38
|
-
const headerWidth = (col.header ?? col.key).length;
|
|
39
|
-
const maxDataWidth = Math.max(
|
|
40
|
-
...data.map((row) => {
|
|
41
|
-
const value = row[col.key];
|
|
42
|
-
const formatted = col.formatter ? col.formatter(value) : String(value ?? "");
|
|
43
|
-
return formatted.length;
|
|
44
|
-
})
|
|
45
|
-
);
|
|
46
|
-
return col.width ?? Math.max(headerWidth, maxDataWidth);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
// Format a row
|
|
50
|
-
const formatRow = (values: string[]): string => {
|
|
51
|
-
return values
|
|
52
|
-
.map((val, i) => {
|
|
53
|
-
const width = widths[i] ?? 10;
|
|
54
|
-
const col = columns[i];
|
|
55
|
-
const align = col?.align ?? "left";
|
|
56
|
-
|
|
57
|
-
if (align === "right") {
|
|
58
|
-
return val.padStart(width);
|
|
59
|
-
} else if (align === "center") {
|
|
60
|
-
const leftPad = Math.floor((width - val.length) / 2);
|
|
61
|
-
return val.padStart(leftPad + val.length).padEnd(width);
|
|
62
|
-
}
|
|
63
|
-
return val.padEnd(width);
|
|
64
|
-
})
|
|
65
|
-
.join(" ");
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const rows: string[] = [];
|
|
69
|
-
|
|
70
|
-
// Add header
|
|
71
|
-
if (showHeaders) {
|
|
72
|
-
const headers = columns.map((col) => col.header ?? col.key);
|
|
73
|
-
rows.push(formatRow(headers));
|
|
74
|
-
rows.push(widths.map((w) => "-".repeat(w)).join(" "));
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Add data rows
|
|
78
|
-
for (const row of data) {
|
|
79
|
-
const values = columns.map((col) => {
|
|
80
|
-
const value = row[col.key];
|
|
81
|
-
return col.formatter ? col.formatter(value) : String(value ?? "");
|
|
82
|
-
});
|
|
83
|
-
rows.push(formatRow(values));
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return rows.join("\n");
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Create a key-value list
|
|
91
|
-
*/
|
|
92
|
-
export function keyValueList(
|
|
93
|
-
data: Record<string, unknown>,
|
|
94
|
-
options: { separator?: string } = {}
|
|
95
|
-
): string {
|
|
96
|
-
const { separator = ":" } = options;
|
|
97
|
-
|
|
98
|
-
const entries = Object.entries(data);
|
|
99
|
-
if (entries.length === 0) return "";
|
|
100
|
-
|
|
101
|
-
const maxKeyLength = Math.max(...entries.map(([key]) => key.length));
|
|
102
|
-
|
|
103
|
-
return entries
|
|
104
|
-
.map(([key, value]) => `${key.padEnd(maxKeyLength)}${separator} ${value}`)
|
|
105
|
-
.join("\n");
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Create a bullet list
|
|
110
|
-
*/
|
|
111
|
-
export function bulletList(
|
|
112
|
-
items: string[],
|
|
113
|
-
options: { bullet?: string; indent?: number } = {}
|
|
114
|
-
): string {
|
|
115
|
-
const { bullet = "•", indent = 0 } = options;
|
|
116
|
-
|
|
117
|
-
if (items.length === 0) return "";
|
|
118
|
-
|
|
119
|
-
const prefix = " ".repeat(indent);
|
|
120
|
-
return items.map((item) => `${prefix}${bullet} ${item}`).join("\n");
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Create a numbered list
|
|
125
|
-
*/
|
|
126
|
-
export function numberedList(
|
|
127
|
-
items: string[],
|
|
128
|
-
options: { start?: number; indent?: number } = {}
|
|
129
|
-
): string {
|
|
130
|
-
const { start = 1, indent = 0 } = options;
|
|
131
|
-
|
|
132
|
-
if (items.length === 0) return "";
|
|
133
|
-
|
|
134
|
-
const prefix = " ".repeat(indent);
|
|
135
|
-
const maxNum = start + items.length - 1;
|
|
136
|
-
const numWidth = String(maxNum).length;
|
|
137
|
-
|
|
138
|
-
return items
|
|
139
|
-
.map((item, i) => `${prefix}${String(start + i).padStart(numWidth)}. ${item}`)
|
|
140
|
-
.join("\n");
|
|
141
|
-
}
|