@pablozaiden/terminatui 0.1.2 → 0.3.0-beta-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +43 -0
- package/CLAUDE.md +1 -0
- package/README.md +64 -43
- package/bun.lock +85 -0
- package/examples/tui-app/commands/config/app/get.ts +62 -0
- package/examples/tui-app/commands/config/app/index.ts +23 -0
- package/examples/tui-app/commands/config/app/set.ts +96 -0
- package/examples/tui-app/commands/config/index.ts +28 -0
- package/examples/tui-app/commands/config/user/get.ts +61 -0
- package/examples/tui-app/commands/config/user/index.ts +23 -0
- package/examples/tui-app/commands/config/user/set.ts +57 -0
- package/examples/tui-app/commands/greet.ts +14 -11
- package/examples/tui-app/commands/math.ts +6 -9
- package/examples/tui-app/commands/status.ts +24 -13
- package/examples/tui-app/index.ts +7 -3
- package/guides/01-hello-world.md +7 -2
- package/guides/02-adding-options.md +2 -2
- package/guides/03-multiple-commands.md +6 -8
- package/guides/04-subcommands.md +8 -8
- package/guides/05-interactive-tui.md +45 -30
- package/guides/06-config-validation.md +4 -12
- package/guides/07-async-cancellation.md +15 -69
- package/guides/08-complete-application.md +13 -179
- package/guides/README.md +7 -3
- package/package.json +4 -8
- package/src/__tests__/application.test.ts +87 -68
- package/src/__tests__/buildCliCommand.test.ts +99 -119
- package/src/__tests__/builtins.test.ts +27 -75
- package/src/__tests__/command.test.ts +100 -131
- package/src/__tests__/context.test.ts +1 -26
- package/src/__tests__/helpCore.test.ts +227 -0
- package/src/__tests__/parser.test.ts +98 -244
- package/src/__tests__/registry.test.ts +33 -160
- package/src/__tests__/schemaToFields.test.ts +75 -158
- package/src/builtins/help.ts +19 -4
- package/src/builtins/settings.ts +18 -32
- package/src/builtins/version.ts +4 -4
- package/src/cli/output/colors.ts +1 -1
- package/src/cli/parser.ts +26 -95
- package/src/core/application.ts +192 -110
- package/src/core/command.ts +26 -9
- package/src/core/context.ts +31 -20
- package/src/core/help.ts +24 -18
- package/src/core/knownCommands.ts +13 -0
- package/src/core/logger.ts +39 -42
- package/src/core/registry.ts +5 -12
- package/src/tui/TuiApplication.tsx +63 -120
- package/src/tui/TuiRoot.tsx +135 -0
- package/src/tui/adapters/factory.ts +19 -0
- package/src/tui/adapters/ink/InkRenderer.tsx +135 -0
- package/src/tui/adapters/ink/components/Button.tsx +12 -0
- package/src/tui/adapters/ink/components/Code.tsx +6 -0
- package/src/tui/adapters/ink/components/CodeHighlight.tsx +6 -0
- package/src/tui/adapters/ink/components/Container.tsx +5 -0
- package/src/tui/adapters/ink/components/Field.tsx +12 -0
- package/src/tui/adapters/ink/components/Label.tsx +24 -0
- package/src/tui/adapters/ink/components/MenuButton.tsx +12 -0
- package/src/tui/adapters/ink/components/MenuItem.tsx +17 -0
- package/src/tui/adapters/ink/components/Overlay.tsx +5 -0
- package/src/tui/adapters/ink/components/Panel.tsx +15 -0
- package/src/tui/adapters/ink/components/ScrollView.tsx +5 -0
- package/src/tui/adapters/ink/components/Select.tsx +44 -0
- package/src/tui/adapters/ink/components/Spacer.tsx +15 -0
- package/src/tui/adapters/ink/components/Spinner.tsx +5 -0
- package/src/tui/adapters/ink/components/TextInput.tsx +22 -0
- package/src/tui/adapters/ink/components/Value.tsx +7 -0
- package/src/tui/adapters/ink/keyboard.ts +97 -0
- package/src/tui/adapters/ink/utils.ts +16 -0
- package/src/tui/adapters/opentui/OpenTuiRenderer.tsx +115 -0
- package/src/tui/adapters/opentui/components/Button.tsx +13 -0
- package/src/tui/adapters/opentui/components/Code.tsx +12 -0
- package/src/tui/adapters/opentui/components/CodeHighlight.tsx +24 -0
- package/src/tui/adapters/opentui/components/Container.tsx +56 -0
- package/src/tui/adapters/opentui/components/Field.tsx +18 -0
- package/src/tui/adapters/opentui/components/Label.tsx +15 -0
- package/src/tui/adapters/opentui/components/MenuButton.tsx +14 -0
- package/src/tui/adapters/opentui/components/MenuItem.tsx +29 -0
- package/src/tui/adapters/opentui/components/Overlay.tsx +21 -0
- package/src/tui/adapters/opentui/components/Panel.tsx +78 -0
- package/src/tui/adapters/opentui/components/ScrollView.tsx +85 -0
- package/src/tui/adapters/opentui/components/Select.tsx +59 -0
- package/src/tui/adapters/opentui/components/Spacer.tsx +5 -0
- package/src/tui/adapters/opentui/components/Spinner.tsx +12 -0
- package/src/tui/adapters/opentui/components/TextInput.tsx +13 -0
- package/src/tui/adapters/opentui/components/Value.tsx +13 -0
- package/src/tui/{hooks → adapters/opentui/hooks}/useSpinner.ts +2 -11
- package/src/tui/adapters/opentui/keyboard.ts +61 -0
- package/src/tui/adapters/types.ts +70 -0
- package/src/tui/components/ActionButton.tsx +0 -36
- package/src/tui/components/CommandSelector.tsx +52 -92
- package/src/tui/components/ConfigForm.tsx +68 -42
- package/src/tui/components/FieldRow.tsx +0 -30
- package/src/tui/components/Header.tsx +14 -13
- package/src/tui/components/JsonHighlight.tsx +10 -17
- package/src/tui/components/ModalBase.tsx +38 -0
- package/src/tui/components/ResultsPanel.tsx +27 -36
- package/src/tui/components/StatusBar.tsx +24 -39
- package/src/tui/components/logColors.ts +12 -0
- package/src/tui/context/ClipboardContext.tsx +87 -0
- package/src/tui/context/ExecutorContext.tsx +139 -0
- package/src/tui/context/KeyboardContext.tsx +85 -71
- package/src/tui/context/LogsContext.tsx +35 -0
- package/src/tui/context/NavigationContext.tsx +194 -0
- package/src/tui/context/RendererContext.tsx +20 -0
- package/src/tui/context/TuiAppContext.tsx +58 -0
- package/src/tui/hooks/useActiveKeyHandler.ts +75 -0
- package/src/tui/hooks/useBackHandler.ts +34 -0
- package/src/tui/hooks/useClipboard.ts +40 -25
- package/src/tui/hooks/useClipboardProvider.ts +42 -0
- package/src/tui/hooks/useGlobalKeyHandler.ts +54 -0
- package/src/tui/modals/CliModal.tsx +82 -0
- package/src/tui/modals/EditorModal.tsx +207 -0
- package/src/tui/modals/LogsModal.tsx +98 -0
- package/src/tui/registry.ts +102 -0
- package/src/tui/screens/CommandSelectScreen.tsx +162 -0
- package/src/tui/screens/ConfigScreen.tsx +160 -0
- package/src/tui/screens/ErrorScreen.tsx +58 -0
- package/src/tui/screens/ResultsScreen.tsx +60 -0
- package/src/tui/screens/RunningScreen.tsx +72 -0
- package/src/tui/screens/ScreenBase.ts +6 -0
- package/src/tui/semantic/Button.tsx +7 -0
- package/src/tui/semantic/Code.tsx +7 -0
- package/src/tui/semantic/CodeHighlight.tsx +7 -0
- package/src/tui/semantic/Container.tsx +7 -0
- package/src/tui/semantic/Field.tsx +7 -0
- package/src/tui/semantic/Label.tsx +7 -0
- package/src/tui/semantic/MenuButton.tsx +7 -0
- package/src/tui/semantic/MenuItem.tsx +7 -0
- package/src/tui/semantic/Overlay.tsx +7 -0
- package/src/tui/semantic/Panel.tsx +7 -0
- package/src/tui/semantic/ScrollView.tsx +9 -0
- package/src/tui/semantic/Select.tsx +7 -0
- package/src/tui/semantic/Spacer.tsx +7 -0
- package/src/tui/semantic/Spinner.tsx +7 -0
- package/src/tui/semantic/TextInput.tsx +7 -0
- package/src/tui/semantic/Value.tsx +7 -0
- package/src/tui/semantic/types.ts +195 -0
- package/src/tui/theme.ts +25 -14
- package/src/tui/utils/buildCliCommand.ts +1 -0
- package/src/tui/utils/getEnumKeys.ts +3 -0
- package/src/tui/utils/parameterPersistence.ts +1 -0
- package/src/types/command.ts +0 -60
- package/examples/tui-app/commands/index.ts +0 -3
- package/src/__tests__/colors.test.ts +0 -127
- package/src/__tests__/commandClass.test.ts +0 -130
- package/src/__tests__/help.test.ts +0 -412
- package/src/__tests__/registryNew.test.ts +0 -160
- package/src/__tests__/table.test.ts +0 -146
- package/src/__tests__/tui.test.ts +0 -26
- package/src/builtins/index.ts +0 -4
- package/src/cli/help.ts +0 -174
- package/src/cli/index.ts +0 -3
- package/src/cli/output/index.ts +0 -2
- package/src/cli/output/table.ts +0 -141
- package/src/commands/help.ts +0 -50
- package/src/commands/index.ts +0 -1
- package/src/components/index.ts +0 -147
- package/src/core/index.ts +0 -15
- package/src/hooks/index.ts +0 -131
- package/src/index.ts +0 -137
- package/src/registry/commandRegistry.ts +0 -77
- package/src/registry/index.ts +0 -1
- package/src/tui/TuiApp.tsx +0 -582
- package/src/tui/app.ts +0 -29
- package/src/tui/components/CliModal.tsx +0 -81
- package/src/tui/components/EditorModal.tsx +0 -177
- package/src/tui/components/LogsPanel.tsx +0 -86
- package/src/tui/components/index.ts +0 -13
- package/src/tui/context/index.ts +0 -7
- package/src/tui/hooks/index.ts +0 -35
- package/src/tui/hooks/useKeyboardHandler.ts +0 -91
- package/src/tui/hooks/useLogStream.ts +0 -96
- package/src/tui/index.ts +0 -65
- package/src/tui/utils/index.ts +0 -13
- package/src/types/index.ts +0 -1
|
@@ -1,176 +1,93 @@
|
|
|
1
|
-
import { describe,
|
|
2
|
-
import {
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { getFieldDisplayValue, schemaToFieldConfigs } from "../tui/utils/schemaToFields.ts";
|
|
3
3
|
import type { OptionSchema } from "../types/command.ts";
|
|
4
4
|
|
|
5
|
-
describe("
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
5
|
+
describe("schemaToFields", () => {
|
|
6
|
+
describe("schemaToFieldConfigs", () => {
|
|
7
|
+
test("maps option schema to field configs", () => {
|
|
8
|
+
const schema: OptionSchema = {
|
|
9
|
+
name: { type: "string", description: "User name" },
|
|
10
|
+
color: {
|
|
11
|
+
type: "string",
|
|
12
|
+
description: "Color choice",
|
|
13
|
+
enum: ["red", "green", "blue"],
|
|
14
|
+
},
|
|
15
|
+
verbose: { type: "boolean", description: "Verbose output" },
|
|
16
|
+
count: { type: "number", description: "Count" },
|
|
17
|
+
files: { type: "array", description: "Files to process" },
|
|
18
|
+
repoPath: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "Repository path",
|
|
21
|
+
label: "Repository",
|
|
22
|
+
},
|
|
23
|
+
hidden: { type: "string", description: "Hidden", tuiHidden: true },
|
|
24
|
+
path: { type: "string", description: "Path", placeholder: "Enter path here" },
|
|
25
|
+
grouped: { type: "string", description: "Grouped", group: "Basic" },
|
|
26
|
+
};
|
|
20
27
|
|
|
21
|
-
|
|
22
|
-
const schema: OptionSchema = {
|
|
23
|
-
color: {
|
|
24
|
-
type: "string",
|
|
25
|
-
description: "Color choice",
|
|
26
|
-
enum: ["red", "green", "blue"],
|
|
27
|
-
},
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const fields = schemaToFieldConfigs(schema);
|
|
31
|
-
expect(fields[0]?.type).toBe("enum");
|
|
32
|
-
expect(fields[0]?.options).toHaveLength(3);
|
|
33
|
-
expect(fields[0]?.options?.[0]?.name).toBe("red");
|
|
34
|
-
});
|
|
28
|
+
const fields = schemaToFieldConfigs(schema);
|
|
35
29
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
type: "boolean",
|
|
40
|
-
description: "Verbose output",
|
|
41
|
-
},
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const fields = schemaToFieldConfigs(schema);
|
|
45
|
-
expect(fields[0]?.type).toBe("boolean");
|
|
46
|
-
});
|
|
30
|
+
// Basic mappings
|
|
31
|
+
const name = fields.find((f) => f.key === "name");
|
|
32
|
+
expect(name).toMatchObject({ type: "text", label: "Name" });
|
|
47
33
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
type: "number",
|
|
52
|
-
description: "Count",
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const fields = schemaToFieldConfigs(schema);
|
|
57
|
-
expect(fields[0]?.type).toBe("number");
|
|
58
|
-
});
|
|
34
|
+
const color = fields.find((f) => f.key === "color");
|
|
35
|
+
expect(color?.type).toBe("enum");
|
|
36
|
+
expect(color?.options?.length).toBe(3);
|
|
59
37
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
type: "array",
|
|
64
|
-
description: "Files to process",
|
|
65
|
-
},
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const fields = schemaToFieldConfigs(schema);
|
|
69
|
-
expect(fields[0]?.type).toBe("text");
|
|
70
|
-
});
|
|
38
|
+
expect(fields.find((f) => f.key === "verbose")?.type).toBe("boolean");
|
|
39
|
+
expect(fields.find((f) => f.key === "count")?.type).toBe("number");
|
|
40
|
+
expect(fields.find((f) => f.key === "files")?.type).toBe("text");
|
|
71
41
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
label: "Repository",
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const fields = schemaToFieldConfigs(schema);
|
|
82
|
-
expect(fields[0]?.label).toBe("Repository");
|
|
42
|
+
// Decorations
|
|
43
|
+
expect(fields.find((f) => f.key === "repoPath")?.label).toBe("Repository");
|
|
44
|
+
expect(fields.some((f) => f.key === "hidden")).toBe(false);
|
|
45
|
+
expect(fields.find((f) => f.key === "path")?.placeholder).toBe("Enter path here");
|
|
46
|
+
expect(fields.find((f) => f.key === "grouped")?.group).toBe("Basic");
|
|
83
47
|
});
|
|
84
48
|
|
|
85
49
|
test("sorts by order", () => {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const fields = schemaToFieldConfigs(schema);
|
|
93
|
-
expect(fields[0]?.key).toBe("first");
|
|
94
|
-
expect(fields[1]?.key).toBe("second");
|
|
95
|
-
expect(fields[2]?.key).toBe("third");
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test("excludes tuiHidden fields", () => {
|
|
99
|
-
const schema: OptionSchema = {
|
|
100
|
-
visible: { type: "string", description: "Visible" },
|
|
101
|
-
hidden: { type: "string", description: "Hidden", tuiHidden: true },
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
const fields = schemaToFieldConfigs(schema);
|
|
105
|
-
expect(fields).toHaveLength(1);
|
|
106
|
-
expect(fields[0]?.key).toBe("visible");
|
|
107
|
-
});
|
|
50
|
+
const schema: OptionSchema = {
|
|
51
|
+
third: { type: "string", description: "Third", order: 3 },
|
|
52
|
+
first: { type: "string", description: "First", order: 1 },
|
|
53
|
+
second: { type: "string", description: "Second", order: 2 },
|
|
54
|
+
};
|
|
108
55
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
path: {
|
|
112
|
-
type: "string",
|
|
113
|
-
description: "Path",
|
|
114
|
-
placeholder: "Enter path here",
|
|
115
|
-
},
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
const fields = schemaToFieldConfigs(schema);
|
|
119
|
-
expect(fields[0]?.placeholder).toBe("Enter path here");
|
|
56
|
+
const fields = schemaToFieldConfigs(schema);
|
|
57
|
+
expect(fields.map((f) => f.key)).toEqual(["first", "second", "third"]);
|
|
120
58
|
});
|
|
59
|
+
});
|
|
121
60
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const fields = schemaToFieldConfigs(schema);
|
|
132
|
-
expect(fields[0]?.group).toBe("Basic");
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
describe("getFieldDisplayValue", () => {
|
|
137
|
-
test("displays boolean as True/False", () => {
|
|
138
|
-
const field = { key: "enabled", label: "Enabled", type: "boolean" as const };
|
|
139
|
-
expect(getFieldDisplayValue(true, field)).toBe("True");
|
|
140
|
-
expect(getFieldDisplayValue(false, field)).toBe("False");
|
|
141
|
-
});
|
|
61
|
+
describe("getFieldDisplayValue", () => {
|
|
62
|
+
test("formats values for display", () => {
|
|
63
|
+
expect(getFieldDisplayValue(true, { key: "enabled", label: "Enabled", type: "boolean" })).toBe(
|
|
64
|
+
"True"
|
|
65
|
+
);
|
|
66
|
+
expect(getFieldDisplayValue(false, { key: "enabled", label: "Enabled", type: "boolean" })).toBe(
|
|
67
|
+
"False"
|
|
68
|
+
);
|
|
142
69
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
expect(getFieldDisplayValue("red", field)).toBe("Red");
|
|
154
|
-
expect(getFieldDisplayValue("green", field)).toBe("Green");
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test("displays (empty) for empty strings", () => {
|
|
158
|
-
const field = { key: "name", label: "Name", type: "text" as const };
|
|
159
|
-
expect(getFieldDisplayValue("", field)).toBe("(empty)");
|
|
160
|
-
expect(getFieldDisplayValue(null, field)).toBe("(empty)");
|
|
161
|
-
expect(getFieldDisplayValue(undefined, field)).toBe("(empty)");
|
|
162
|
-
});
|
|
70
|
+
const enumField = {
|
|
71
|
+
key: "color",
|
|
72
|
+
label: "Color",
|
|
73
|
+
type: "enum" as const,
|
|
74
|
+
options: [
|
|
75
|
+
{ name: "Red", value: "red" },
|
|
76
|
+
{ name: "Green", value: "green" },
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
expect(getFieldDisplayValue("red", enumField)).toBe("Red");
|
|
163
80
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
expect(result.endsWith("...")).toBe(true);
|
|
170
|
-
});
|
|
81
|
+
const textField = { key: "name", label: "Name", type: "text" as const };
|
|
82
|
+
expect(getFieldDisplayValue("", textField)).toBe("(empty)");
|
|
83
|
+
expect(getFieldDisplayValue(null, textField)).toBe("(empty)");
|
|
84
|
+
expect(getFieldDisplayValue(undefined, textField)).toBe("(empty)");
|
|
85
|
+
expect(getFieldDisplayValue("hello", textField)).toBe("hello");
|
|
171
86
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
87
|
+
const longValue = "a".repeat(100);
|
|
88
|
+
const longResult = getFieldDisplayValue(longValue, { key: "desc", label: "Description", type: "text" });
|
|
89
|
+
expect(longResult.length).toBe(60);
|
|
90
|
+
expect(longResult.endsWith("...")).toBe(true);
|
|
175
91
|
});
|
|
92
|
+
});
|
|
176
93
|
});
|
package/src/builtins/help.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Command, type AnyCommand } from "../core/command.ts";
|
|
2
|
-
import type { AppContext } from "../core/context.ts";
|
|
3
2
|
import { generateCommandHelp, generateAppHelp } from "../core/help.ts";
|
|
3
|
+
import { KNOWN_COMMANDS } from "../core/knownCommands.ts";
|
|
4
4
|
import type { OptionSchema } from "../types/command.ts";
|
|
5
|
+
import { GLOBAL_OPTIONS_SCHEMA } from "../core/application.ts";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Built-in help command that is auto-injected as a subcommand into all commands.
|
|
@@ -11,8 +12,9 @@ import type { OptionSchema } from "../types/command.ts";
|
|
|
11
12
|
* be instantiated directly.
|
|
12
13
|
*/
|
|
13
14
|
export class HelpCommand extends Command<OptionSchema> {
|
|
14
|
-
readonly name =
|
|
15
|
+
readonly name = KNOWN_COMMANDS.help;
|
|
15
16
|
readonly description = "Show help for this command";
|
|
17
|
+
override readonly tuiHidden = true;
|
|
16
18
|
readonly options = {} as const;
|
|
17
19
|
|
|
18
20
|
private parentCommand: AnyCommand | null = null;
|
|
@@ -33,7 +35,14 @@ export class HelpCommand extends Command<OptionSchema> {
|
|
|
33
35
|
this.appVersion = config.appVersion;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Help command is CLI-only (auto-injected for CLI use, not shown in TUI).
|
|
40
|
+
*/
|
|
41
|
+
override supportsTui(): boolean {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
override async execute(): Promise<void> {
|
|
37
46
|
let helpText: string;
|
|
38
47
|
|
|
39
48
|
if (this.parentCommand) {
|
|
@@ -41,12 +50,18 @@ export class HelpCommand extends Command<OptionSchema> {
|
|
|
41
50
|
helpText = generateCommandHelp(this.parentCommand, {
|
|
42
51
|
appName: this.appName,
|
|
43
52
|
version: this.appVersion,
|
|
53
|
+
globalOptionsSchema: GLOBAL_OPTIONS_SCHEMA,
|
|
44
54
|
});
|
|
45
55
|
} else {
|
|
46
56
|
// Show help for the entire application
|
|
47
|
-
|
|
57
|
+
const visibleCommands = this.allCommands.filter(
|
|
58
|
+
(cmd) => !cmd.tuiHidden || cmd.supportsCli()
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
helpText = generateAppHelp(visibleCommands, {
|
|
48
62
|
appName: this.appName,
|
|
49
63
|
version: this.appVersion,
|
|
64
|
+
globalOptionsSchema: GLOBAL_OPTIONS_SCHEMA,
|
|
50
65
|
});
|
|
51
66
|
}
|
|
52
67
|
|
package/src/builtins/settings.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Command } from "../core/command.ts";
|
|
2
|
-
import
|
|
2
|
+
import { AppContext } from "../core/context.ts";
|
|
3
3
|
import { LogLevel } from "../core/logger.ts";
|
|
4
4
|
import type { OptionSchema, OptionValues } from "../types/command.ts";
|
|
5
5
|
import type { CommandResult } from "../core/command.ts";
|
|
6
|
+
import { getEnumKeys } from "../tui/utils/getEnumKeys.ts";
|
|
7
|
+
import { KNOWN_COMMANDS } from "../core/knownCommands.ts";
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Options schema for the settings command.
|
|
@@ -12,7 +14,7 @@ const settingsOptions = {
|
|
|
12
14
|
type: "string",
|
|
13
15
|
description: "Minimum log level to emit",
|
|
14
16
|
default: "info",
|
|
15
|
-
enum:
|
|
17
|
+
enum: getEnumKeys(LogLevel) as (keyof typeof LogLevel)[],
|
|
16
18
|
label: "Log Level",
|
|
17
19
|
order: 1,
|
|
18
20
|
},
|
|
@@ -35,28 +37,6 @@ interface SettingsConfig {
|
|
|
35
37
|
detailedLogs: boolean;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
/**
|
|
39
|
-
* Map of string log level names to LogLevel enum values.
|
|
40
|
-
*/
|
|
41
|
-
const logLevelMap: Record<string, LogLevel> = {
|
|
42
|
-
silly: LogLevel.Silly,
|
|
43
|
-
trace: LogLevel.Trace,
|
|
44
|
-
debug: LogLevel.Debug,
|
|
45
|
-
info: LogLevel.Info,
|
|
46
|
-
warn: LogLevel.Warn,
|
|
47
|
-
error: LogLevel.Error,
|
|
48
|
-
fatal: LogLevel.Fatal,
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Parse a string log level to the LogLevel enum.
|
|
53
|
-
*/
|
|
54
|
-
function parseLogLevel(value?: string): LogLevel {
|
|
55
|
-
if (!value) return LogLevel.Info;
|
|
56
|
-
const level = logLevelMap[value.toLowerCase()];
|
|
57
|
-
return level ?? LogLevel.Info;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
40
|
/**
|
|
61
41
|
* Built-in settings command for configuring logging.
|
|
62
42
|
*
|
|
@@ -69,32 +49,38 @@ function parseLogLevel(value?: string): LogLevel {
|
|
|
69
49
|
* In TUI mode, this command provides a UI for configuring these settings.
|
|
70
50
|
*/
|
|
71
51
|
export class SettingsCommand extends Command<typeof settingsOptions, SettingsConfig> {
|
|
72
|
-
|
|
52
|
+
override supportsCli(): boolean {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
readonly name = KNOWN_COMMANDS.settings;
|
|
73
57
|
override readonly displayName = "Settings";
|
|
58
|
+
override readonly tuiHidden = true;
|
|
74
59
|
readonly description = "Configure logging level and output format";
|
|
75
60
|
readonly options = settingsOptions;
|
|
76
61
|
|
|
77
62
|
override readonly actionLabel = "Save Settings";
|
|
78
63
|
override readonly immediateExecution = false;
|
|
79
64
|
|
|
80
|
-
override buildConfig(
|
|
81
|
-
const
|
|
65
|
+
override buildConfig(opts: SettingsOptions): SettingsConfig {
|
|
66
|
+
const logLevelStr = opts["log-level"];
|
|
67
|
+
const logLevel = LogLevel[logLevelStr as keyof typeof LogLevel] ?? LogLevel.info;
|
|
82
68
|
const detailedLogs = Boolean(opts["detailed-logs"]);
|
|
83
69
|
|
|
84
70
|
return { logLevel, detailedLogs };
|
|
85
71
|
}
|
|
86
72
|
|
|
87
|
-
override async execute(
|
|
88
|
-
this.applySettings(
|
|
73
|
+
override async execute(config: SettingsConfig): Promise<CommandResult> {
|
|
74
|
+
this.applySettings(config);
|
|
89
75
|
return {
|
|
90
76
|
success: true,
|
|
91
77
|
message: `Logging set to ${LogLevel[config.logLevel]}${config.detailedLogs ? " with detailed format" : ""}`,
|
|
92
78
|
};
|
|
93
79
|
}
|
|
94
80
|
|
|
95
|
-
private applySettings(
|
|
96
|
-
|
|
97
|
-
|
|
81
|
+
private applySettings(config: SettingsConfig): void {
|
|
82
|
+
AppContext.current.logger.setMinLevel(config.logLevel);
|
|
83
|
+
AppContext.current.logger.setDetailed(config.detailedLogs);
|
|
98
84
|
}
|
|
99
85
|
}
|
|
100
86
|
|
package/src/builtins/version.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { Command } from "../core/command.ts";
|
|
2
|
-
import type { AppContext } from "../core/context.ts";
|
|
3
2
|
import { colors } from "../cli/output/colors.ts";
|
|
4
3
|
import type { OptionSchema } from "../types/command.ts";
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Configuration for version command.
|
|
8
7
|
*/
|
|
9
|
-
|
|
8
|
+
interface VersionConfig {
|
|
10
9
|
/** Application name */
|
|
11
10
|
appName: string;
|
|
12
11
|
/** Application version (e.g., "1.0.0") */
|
|
@@ -19,7 +18,7 @@ export interface VersionConfig {
|
|
|
19
18
|
* Format version string with optional commit hash.
|
|
20
19
|
* If commitHash is empty or undefined, shows "(dev)".
|
|
21
20
|
*/
|
|
22
|
-
|
|
21
|
+
function formatVersion(version: string, commitHash?: string): string {
|
|
23
22
|
const hashPart = commitHash && commitHash.length > 0
|
|
24
23
|
? commitHash.substring(0, 7)
|
|
25
24
|
: "(dev)";
|
|
@@ -33,6 +32,7 @@ export function formatVersion(version: string, commitHash?: string): string {
|
|
|
33
32
|
export class VersionCommand extends Command<OptionSchema> {
|
|
34
33
|
readonly name = "version";
|
|
35
34
|
readonly description = "Show version information";
|
|
35
|
+
override readonly tuiHidden = true;
|
|
36
36
|
readonly options = {} as const;
|
|
37
37
|
readonly aliases = ["--version", "-v"];
|
|
38
38
|
|
|
@@ -54,7 +54,7 @@ export class VersionCommand extends Command<OptionSchema> {
|
|
|
54
54
|
return formatVersion(this.appVersion, this.commitHash);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
override async execute(
|
|
57
|
+
override async execute(): Promise<void> {
|
|
58
58
|
const versionDisplay = this.getFormattedVersion();
|
|
59
59
|
console.log(`${colors.bold(this.appName)} ${colors.dim(`v${versionDisplay}`)}`);
|
|
60
60
|
}
|
package/src/cli/output/colors.ts
CHANGED
|
@@ -23,7 +23,7 @@ const BG_BLUE = "\x1b[44m";
|
|
|
23
23
|
/**
|
|
24
24
|
* Check if terminal supports colors
|
|
25
25
|
*/
|
|
26
|
-
|
|
26
|
+
function supportsColors(): boolean {
|
|
27
27
|
if (typeof process === "undefined") return false;
|
|
28
28
|
if (process.env["NO_COLOR"]) return false;
|
|
29
29
|
if (process.env["FORCE_COLOR"]) return true;
|
package/src/cli/parser.ts
CHANGED
|
@@ -1,21 +1,6 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
1
|
+
import type { OptionSchema, OptionValues } from "../types/command.ts";
|
|
2
|
+
import { type ParseArgsConfig } from "util";
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* Result of parsing CLI arguments
|
|
6
|
-
*/
|
|
7
|
-
export interface ParseResult<T extends OptionSchema = OptionSchema> {
|
|
8
|
-
command: Command<T> | null;
|
|
9
|
-
commandPath: string[];
|
|
10
|
-
options: OptionValues<T>;
|
|
11
|
-
args: string[];
|
|
12
|
-
showHelp: boolean;
|
|
13
|
-
error?: ParseError;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Error during parsing
|
|
18
|
-
*/
|
|
19
4
|
export interface ParseError {
|
|
20
5
|
type: "unknown_command" | "invalid_option" | "missing_required" | "validation";
|
|
21
6
|
message: string;
|
|
@@ -30,21 +15,38 @@ export function extractCommandChain(args: string[]): {
|
|
|
30
15
|
remaining: string[];
|
|
31
16
|
} {
|
|
32
17
|
const commands: string[] = [];
|
|
33
|
-
|
|
18
|
+
const remaining: string[] = [];
|
|
34
19
|
|
|
35
|
-
for (; i < args.length; i++) {
|
|
20
|
+
for (let i = 0; i < args.length; i++) {
|
|
36
21
|
const arg = args[i];
|
|
37
|
-
|
|
38
|
-
|
|
22
|
+
|
|
23
|
+
if (!arg) {
|
|
24
|
+
continue;
|
|
39
25
|
}
|
|
40
|
-
|
|
41
|
-
|
|
26
|
+
|
|
27
|
+
if (arg.startsWith("-")) {
|
|
28
|
+
remaining.push(arg);
|
|
29
|
+
|
|
30
|
+
const next = args[i + 1];
|
|
31
|
+
if (next && !next.startsWith("-")) {
|
|
32
|
+
remaining.push(next);
|
|
33
|
+
i += 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
continue;
|
|
42
37
|
}
|
|
38
|
+
|
|
39
|
+
if (remaining.length > 0) {
|
|
40
|
+
remaining.push(arg);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
commands.push(arg);
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
return {
|
|
46
48
|
commands,
|
|
47
|
-
remaining
|
|
49
|
+
remaining,
|
|
48
50
|
};
|
|
49
51
|
}
|
|
50
52
|
|
|
@@ -168,74 +170,3 @@ export function validateOptions<T extends OptionSchema>(
|
|
|
168
170
|
return errors;
|
|
169
171
|
}
|
|
170
172
|
|
|
171
|
-
interface ParseCliArgsOptions<T extends OptionSchema> {
|
|
172
|
-
args: string[];
|
|
173
|
-
commands: Record<string, Command<T>>;
|
|
174
|
-
defaultCommand?: string;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Parse CLI arguments into a result
|
|
179
|
-
*/
|
|
180
|
-
export function parseCliArgs<T extends OptionSchema>(
|
|
181
|
-
options: ParseCliArgsOptions<T>
|
|
182
|
-
): ParseResult<T> {
|
|
183
|
-
const { args, commands, defaultCommand } = options;
|
|
184
|
-
const { commands: commandChain, remaining } = extractCommandChain(args);
|
|
185
|
-
|
|
186
|
-
// Check for help flag
|
|
187
|
-
const showHelp = remaining.includes("--help") || remaining.includes("-h");
|
|
188
|
-
|
|
189
|
-
// Find command
|
|
190
|
-
const commandName = commandChain[0] ?? defaultCommand;
|
|
191
|
-
if (!commandName) {
|
|
192
|
-
return {
|
|
193
|
-
command: null,
|
|
194
|
-
commandPath: [],
|
|
195
|
-
options: {} as OptionValues<T>,
|
|
196
|
-
args: remaining,
|
|
197
|
-
showHelp,
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const command = commands[commandName];
|
|
202
|
-
if (!command) {
|
|
203
|
-
return {
|
|
204
|
-
command: null,
|
|
205
|
-
commandPath: commandChain,
|
|
206
|
-
options: {} as OptionValues<T>,
|
|
207
|
-
args: remaining,
|
|
208
|
-
showHelp,
|
|
209
|
-
error: {
|
|
210
|
-
type: "unknown_command",
|
|
211
|
-
message: `Unknown command: ${commandName}`,
|
|
212
|
-
},
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Parse options
|
|
217
|
-
const schema = command.options ?? ({} as T);
|
|
218
|
-
const parseArgsConfig = schemaToParseArgsOptions(schema);
|
|
219
|
-
|
|
220
|
-
let parsedValues: Record<string, unknown> = {};
|
|
221
|
-
try {
|
|
222
|
-
const { values } = parseArgs({
|
|
223
|
-
args: remaining,
|
|
224
|
-
...parseArgsConfig,
|
|
225
|
-
allowPositionals: false,
|
|
226
|
-
});
|
|
227
|
-
parsedValues = values;
|
|
228
|
-
} catch {
|
|
229
|
-
// Ignore parse errors for now
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const optionValues = parseOptionValues(schema, parsedValues);
|
|
233
|
-
|
|
234
|
-
return {
|
|
235
|
-
command,
|
|
236
|
-
commandPath: commandChain,
|
|
237
|
-
options: optionValues,
|
|
238
|
-
args: remaining,
|
|
239
|
-
showHelp,
|
|
240
|
-
};
|
|
241
|
-
}
|