@pablozaiden/terminatui 0.2.0 → 0.3.0-beta-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +14 -2
- package/CLAUDE.md +1 -0
- package/README.md +64 -43
- package/bun.lock +85 -0
- package/examples/tui-app/commands/config/app/get.ts +6 -10
- package/examples/tui-app/commands/config/app/index.ts +2 -6
- package/examples/tui-app/commands/config/app/set.ts +23 -13
- package/examples/tui-app/commands/config/index.ts +2 -6
- package/examples/tui-app/commands/config/user/get.ts +6 -10
- package/examples/tui-app/commands/config/user/index.ts +2 -6
- package/examples/tui-app/commands/config/user/set.ts +6 -10
- package/examples/tui-app/commands/greet.ts +13 -11
- package/examples/tui-app/commands/math.ts +5 -9
- package/examples/tui-app/commands/status.ts +21 -12
- package/examples/tui-app/index.ts +6 -3
- package/guides/01-hello-world.md +7 -2
- package/guides/02-adding-options.md +2 -2
- package/guides/03-multiple-commands.md +6 -8
- package/guides/04-subcommands.md +8 -8
- package/guides/05-interactive-tui.md +45 -30
- package/guides/06-config-validation.md +4 -12
- package/guides/07-async-cancellation.md +14 -16
- package/guides/08-complete-application.md +12 -42
- package/guides/README.md +7 -3
- package/package.json +4 -8
- package/src/__tests__/application.test.ts +87 -68
- package/src/__tests__/buildCliCommand.test.ts +99 -119
- package/src/__tests__/builtins.test.ts +27 -75
- package/src/__tests__/command.test.ts +100 -131
- package/src/__tests__/context.test.ts +1 -26
- package/src/__tests__/helpCore.test.ts +227 -0
- package/src/__tests__/parser.test.ts +98 -244
- package/src/__tests__/registry.test.ts +33 -160
- package/src/__tests__/schemaToFields.test.ts +75 -158
- package/src/builtins/help.ts +12 -4
- package/src/builtins/settings.ts +18 -32
- package/src/builtins/version.ts +4 -4
- package/src/cli/output/colors.ts +1 -1
- package/src/cli/parser.ts +26 -95
- package/src/core/application.ts +192 -110
- package/src/core/command.ts +26 -9
- package/src/core/context.ts +31 -20
- package/src/core/help.ts +24 -18
- package/src/core/knownCommands.ts +13 -0
- package/src/core/logger.ts +39 -42
- package/src/core/registry.ts +5 -12
- package/src/tui/TuiApplication.tsx +63 -120
- package/src/tui/TuiRoot.tsx +135 -0
- package/src/tui/adapters/factory.ts +19 -0
- package/src/tui/adapters/ink/InkRenderer.tsx +135 -0
- package/src/tui/adapters/ink/components/Button.tsx +12 -0
- package/src/tui/adapters/ink/components/Code.tsx +6 -0
- package/src/tui/adapters/ink/components/CodeHighlight.tsx +6 -0
- package/src/tui/adapters/ink/components/Container.tsx +5 -0
- package/src/tui/adapters/ink/components/Field.tsx +12 -0
- package/src/tui/adapters/ink/components/Label.tsx +24 -0
- package/src/tui/adapters/ink/components/MenuButton.tsx +12 -0
- package/src/tui/adapters/ink/components/MenuItem.tsx +17 -0
- package/src/tui/adapters/ink/components/Overlay.tsx +5 -0
- package/src/tui/adapters/ink/components/Panel.tsx +15 -0
- package/src/tui/adapters/ink/components/ScrollView.tsx +5 -0
- package/src/tui/adapters/ink/components/Select.tsx +44 -0
- package/src/tui/adapters/ink/components/Spacer.tsx +15 -0
- package/src/tui/adapters/ink/components/Spinner.tsx +5 -0
- package/src/tui/adapters/ink/components/TextInput.tsx +22 -0
- package/src/tui/adapters/ink/components/Value.tsx +7 -0
- package/src/tui/adapters/ink/keyboard.ts +97 -0
- package/src/tui/adapters/ink/utils.ts +16 -0
- package/src/tui/adapters/opentui/OpenTuiRenderer.tsx +115 -0
- package/src/tui/adapters/opentui/components/Button.tsx +13 -0
- package/src/tui/adapters/opentui/components/Code.tsx +12 -0
- package/src/tui/adapters/opentui/components/CodeHighlight.tsx +24 -0
- package/src/tui/adapters/opentui/components/Container.tsx +56 -0
- package/src/tui/adapters/opentui/components/Field.tsx +18 -0
- package/src/tui/adapters/opentui/components/Label.tsx +15 -0
- package/src/tui/adapters/opentui/components/MenuButton.tsx +14 -0
- package/src/tui/adapters/opentui/components/MenuItem.tsx +29 -0
- package/src/tui/adapters/opentui/components/Overlay.tsx +21 -0
- package/src/tui/adapters/opentui/components/Panel.tsx +78 -0
- package/src/tui/adapters/opentui/components/ScrollView.tsx +85 -0
- package/src/tui/adapters/opentui/components/Select.tsx +59 -0
- package/src/tui/adapters/opentui/components/Spacer.tsx +5 -0
- package/src/tui/adapters/opentui/components/Spinner.tsx +12 -0
- package/src/tui/adapters/opentui/components/TextInput.tsx +13 -0
- package/src/tui/adapters/opentui/components/Value.tsx +13 -0
- package/src/tui/{hooks → adapters/opentui/hooks}/useSpinner.ts +2 -11
- package/src/tui/adapters/opentui/keyboard.ts +61 -0
- package/src/tui/adapters/types.ts +70 -0
- package/src/tui/components/ActionButton.tsx +0 -36
- package/src/tui/components/CommandSelector.tsx +45 -92
- package/src/tui/components/ConfigForm.tsx +68 -42
- package/src/tui/components/FieldRow.tsx +0 -30
- package/src/tui/components/Header.tsx +14 -13
- package/src/tui/components/JsonHighlight.tsx +10 -17
- package/src/tui/components/ModalBase.tsx +38 -0
- package/src/tui/components/ResultsPanel.tsx +27 -36
- package/src/tui/components/StatusBar.tsx +24 -39
- package/src/tui/components/logColors.ts +12 -0
- package/src/tui/context/ClipboardContext.tsx +87 -0
- package/src/tui/context/ExecutorContext.tsx +139 -0
- package/src/tui/context/KeyboardContext.tsx +85 -71
- package/src/tui/context/LogsContext.tsx +35 -0
- package/src/tui/context/NavigationContext.tsx +194 -0
- package/src/tui/context/RendererContext.tsx +20 -0
- package/src/tui/context/TuiAppContext.tsx +58 -0
- package/src/tui/hooks/useActiveKeyHandler.ts +75 -0
- package/src/tui/hooks/useBackHandler.ts +34 -0
- package/src/tui/hooks/useClipboard.ts +40 -25
- package/src/tui/hooks/useClipboardProvider.ts +42 -0
- package/src/tui/hooks/useGlobalKeyHandler.ts +54 -0
- package/src/tui/modals/CliModal.tsx +82 -0
- package/src/tui/modals/EditorModal.tsx +207 -0
- package/src/tui/modals/LogsModal.tsx +98 -0
- package/src/tui/registry.ts +102 -0
- package/src/tui/screens/CommandSelectScreen.tsx +162 -0
- package/src/tui/screens/ConfigScreen.tsx +160 -0
- package/src/tui/screens/ErrorScreen.tsx +58 -0
- package/src/tui/screens/ResultsScreen.tsx +60 -0
- package/src/tui/screens/RunningScreen.tsx +72 -0
- package/src/tui/screens/ScreenBase.ts +6 -0
- package/src/tui/semantic/Button.tsx +7 -0
- package/src/tui/semantic/Code.tsx +7 -0
- package/src/tui/semantic/CodeHighlight.tsx +7 -0
- package/src/tui/semantic/Container.tsx +7 -0
- package/src/tui/semantic/Field.tsx +7 -0
- package/src/tui/semantic/Label.tsx +7 -0
- package/src/tui/semantic/MenuButton.tsx +7 -0
- package/src/tui/semantic/MenuItem.tsx +7 -0
- package/src/tui/semantic/Overlay.tsx +7 -0
- package/src/tui/semantic/Panel.tsx +7 -0
- package/src/tui/semantic/ScrollView.tsx +9 -0
- package/src/tui/semantic/Select.tsx +7 -0
- package/src/tui/semantic/Spacer.tsx +7 -0
- package/src/tui/semantic/Spinner.tsx +7 -0
- package/src/tui/semantic/TextInput.tsx +7 -0
- package/src/tui/semantic/Value.tsx +7 -0
- package/src/tui/semantic/types.ts +195 -0
- package/src/tui/theme.ts +25 -14
- package/src/tui/utils/buildCliCommand.ts +1 -0
- package/src/tui/utils/getEnumKeys.ts +3 -0
- package/src/tui/utils/parameterPersistence.ts +1 -0
- package/src/types/command.ts +0 -60
- package/examples/tui-app/commands/index.ts +0 -4
- package/src/__tests__/colors.test.ts +0 -127
- package/src/__tests__/commandClass.test.ts +0 -130
- package/src/__tests__/help.test.ts +0 -412
- package/src/__tests__/registryNew.test.ts +0 -160
- package/src/__tests__/table.test.ts +0 -146
- package/src/__tests__/tui.test.ts +0 -26
- package/src/builtins/index.ts +0 -4
- package/src/cli/help.ts +0 -174
- package/src/cli/index.ts +0 -3
- package/src/cli/output/index.ts +0 -2
- package/src/cli/output/table.ts +0 -141
- package/src/commands/help.ts +0 -50
- package/src/commands/index.ts +0 -1
- package/src/components/index.ts +0 -147
- package/src/core/index.ts +0 -15
- package/src/hooks/index.ts +0 -131
- package/src/index.ts +0 -137
- package/src/registry/commandRegistry.ts +0 -77
- package/src/registry/index.ts +0 -1
- package/src/tui/TuiApp.tsx +0 -619
- package/src/tui/app.ts +0 -29
- package/src/tui/components/CliModal.tsx +0 -81
- package/src/tui/components/EditorModal.tsx +0 -177
- package/src/tui/components/LogsPanel.tsx +0 -86
- package/src/tui/components/index.ts +0 -13
- package/src/tui/context/index.ts +0 -7
- package/src/tui/hooks/index.ts +0 -35
- package/src/tui/hooks/useKeyboardHandler.ts +0 -91
- package/src/tui/hooks/useLogStream.ts +0 -96
- package/src/tui/index.ts +0 -65
- package/src/tui/utils/index.ts +0 -13
- package/src/types/index.ts +0 -1
|
@@ -23,7 +23,6 @@ import path from "node:path";
|
|
|
23
23
|
import {
|
|
24
24
|
Command,
|
|
25
25
|
ConfigValidationError,
|
|
26
|
-
type AppContext,
|
|
27
26
|
type OptionSchema,
|
|
28
27
|
type OptionValues,
|
|
29
28
|
type CommandResult,
|
|
@@ -69,10 +68,7 @@ export class DownloadCommand extends Command<typeof options, DownloadConfig> {
|
|
|
69
68
|
readonly displayName = "File Downloader";
|
|
70
69
|
readonly actionLabel = "Download";
|
|
71
70
|
|
|
72
|
-
override buildConfig(
|
|
73
|
-
_ctx: AppContext,
|
|
74
|
-
opts: OptionValues<typeof options>
|
|
75
|
-
): DownloadConfig {
|
|
71
|
+
override buildConfig(opts: OptionValues<typeof options>): DownloadConfig {
|
|
76
72
|
// Validate URL
|
|
77
73
|
const urlStr = opts["url"] as string;
|
|
78
74
|
if (!urlStr) {
|
|
@@ -107,16 +103,15 @@ export class DownloadCommand extends Command<typeof options, DownloadConfig> {
|
|
|
107
103
|
|
|
108
104
|
```typescript
|
|
109
105
|
async execute(
|
|
110
|
-
ctx: AppContext,
|
|
111
106
|
config: DownloadConfig,
|
|
112
|
-
execCtx
|
|
107
|
+
execCtx: CommandExecutionContext
|
|
113
108
|
): Promise<CommandResult> {
|
|
114
109
|
const { url, outputDir, fileName, chunkSize } = config;
|
|
115
110
|
const outputPath = path.join(outputDir, fileName);
|
|
116
|
-
const signal = execCtx
|
|
111
|
+
const signal = execCtx.signal;
|
|
117
112
|
|
|
118
|
-
|
|
119
|
-
|
|
113
|
+
console.log(`Starting download: ${url}`);
|
|
114
|
+
console.log(`Output: ${outputPath}`);
|
|
120
115
|
|
|
121
116
|
// Create output directory
|
|
122
117
|
await Bun.write(path.join(outputDir, ".keep"), "");
|
|
@@ -128,7 +123,8 @@ export class DownloadCommand extends Command<typeof options, DownloadConfig> {
|
|
|
128
123
|
|
|
129
124
|
try {
|
|
130
125
|
// Check for cancellation before starting
|
|
131
|
-
|
|
126
|
+
if (signal.aborted) {
|
|
127
|
+
|
|
132
128
|
return { success: false, message: "Download cancelled before start" };
|
|
133
129
|
}
|
|
134
130
|
|
|
@@ -157,8 +153,9 @@ export class DownloadCommand extends Command<typeof options, DownloadConfig> {
|
|
|
157
153
|
// Read chunks with cancellation checks
|
|
158
154
|
while (true) {
|
|
159
155
|
// Check for cancellation between chunks
|
|
160
|
-
|
|
161
|
-
|
|
156
|
+
if (signal.aborted) {
|
|
157
|
+
|
|
158
|
+
console.warn("Download cancelled by user");
|
|
162
159
|
throw new Error("AbortError");
|
|
163
160
|
}
|
|
164
161
|
|
|
@@ -177,10 +174,10 @@ export class DownloadCommand extends Command<typeof options, DownloadConfig> {
|
|
|
177
174
|
const percent = ((downloadedBytes / totalBytes) * 100).toFixed(1);
|
|
178
175
|
const mbDownloaded = (downloadedBytes / 1024 / 1024).toFixed(2);
|
|
179
176
|
const mbTotal = (totalBytes / 1024 / 1024).toFixed(2);
|
|
180
|
-
|
|
177
|
+
Bun.write(Bun.stdout, `\rProgress: ${percent}% (${mbDownloaded}/${mbTotal} MB)`);
|
|
181
178
|
} else {
|
|
182
179
|
const mbDownloaded = (downloadedBytes / 1024 / 1024).toFixed(2);
|
|
183
|
-
|
|
180
|
+
Bun.write(Bun.stdout, `\rDownloaded: ${mbDownloaded} MB`);
|
|
184
181
|
}
|
|
185
182
|
}
|
|
186
183
|
|
|
@@ -200,7 +197,8 @@ export class DownloadCommand extends Command<typeof options, DownloadConfig> {
|
|
|
200
197
|
|
|
201
198
|
} catch (error) {
|
|
202
199
|
// Handle cancellation
|
|
203
|
-
|
|
200
|
+
if (signal.aborted || (error as Error).name === "AbortError") {
|
|
201
|
+
|
|
204
202
|
console.log("\nDownload cancelled.");
|
|
205
203
|
|
|
206
204
|
// Cleanup partial file
|
|
@@ -152,7 +152,6 @@ Create `src/commands/add.ts`:
|
|
|
152
152
|
import {
|
|
153
153
|
Command,
|
|
154
154
|
ConfigValidationError,
|
|
155
|
-
type AppContext,
|
|
156
155
|
type OptionSchema,
|
|
157
156
|
type OptionValues,
|
|
158
157
|
type CommandResult,
|
|
@@ -190,15 +189,10 @@ export class AddCommand extends Command<typeof options, AddConfig> {
|
|
|
190
189
|
readonly options = options;
|
|
191
190
|
readonly displayName = "Add Task";
|
|
192
191
|
readonly actionLabel = "Create Task";
|
|
193
|
-
readonly group = "Tasks";
|
|
194
|
-
readonly order = 1;
|
|
195
192
|
|
|
196
193
|
private db = new Database();
|
|
197
194
|
|
|
198
|
-
override buildConfig(
|
|
199
|
-
_ctx: AppContext,
|
|
200
|
-
opts: OptionValues<typeof options>
|
|
201
|
-
): AddConfig {
|
|
195
|
+
override buildConfig(opts: OptionValues<typeof options>): AddConfig {
|
|
202
196
|
const title = (opts["title"] as string)?.trim();
|
|
203
197
|
if (!title) {
|
|
204
198
|
throw new ConfigValidationError("Task title cannot be empty", "title");
|
|
@@ -215,13 +209,11 @@ export class AddCommand extends Command<typeof options, AddConfig> {
|
|
|
215
209
|
return { title, priority };
|
|
216
210
|
}
|
|
217
211
|
|
|
218
|
-
async execute(
|
|
219
|
-
ctx.logger.debug(`Creating task: ${config.title}`);
|
|
220
|
-
|
|
212
|
+
async execute(config: AddConfig): Promise<CommandResult> {
|
|
221
213
|
const task = await this.db.addTask(config.title, config.priority);
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
214
|
+
|
|
215
|
+
// (Example) optional: log to console or your own logger
|
|
216
|
+
console.log(`Created task: ${task.title}`);
|
|
225
217
|
|
|
226
218
|
return {
|
|
227
219
|
success: true,
|
|
@@ -237,7 +229,6 @@ Create `src/commands/list.ts`:
|
|
|
237
229
|
```typescript
|
|
238
230
|
import {
|
|
239
231
|
Command,
|
|
240
|
-
type AppContext,
|
|
241
232
|
type OptionSchema,
|
|
242
233
|
type OptionValues,
|
|
243
234
|
type CommandResult,
|
|
@@ -272,27 +263,20 @@ export class ListCommand extends Command<typeof options, ListConfig> {
|
|
|
272
263
|
readonly options = options;
|
|
273
264
|
readonly displayName = "List Tasks";
|
|
274
265
|
readonly actionLabel = "Show Tasks";
|
|
275
|
-
readonly group = "Tasks";
|
|
276
|
-
readonly order = 2;
|
|
277
266
|
|
|
278
267
|
// Execute immediately when selected in TUI
|
|
279
268
|
readonly immediateExecution = true;
|
|
280
269
|
|
|
281
270
|
private db = new Database();
|
|
282
271
|
|
|
283
|
-
override buildConfig(
|
|
284
|
-
_ctx: AppContext,
|
|
285
|
-
opts: OptionValues<typeof options>
|
|
286
|
-
): ListConfig {
|
|
272
|
+
override buildConfig(opts: OptionValues<typeof options>): ListConfig {
|
|
287
273
|
return {
|
|
288
274
|
filter: (opts["filter"] as ListConfig["filter"]) ?? "all",
|
|
289
275
|
priority: opts["priority"] as Task["priority"] | undefined,
|
|
290
276
|
};
|
|
291
277
|
}
|
|
292
278
|
|
|
293
|
-
async execute(
|
|
294
|
-
ctx.logger.debug(`Listing tasks: filter=${config.filter}`);
|
|
295
|
-
|
|
279
|
+
async execute(config: ListConfig): Promise<CommandResult> {
|
|
296
280
|
let tasks = await this.db.listTasks(config.filter);
|
|
297
281
|
|
|
298
282
|
if (config.priority) {
|
|
@@ -314,7 +298,6 @@ Create `src/commands/complete.ts`:
|
|
|
314
298
|
import {
|
|
315
299
|
Command,
|
|
316
300
|
ConfigValidationError,
|
|
317
|
-
type AppContext,
|
|
318
301
|
type OptionSchema,
|
|
319
302
|
type OptionValues,
|
|
320
303
|
type CommandResult,
|
|
@@ -342,15 +325,10 @@ export class CompleteCommand extends Command<typeof options, CompleteConfig> {
|
|
|
342
325
|
readonly options = options;
|
|
343
326
|
readonly displayName = "Complete Task";
|
|
344
327
|
readonly actionLabel = "Mark Complete";
|
|
345
|
-
readonly group = "Tasks";
|
|
346
|
-
readonly order = 3;
|
|
347
328
|
|
|
348
329
|
private db = new Database();
|
|
349
330
|
|
|
350
|
-
override buildConfig(
|
|
351
|
-
_ctx: AppContext,
|
|
352
|
-
opts: OptionValues<typeof options>
|
|
353
|
-
): CompleteConfig {
|
|
331
|
+
override buildConfig(opts: OptionValues<typeof options>): CompleteConfig {
|
|
354
332
|
const id = opts["id"] as string;
|
|
355
333
|
if (!id?.trim()) {
|
|
356
334
|
throw new ConfigValidationError("Task ID is required", "id");
|
|
@@ -358,11 +336,9 @@ export class CompleteCommand extends Command<typeof options, CompleteConfig> {
|
|
|
358
336
|
return { id: id.trim() };
|
|
359
337
|
}
|
|
360
338
|
|
|
361
|
-
async execute(
|
|
362
|
-
ctx.logger.debug(`Completing task: ${config.id}`);
|
|
363
|
-
|
|
339
|
+
async execute(config: CompleteConfig): Promise<CommandResult> {
|
|
364
340
|
const task = await this.db.completeTask(config.id);
|
|
365
|
-
|
|
341
|
+
|
|
366
342
|
if (!task) {
|
|
367
343
|
return {
|
|
368
344
|
success: false,
|
|
@@ -370,8 +346,7 @@ export class CompleteCommand extends Command<typeof options, CompleteConfig> {
|
|
|
370
346
|
};
|
|
371
347
|
}
|
|
372
348
|
|
|
373
|
-
|
|
374
|
-
notifications.taskCompleted(task);
|
|
349
|
+
console.log(`Completed task: ${task.title}`);
|
|
375
350
|
|
|
376
351
|
return {
|
|
377
352
|
success: true,
|
|
@@ -387,7 +362,6 @@ Create `src/commands/stats.ts`:
|
|
|
387
362
|
```typescript
|
|
388
363
|
import {
|
|
389
364
|
Command,
|
|
390
|
-
type AppContext,
|
|
391
365
|
type OptionSchema,
|
|
392
366
|
type CommandResult,
|
|
393
367
|
} from "@pablozaiden/terminatui";
|
|
@@ -402,15 +376,11 @@ export class StatsCommand extends Command<typeof options> {
|
|
|
402
376
|
readonly options = options;
|
|
403
377
|
readonly displayName = "Statistics";
|
|
404
378
|
readonly actionLabel = "Show Stats";
|
|
405
|
-
readonly group = "Analytics";
|
|
406
|
-
readonly order = 1;
|
|
407
379
|
readonly immediateExecution = true;
|
|
408
380
|
|
|
409
381
|
private db = new Database();
|
|
410
382
|
|
|
411
|
-
async execute(
|
|
412
|
-
ctx.logger.debug("Fetching statistics");
|
|
413
|
-
|
|
383
|
+
async execute(): Promise<CommandResult> {
|
|
414
384
|
const stats = await this.db.getStats();
|
|
415
385
|
|
|
416
386
|
return {
|
package/guides/README.md
CHANGED
|
@@ -36,7 +36,7 @@ bun add @pablozaiden/terminatui
|
|
|
36
36
|
|
|
37
37
|
# Create your first command
|
|
38
38
|
cat > src/index.ts << 'EOF'
|
|
39
|
-
import { Command, Application, type
|
|
39
|
+
import { Command, Application, type OptionSchema } from "@pablozaiden/terminatui";
|
|
40
40
|
|
|
41
41
|
const options = {
|
|
42
42
|
name: { type: "string", description: "Your name" },
|
|
@@ -47,7 +47,7 @@ class HelloCommand extends Command<typeof options> {
|
|
|
47
47
|
readonly description = "Say hello";
|
|
48
48
|
readonly options = options;
|
|
49
49
|
|
|
50
|
-
async execute(
|
|
50
|
+
async execute(config: Record<string, unknown>) {
|
|
51
51
|
const name = config["name"] ?? "World";
|
|
52
52
|
console.log(`Hello, ${name}!`);
|
|
53
53
|
return { success: true };
|
|
@@ -60,7 +60,11 @@ class MyCLI extends Application {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
// Recommended: let Terminatui read `Bun.argv.slice(2)`
|
|
63
64
|
await new MyCLI().run();
|
|
65
|
+
|
|
66
|
+
// For tests or programmatic invocation:
|
|
67
|
+
// await new MyCLI().runFromArgs(["hello", "--name", "Developer"]);
|
|
64
68
|
EOF
|
|
65
69
|
|
|
66
70
|
# Run it
|
|
@@ -69,6 +73,6 @@ bun src/index.ts hello --name "Developer"
|
|
|
69
73
|
|
|
70
74
|
## Prerequisites
|
|
71
75
|
|
|
72
|
-
- [Bun](https://bun.sh)
|
|
76
|
+
- [Bun](https://bun.sh)
|
|
73
77
|
- Basic TypeScript knowledge
|
|
74
78
|
- Terminal/command-line familiarity
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pablozaiden/terminatui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-beta-1",
|
|
4
4
|
"description": "Terminal UI and Command Line Application Framework",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "https://github.com/PabloZaiden/terminatui"
|
|
@@ -8,13 +8,6 @@
|
|
|
8
8
|
"type": "module",
|
|
9
9
|
"main": "src/index.ts",
|
|
10
10
|
"types": "src/index.ts",
|
|
11
|
-
"exports": {
|
|
12
|
-
".": "./src/index.ts",
|
|
13
|
-
"./cli": "./src/cli/index.ts",
|
|
14
|
-
"./tui": "./src/tui/index.ts",
|
|
15
|
-
"./components": "./src/components/index.ts",
|
|
16
|
-
"./hooks": "./src/hooks/index.ts"
|
|
17
|
-
},
|
|
18
11
|
"scripts": {
|
|
19
12
|
"build": "bunx tsc --noEmit",
|
|
20
13
|
"test": "bun test",
|
|
@@ -22,6 +15,9 @@
|
|
|
22
15
|
},
|
|
23
16
|
"dependencies": {
|
|
24
17
|
"@opentui/react": "0.1.68",
|
|
18
|
+
"ink": "^6.6.0",
|
|
19
|
+
"ink-select-input": "^6.2.0",
|
|
20
|
+
"ink-text-input": "^6.0.0",
|
|
25
21
|
"tslog": "^4.9.3"
|
|
26
22
|
},
|
|
27
23
|
"devDependencies": {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { describe, test, expect
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
2
|
import { Application } from "../core/application.ts";
|
|
3
3
|
import { Command } from "../core/command.ts";
|
|
4
|
-
import { AppContext } from "../core/context.ts";
|
|
5
4
|
import type { OptionSchema, OptionValues, OptionDef } from "../types/command.ts";
|
|
5
|
+
import { AppContext } from "../core/context.ts";
|
|
6
|
+
import { LogLevel } from "../core/logger.ts";
|
|
7
|
+
import { KNOWN_COMMANDS } from "../core/knownCommands.ts";
|
|
6
8
|
|
|
7
9
|
// Define a proper option schema
|
|
8
10
|
const testOptions = {
|
|
@@ -21,7 +23,6 @@ class TestCommand extends Command<typeof testOptions> {
|
|
|
21
23
|
executedWith: Record<string, unknown> | null = null;
|
|
22
24
|
|
|
23
25
|
override async execute(
|
|
24
|
-
_ctx: AppContext,
|
|
25
26
|
opts: OptionValues<typeof testOptions>
|
|
26
27
|
): Promise<void> {
|
|
27
28
|
this.executedWith = opts as Record<string, unknown>;
|
|
@@ -35,17 +36,57 @@ class TuiCommand extends Command<OptionSchema> {
|
|
|
35
36
|
|
|
36
37
|
executed = false;
|
|
37
38
|
|
|
38
|
-
override async execute(
|
|
39
|
+
override async execute(): Promise<void> {
|
|
39
40
|
this.executed = true;
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
describe("Application", () => {
|
|
44
|
-
afterEach(() => {
|
|
45
|
-
AppContext.clearCurrent();
|
|
46
|
-
});
|
|
47
|
-
|
|
48
45
|
describe("constructor", () => {
|
|
46
|
+
test("rejects reserved help command definitions", () => {
|
|
47
|
+
class ReservedCommand extends Command<OptionSchema> {
|
|
48
|
+
readonly name = KNOWN_COMMANDS.help;
|
|
49
|
+
readonly description = "tries to override built-in";
|
|
50
|
+
readonly options = {};
|
|
51
|
+
|
|
52
|
+
override async execute(): Promise<void> {}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
expect(() => {
|
|
56
|
+
new Application({
|
|
57
|
+
name: "test-app",
|
|
58
|
+
version: "1.0.0",
|
|
59
|
+
commands: [new ReservedCommand()],
|
|
60
|
+
});
|
|
61
|
+
}).toThrow(/reserved/i);
|
|
62
|
+
|
|
63
|
+
class SubCommand extends Command<OptionSchema> {
|
|
64
|
+
readonly name = KNOWN_COMMANDS.help;
|
|
65
|
+
readonly description = "user help";
|
|
66
|
+
readonly options = {};
|
|
67
|
+
|
|
68
|
+
override async execute(): Promise<void> {}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
class ParentCommand extends Command<OptionSchema> {
|
|
72
|
+
readonly name = "parent";
|
|
73
|
+
readonly description = "parent";
|
|
74
|
+
readonly options = {};
|
|
75
|
+
|
|
76
|
+
override subCommands = [new SubCommand()];
|
|
77
|
+
|
|
78
|
+
override async execute(): Promise<void> {}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
expect(() => {
|
|
82
|
+
new Application({
|
|
83
|
+
name: "test-app",
|
|
84
|
+
version: "1.0.0",
|
|
85
|
+
commands: [new ParentCommand()],
|
|
86
|
+
});
|
|
87
|
+
}).toThrow(/automatically injected/i);
|
|
88
|
+
});
|
|
89
|
+
|
|
49
90
|
test("creates application with name and version", () => {
|
|
50
91
|
const app = new Application({
|
|
51
92
|
name: "test-app",
|
|
@@ -56,14 +97,16 @@ describe("Application", () => {
|
|
|
56
97
|
expect(app.version).toBe("1.0.0");
|
|
57
98
|
});
|
|
58
99
|
|
|
59
|
-
test("creates context
|
|
60
|
-
|
|
100
|
+
test("creates context as side effect of creating application", () => {
|
|
101
|
+
// side effect of creating an application is setting the current context
|
|
102
|
+
new Application({
|
|
61
103
|
name: "test-app",
|
|
62
104
|
version: "1.0.0",
|
|
63
105
|
commands: [],
|
|
64
106
|
});
|
|
65
|
-
|
|
66
|
-
expect(
|
|
107
|
+
|
|
108
|
+
expect(AppContext.current.config.name).toBe("test-app");
|
|
109
|
+
expect(AppContext.current.config.version).toBe("1.0.0");
|
|
67
110
|
});
|
|
68
111
|
|
|
69
112
|
test("registers provided commands", () => {
|
|
@@ -91,7 +134,7 @@ describe("Application", () => {
|
|
|
91
134
|
version: "1.0.0",
|
|
92
135
|
commands: [],
|
|
93
136
|
});
|
|
94
|
-
expect(app.registry.has(
|
|
137
|
+
expect(app.registry.has(KNOWN_COMMANDS.help)).toBe(true);
|
|
95
138
|
});
|
|
96
139
|
|
|
97
140
|
test("injects help subcommand into commands", () => {
|
|
@@ -103,32 +146,11 @@ describe("Application", () => {
|
|
|
103
146
|
commands: [cmd],
|
|
104
147
|
});
|
|
105
148
|
expect(cmd.subCommands).toBeDefined();
|
|
106
|
-
expect(cmd.subCommands?.some((c) => c.name ===
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
describe("getContext", () => {
|
|
111
|
-
test("returns the application context", () => {
|
|
112
|
-
const app = new Application({
|
|
113
|
-
name: "test-app",
|
|
114
|
-
version: "1.0.0",
|
|
115
|
-
commands: [],
|
|
116
|
-
});
|
|
117
|
-
expect(app.getContext()).toBe(app.context);
|
|
149
|
+
expect(cmd.subCommands?.some((c) => c.name === KNOWN_COMMANDS.help)).toBe(true);
|
|
118
150
|
});
|
|
119
151
|
});
|
|
120
152
|
|
|
121
153
|
describe("run", () => {
|
|
122
|
-
test("shows help when no args and no default command", async () => {
|
|
123
|
-
const app = new Application({
|
|
124
|
-
name: "test-app",
|
|
125
|
-
version: "1.0.0",
|
|
126
|
-
commands: [new TestCommand()],
|
|
127
|
-
});
|
|
128
|
-
// Should not throw
|
|
129
|
-
await app.run([]);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
154
|
test("runs default command when no args", async () => {
|
|
133
155
|
const cmd = new TuiCommand();
|
|
134
156
|
const app = new Application({
|
|
@@ -137,31 +159,30 @@ describe("Application", () => {
|
|
|
137
159
|
commands: [cmd],
|
|
138
160
|
defaultCommand: "tui-cmd",
|
|
139
161
|
});
|
|
140
|
-
await app.
|
|
162
|
+
await app.runFromArgs([]);
|
|
141
163
|
expect(cmd.executed).toBe(true);
|
|
142
164
|
});
|
|
143
165
|
|
|
144
|
-
test("runs specified command", async () => {
|
|
166
|
+
test("runs specified command and passes options", async () => {
|
|
145
167
|
const cmd = new TestCommand();
|
|
146
168
|
const app = new Application({
|
|
147
169
|
name: "test-app",
|
|
148
170
|
version: "1.0.0",
|
|
149
171
|
commands: [cmd],
|
|
150
172
|
});
|
|
151
|
-
|
|
152
|
-
|
|
173
|
+
|
|
174
|
+
await app.runFromArgs(["test", "--value", "hello"]);
|
|
175
|
+
expect(cmd.executedWith?.["value"]).toBe("hello");
|
|
153
176
|
});
|
|
154
177
|
|
|
155
|
-
test("
|
|
156
|
-
const cmd = new TestCommand();
|
|
178
|
+
test("with no args and no default, prints help (no throw)", async () => {
|
|
157
179
|
const app = new Application({
|
|
158
180
|
name: "test-app",
|
|
159
181
|
version: "1.0.0",
|
|
160
|
-
commands: [
|
|
182
|
+
commands: [new TestCommand()],
|
|
161
183
|
});
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
expect(cmd.executedWith?.["value"]).toBe("hello");
|
|
184
|
+
|
|
185
|
+
await app.runFromArgs([]);
|
|
165
186
|
});
|
|
166
187
|
});
|
|
167
188
|
|
|
@@ -179,7 +200,7 @@ describe("Application", () => {
|
|
|
179
200
|
called = true;
|
|
180
201
|
},
|
|
181
202
|
});
|
|
182
|
-
await app.
|
|
203
|
+
await app.runFromArgs(["test"]);
|
|
183
204
|
expect(called).toBe(true);
|
|
184
205
|
});
|
|
185
206
|
|
|
@@ -196,7 +217,7 @@ describe("Application", () => {
|
|
|
196
217
|
called = true;
|
|
197
218
|
},
|
|
198
219
|
});
|
|
199
|
-
await app.
|
|
220
|
+
await app.runFromArgs(["test"]);
|
|
200
221
|
expect(called).toBe(true);
|
|
201
222
|
});
|
|
202
223
|
|
|
@@ -219,11 +240,11 @@ describe("Application", () => {
|
|
|
219
240
|
commands: [new ErrorCommand()],
|
|
220
241
|
});
|
|
221
242
|
app.setHooks({
|
|
222
|
-
onError: async (
|
|
243
|
+
onError: async (error) => {
|
|
223
244
|
errorCaught = error;
|
|
224
245
|
},
|
|
225
246
|
});
|
|
226
|
-
await app.
|
|
247
|
+
await app.runFromArgs(["error-cmd"]);
|
|
227
248
|
expect(errorCaught?.message).toBe("Test error");
|
|
228
249
|
});
|
|
229
250
|
});
|
|
@@ -250,7 +271,6 @@ describe("Application", () => {
|
|
|
250
271
|
readonly options = configOptions;
|
|
251
272
|
|
|
252
273
|
override buildConfig(
|
|
253
|
-
_ctx: AppContext,
|
|
254
274
|
opts: OptionValues<typeof configOptions>
|
|
255
275
|
): ParsedConfig {
|
|
256
276
|
buildConfigCalled = true;
|
|
@@ -260,7 +280,7 @@ describe("Application", () => {
|
|
|
260
280
|
};
|
|
261
281
|
}
|
|
262
282
|
|
|
263
|
-
override async execute(
|
|
283
|
+
override async execute(config: ParsedConfig): Promise<void> {
|
|
264
284
|
receivedConfig = config;
|
|
265
285
|
}
|
|
266
286
|
}
|
|
@@ -271,7 +291,7 @@ describe("Application", () => {
|
|
|
271
291
|
commands: [new ConfigCommand()],
|
|
272
292
|
});
|
|
273
293
|
|
|
274
|
-
await app.
|
|
294
|
+
await app.runFromArgs(["config-cmd", "--value", "test", "--count", "42"]);
|
|
275
295
|
|
|
276
296
|
expect(buildConfigCalled).toBe(true);
|
|
277
297
|
expect(receivedConfig).toEqual({ value: "test", count: 42 });
|
|
@@ -286,7 +306,6 @@ describe("Application", () => {
|
|
|
286
306
|
readonly options = testOptions;
|
|
287
307
|
|
|
288
308
|
override async execute(
|
|
289
|
-
_ctx: AppContext,
|
|
290
309
|
opts: OptionValues<typeof testOptions>
|
|
291
310
|
): Promise<void> {
|
|
292
311
|
receivedOpts = opts as Record<string, unknown>;
|
|
@@ -299,7 +318,7 @@ describe("Application", () => {
|
|
|
299
318
|
commands: [new NoConfigCommand()],
|
|
300
319
|
});
|
|
301
320
|
|
|
302
|
-
await app.
|
|
321
|
+
await app.runFromArgs(["no-config-cmd", "--value", "hello"]);
|
|
303
322
|
|
|
304
323
|
expect(receivedOpts).toEqual({ value: "hello" });
|
|
305
324
|
});
|
|
@@ -328,12 +347,12 @@ describe("Application", () => {
|
|
|
328
347
|
});
|
|
329
348
|
|
|
330
349
|
app.setHooks({
|
|
331
|
-
onError: async (
|
|
350
|
+
onError: async (error) => {
|
|
332
351
|
errorCaught = error;
|
|
333
352
|
},
|
|
334
353
|
});
|
|
335
354
|
|
|
336
|
-
await app.
|
|
355
|
+
await app.runFromArgs(["fail-config", "--value", "test"]);
|
|
337
356
|
|
|
338
357
|
expect(errorCaught?.message).toBe("Config validation failed");
|
|
339
358
|
});
|
|
@@ -349,7 +368,7 @@ describe("Application", () => {
|
|
|
349
368
|
});
|
|
350
369
|
|
|
351
370
|
// Should not throw - global option should be parsed and removed
|
|
352
|
-
await app.
|
|
371
|
+
await app.runFromArgs(["--log-level", "debug", "test", "--value", "hello"]);
|
|
353
372
|
expect(cmd.executedWith?.["value"]).toBe("hello");
|
|
354
373
|
});
|
|
355
374
|
|
|
@@ -361,7 +380,7 @@ describe("Application", () => {
|
|
|
361
380
|
commands: [cmd],
|
|
362
381
|
});
|
|
363
382
|
|
|
364
|
-
await app.
|
|
383
|
+
await app.runFromArgs(["test", "--log-level", "debug", "--value", "hello"]);
|
|
365
384
|
expect(cmd.executedWith?.["value"]).toBe("hello");
|
|
366
385
|
});
|
|
367
386
|
|
|
@@ -374,14 +393,14 @@ describe("Application", () => {
|
|
|
374
393
|
});
|
|
375
394
|
|
|
376
395
|
// All of these should work (case-insensitive)
|
|
377
|
-
await app.
|
|
378
|
-
expect(
|
|
396
|
+
await app.runFromArgs(["--log-level", "debug", "test"]);
|
|
397
|
+
expect(AppContext.current.logger.getMinLevel()).toBe(LogLevel.debug);
|
|
379
398
|
|
|
380
|
-
await app.
|
|
381
|
-
expect(
|
|
399
|
+
await app.runFromArgs(["--log-level", "Debug", "test"]);
|
|
400
|
+
expect(AppContext.current.logger.getMinLevel()).toBe(LogLevel.debug);
|
|
382
401
|
|
|
383
|
-
await app.
|
|
384
|
-
expect(
|
|
402
|
+
await app.runFromArgs(["--log-level", "DEBUG", "test"]);
|
|
403
|
+
expect(AppContext.current.logger.getMinLevel()).toBe(LogLevel.debug);
|
|
385
404
|
});
|
|
386
405
|
|
|
387
406
|
test("parses --detailed-logs flag", async () => {
|
|
@@ -392,7 +411,7 @@ describe("Application", () => {
|
|
|
392
411
|
commands: [cmd],
|
|
393
412
|
});
|
|
394
413
|
|
|
395
|
-
await app.
|
|
414
|
+
await app.runFromArgs(["--detailed-logs", "test"]);
|
|
396
415
|
// Should not throw - flag is recognized
|
|
397
416
|
expect(cmd.executedWith).not.toBeNull();
|
|
398
417
|
});
|
|
@@ -405,7 +424,7 @@ describe("Application", () => {
|
|
|
405
424
|
commands: [cmd],
|
|
406
425
|
});
|
|
407
426
|
|
|
408
|
-
await app.
|
|
427
|
+
await app.runFromArgs(["--no-detailed-logs", "test"]);
|
|
409
428
|
// Should not throw - flag is recognized
|
|
410
429
|
expect(cmd.executedWith).not.toBeNull();
|
|
411
430
|
});
|
|
@@ -418,8 +437,8 @@ describe("Application", () => {
|
|
|
418
437
|
commands: [cmd],
|
|
419
438
|
});
|
|
420
439
|
|
|
421
|
-
await app.
|
|
422
|
-
expect(
|
|
440
|
+
await app.runFromArgs(["--log-level=warn", "test"]);
|
|
441
|
+
expect(AppContext.current.logger.getMinLevel()).toBe(LogLevel.warn);
|
|
423
442
|
});
|
|
424
443
|
});
|
|
425
444
|
});
|