@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/dist/commands.js
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import { mkdir, writeFile, readFile } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { detectCapabilities, describeEnvironment } from '@textui/terminal';
|
|
5
|
+
import { BUILTIN_THEMES, renderToString } from '@textui/core';
|
|
6
|
+
import { CATALOG } from '@textui/widgets';
|
|
7
|
+
import { CliError, promptConfirm } from './app.js';
|
|
8
|
+
import { DEFAULT_CONFIG, copyComponent, diffInstalled, findProjectRoot, loadRegistry, readConfig, readReceipt, resolveDependencies, writeConfig, writeReceipt, } from './registry.js';
|
|
9
|
+
/**
|
|
10
|
+
* The developer CLI.
|
|
11
|
+
*
|
|
12
|
+
* Every command that writes to a project says what it is going to write before
|
|
13
|
+
* it writes it, and refuses to overwrite a file the user has edited. That
|
|
14
|
+
* refusal is the whole reason the receipt file exists.
|
|
15
|
+
*/
|
|
16
|
+
async function requireProject() {
|
|
17
|
+
const root = await findProjectRoot();
|
|
18
|
+
if (!root) {
|
|
19
|
+
throw new CliError('no textui.config.json found. Run `textui init` first.');
|
|
20
|
+
}
|
|
21
|
+
return { root, config: await readConfig(root) };
|
|
22
|
+
}
|
|
23
|
+
export const initCommand = {
|
|
24
|
+
name: 'init',
|
|
25
|
+
description: 'Set up TextUI in this project',
|
|
26
|
+
options: [
|
|
27
|
+
{ name: 'components', type: 'string', description: 'Where components are copied', default: DEFAULT_CONFIG.componentsDir },
|
|
28
|
+
{ name: 'templates', type: 'string', description: 'Where screens are copied', default: DEFAULT_CONFIG.templatesDir },
|
|
29
|
+
{ name: 'theme', type: 'string', description: 'Default theme', default: 'dark', choices: BUILTIN_THEMES.map((t) => t.id) },
|
|
30
|
+
{ name: 'shell', type: 'string', description: 'Default shell', default: 'plain', choices: ['plain', 'console', 'paper', 'workbench'] },
|
|
31
|
+
{ name: 'yes', short: 'y', type: 'boolean', description: 'Accept the defaults' },
|
|
32
|
+
],
|
|
33
|
+
examples: ['textui init', 'textui init --shell workbench --theme workbench'],
|
|
34
|
+
async run(args, cli) {
|
|
35
|
+
const root = process.cwd();
|
|
36
|
+
const existing = await findProjectRoot(root);
|
|
37
|
+
if (existing === root) {
|
|
38
|
+
throw new CliError('this project is already initialised');
|
|
39
|
+
}
|
|
40
|
+
const config = {
|
|
41
|
+
...DEFAULT_CONFIG,
|
|
42
|
+
componentsDir: String(args.options.components),
|
|
43
|
+
templatesDir: String(args.options.templates),
|
|
44
|
+
theme: String(args.options.theme),
|
|
45
|
+
shell: String(args.options.shell),
|
|
46
|
+
};
|
|
47
|
+
cli.write('This will create:\n');
|
|
48
|
+
cli.write(` textui.config.json\n`);
|
|
49
|
+
cli.write(` ${config.componentsDir}/\n`);
|
|
50
|
+
cli.write(` ${config.templatesDir}/\n\n`);
|
|
51
|
+
if (!args.options.yes && !(await promptConfirm('Continue?', { default: true }))) {
|
|
52
|
+
cli.write('Nothing was written.\n');
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
const path = await writeConfig(root, config);
|
|
56
|
+
await mkdir(join(root, config.componentsDir), { recursive: true });
|
|
57
|
+
await mkdir(join(root, config.templatesDir), { recursive: true });
|
|
58
|
+
cli.write(`\nWrote ${path}\n`);
|
|
59
|
+
cli.write('\nNext:\n');
|
|
60
|
+
cli.write(' textui list see what is available\n');
|
|
61
|
+
cli.write(' textui add table copy a component into your project\n');
|
|
62
|
+
cli.write(' textui create dashboard\n');
|
|
63
|
+
return 0;
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
export const listCommand = {
|
|
67
|
+
name: 'list',
|
|
68
|
+
aliases: ['ls'],
|
|
69
|
+
description: 'List registry components, templates and themes',
|
|
70
|
+
options: [
|
|
71
|
+
{ name: 'category', type: 'string', description: 'Filter by category' },
|
|
72
|
+
{ name: 'catalog', type: 'boolean', description: 'List the built-in component catalog instead' },
|
|
73
|
+
{ name: 'json', type: 'boolean', description: 'Machine-readable output' },
|
|
74
|
+
],
|
|
75
|
+
async run(args, cli) {
|
|
76
|
+
if (args.options.catalog) {
|
|
77
|
+
const category = args.options.category;
|
|
78
|
+
const items = CATALOG
|
|
79
|
+
.filter((c) => !category || c.category === category)
|
|
80
|
+
.map((c) => ({ name: c.component, category: c.category ?? '-', description: c.description ?? '' }));
|
|
81
|
+
if (args.options.json) {
|
|
82
|
+
cli.write(`${JSON.stringify(items, null, 2)}\n`);
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
cli.write(table(['COMPONENT', 'CATEGORY', 'DESCRIPTION'], items.map((i) => [i.name, i.category, i.description])));
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
const { manifest } = await loadRegistry();
|
|
89
|
+
if (args.options.json) {
|
|
90
|
+
cli.write(`${JSON.stringify(manifest, null, 2)}\n`);
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
cli.write(`${manifest.name} ${manifest.version}\n\n`);
|
|
94
|
+
cli.write('Components:\n');
|
|
95
|
+
cli.write(table(['NAME', 'CATEGORY', 'DEPENDS ON', 'DESCRIPTION'], manifest.components.map((c) => [
|
|
96
|
+
c.name, c.category ?? '-', (c.dependencies ?? []).join(', ') || '-', c.description ?? '',
|
|
97
|
+
])));
|
|
98
|
+
cli.write('\nTemplates:\n');
|
|
99
|
+
cli.write(table(['NAME', 'DESCRIPTION'], (manifest.templates ?? []).map((t) => [t.name, t.description ?? ''])));
|
|
100
|
+
cli.write('\nThemes:\n');
|
|
101
|
+
cli.write(table(['NAME', 'APPEARANCE'], (manifest.themes ?? []).map((t) => [t.name, t.appearance])));
|
|
102
|
+
return 0;
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
export const addCommand = {
|
|
106
|
+
name: 'add',
|
|
107
|
+
description: 'Copy components into your project',
|
|
108
|
+
arguments: [{ name: 'components', required: true, variadic: true, description: 'Component names' }],
|
|
109
|
+
options: [
|
|
110
|
+
{ name: 'force', short: 'f', type: 'boolean', description: 'Overwrite files you have edited' },
|
|
111
|
+
{ name: 'dry-run', type: 'boolean', description: 'Show what would happen' },
|
|
112
|
+
{ name: 'registry', type: 'string', description: 'Registry path or name' },
|
|
113
|
+
{ name: 'yes', short: 'y', type: 'boolean', description: 'Do not ask' },
|
|
114
|
+
],
|
|
115
|
+
examples: ['textui add service-table', 'textui add status-dot metric-card --dry-run'],
|
|
116
|
+
async run(args, cli) {
|
|
117
|
+
const { root, config } = await requireProject();
|
|
118
|
+
const registrySource = resolveRegistrySource(config, args.options.registry);
|
|
119
|
+
const { manifest, dir } = await loadRegistry(registrySource);
|
|
120
|
+
const requested = args.positionals;
|
|
121
|
+
const components = resolveDependencies(manifest, requested);
|
|
122
|
+
const receipt = await readReceipt(root);
|
|
123
|
+
const implied = components.filter((c) => !requested.includes(c.name));
|
|
124
|
+
if (implied.length > 0) {
|
|
125
|
+
cli.write(`Also adding, as dependencies: ${implied.map((c) => c.name).join(', ')}\n`);
|
|
126
|
+
}
|
|
127
|
+
cli.write('\nWill write:\n');
|
|
128
|
+
for (const component of components) {
|
|
129
|
+
for (const file of component.files) {
|
|
130
|
+
const target = file.target ?? join(config.componentsDir, file.path.split('/').pop());
|
|
131
|
+
cli.write(` ${target}${existsSync(join(root, target)) ? ' (exists)' : ''}\n`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
cli.write('\n');
|
|
135
|
+
if (!args.options['dry-run'] && !args.options.yes) {
|
|
136
|
+
if (!(await promptConfirm('Continue?', { default: true }))) {
|
|
137
|
+
cli.write('Nothing was written.\n');
|
|
138
|
+
return 1;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
let refused = 0;
|
|
142
|
+
for (const component of components) {
|
|
143
|
+
const result = await copyComponent(component, {
|
|
144
|
+
root,
|
|
145
|
+
registryDir: dir,
|
|
146
|
+
registryName: manifest.name,
|
|
147
|
+
config,
|
|
148
|
+
receipt,
|
|
149
|
+
force: Boolean(args.options.force),
|
|
150
|
+
dryRun: Boolean(args.options['dry-run']),
|
|
151
|
+
});
|
|
152
|
+
for (const path of result.written)
|
|
153
|
+
cli.write(` + ${path}\n`);
|
|
154
|
+
for (const skip of result.skipped) {
|
|
155
|
+
refused++;
|
|
156
|
+
cli.write(skip.reason === 'modified'
|
|
157
|
+
? ` ! ${skip.path} - you have edited this; left alone (use --force to replace)\n`
|
|
158
|
+
: ` = ${skip.path} - already present; left alone\n`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (!args.options['dry-run'])
|
|
162
|
+
await writeReceipt(root, receipt);
|
|
163
|
+
if (args.options['dry-run'])
|
|
164
|
+
cli.write('\nDry run: nothing was written.\n');
|
|
165
|
+
return refused > 0 ? 1 : 0;
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
export const createCommand = {
|
|
169
|
+
name: 'create',
|
|
170
|
+
description: 'Scaffold a template into your project',
|
|
171
|
+
arguments: [{ name: 'template', required: true, description: 'Template name' }],
|
|
172
|
+
options: [
|
|
173
|
+
{ name: 'name', type: 'string', description: 'File name to write' },
|
|
174
|
+
{ name: 'yes', short: 'y', type: 'boolean', description: 'Do not ask' },
|
|
175
|
+
],
|
|
176
|
+
examples: ['textui create dashboard', 'textui create logs --name log-screen'],
|
|
177
|
+
async run(args, cli) {
|
|
178
|
+
const { root, config } = await requireProject();
|
|
179
|
+
const { manifest, dir } = await loadRegistry();
|
|
180
|
+
const name = args.positionals[0];
|
|
181
|
+
const template = (manifest.templates ?? []).find((t) => t.name === name);
|
|
182
|
+
if (!template) {
|
|
183
|
+
const known = (manifest.templates ?? []).map((t) => t.name).join(', ');
|
|
184
|
+
throw new CliError(`unknown template "${name}". Available: ${known}`);
|
|
185
|
+
}
|
|
186
|
+
// A template is composed from public components, so pull in what it needs.
|
|
187
|
+
if (template.components.length > 0) {
|
|
188
|
+
const components = resolveDependencies(manifest, template.components);
|
|
189
|
+
const receipt = await readReceipt(root);
|
|
190
|
+
for (const component of components) {
|
|
191
|
+
const result = await copyComponent(component, {
|
|
192
|
+
root, registryDir: dir, registryName: manifest.name, config, receipt,
|
|
193
|
+
});
|
|
194
|
+
for (const path of result.written)
|
|
195
|
+
cli.write(` + ${path}\n`);
|
|
196
|
+
}
|
|
197
|
+
await writeReceipt(root, receipt);
|
|
198
|
+
}
|
|
199
|
+
for (const file of template.files) {
|
|
200
|
+
const source = join(dir, file);
|
|
201
|
+
if (!existsSync(source)) {
|
|
202
|
+
cli.write(` ! ${file} is listed in the registry but missing from it\n`);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const base = args.options.name ?? file.split('/').pop().replace(/\.tsx?$/, '');
|
|
206
|
+
const target = join(root, config.templatesDir, `${base}.tsx`);
|
|
207
|
+
if (existsSync(target) && !args.options.yes) {
|
|
208
|
+
if (!(await promptConfirm(`${config.templatesDir}/${base}.tsx exists. Overwrite?`, { default: false }))) {
|
|
209
|
+
cli.write(` = skipped ${base}.tsx\n`);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const content = (await readFile(source, 'utf8'))
|
|
214
|
+
.replace(/from '@textui\/core'/g, `from '${config.alias}'`);
|
|
215
|
+
await mkdir(join(root, config.templatesDir), { recursive: true });
|
|
216
|
+
await writeFile(target, content, 'utf8');
|
|
217
|
+
cli.write(` + ${config.templatesDir}/${base}.tsx\n`);
|
|
218
|
+
}
|
|
219
|
+
cli.write(`\nCreated "${name}".\n`);
|
|
220
|
+
return 0;
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
export const themeCommand = {
|
|
224
|
+
name: 'theme',
|
|
225
|
+
description: 'List themes, or preview one',
|
|
226
|
+
arguments: [{ name: 'theme', description: 'Theme to preview' }],
|
|
227
|
+
options: [
|
|
228
|
+
{ name: 'set', type: 'string', description: 'Set the project default' },
|
|
229
|
+
{ name: 'width', short: 'w', type: 'number', description: 'Preview width', default: 60 },
|
|
230
|
+
],
|
|
231
|
+
examples: ['textui theme', 'textui theme workbench', 'textui theme --set paper'],
|
|
232
|
+
async run(args, cli) {
|
|
233
|
+
if (args.options.set) {
|
|
234
|
+
const { root, config } = await requireProject();
|
|
235
|
+
const id = String(args.options.set);
|
|
236
|
+
if (!BUILTIN_THEMES.some((t) => t.id === id)) {
|
|
237
|
+
throw new CliError(`unknown theme "${id}"`);
|
|
238
|
+
}
|
|
239
|
+
await writeConfig(root, { ...config, theme: id });
|
|
240
|
+
cli.write(`Default theme is now "${id}".\n`);
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
243
|
+
const name = args.positionals[0];
|
|
244
|
+
if (!name) {
|
|
245
|
+
cli.write(table(['THEME', 'APPEARANCE', 'BORDER', 'DENSITY'], BUILTIN_THEMES.map((t) => [t.id, t.appearance, t.border ?? '-', t.density ?? 'normal'])));
|
|
246
|
+
cli.write('\nPreview one with `textui theme <name>`.\n');
|
|
247
|
+
return 0;
|
|
248
|
+
}
|
|
249
|
+
if (!BUILTIN_THEMES.some((t) => t.id === name)) {
|
|
250
|
+
throw new CliError(`unknown theme "${name}"`);
|
|
251
|
+
}
|
|
252
|
+
cli.write(`${renderToString(themePreview(), {
|
|
253
|
+
theme: name,
|
|
254
|
+
width: Number(args.options.width),
|
|
255
|
+
// The preview is written as data, so its components come by name.
|
|
256
|
+
components: CATALOG,
|
|
257
|
+
})}\n`);
|
|
258
|
+
return 0;
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
function themePreview() {
|
|
262
|
+
return {
|
|
263
|
+
component: 'box',
|
|
264
|
+
direction: 'column',
|
|
265
|
+
gap: 1,
|
|
266
|
+
padding: 1,
|
|
267
|
+
children: [
|
|
268
|
+
{ component: 'Heading', content: 'Theme preview' },
|
|
269
|
+
{
|
|
270
|
+
component: 'box',
|
|
271
|
+
direction: 'row',
|
|
272
|
+
gap: 2,
|
|
273
|
+
children: [
|
|
274
|
+
{ component: 'StatusDot', status: 'up', label: 'up' },
|
|
275
|
+
{ component: 'StatusDot', status: 'degraded', label: 'degraded' },
|
|
276
|
+
{ component: 'StatusDot', status: 'down', label: 'down' },
|
|
277
|
+
],
|
|
278
|
+
},
|
|
279
|
+
{ component: 'Progress', value: 0.62, label: 'progress', barWidth: 24 },
|
|
280
|
+
{ component: 'Sparkline', values: [1, 4, 2, 8, 5, 9, 3, 7], chartWidth: 24 },
|
|
281
|
+
{
|
|
282
|
+
component: 'Panel',
|
|
283
|
+
title: 'Panel',
|
|
284
|
+
children: { component: 'text', content: 'Body text inside a panel.' },
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
component: 'box',
|
|
288
|
+
direction: 'row',
|
|
289
|
+
gap: 1,
|
|
290
|
+
children: [
|
|
291
|
+
{ component: 'Badge', label: 'default' },
|
|
292
|
+
{ component: 'Badge', label: 'success', tone: 'success' },
|
|
293
|
+
{ component: 'Badge', label: 'danger', tone: 'danger' },
|
|
294
|
+
],
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
export const registryCommand = {
|
|
300
|
+
name: 'registry',
|
|
301
|
+
description: 'Manage component registries',
|
|
302
|
+
commands: [
|
|
303
|
+
{
|
|
304
|
+
name: 'add',
|
|
305
|
+
description: 'Register another registry by name',
|
|
306
|
+
arguments: [
|
|
307
|
+
{ name: 'name', required: true },
|
|
308
|
+
{ name: 'source', required: true, description: 'Path to a directory containing registry.json' },
|
|
309
|
+
],
|
|
310
|
+
async run(args, cli) {
|
|
311
|
+
const { root, config } = await requireProject();
|
|
312
|
+
const [name, source] = args.positionals;
|
|
313
|
+
await loadRegistry(source);
|
|
314
|
+
await writeConfig(root, {
|
|
315
|
+
...config,
|
|
316
|
+
registries: { ...config.registries, [name]: source },
|
|
317
|
+
});
|
|
318
|
+
cli.write(`Registry "${name}" added.\n`);
|
|
319
|
+
return 0;
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
name: 'list',
|
|
324
|
+
description: 'Show registered registries',
|
|
325
|
+
async run(_args, cli) {
|
|
326
|
+
const { config } = await requireProject();
|
|
327
|
+
const entries = Object.entries(config.registries ?? {});
|
|
328
|
+
cli.write(table(['NAME', 'SOURCE'], [['textui', '(built in)'], ...entries]));
|
|
329
|
+
return 0;
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
};
|
|
334
|
+
export const diffCommand = {
|
|
335
|
+
name: 'diff',
|
|
336
|
+
description: 'Show which copied components have drifted from upstream',
|
|
337
|
+
async run(_args, cli) {
|
|
338
|
+
const { root, config } = await requireProject();
|
|
339
|
+
const { manifest, dir } = await loadRegistry();
|
|
340
|
+
const receipt = await readReceipt(root);
|
|
341
|
+
const report = await diffInstalled(root, dir, manifest, receipt, config);
|
|
342
|
+
if (report.length === 0) {
|
|
343
|
+
cli.write('Nothing installed from the registry yet.\n');
|
|
344
|
+
return 0;
|
|
345
|
+
}
|
|
346
|
+
const symbols = { unchanged: '=', modified: 'M', outdated: 'U', missing: '!' };
|
|
347
|
+
cli.write(table(['', 'COMPONENT', 'FILE', 'STATE'], report.map((r) => [symbols[r.state], r.component, r.file, r.state])));
|
|
348
|
+
cli.write('\nM = you edited it U = upstream changed ! = file is gone\n');
|
|
349
|
+
return report.some((r) => r.state === 'missing') ? 1 : 0;
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
export const doctorCommand = {
|
|
353
|
+
name: 'doctor',
|
|
354
|
+
description: 'Report what this terminal can do, and what the project looks like',
|
|
355
|
+
options: [{ name: 'json', type: 'boolean', description: 'Machine-readable output' }],
|
|
356
|
+
async run(args, cli) {
|
|
357
|
+
const env = describeEnvironment({
|
|
358
|
+
env: process.env,
|
|
359
|
+
isTTY: Boolean(process.stdout.isTTY),
|
|
360
|
+
columns: process.stdout.columns,
|
|
361
|
+
rows: process.stdout.rows,
|
|
362
|
+
});
|
|
363
|
+
const caps = detectCapabilities({
|
|
364
|
+
env: process.env,
|
|
365
|
+
isTTY: Boolean(process.stdout.isTTY),
|
|
366
|
+
columns: process.stdout.columns,
|
|
367
|
+
rows: process.stdout.rows,
|
|
368
|
+
});
|
|
369
|
+
const root = await findProjectRoot();
|
|
370
|
+
const project = root ? await readConfig(root) : null;
|
|
371
|
+
if (args.options.json) {
|
|
372
|
+
cli.write(`${JSON.stringify({ env, capabilities: caps, project, root }, null, 2)}\n`);
|
|
373
|
+
return 0;
|
|
374
|
+
}
|
|
375
|
+
cli.write('Environment\n');
|
|
376
|
+
cli.write(table(['KEY', 'VALUE'], Object.entries(env)));
|
|
377
|
+
cli.write('\nCapabilities\n');
|
|
378
|
+
cli.write(table(['CAPABILITY', 'VALUE'], Object.entries(caps).map(([k, v]) => [k, String(v)])));
|
|
379
|
+
cli.write('\nProject\n');
|
|
380
|
+
if (!project) {
|
|
381
|
+
cli.write(' no textui.config.json found - run `textui init`\n');
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
cli.write(table(['KEY', 'VALUE'], [
|
|
385
|
+
['root', root],
|
|
386
|
+
['components', project.componentsDir],
|
|
387
|
+
['templates', project.templatesDir],
|
|
388
|
+
['theme', project.theme ?? 'dark'],
|
|
389
|
+
['shell', project.shell ?? 'plain'],
|
|
390
|
+
]));
|
|
391
|
+
}
|
|
392
|
+
const warnings = [];
|
|
393
|
+
if (caps.colorDepth === 0)
|
|
394
|
+
warnings.push('No colour: output will rely on glyphs alone (this is supported).');
|
|
395
|
+
if (caps.unicode === 'ascii')
|
|
396
|
+
warnings.push('No Unicode: borders and glyphs degrade to ASCII.');
|
|
397
|
+
if (!process.stdout.isTTY)
|
|
398
|
+
warnings.push('Not a terminal: interactive apps will not start, static rendering still works.');
|
|
399
|
+
if (warnings.length > 0) {
|
|
400
|
+
cli.write('\nNotes\n');
|
|
401
|
+
for (const warning of warnings)
|
|
402
|
+
cli.write(` - ${warning}\n`);
|
|
403
|
+
}
|
|
404
|
+
return 0;
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
function resolveRegistrySource(config, requested) {
|
|
408
|
+
if (!requested)
|
|
409
|
+
return undefined;
|
|
410
|
+
return config.registries?.[requested] ?? requested;
|
|
411
|
+
}
|
|
412
|
+
/** A plain aligned table. The CLI's own output has no dependency on the TUI. */
|
|
413
|
+
function table(headers, rows) {
|
|
414
|
+
const all = [headers, ...rows];
|
|
415
|
+
const widths = headers.map((_, i) => Math.max(...all.map((r) => (r[i] ?? '').length)));
|
|
416
|
+
const line = (cells) => ` ${cells.map((c, i) => (c ?? '').padEnd(widths[i])).join(' ')}`.trimEnd();
|
|
417
|
+
return [line(headers), ...rows.map(line)].join('\n') + '\n';
|
|
418
|
+
}
|
|
419
|
+
export const COMMANDS = [
|
|
420
|
+
initCommand,
|
|
421
|
+
addCommand,
|
|
422
|
+
createCommand,
|
|
423
|
+
listCommand,
|
|
424
|
+
themeCommand,
|
|
425
|
+
registryCommand,
|
|
426
|
+
diffCommand,
|
|
427
|
+
doctorCommand,
|
|
428
|
+
];
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { RegistryComponentManifest, RegistryManifest } from '@textui/core';
|
|
2
|
+
/**
|
|
3
|
+
* The registry, and copying out of it.
|
|
4
|
+
*
|
|
5
|
+
* The model is shadcn's: a component's source is copied into the project so
|
|
6
|
+
* the person who has to maintain it owns it. What makes that survivable is the
|
|
7
|
+
* receipt written alongside - origin, version and a content hash - so the CLI
|
|
8
|
+
* can later tell "you have not touched this" from "you changed it", and never
|
|
9
|
+
* overwrite the second kind without being asked.
|
|
10
|
+
*/
|
|
11
|
+
export interface ProjectConfig {
|
|
12
|
+
$schema?: string;
|
|
13
|
+
/** Where components are copied. */
|
|
14
|
+
componentsDir: string;
|
|
15
|
+
/** Where templates are copied. */
|
|
16
|
+
templatesDir: string;
|
|
17
|
+
/** Import alias the copied source should use for the runtime. */
|
|
18
|
+
alias: string;
|
|
19
|
+
theme?: string;
|
|
20
|
+
shell?: string;
|
|
21
|
+
/** Extra registries, by name. Values are paths or URLs. */
|
|
22
|
+
registries?: Record<string, string>;
|
|
23
|
+
typescript?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export declare const DEFAULT_CONFIG: ProjectConfig;
|
|
26
|
+
export declare const CONFIG_FILES: string[];
|
|
27
|
+
export declare const RECEIPT_FILE = ".textui/components.json";
|
|
28
|
+
export interface Receipt {
|
|
29
|
+
/** Component name to what was installed. */
|
|
30
|
+
[name: string]: {
|
|
31
|
+
registry: string;
|
|
32
|
+
version: string;
|
|
33
|
+
files: {
|
|
34
|
+
path: string;
|
|
35
|
+
hash: string;
|
|
36
|
+
}[];
|
|
37
|
+
installedAt: string;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export declare function hashContent(content: string): string;
|
|
41
|
+
export declare function findProjectRoot(from?: string): Promise<string | null>;
|
|
42
|
+
export declare function readConfig(root: string): Promise<ProjectConfig>;
|
|
43
|
+
export declare function writeConfig(root: string, config: ProjectConfig): Promise<string>;
|
|
44
|
+
export declare function readReceipt(root: string): Promise<Receipt>;
|
|
45
|
+
export declare function writeReceipt(root: string, receipt: Receipt): Promise<void>;
|
|
46
|
+
/** Where the shipped registry lives, relative to this package. */
|
|
47
|
+
export declare function builtinRegistryDir(): string;
|
|
48
|
+
export declare function loadRegistry(source?: string): Promise<{
|
|
49
|
+
manifest: RegistryManifest;
|
|
50
|
+
dir: string;
|
|
51
|
+
}>;
|
|
52
|
+
export declare function findComponent(manifest: RegistryManifest, name: string): RegistryComponentManifest | undefined;
|
|
53
|
+
/** A component plus everything it depends on, dependencies first. */
|
|
54
|
+
export declare function resolveDependencies(manifest: RegistryManifest, names: string[]): RegistryComponentManifest[];
|
|
55
|
+
export interface CopyResult {
|
|
56
|
+
component: string;
|
|
57
|
+
written: string[];
|
|
58
|
+
skipped: {
|
|
59
|
+
path: string;
|
|
60
|
+
reason: 'modified' | 'exists';
|
|
61
|
+
}[];
|
|
62
|
+
}
|
|
63
|
+
export interface CopyOptions {
|
|
64
|
+
root: string;
|
|
65
|
+
registryDir: string;
|
|
66
|
+
registryName?: string;
|
|
67
|
+
config: ProjectConfig;
|
|
68
|
+
receipt: Receipt;
|
|
69
|
+
force?: boolean;
|
|
70
|
+
dryRun?: boolean;
|
|
71
|
+
}
|
|
72
|
+
export declare function copyComponent(component: RegistryComponentManifest, options: CopyOptions): Promise<CopyResult>;
|
|
73
|
+
export interface DriftReport {
|
|
74
|
+
component: string;
|
|
75
|
+
file: string;
|
|
76
|
+
state: 'unchanged' | 'modified' | 'outdated' | 'missing';
|
|
77
|
+
}
|
|
78
|
+
/** What has drifted from upstream, and in which direction. */
|
|
79
|
+
export declare function diffInstalled(root: string, registryDir: string, manifest: RegistryManifest, receipt: Receipt, config: ProjectConfig): Promise<DriftReport[]>;
|
|
80
|
+
export declare function listSourceFiles(dir: string): Promise<string[]>;
|
|
81
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"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"}
|