@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 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,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=bin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":""}
package/dist/bin.js ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { createCli } from './cli.js';
3
+ const code = await createCli().run(process.argv.slice(2));
4
+ process.exit(code);
package/dist/cli.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { Cli } from './app.js';
2
+ export declare const VERSION = "0.1.0";
3
+ export declare function createCli(options?: {
4
+ write?(t: string): void;
5
+ writeError?(t: string): void;
6
+ }): Cli;
7
+ //# sourceMappingURL=cli.d.ts.map
@@ -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"}