@textui/cli 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/LICENSE +21 -0
- package/README.md +58 -0
- package/dist/app.d.ts +114 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +372 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +4 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +16 -0
- package/dist/commands.d.ts +11 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +428 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/registry.d.ts +81 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +226 -0
- package/package.json +61 -0
- package/src/app.ts +486 -0
- package/src/bin.ts +5 -0
- package/src/cli.ts +18 -0
- package/src/commands.ts +502 -0
- package/src/index.ts +4 -0
- package/src/registry.ts +321 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Softov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# @textui/cli
|
|
2
|
+
|
|
3
|
+
`textui` - set a project up, copy components into it, and find out what the
|
|
4
|
+
terminal in front of you can actually do.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npx @textui/cli init
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Components are copied, not imported
|
|
11
|
+
|
|
12
|
+
`@textui/widgets` is a dependency you install. The registry behind `textui add`
|
|
13
|
+
is the opposite: it writes the source into your project and you own it from
|
|
14
|
+
there, edits included.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
textui add service-table # writes src/ui/service-table.tsx
|
|
18
|
+
textui create dashboard # writes a whole screen
|
|
19
|
+
textui diff # what you have edited since it was copied
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`diff` matters because owning the source means upstream fixes do not reach you.
|
|
23
|
+
It tells you which files have drifted, so an update is a decision rather than a
|
|
24
|
+
surprise.
|
|
25
|
+
|
|
26
|
+
## Commands
|
|
27
|
+
|
|
28
|
+
| | |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `init` | Set up TextUI in this project |
|
|
31
|
+
| `add` | Copy components into your project |
|
|
32
|
+
| `create` | Scaffold a template into your project |
|
|
33
|
+
| `list` | Registry components, templates and themes |
|
|
34
|
+
| `theme` | List themes, or preview one |
|
|
35
|
+
| `registry` | Manage component registries |
|
|
36
|
+
| `diff` | Which copied components have drifted from upstream |
|
|
37
|
+
| `doctor` | What this terminal can do, and what the project looks like |
|
|
38
|
+
|
|
39
|
+
`doctor` is the one to run first when something renders wrong: it reports the
|
|
40
|
+
unicode level, colour depth, mouse and keyboard protocol actually detected,
|
|
41
|
+
which is usually the answer.
|
|
42
|
+
|
|
43
|
+
## Building your own
|
|
44
|
+
|
|
45
|
+
`createCli()` is exported, and `@textui/cli/app` is the primitives under it -
|
|
46
|
+
commands, options, help and prompts. Deliberately small: the point is that an
|
|
47
|
+
application built on TextUI can combine plain commands with interactive screens
|
|
48
|
+
without a second framework and a second idea of what an argument is.
|
|
49
|
+
|
|
50
|
+
## Runtime
|
|
51
|
+
|
|
52
|
+
Depends on `@textui/core` and `@textui/terminal`. Uses `node:fs`, `node:path`
|
|
53
|
+
and `node:crypto` - it is the one library package here that touches a disk.
|
|
54
|
+
Node 22+ and Bun.
|
|
55
|
+
|
|
56
|
+
## Documentation
|
|
57
|
+
|
|
58
|
+
<https://softov.github.io/textui/>
|
package/dist/app.d.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { TerminalInput, TerminalOutput } from '@textui/terminal';
|
|
2
|
+
/**
|
|
3
|
+
* CLI application primitives.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately small: commands, options, help, and prompts. The point is not
|
|
6
|
+
* to replace a full CLI ecosystem - it is that an application built on TextUI
|
|
7
|
+
* should be able to combine plain commands with interactive screens without
|
|
8
|
+
* reaching for a second framework and a second idea of what an argument is.
|
|
9
|
+
*/
|
|
10
|
+
export type OptionType = 'string' | 'number' | 'boolean';
|
|
11
|
+
export interface OptionSpec {
|
|
12
|
+
name: string;
|
|
13
|
+
type?: OptionType;
|
|
14
|
+
/** Single-letter alias: `-w`. */
|
|
15
|
+
short?: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
default?: unknown;
|
|
18
|
+
required?: boolean;
|
|
19
|
+
/** Accept the flag more than once, collecting values. */
|
|
20
|
+
multiple?: boolean;
|
|
21
|
+
choices?: string[];
|
|
22
|
+
}
|
|
23
|
+
export interface ArgumentSpec {
|
|
24
|
+
name: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
required?: boolean;
|
|
27
|
+
/** Swallow the rest of the command line. */
|
|
28
|
+
variadic?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface ParsedArgs {
|
|
31
|
+
options: Record<string, unknown>;
|
|
32
|
+
positionals: string[];
|
|
33
|
+
/** Everything after a bare `--`. */
|
|
34
|
+
rest: string[];
|
|
35
|
+
}
|
|
36
|
+
export interface CliCommand {
|
|
37
|
+
name: string;
|
|
38
|
+
description?: string;
|
|
39
|
+
aliases?: string[];
|
|
40
|
+
arguments?: ArgumentSpec[];
|
|
41
|
+
options?: OptionSpec[];
|
|
42
|
+
/** Subcommands. A command with children may still have a handler. */
|
|
43
|
+
commands?: CliCommand[];
|
|
44
|
+
examples?: string[];
|
|
45
|
+
hidden?: boolean;
|
|
46
|
+
run?(args: ParsedArgs, cli: Cli): Promise<number | void> | number | void;
|
|
47
|
+
}
|
|
48
|
+
export interface CliOptions {
|
|
49
|
+
name: string;
|
|
50
|
+
version?: string;
|
|
51
|
+
description?: string;
|
|
52
|
+
commands: CliCommand[];
|
|
53
|
+
/** Options accepted before the command name. */
|
|
54
|
+
globalOptions?: OptionSpec[];
|
|
55
|
+
/** Where output goes. Swappable so tests can capture it. */
|
|
56
|
+
write?(text: string): void;
|
|
57
|
+
writeError?(text: string): void;
|
|
58
|
+
}
|
|
59
|
+
export declare class CliError extends Error {
|
|
60
|
+
readonly exitCode: number;
|
|
61
|
+
constructor(message: string, exitCode?: number);
|
|
62
|
+
}
|
|
63
|
+
export declare class Cli {
|
|
64
|
+
private options;
|
|
65
|
+
readonly name: string;
|
|
66
|
+
readonly version: string;
|
|
67
|
+
constructor(options: CliOptions);
|
|
68
|
+
write(text: string): void;
|
|
69
|
+
writeError(text: string): void;
|
|
70
|
+
/** Run and return an exit code. Never throws for a user-facing problem. */
|
|
71
|
+
run(argv: string[]): Promise<number>;
|
|
72
|
+
private dispatch;
|
|
73
|
+
help(path?: CliCommand[]): string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Parse a command line.
|
|
77
|
+
*
|
|
78
|
+
* Supports `--flag`, `--no-flag`, `--key value`, `--key=value`, `-k value`,
|
|
79
|
+
* bundled short flags `-abc`, and `--` to stop parsing. Unknown options are an
|
|
80
|
+
* error rather than a positional, because a typo silently becoming a filename
|
|
81
|
+
* is how a CLI deletes the wrong thing.
|
|
82
|
+
*/
|
|
83
|
+
export declare function parseArgs(argv: string[], specs: OptionSpec[]): ParsedArgs;
|
|
84
|
+
export interface PromptOptions {
|
|
85
|
+
message: string;
|
|
86
|
+
default?: string;
|
|
87
|
+
/** Hide what is typed. */
|
|
88
|
+
mask?: boolean;
|
|
89
|
+
validate?(value: string): string | null;
|
|
90
|
+
input?: TerminalInput;
|
|
91
|
+
output?: TerminalOutput;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* A line prompt for non-interactive stretches of a CLI.
|
|
95
|
+
*
|
|
96
|
+
* This is deliberately not a TUI: mixing a full-screen renderer into a prompt
|
|
97
|
+
* means a resize or a stray escape sequence can eat the answer. When the input
|
|
98
|
+
* is not a terminal it returns the default rather than hanging, which is what
|
|
99
|
+
* makes a CLI usable from a script.
|
|
100
|
+
*/
|
|
101
|
+
export declare function promptLine(options: PromptOptions): Promise<string>;
|
|
102
|
+
export declare function promptConfirm(message: string, options?: {
|
|
103
|
+
default?: boolean;
|
|
104
|
+
input?: TerminalInput;
|
|
105
|
+
output?: TerminalOutput;
|
|
106
|
+
}): Promise<boolean>;
|
|
107
|
+
export declare function promptSelect(message: string, choices: {
|
|
108
|
+
value: string;
|
|
109
|
+
label: string;
|
|
110
|
+
}[], options?: {
|
|
111
|
+
input?: TerminalInput;
|
|
112
|
+
output?: TerminalOutput;
|
|
113
|
+
}): Promise<string>;
|
|
114
|
+
//# sourceMappingURL=app.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
|
package/dist/app.js
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { stringWidth } from '@textui/core';
|
|
2
|
+
export class CliError extends Error {
|
|
3
|
+
exitCode;
|
|
4
|
+
constructor(message, exitCode = 1) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.exitCode = exitCode;
|
|
7
|
+
this.name = 'CliError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
const HELP_OPTION = { name: 'help', short: 'h', type: 'boolean', description: 'Show this help' };
|
|
11
|
+
const VERSION_OPTION = { name: 'version', short: 'v', type: 'boolean', description: 'Print the version' };
|
|
12
|
+
export class Cli {
|
|
13
|
+
options;
|
|
14
|
+
name;
|
|
15
|
+
version;
|
|
16
|
+
constructor(options) {
|
|
17
|
+
this.options = options;
|
|
18
|
+
this.name = options.name;
|
|
19
|
+
this.version = options.version ?? '0.0.0';
|
|
20
|
+
}
|
|
21
|
+
write(text) {
|
|
22
|
+
(this.options.write ?? ((t) => process.stdout.write(t)))(text);
|
|
23
|
+
}
|
|
24
|
+
writeError(text) {
|
|
25
|
+
(this.options.writeError ?? ((t) => process.stderr.write(t)))(text);
|
|
26
|
+
}
|
|
27
|
+
/** Run and return an exit code. Never throws for a user-facing problem. */
|
|
28
|
+
async run(argv) {
|
|
29
|
+
try {
|
|
30
|
+
return await this.dispatch(argv);
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
if (err instanceof CliError) {
|
|
34
|
+
this.writeError(`${this.name}: ${err.message}\n`);
|
|
35
|
+
return err.exitCode;
|
|
36
|
+
}
|
|
37
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
38
|
+
this.writeError(`${this.name}: ${message}\n`);
|
|
39
|
+
return 1;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async dispatch(argv) {
|
|
43
|
+
const path = [];
|
|
44
|
+
let commands = this.options.commands;
|
|
45
|
+
let index = 0;
|
|
46
|
+
while (index < argv.length) {
|
|
47
|
+
const token = argv[index];
|
|
48
|
+
if (token.startsWith('-'))
|
|
49
|
+
break;
|
|
50
|
+
const found = commands.find((c) => c.name === token || (c.aliases ?? []).includes(token));
|
|
51
|
+
if (!found)
|
|
52
|
+
break;
|
|
53
|
+
path.push(found);
|
|
54
|
+
commands = found.commands ?? [];
|
|
55
|
+
index++;
|
|
56
|
+
}
|
|
57
|
+
const command = path[path.length - 1];
|
|
58
|
+
const rest = argv.slice(index);
|
|
59
|
+
// Help and version are answered before anything is validated, because a
|
|
60
|
+
// user asking for help has by definition not got the arguments right yet.
|
|
61
|
+
if (rest.includes('--help') || rest.includes('-h')) {
|
|
62
|
+
this.write(this.help(path));
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
if (path.length === 0 && (rest.includes('--version') || rest.includes('-v'))) {
|
|
66
|
+
this.write(`${this.version}\n`);
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
if (!command) {
|
|
70
|
+
this.write(this.help([]));
|
|
71
|
+
return argv.length === 0 ? 0 : 1;
|
|
72
|
+
}
|
|
73
|
+
if (!command.run) {
|
|
74
|
+
this.write(this.help(path));
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
const specs = [
|
|
78
|
+
...(this.options.globalOptions ?? []),
|
|
79
|
+
...(command.options ?? []),
|
|
80
|
+
HELP_OPTION,
|
|
81
|
+
];
|
|
82
|
+
const parsed = parseArgs(rest, specs);
|
|
83
|
+
validate(command, parsed);
|
|
84
|
+
const code = await command.run(parsed, this);
|
|
85
|
+
return typeof code === 'number' ? code : 0;
|
|
86
|
+
}
|
|
87
|
+
help(path = []) {
|
|
88
|
+
const command = path[path.length - 1];
|
|
89
|
+
const lines = [];
|
|
90
|
+
if (!command) {
|
|
91
|
+
lines.push(this.options.description ?? `${this.name} ${this.version}`);
|
|
92
|
+
lines.push('');
|
|
93
|
+
lines.push(`Usage: ${this.name} <command> [options]`);
|
|
94
|
+
lines.push('');
|
|
95
|
+
lines.push('Commands:');
|
|
96
|
+
lines.push(...describeCommands(this.options.commands));
|
|
97
|
+
if (this.options.globalOptions?.length) {
|
|
98
|
+
lines.push('');
|
|
99
|
+
lines.push('Options:');
|
|
100
|
+
lines.push(...describeOptions([...this.options.globalOptions, HELP_OPTION, VERSION_OPTION]));
|
|
101
|
+
}
|
|
102
|
+
lines.push('');
|
|
103
|
+
lines.push(`Run \`${this.name} <command> --help\` for details.`);
|
|
104
|
+
return lines.join('\n') + '\n';
|
|
105
|
+
}
|
|
106
|
+
const names = path.map((c) => c.name).join(' ');
|
|
107
|
+
const args = (command.arguments ?? [])
|
|
108
|
+
.map((a) => (a.required ? `<${a.name}${a.variadic ? '...' : ''}>` : `[${a.name}${a.variadic ? '...' : ''}]`))
|
|
109
|
+
.join(' ');
|
|
110
|
+
if (command.description) {
|
|
111
|
+
lines.push(command.description);
|
|
112
|
+
lines.push('');
|
|
113
|
+
}
|
|
114
|
+
lines.push(`Usage: ${this.name} ${names}${args ? ` ${args}` : ''} [options]`);
|
|
115
|
+
if (command.arguments?.length) {
|
|
116
|
+
lines.push('');
|
|
117
|
+
lines.push('Arguments:');
|
|
118
|
+
const width = Math.max(...command.arguments.map((a) => stringWidth(a.name)));
|
|
119
|
+
for (const arg of command.arguments) {
|
|
120
|
+
lines.push(` ${arg.name.padEnd(width)} ${arg.description ?? ''}`.trimEnd());
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (command.commands?.length) {
|
|
124
|
+
lines.push('');
|
|
125
|
+
lines.push('Commands:');
|
|
126
|
+
lines.push(...describeCommands(command.commands));
|
|
127
|
+
}
|
|
128
|
+
const options = [...(command.options ?? []), HELP_OPTION];
|
|
129
|
+
lines.push('');
|
|
130
|
+
lines.push('Options:');
|
|
131
|
+
lines.push(...describeOptions(options));
|
|
132
|
+
if (command.examples?.length) {
|
|
133
|
+
lines.push('');
|
|
134
|
+
lines.push('Examples:');
|
|
135
|
+
for (const example of command.examples)
|
|
136
|
+
lines.push(` ${example}`);
|
|
137
|
+
}
|
|
138
|
+
return lines.join('\n') + '\n';
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function describeCommands(commands) {
|
|
142
|
+
const visible = commands.filter((c) => !c.hidden);
|
|
143
|
+
if (visible.length === 0)
|
|
144
|
+
return [' (none)'];
|
|
145
|
+
const width = Math.max(...visible.map((c) => stringWidth(c.name)));
|
|
146
|
+
return visible.map((c) => ` ${c.name.padEnd(width)} ${c.description ?? ''}`.trimEnd());
|
|
147
|
+
}
|
|
148
|
+
function describeOptions(options) {
|
|
149
|
+
const labels = options.map((o) => {
|
|
150
|
+
const short = o.short ? `-${o.short}, ` : ' ';
|
|
151
|
+
const value = o.type === 'boolean' || o.type === undefined ? '' : ` <${o.type}>`;
|
|
152
|
+
return `${short}--${o.name}${value}`;
|
|
153
|
+
});
|
|
154
|
+
const width = Math.max(...labels.map(stringWidth));
|
|
155
|
+
return options.map((o, i) => {
|
|
156
|
+
const extra = [];
|
|
157
|
+
if (o.choices)
|
|
158
|
+
extra.push(`(${o.choices.join(' | ')})`);
|
|
159
|
+
if (o.default !== undefined)
|
|
160
|
+
extra.push(`[default: ${String(o.default)}]`);
|
|
161
|
+
const suffix = extra.length ? ` ${extra.join(' ')}` : '';
|
|
162
|
+
return ` ${labels[i].padEnd(width)} ${o.description ?? ''}${suffix}`.trimEnd();
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Parse a command line.
|
|
167
|
+
*
|
|
168
|
+
* Supports `--flag`, `--no-flag`, `--key value`, `--key=value`, `-k value`,
|
|
169
|
+
* bundled short flags `-abc`, and `--` to stop parsing. Unknown options are an
|
|
170
|
+
* error rather than a positional, because a typo silently becoming a filename
|
|
171
|
+
* is how a CLI deletes the wrong thing.
|
|
172
|
+
*/
|
|
173
|
+
export function parseArgs(argv, specs) {
|
|
174
|
+
const byName = new Map(specs.map((s) => [s.name, s]));
|
|
175
|
+
const byShort = new Map(specs.filter((s) => s.short).map((s) => [s.short, s]));
|
|
176
|
+
const options = {};
|
|
177
|
+
const positionals = [];
|
|
178
|
+
const rest = [];
|
|
179
|
+
for (const spec of specs) {
|
|
180
|
+
if (spec.default !== undefined)
|
|
181
|
+
options[spec.name] = spec.default;
|
|
182
|
+
else if (spec.type === 'boolean')
|
|
183
|
+
options[spec.name] = false;
|
|
184
|
+
}
|
|
185
|
+
const assign = (spec, raw) => {
|
|
186
|
+
const value = coerce(spec, raw);
|
|
187
|
+
if (spec.choices && typeof value === 'string' && !spec.choices.includes(value)) {
|
|
188
|
+
throw new CliError(`--${spec.name} must be one of: ${spec.choices.join(', ')} (got "${value}")`);
|
|
189
|
+
}
|
|
190
|
+
if (spec.multiple) {
|
|
191
|
+
const list = Array.isArray(options[spec.name]) ? options[spec.name] : [];
|
|
192
|
+
options[spec.name] = [...list, value];
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
options[spec.name] = value;
|
|
196
|
+
};
|
|
197
|
+
let i = 0;
|
|
198
|
+
while (i < argv.length) {
|
|
199
|
+
const token = argv[i];
|
|
200
|
+
if (token === '--') {
|
|
201
|
+
rest.push(...argv.slice(i + 1));
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
if (token.startsWith('--')) {
|
|
205
|
+
const body = token.slice(2);
|
|
206
|
+
const eq = body.indexOf('=');
|
|
207
|
+
const name = eq === -1 ? body : body.slice(0, eq);
|
|
208
|
+
const inline = eq === -1 ? undefined : body.slice(eq + 1);
|
|
209
|
+
if (name.startsWith('no-') && byName.has(name.slice(3))) {
|
|
210
|
+
assign(byName.get(name.slice(3)), false);
|
|
211
|
+
i++;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const spec = byName.get(name);
|
|
215
|
+
if (!spec)
|
|
216
|
+
throw new CliError(`unknown option --${name}`);
|
|
217
|
+
if (spec.type === 'boolean' || spec.type === undefined) {
|
|
218
|
+
assign(spec, inline === undefined ? true : inline !== 'false');
|
|
219
|
+
i++;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const value = inline ?? argv[i + 1];
|
|
223
|
+
if (value === undefined)
|
|
224
|
+
throw new CliError(`--${name} needs a value`);
|
|
225
|
+
assign(spec, value);
|
|
226
|
+
i += inline === undefined ? 2 : 1;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (token.startsWith('-') && token.length > 1) {
|
|
230
|
+
const letters = [...token.slice(1)];
|
|
231
|
+
for (let j = 0; j < letters.length; j++) {
|
|
232
|
+
const letter = letters[j];
|
|
233
|
+
const spec = byShort.get(letter);
|
|
234
|
+
if (!spec)
|
|
235
|
+
throw new CliError(`unknown option -${letter}`);
|
|
236
|
+
if (spec.type === 'boolean' || spec.type === undefined) {
|
|
237
|
+
assign(spec, true);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
// A value-taking short flag consumes the rest of the bundle, or the
|
|
241
|
+
// next argument: `-w80` and `-w 80` both work.
|
|
242
|
+
const inline = letters.slice(j + 1).join('');
|
|
243
|
+
const value = inline !== '' ? inline : argv[++i];
|
|
244
|
+
if (value === undefined)
|
|
245
|
+
throw new CliError(`-${letter} needs a value`);
|
|
246
|
+
assign(spec, value);
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
i++;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
positionals.push(token);
|
|
253
|
+
i++;
|
|
254
|
+
}
|
|
255
|
+
return { options, positionals, rest };
|
|
256
|
+
}
|
|
257
|
+
function coerce(spec, raw) {
|
|
258
|
+
if (typeof raw === 'boolean')
|
|
259
|
+
return raw;
|
|
260
|
+
if (spec.type === 'number') {
|
|
261
|
+
const n = Number(raw);
|
|
262
|
+
if (Number.isNaN(n))
|
|
263
|
+
throw new CliError(`--${spec.name} must be a number (got "${raw}")`);
|
|
264
|
+
return n;
|
|
265
|
+
}
|
|
266
|
+
if (spec.type === 'boolean')
|
|
267
|
+
return raw !== 'false';
|
|
268
|
+
return raw;
|
|
269
|
+
}
|
|
270
|
+
function validate(command, parsed) {
|
|
271
|
+
for (const spec of command.options ?? []) {
|
|
272
|
+
if (spec.required && parsed.options[spec.name] === undefined) {
|
|
273
|
+
throw new CliError(`--${spec.name} is required`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const required = (command.arguments ?? []).filter((a) => a.required);
|
|
277
|
+
if (parsed.positionals.length < required.length) {
|
|
278
|
+
const missing = required.slice(parsed.positionals.length).map((a) => a.name);
|
|
279
|
+
throw new CliError(`missing argument: ${missing.join(', ')}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* A line prompt for non-interactive stretches of a CLI.
|
|
284
|
+
*
|
|
285
|
+
* This is deliberately not a TUI: mixing a full-screen renderer into a prompt
|
|
286
|
+
* means a resize or a stray escape sequence can eat the answer. When the input
|
|
287
|
+
* is not a terminal it returns the default rather than hanging, which is what
|
|
288
|
+
* makes a CLI usable from a script.
|
|
289
|
+
*/
|
|
290
|
+
export async function promptLine(options) {
|
|
291
|
+
const input = options.input ?? process.stdin;
|
|
292
|
+
const output = options.output ?? process.stdout;
|
|
293
|
+
const suffix = options.default ? ` (${options.default})` : '';
|
|
294
|
+
if (!input.isTTY)
|
|
295
|
+
return options.default ?? '';
|
|
296
|
+
for (;;) {
|
|
297
|
+
output.write(`${options.message}${suffix}: `);
|
|
298
|
+
const answer = await readLine(input, options.mask ? output : undefined);
|
|
299
|
+
const value = answer.trim() === '' ? options.default ?? '' : answer.trim();
|
|
300
|
+
const problem = options.validate?.(value);
|
|
301
|
+
if (!problem) {
|
|
302
|
+
if (options.mask)
|
|
303
|
+
output.write('\n');
|
|
304
|
+
return value;
|
|
305
|
+
}
|
|
306
|
+
output.write(` ${problem}\n`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
export async function promptConfirm(message, options = {}) {
|
|
310
|
+
const fallback = options.default ?? false;
|
|
311
|
+
const answer = await promptLine({
|
|
312
|
+
message: `${message} ${fallback ? '[Y/n]' : '[y/N]'}`,
|
|
313
|
+
default: fallback ? 'y' : 'n',
|
|
314
|
+
input: options.input,
|
|
315
|
+
output: options.output,
|
|
316
|
+
});
|
|
317
|
+
return /^y(es)?$/i.test(answer);
|
|
318
|
+
}
|
|
319
|
+
export async function promptSelect(message, choices, options = {}) {
|
|
320
|
+
const output = options.output ?? process.stdout;
|
|
321
|
+
output.write(`${message}\n`);
|
|
322
|
+
choices.forEach((choice, i) => output.write(` ${i + 1}) ${choice.label}\n`));
|
|
323
|
+
const answer = await promptLine({
|
|
324
|
+
message: `Choose 1-${choices.length}`,
|
|
325
|
+
default: '1',
|
|
326
|
+
input: options.input,
|
|
327
|
+
output,
|
|
328
|
+
validate: (value) => {
|
|
329
|
+
const n = Number(value);
|
|
330
|
+
return Number.isInteger(n) && n >= 1 && n <= choices.length ? null : 'Out of range';
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
return choices[Number(answer) - 1].value;
|
|
334
|
+
}
|
|
335
|
+
const CTRL_C = String.fromCharCode(3);
|
|
336
|
+
const BACKSPACE = String.fromCharCode(8);
|
|
337
|
+
const DEL = String.fromCharCode(127);
|
|
338
|
+
function readLine(input, maskTo) {
|
|
339
|
+
return new Promise((resolve) => {
|
|
340
|
+
let buffer = '';
|
|
341
|
+
const wasRaw = input.isRaw;
|
|
342
|
+
const finish = () => {
|
|
343
|
+
input.off('data', onData);
|
|
344
|
+
if (maskTo && wasRaw === false && input.setRawMode)
|
|
345
|
+
input.setRawMode(false);
|
|
346
|
+
input.pause();
|
|
347
|
+
resolve(buffer);
|
|
348
|
+
};
|
|
349
|
+
const onData = (chunk) => {
|
|
350
|
+
const text = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
|
|
351
|
+
for (const char of text) {
|
|
352
|
+
if (char === '\n' || char === '\r')
|
|
353
|
+
return finish();
|
|
354
|
+
// ctrl+c during a prompt has to exit, not be typed into the answer.
|
|
355
|
+
if (char === CTRL_C)
|
|
356
|
+
process.exit(130);
|
|
357
|
+
if (char === DEL || char === BACKSPACE) {
|
|
358
|
+
buffer = buffer.slice(0, -1);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
buffer += char;
|
|
362
|
+
if (maskTo)
|
|
363
|
+
maskTo.write('*');
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
if (maskTo && input.setRawMode)
|
|
367
|
+
input.setRawMode(true);
|
|
368
|
+
input.setEncoding('utf8');
|
|
369
|
+
input.resume();
|
|
370
|
+
input.on('data', onData);
|
|
371
|
+
});
|
|
372
|
+
}
|
package/dist/bin.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":""}
|
package/dist/bin.js
ADDED
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAG/B,eAAO,MAAM,OAAO,UAAU,CAAC;AAE/B,wBAAgB,SAAS,CAAC,OAAO,GAAE;IAAE,KAAK,CAAC,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,UAAU,CAAC,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAO,GAAG,GAAG,CAYtG"}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Cli } from './app.js';
|
|
2
|
+
import { COMMANDS } from './commands.js';
|
|
3
|
+
export const VERSION = '0.1.0';
|
|
4
|
+
export function createCli(options = {}) {
|
|
5
|
+
return new Cli({
|
|
6
|
+
name: 'textui',
|
|
7
|
+
version: VERSION,
|
|
8
|
+
description: 'TextUI - build terminal interfaces you own.',
|
|
9
|
+
commands: COMMANDS,
|
|
10
|
+
globalOptions: [
|
|
11
|
+
{ name: 'cwd', type: 'string', description: 'Run as if from this directory' },
|
|
12
|
+
],
|
|
13
|
+
write: options.write,
|
|
14
|
+
writeError: options.writeError,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CliCommand } from './app.js';
|
|
2
|
+
export declare const initCommand: CliCommand;
|
|
3
|
+
export declare const listCommand: CliCommand;
|
|
4
|
+
export declare const addCommand: CliCommand;
|
|
5
|
+
export declare const createCommand: CliCommand;
|
|
6
|
+
export declare const themeCommand: CliCommand;
|
|
7
|
+
export declare const registryCommand: CliCommand;
|
|
8
|
+
export declare const diffCommand: CliCommand;
|
|
9
|
+
export declare const doctorCommand: CliCommand;
|
|
10
|
+
export declare const COMMANDS: CliCommand[];
|
|
11
|
+
//# sourceMappingURL=commands.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAwB3C,eAAO,MAAM,WAAW,EAAE,UA+CzB,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,UAwDzB,CAAC;AAEF,eAAO,MAAM,UAAU,EAAE,UAqExB,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,UA2D3B,CAAC;AAEF,eAAO,MAAM,YAAY,EAAE,UA2C1B,CAAC;AAyCF,eAAO,MAAM,eAAe,EAAE,UAqC7B,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,UAsBzB,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,UA2D3B,CAAC;AAiBF,eAAO,MAAM,QAAQ,EAAE,UAAU,EAShC,CAAC"}
|