@pablozaiden/terminatui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.devcontainer/devcontainer.json +19 -0
- package/.devcontainer/install-prerequisites.sh +49 -0
- package/.github/workflows/copilot-setup-steps.yml +32 -0
- package/.github/workflows/pull-request.yml +27 -0
- package/.github/workflows/release-npm-package.yml +78 -0
- package/LICENSE +21 -0
- package/README.md +524 -0
- package/examples/tui-app/commands/greet.ts +75 -0
- package/examples/tui-app/commands/index.ts +3 -0
- package/examples/tui-app/commands/math.ts +114 -0
- package/examples/tui-app/commands/status.ts +75 -0
- package/examples/tui-app/index.ts +34 -0
- package/guides/01-hello-world.md +96 -0
- package/guides/02-adding-options.md +103 -0
- package/guides/03-multiple-commands.md +163 -0
- package/guides/04-subcommands.md +206 -0
- package/guides/05-interactive-tui.md +194 -0
- package/guides/06-config-validation.md +264 -0
- package/guides/07-async-cancellation.md +388 -0
- package/guides/08-complete-application.md +673 -0
- package/guides/README.md +74 -0
- package/package.json +32 -0
- package/src/__tests__/application.test.ts +425 -0
- package/src/__tests__/buildCliCommand.test.ts +125 -0
- package/src/__tests__/builtins.test.ts +133 -0
- package/src/__tests__/colors.test.ts +127 -0
- package/src/__tests__/command.test.ts +157 -0
- package/src/__tests__/commandClass.test.ts +130 -0
- package/src/__tests__/context.test.ts +97 -0
- package/src/__tests__/help.test.ts +412 -0
- package/src/__tests__/parser.test.ts +268 -0
- package/src/__tests__/registry.test.ts +195 -0
- package/src/__tests__/registryNew.test.ts +160 -0
- package/src/__tests__/schemaToFields.test.ts +176 -0
- package/src/__tests__/table.test.ts +146 -0
- package/src/__tests__/tui.test.ts +26 -0
- package/src/builtins/help.ts +85 -0
- package/src/builtins/index.ts +4 -0
- package/src/builtins/settings.ts +106 -0
- package/src/builtins/version.ts +72 -0
- package/src/cli/help.ts +174 -0
- package/src/cli/index.ts +3 -0
- package/src/cli/output/colors.ts +74 -0
- package/src/cli/output/index.ts +2 -0
- package/src/cli/output/table.ts +141 -0
- package/src/cli/parser.ts +241 -0
- package/src/commands/help.ts +50 -0
- package/src/commands/index.ts +1 -0
- package/src/components/index.ts +147 -0
- package/src/core/application.ts +461 -0
- package/src/core/command.ts +269 -0
- package/src/core/context.ts +112 -0
- package/src/core/help.ts +214 -0
- package/src/core/index.ts +15 -0
- package/src/core/logger.ts +164 -0
- package/src/core/registry.ts +140 -0
- package/src/hooks/index.ts +131 -0
- package/src/index.ts +137 -0
- package/src/registry/commandRegistry.ts +77 -0
- package/src/registry/index.ts +1 -0
- package/src/tui/TuiApp.tsx +582 -0
- package/src/tui/TuiApplication.tsx +230 -0
- package/src/tui/app.ts +29 -0
- package/src/tui/components/ActionButton.tsx +36 -0
- package/src/tui/components/CliModal.tsx +81 -0
- package/src/tui/components/CommandSelector.tsx +159 -0
- package/src/tui/components/ConfigForm.tsx +148 -0
- package/src/tui/components/EditorModal.tsx +177 -0
- package/src/tui/components/FieldRow.tsx +30 -0
- package/src/tui/components/Header.tsx +31 -0
- package/src/tui/components/JsonHighlight.tsx +128 -0
- package/src/tui/components/LogsPanel.tsx +86 -0
- package/src/tui/components/ResultsPanel.tsx +93 -0
- package/src/tui/components/StatusBar.tsx +59 -0
- package/src/tui/components/index.ts +13 -0
- package/src/tui/components/types.ts +30 -0
- package/src/tui/context/KeyboardContext.tsx +118 -0
- package/src/tui/context/index.ts +7 -0
- package/src/tui/hooks/index.ts +35 -0
- package/src/tui/hooks/useClipboard.ts +66 -0
- package/src/tui/hooks/useCommandExecutor.ts +131 -0
- package/src/tui/hooks/useConfigState.ts +171 -0
- package/src/tui/hooks/useKeyboardHandler.ts +91 -0
- package/src/tui/hooks/useLogStream.ts +96 -0
- package/src/tui/hooks/useSpinner.ts +46 -0
- package/src/tui/index.ts +65 -0
- package/src/tui/theme.ts +21 -0
- package/src/tui/utils/buildCliCommand.ts +90 -0
- package/src/tui/utils/index.ts +13 -0
- package/src/tui/utils/parameterPersistence.ts +96 -0
- package/src/tui/utils/schemaToFields.ts +144 -0
- package/src/types/command.ts +103 -0
- package/src/types/execution.ts +11 -0
- package/src/types/index.ts +1 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Command, type AnyCommand } from "../core/command.ts";
|
|
2
|
+
import type { AppContext } from "../core/context.ts";
|
|
3
|
+
import { generateCommandHelp, generateAppHelp } from "../core/help.ts";
|
|
4
|
+
import type { OptionSchema } from "../types/command.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Built-in help command that is auto-injected as a subcommand into all commands.
|
|
8
|
+
* When invoked, it displays help for the parent command.
|
|
9
|
+
*
|
|
10
|
+
* This command is created internally by the Application class and should not
|
|
11
|
+
* be instantiated directly.
|
|
12
|
+
*/
|
|
13
|
+
export class HelpCommand extends Command<OptionSchema> {
|
|
14
|
+
readonly name = "help";
|
|
15
|
+
readonly description = "Show help for this command";
|
|
16
|
+
readonly options = {} as const;
|
|
17
|
+
|
|
18
|
+
private parentCommand: AnyCommand | null = null;
|
|
19
|
+
private allCommands: AnyCommand[] = [];
|
|
20
|
+
private appName: string;
|
|
21
|
+
private appVersion: string;
|
|
22
|
+
|
|
23
|
+
constructor(config: {
|
|
24
|
+
parentCommand?: AnyCommand;
|
|
25
|
+
allCommands?: AnyCommand[];
|
|
26
|
+
appName: string;
|
|
27
|
+
appVersion: string;
|
|
28
|
+
}) {
|
|
29
|
+
super();
|
|
30
|
+
this.parentCommand = config.parentCommand ?? null;
|
|
31
|
+
this.allCommands = config.allCommands ?? [];
|
|
32
|
+
this.appName = config.appName;
|
|
33
|
+
this.appVersion = config.appVersion;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
override async execute(_ctx: AppContext): Promise<void> {
|
|
37
|
+
let helpText: string;
|
|
38
|
+
|
|
39
|
+
if (this.parentCommand) {
|
|
40
|
+
// Show help for the parent command
|
|
41
|
+
helpText = generateCommandHelp(this.parentCommand, {
|
|
42
|
+
appName: this.appName,
|
|
43
|
+
version: this.appVersion,
|
|
44
|
+
});
|
|
45
|
+
} else {
|
|
46
|
+
// Show help for the entire application
|
|
47
|
+
helpText = generateAppHelp(this.allCommands, {
|
|
48
|
+
appName: this.appName,
|
|
49
|
+
version: this.appVersion,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log(helpText);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Create a help command for a specific parent command.
|
|
59
|
+
*/
|
|
60
|
+
export function createHelpCommandForParent(
|
|
61
|
+
parentCommand: AnyCommand,
|
|
62
|
+
appName: string,
|
|
63
|
+
appVersion: string
|
|
64
|
+
): HelpCommand {
|
|
65
|
+
return new HelpCommand({
|
|
66
|
+
parentCommand,
|
|
67
|
+
appName,
|
|
68
|
+
appVersion,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Create a help command for the application root.
|
|
74
|
+
*/
|
|
75
|
+
export function createRootHelpCommand(
|
|
76
|
+
allCommands: AnyCommand[],
|
|
77
|
+
appName: string,
|
|
78
|
+
appVersion: string
|
|
79
|
+
): HelpCommand {
|
|
80
|
+
return new HelpCommand({
|
|
81
|
+
allCommands,
|
|
82
|
+
appName,
|
|
83
|
+
appVersion,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
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";
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Command } from "../core/command.ts";
|
|
2
|
+
import type { AppContext } from "../core/context.ts";
|
|
3
|
+
import { LogLevel } from "../core/logger.ts";
|
|
4
|
+
import type { OptionSchema, OptionValues } from "../types/command.ts";
|
|
5
|
+
import type { CommandResult } from "../core/command.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Options schema for the settings command.
|
|
9
|
+
*/
|
|
10
|
+
const settingsOptions = {
|
|
11
|
+
"log-level": {
|
|
12
|
+
type: "string",
|
|
13
|
+
description: "Minimum log level to emit",
|
|
14
|
+
default: "info",
|
|
15
|
+
enum: ["silly", "trace", "debug", "info", "warn", "error", "fatal"],
|
|
16
|
+
label: "Log Level",
|
|
17
|
+
order: 1,
|
|
18
|
+
},
|
|
19
|
+
"detailed-logs": {
|
|
20
|
+
type: "boolean",
|
|
21
|
+
description: "Include timestamp and level in log output",
|
|
22
|
+
default: false,
|
|
23
|
+
label: "Detailed Logs",
|
|
24
|
+
order: 2,
|
|
25
|
+
},
|
|
26
|
+
} as const satisfies OptionSchema;
|
|
27
|
+
|
|
28
|
+
type SettingsOptions = OptionValues<typeof settingsOptions>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parsed settings configuration.
|
|
32
|
+
*/
|
|
33
|
+
interface SettingsConfig {
|
|
34
|
+
logLevel: LogLevel;
|
|
35
|
+
detailedLogs: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
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
|
+
/**
|
|
61
|
+
* Built-in settings command for configuring logging.
|
|
62
|
+
*
|
|
63
|
+
* This command allows users to configure the log level and detailed logging
|
|
64
|
+
* format at runtime. It's automatically registered by TuiApplication.
|
|
65
|
+
*
|
|
66
|
+
* In CLI mode, these settings are typically passed as global options:
|
|
67
|
+
* --log-level <level> and --detailed-logs
|
|
68
|
+
*
|
|
69
|
+
* In TUI mode, this command provides a UI for configuring these settings.
|
|
70
|
+
*/
|
|
71
|
+
export class SettingsCommand extends Command<typeof settingsOptions, SettingsConfig> {
|
|
72
|
+
readonly name = "settings";
|
|
73
|
+
override readonly displayName = "Settings";
|
|
74
|
+
readonly description = "Configure logging level and output format";
|
|
75
|
+
readonly options = settingsOptions;
|
|
76
|
+
|
|
77
|
+
override readonly actionLabel = "Save Settings";
|
|
78
|
+
override readonly immediateExecution = false;
|
|
79
|
+
|
|
80
|
+
override buildConfig(_ctx: AppContext, opts: SettingsOptions): SettingsConfig {
|
|
81
|
+
const logLevel = parseLogLevel(opts["log-level"] as string | undefined);
|
|
82
|
+
const detailedLogs = Boolean(opts["detailed-logs"]);
|
|
83
|
+
|
|
84
|
+
return { logLevel, detailedLogs };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
override async execute(ctx: AppContext, config: SettingsConfig): Promise<CommandResult> {
|
|
88
|
+
this.applySettings(ctx, config);
|
|
89
|
+
return {
|
|
90
|
+
success: true,
|
|
91
|
+
message: `Logging set to ${LogLevel[config.logLevel]}${config.detailedLogs ? " with detailed format" : ""}`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private applySettings(ctx: AppContext, config: SettingsConfig): void {
|
|
96
|
+
ctx.logger.setMinLevel(config.logLevel);
|
|
97
|
+
ctx.logger.setDetailed(config.detailedLogs);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Create the built-in settings command.
|
|
103
|
+
*/
|
|
104
|
+
export function createSettingsCommand(): SettingsCommand {
|
|
105
|
+
return new SettingsCommand();
|
|
106
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Command } from "../core/command.ts";
|
|
2
|
+
import type { AppContext } from "../core/context.ts";
|
|
3
|
+
import { colors } from "../cli/output/colors.ts";
|
|
4
|
+
import type { OptionSchema } from "../types/command.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Configuration for version command.
|
|
8
|
+
*/
|
|
9
|
+
export interface VersionConfig {
|
|
10
|
+
/** Application name */
|
|
11
|
+
appName: string;
|
|
12
|
+
/** Application version (e.g., "1.0.0") */
|
|
13
|
+
appVersion: string;
|
|
14
|
+
/** Optional commit hash for version display */
|
|
15
|
+
commitHash?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Format version string with optional commit hash.
|
|
20
|
+
* If commitHash is empty or undefined, shows "(dev)".
|
|
21
|
+
*/
|
|
22
|
+
export function formatVersion(version: string, commitHash?: string): string {
|
|
23
|
+
const hashPart = commitHash && commitHash.length > 0
|
|
24
|
+
? commitHash.substring(0, 7)
|
|
25
|
+
: "(dev)";
|
|
26
|
+
return `${version} - ${hashPart}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Built-in version command that displays the application version.
|
|
31
|
+
* Automatically registered at the top level by the Application class.
|
|
32
|
+
*/
|
|
33
|
+
export class VersionCommand extends Command<OptionSchema> {
|
|
34
|
+
readonly name = "version";
|
|
35
|
+
readonly description = "Show version information";
|
|
36
|
+
readonly options = {} as const;
|
|
37
|
+
readonly aliases = ["--version", "-v"];
|
|
38
|
+
|
|
39
|
+
private appName: string;
|
|
40
|
+
private appVersion: string;
|
|
41
|
+
private commitHash?: string;
|
|
42
|
+
|
|
43
|
+
constructor(config: VersionConfig) {
|
|
44
|
+
super();
|
|
45
|
+
this.appName = config.appName;
|
|
46
|
+
this.appVersion = config.appVersion;
|
|
47
|
+
this.commitHash = config.commitHash;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get the formatted version string.
|
|
52
|
+
*/
|
|
53
|
+
getFormattedVersion(): string {
|
|
54
|
+
return formatVersion(this.appVersion, this.commitHash);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
override async execute(_ctx: AppContext): Promise<void> {
|
|
58
|
+
const versionDisplay = this.getFormattedVersion();
|
|
59
|
+
console.log(`${colors.bold(this.appName)} ${colors.dim(`v${versionDisplay}`)}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Create a version command for the application.
|
|
65
|
+
*/
|
|
66
|
+
export function createVersionCommand(
|
|
67
|
+
appName: string,
|
|
68
|
+
appVersion: string,
|
|
69
|
+
commitHash?: string
|
|
70
|
+
): VersionCommand {
|
|
71
|
+
return new VersionCommand({ appName, appVersion, commitHash });
|
|
72
|
+
}
|
package/src/cli/help.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// ANSI escape codes
|
|
2
|
+
const RESET = "\x1b[0m";
|
|
3
|
+
const BOLD = "\x1b[1m";
|
|
4
|
+
const DIM = "\x1b[2m";
|
|
5
|
+
const ITALIC = "\x1b[3m";
|
|
6
|
+
const UNDERLINE = "\x1b[4m";
|
|
7
|
+
const STRIKETHROUGH = "\x1b[9m";
|
|
8
|
+
|
|
9
|
+
const RED = "\x1b[31m";
|
|
10
|
+
const GREEN = "\x1b[32m";
|
|
11
|
+
const YELLOW = "\x1b[33m";
|
|
12
|
+
const BLUE = "\x1b[34m";
|
|
13
|
+
const MAGENTA = "\x1b[35m";
|
|
14
|
+
const CYAN = "\x1b[36m";
|
|
15
|
+
const WHITE = "\x1b[37m";
|
|
16
|
+
const GRAY = "\x1b[90m";
|
|
17
|
+
|
|
18
|
+
const BG_RED = "\x1b[41m";
|
|
19
|
+
const BG_GREEN = "\x1b[42m";
|
|
20
|
+
const BG_YELLOW = "\x1b[43m";
|
|
21
|
+
const BG_BLUE = "\x1b[44m";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check if terminal supports colors
|
|
25
|
+
*/
|
|
26
|
+
export function supportsColors(): boolean {
|
|
27
|
+
if (typeof process === "undefined") return false;
|
|
28
|
+
if (process.env["NO_COLOR"]) return false;
|
|
29
|
+
if (process.env["FORCE_COLOR"]) return true;
|
|
30
|
+
if (process.stdout?.isTTY) return true;
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function wrap(code: string, text: string): string {
|
|
35
|
+
if (!supportsColors()) return text;
|
|
36
|
+
return `${code}${text}${RESET}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Color utilities for terminal output
|
|
41
|
+
*/
|
|
42
|
+
export const colors = {
|
|
43
|
+
// Basic colors
|
|
44
|
+
red: (text: string) => wrap(RED, text),
|
|
45
|
+
green: (text: string) => wrap(GREEN, text),
|
|
46
|
+
yellow: (text: string) => wrap(YELLOW, text),
|
|
47
|
+
blue: (text: string) => wrap(BLUE, text),
|
|
48
|
+
magenta: (text: string) => wrap(MAGENTA, text),
|
|
49
|
+
cyan: (text: string) => wrap(CYAN, text),
|
|
50
|
+
white: (text: string) => wrap(WHITE, text),
|
|
51
|
+
gray: (text: string) => wrap(GRAY, text),
|
|
52
|
+
|
|
53
|
+
// Styles
|
|
54
|
+
bold: (text: string) => wrap(BOLD, text),
|
|
55
|
+
dim: (text: string) => wrap(DIM, text),
|
|
56
|
+
italic: (text: string) => wrap(ITALIC, text),
|
|
57
|
+
underline: (text: string) => wrap(UNDERLINE, text),
|
|
58
|
+
strikethrough: (text: string) => wrap(STRIKETHROUGH, text),
|
|
59
|
+
|
|
60
|
+
// Background colors
|
|
61
|
+
bgRed: (text: string) => wrap(BG_RED, text),
|
|
62
|
+
bgGreen: (text: string) => wrap(BG_GREEN, text),
|
|
63
|
+
bgYellow: (text: string) => wrap(BG_YELLOW, text),
|
|
64
|
+
bgBlue: (text: string) => wrap(BG_BLUE, text),
|
|
65
|
+
|
|
66
|
+
// Semantic colors
|
|
67
|
+
success: (text: string) => wrap(GREEN, `✓ ${text}`),
|
|
68
|
+
error: (text: string) => wrap(RED, `✗ ${text}`),
|
|
69
|
+
warning: (text: string) => wrap(YELLOW, `⚠ ${text}`),
|
|
70
|
+
info: (text: string) => wrap(BLUE, `ℹ ${text}`),
|
|
71
|
+
|
|
72
|
+
// Reset
|
|
73
|
+
reset: RESET,
|
|
74
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
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
|
+
}
|