@pablozaiden/terminatui 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/README.md +64 -43
  2. package/package.json +11 -8
  3. package/src/__tests__/application.test.ts +87 -68
  4. package/src/__tests__/buildCliCommand.test.ts +99 -119
  5. package/src/__tests__/builtins.test.ts +27 -75
  6. package/src/__tests__/command.test.ts +100 -131
  7. package/src/__tests__/configOnChange.test.ts +63 -0
  8. package/src/__tests__/context.test.ts +1 -26
  9. package/src/__tests__/helpCore.test.ts +227 -0
  10. package/src/__tests__/parser.test.ts +98 -244
  11. package/src/__tests__/registry.test.ts +33 -160
  12. package/src/__tests__/schemaToFields.test.ts +75 -158
  13. package/src/builtins/help.ts +12 -4
  14. package/src/builtins/settings.ts +18 -32
  15. package/src/builtins/version.ts +3 -3
  16. package/src/cli/output/colors.ts +1 -1
  17. package/src/cli/parser.ts +26 -95
  18. package/src/core/application.ts +192 -110
  19. package/src/core/command.ts +26 -9
  20. package/src/core/context.ts +31 -20
  21. package/src/core/help.ts +24 -18
  22. package/src/core/knownCommands.ts +13 -0
  23. package/src/core/logger.ts +39 -42
  24. package/src/core/registry.ts +5 -12
  25. package/src/index.ts +22 -137
  26. package/src/tui/TuiApplication.tsx +63 -120
  27. package/src/tui/TuiRoot.tsx +135 -0
  28. package/src/tui/adapters/factory.ts +19 -0
  29. package/src/tui/adapters/ink/InkRenderer.tsx +139 -0
  30. package/src/tui/adapters/ink/components/Button.tsx +12 -0
  31. package/src/tui/adapters/ink/components/Code.tsx +6 -0
  32. package/src/tui/adapters/ink/components/CodeHighlight.tsx +6 -0
  33. package/src/tui/adapters/ink/components/Container.tsx +5 -0
  34. package/src/tui/adapters/ink/components/Field.tsx +12 -0
  35. package/src/tui/adapters/ink/components/Label.tsx +24 -0
  36. package/src/tui/adapters/ink/components/MenuButton.tsx +12 -0
  37. package/src/tui/adapters/ink/components/MenuItem.tsx +17 -0
  38. package/src/tui/adapters/ink/components/Overlay.tsx +5 -0
  39. package/src/tui/adapters/ink/components/Panel.tsx +15 -0
  40. package/src/tui/adapters/ink/components/ScrollView.tsx +5 -0
  41. package/src/tui/adapters/ink/components/Select.tsx +44 -0
  42. package/src/tui/adapters/ink/components/Spacer.tsx +15 -0
  43. package/src/tui/adapters/ink/components/Spinner.tsx +5 -0
  44. package/src/tui/adapters/ink/components/TextInput.tsx +22 -0
  45. package/src/tui/adapters/ink/components/Value.tsx +7 -0
  46. package/src/tui/adapters/ink/keyboard.ts +97 -0
  47. package/src/tui/adapters/ink/utils.ts +16 -0
  48. package/src/tui/adapters/opentui/OpenTuiRenderer.tsx +119 -0
  49. package/src/tui/adapters/opentui/components/Button.tsx +13 -0
  50. package/src/tui/adapters/opentui/components/Code.tsx +12 -0
  51. package/src/tui/adapters/opentui/components/CodeHighlight.tsx +24 -0
  52. package/src/tui/adapters/opentui/components/Container.tsx +56 -0
  53. package/src/tui/adapters/opentui/components/Field.tsx +18 -0
  54. package/src/tui/adapters/opentui/components/Label.tsx +15 -0
  55. package/src/tui/adapters/opentui/components/MenuButton.tsx +14 -0
  56. package/src/tui/adapters/opentui/components/MenuItem.tsx +29 -0
  57. package/src/tui/adapters/opentui/components/Overlay.tsx +21 -0
  58. package/src/tui/adapters/opentui/components/Panel.tsx +78 -0
  59. package/src/tui/adapters/opentui/components/ScrollView.tsx +85 -0
  60. package/src/tui/adapters/opentui/components/Select.tsx +59 -0
  61. package/src/tui/adapters/opentui/components/Spacer.tsx +5 -0
  62. package/src/tui/adapters/opentui/components/Spinner.tsx +12 -0
  63. package/src/tui/adapters/opentui/components/TextInput.tsx +13 -0
  64. package/src/tui/adapters/opentui/components/Value.tsx +13 -0
  65. package/src/tui/{hooks → adapters/opentui/hooks}/useSpinner.ts +2 -11
  66. package/src/tui/adapters/opentui/keyboard.ts +61 -0
  67. package/src/tui/adapters/types.ts +71 -0
  68. package/src/tui/components/ActionButton.tsx +0 -36
  69. package/src/tui/components/CommandSelector.tsx +45 -92
  70. package/src/tui/components/ConfigForm.tsx +68 -42
  71. package/src/tui/components/FieldRow.tsx +0 -30
  72. package/src/tui/components/Header.tsx +14 -13
  73. package/src/tui/components/JsonHighlight.tsx +10 -17
  74. package/src/tui/components/ModalBase.tsx +38 -0
  75. package/src/tui/components/ResultsPanel.tsx +27 -36
  76. package/src/tui/components/StatusBar.tsx +24 -39
  77. package/src/tui/components/logColors.ts +12 -0
  78. package/src/tui/context/ClipboardContext.tsx +87 -0
  79. package/src/tui/context/ExecutorContext.tsx +139 -0
  80. package/src/tui/context/KeyboardContext.tsx +85 -71
  81. package/src/tui/context/LogsContext.tsx +35 -0
  82. package/src/tui/context/NavigationContext.tsx +194 -0
  83. package/src/tui/context/RendererContext.tsx +20 -0
  84. package/src/tui/context/TuiAppContext.tsx +58 -0
  85. package/src/tui/hooks/useActiveKeyHandler.ts +75 -0
  86. package/src/tui/hooks/useBackHandler.ts +34 -0
  87. package/src/tui/hooks/useClipboard.ts +40 -25
  88. package/src/tui/hooks/useClipboardProvider.ts +42 -0
  89. package/src/tui/hooks/useGlobalKeyHandler.ts +54 -0
  90. package/src/tui/modals/CliModal.tsx +82 -0
  91. package/src/tui/modals/EditorModal.tsx +207 -0
  92. package/src/tui/modals/LogsModal.tsx +98 -0
  93. package/src/tui/registry.ts +102 -0
  94. package/src/tui/screens/CommandSelectScreen.tsx +162 -0
  95. package/src/tui/screens/ConfigScreen.tsx +165 -0
  96. package/src/tui/screens/ErrorScreen.tsx +58 -0
  97. package/src/tui/screens/ResultsScreen.tsx +68 -0
  98. package/src/tui/screens/RunningScreen.tsx +72 -0
  99. package/src/tui/screens/ScreenBase.ts +6 -0
  100. package/src/tui/semantic/Button.tsx +7 -0
  101. package/src/tui/semantic/Code.tsx +7 -0
  102. package/src/tui/semantic/CodeHighlight.tsx +7 -0
  103. package/src/tui/semantic/Container.tsx +7 -0
  104. package/src/tui/semantic/Field.tsx +7 -0
  105. package/src/tui/semantic/Label.tsx +7 -0
  106. package/src/tui/semantic/MenuButton.tsx +7 -0
  107. package/src/tui/semantic/MenuItem.tsx +7 -0
  108. package/src/tui/semantic/Overlay.tsx +7 -0
  109. package/src/tui/semantic/Panel.tsx +7 -0
  110. package/src/tui/semantic/ScrollView.tsx +9 -0
  111. package/src/tui/semantic/Select.tsx +7 -0
  112. package/src/tui/semantic/Spacer.tsx +7 -0
  113. package/src/tui/semantic/Spinner.tsx +7 -0
  114. package/src/tui/semantic/TextInput.tsx +7 -0
  115. package/src/tui/semantic/Value.tsx +7 -0
  116. package/src/tui/semantic/types.ts +195 -0
  117. package/src/tui/theme.ts +25 -14
  118. package/src/tui/utils/buildCliCommand.ts +1 -0
  119. package/src/tui/utils/getEnumKeys.ts +3 -0
  120. package/src/tui/utils/parameterPersistence.ts +1 -0
  121. package/src/types/command.ts +0 -60
  122. package/.devcontainer/devcontainer.json +0 -19
  123. package/.devcontainer/install-prerequisites.sh +0 -49
  124. package/.github/workflows/copilot-setup-steps.yml +0 -32
  125. package/.github/workflows/pull-request.yml +0 -27
  126. package/.github/workflows/release-npm-package.yml +0 -81
  127. package/AGENTS.md +0 -31
  128. package/bun.lock +0 -236
  129. package/examples/tui-app/commands/config/app/get.ts +0 -66
  130. package/examples/tui-app/commands/config/app/index.ts +0 -27
  131. package/examples/tui-app/commands/config/app/set.ts +0 -86
  132. package/examples/tui-app/commands/config/index.ts +0 -32
  133. package/examples/tui-app/commands/config/user/get.ts +0 -65
  134. package/examples/tui-app/commands/config/user/index.ts +0 -27
  135. package/examples/tui-app/commands/config/user/set.ts +0 -61
  136. package/examples/tui-app/commands/greet.ts +0 -76
  137. package/examples/tui-app/commands/index.ts +0 -4
  138. package/examples/tui-app/commands/math.ts +0 -115
  139. package/examples/tui-app/commands/status.ts +0 -77
  140. package/examples/tui-app/index.ts +0 -35
  141. package/guides/01-hello-world.md +0 -96
  142. package/guides/02-adding-options.md +0 -103
  143. package/guides/03-multiple-commands.md +0 -163
  144. package/guides/04-subcommands.md +0 -206
  145. package/guides/05-interactive-tui.md +0 -194
  146. package/guides/06-config-validation.md +0 -264
  147. package/guides/07-async-cancellation.md +0 -336
  148. package/guides/08-complete-application.md +0 -537
  149. package/guides/README.md +0 -74
  150. package/src/__tests__/colors.test.ts +0 -127
  151. package/src/__tests__/commandClass.test.ts +0 -130
  152. package/src/__tests__/help.test.ts +0 -412
  153. package/src/__tests__/registryNew.test.ts +0 -160
  154. package/src/__tests__/table.test.ts +0 -146
  155. package/src/__tests__/tui.test.ts +0 -26
  156. package/src/builtins/index.ts +0 -4
  157. package/src/cli/help.ts +0 -174
  158. package/src/cli/index.ts +0 -3
  159. package/src/cli/output/index.ts +0 -2
  160. package/src/cli/output/table.ts +0 -141
  161. package/src/commands/help.ts +0 -50
  162. package/src/commands/index.ts +0 -1
  163. package/src/components/index.ts +0 -147
  164. package/src/core/index.ts +0 -15
  165. package/src/hooks/index.ts +0 -131
  166. package/src/registry/commandRegistry.ts +0 -77
  167. package/src/registry/index.ts +0 -1
  168. package/src/tui/TuiApp.tsx +0 -619
  169. package/src/tui/app.ts +0 -29
  170. package/src/tui/components/CliModal.tsx +0 -81
  171. package/src/tui/components/EditorModal.tsx +0 -177
  172. package/src/tui/components/LogsPanel.tsx +0 -86
  173. package/src/tui/components/index.ts +0 -13
  174. package/src/tui/context/index.ts +0 -7
  175. package/src/tui/hooks/index.ts +0 -35
  176. package/src/tui/hooks/useKeyboardHandler.ts +0 -91
  177. package/src/tui/hooks/useLogStream.ts +0 -96
  178. package/src/tui/index.ts +0 -65
  179. package/src/tui/utils/index.ts +0 -13
  180. package/src/types/index.ts +0 -1
  181. package/tsconfig.json +0 -25
@@ -1,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
-
@@ -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
@@ -1,3 +0,0 @@
1
- export * from "./parser.ts";
2
- export * from "./help.ts";
3
- export * from "./output/index.ts";
@@ -1,2 +0,0 @@
1
- export * from "./colors.ts";
2
- export * from "./table.ts";
@@ -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
- }