uipkge-ng 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.
@@ -0,0 +1,149 @@
1
+ import * as path from 'node:path';
2
+ import { createInterface } from 'node:readline';
3
+ import { readConfig, registryFor } from '../config.js';
4
+ import { findAppRoot } from '../project.js';
5
+ import { refKind } from '../registry.js';
6
+ import { firstSentence, isListable } from './list.js';
7
+ import { describeItem } from './view.js';
8
+ /**
9
+ * A minimal Model Context Protocol server over stdio (newline-delimited JSON-RPC 2.0).
10
+ * Read-only: it browses the registry and hands back `uipkge-ng add` commands; it never writes files.
11
+ */
12
+ const PROTOCOL_VERSIONS = ['2025-06-18', '2025-03-26', '2024-11-05'];
13
+ const stringList = (description) => ({ type: 'array', items: { type: 'string' }, minItems: 1, description });
14
+ export const TOOLS = [
15
+ {
16
+ name: 'list_components',
17
+ description: 'List uipkge Angular components, optionally filtered by a search query or category.',
18
+ inputSchema: {
19
+ type: 'object',
20
+ properties: {
21
+ query: { type: 'string', description: 'Matches name, title or description' },
22
+ category: { type: 'string' },
23
+ registry: { type: 'string', description: 'A named registry from components.json, e.g. @acme' },
24
+ },
25
+ },
26
+ async run(args, ctx) {
27
+ const ns = typeof args.registry === 'string' && args.registry ? args.registry : undefined;
28
+ const query = String(args.query ?? '').toLowerCase();
29
+ const category = String(args.category ?? '').toLowerCase();
30
+ const items = (await ctx.registry.index(ns)).items.filter(isListable).filter(i => (!category || (i.categories ?? []).some(c => c.toLowerCase() === category)) &&
31
+ (!query || [i.name, i.title, i.description].some(v => v?.toLowerCase().includes(query))));
32
+ if (!items.length)
33
+ return 'No components match.';
34
+ return items
35
+ .map(i => `${ns ? `${ns}/` : ''}${i.name}${i.categories?.length ? ` [${i.categories[0]}]` : ''} — ${firstSentence(i.description ?? '')}`)
36
+ .join('\n');
37
+ },
38
+ },
39
+ {
40
+ name: 'view_components',
41
+ description: 'Show components in full: description, dependencies, usage notes and the source of every file.',
42
+ inputSchema: { type: 'object', properties: { names: stringList('Names such as "button" or "@acme/card"') }, required: ['names'] },
43
+ async run(args, ctx) {
44
+ const items = await Promise.all(names(args).map(n => ctx.registry.item(n)));
45
+ return items.map(describeItem).join('\n\n---\n\n');
46
+ },
47
+ },
48
+ {
49
+ name: 'get_add_command',
50
+ description: 'Return the shell command that adds components (and their dependencies) to the project.',
51
+ inputSchema: { type: 'object', properties: { names: stringList('Component names') }, required: ['names'] },
52
+ async run(args, ctx) {
53
+ const list = names(args);
54
+ // Check bare names exist so the command isn't a guess.
55
+ const bare = list.filter(n => refKind(n) === 'name');
56
+ if (bare.length) {
57
+ const known = new Set((await ctx.registry.index()).items.map(i => i.name));
58
+ const unknown = bare.filter(n => !known.has(n));
59
+ if (unknown.length)
60
+ throw new Error(`Unknown component: ${unknown.join(', ')}`);
61
+ }
62
+ return `npx uipkge-ng add ${list.join(' ')}`;
63
+ },
64
+ },
65
+ {
66
+ name: 'list_registries',
67
+ description: 'List the registries this project can install from.',
68
+ inputSchema: { type: 'object', properties: {} },
69
+ async run(_args, ctx) {
70
+ return [`default: ${ctx.registry.baseUrl}`, ...ctx.namespaces.map(n => `${n} (use ${n}/<name>)`)].join('\n');
71
+ },
72
+ },
73
+ ];
74
+ function names(args) {
75
+ const list = Array.isArray(args.names) ? args.names.filter((n) => typeof n === 'string' && n.length > 0) : [];
76
+ if (!list.length)
77
+ throw new Error('"names" must be a non-empty list of strings');
78
+ return list;
79
+ }
80
+ /** Answers one JSON-RPC message; `undefined` for notifications (no reply). Exposed for tests. */
81
+ export async function handleMessage(message, ctx, version) {
82
+ const reply = (result) => ({ jsonrpc: '2.0', id: message.id, result });
83
+ const fail = (code, msg) => ({ jsonrpc: '2.0', id: message.id ?? null, error: { code, message: msg } });
84
+ if (message?.jsonrpc !== '2.0' || typeof message.method !== 'string')
85
+ return fail(-32600, 'Invalid request');
86
+ const isNotification = message.id === undefined;
87
+ switch (message.method) {
88
+ case 'initialize': {
89
+ const asked = String(message.params?.protocolVersion ?? '');
90
+ return reply({
91
+ protocolVersion: PROTOCOL_VERSIONS.includes(asked) ? asked : PROTOCOL_VERSIONS[0],
92
+ capabilities: { tools: {} },
93
+ serverInfo: { name: 'uipkge-ng', version },
94
+ instructions: 'Browse uipkge Angular components. To install, run the command from get_add_command in the project.',
95
+ });
96
+ }
97
+ case 'ping':
98
+ return reply({});
99
+ case 'tools/list':
100
+ return reply({ tools: TOOLS.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })) });
101
+ case 'tools/call': {
102
+ const tool = TOOLS.find(t => t.name === message.params?.name);
103
+ if (!tool)
104
+ return fail(-32602, `Unknown tool: ${String(message.params?.name)}`);
105
+ try {
106
+ const text = await tool.run(message.params?.arguments ?? {}, ctx);
107
+ return reply({ content: [{ type: 'text', text }] });
108
+ }
109
+ catch (error) {
110
+ // Tool failures are results the model can read, not protocol errors.
111
+ return reply({ content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }], isError: true });
112
+ }
113
+ }
114
+ default:
115
+ return isNotification ? undefined : fail(-32601, `Method not found: ${message.method}`);
116
+ }
117
+ }
118
+ export async function runMcp(options) {
119
+ const cwd = path.resolve(options.cwd);
120
+ const root = findAppRoot(cwd);
121
+ const config = root ? await readConfig(root).catch(() => null) : null;
122
+ const ctx = { registry: registryFor(root, config, cwd), namespaces: Object.keys(config?.registries ?? {}) };
123
+ // stdout carries protocol messages only; anything else goes to stderr.
124
+ const lines = createInterface({ input: process.stdin });
125
+ const pending = new Set();
126
+ lines.on('line', line => {
127
+ if (!line.trim())
128
+ return;
129
+ const task = (async () => {
130
+ let message;
131
+ try {
132
+ message = JSON.parse(line);
133
+ }
134
+ catch {
135
+ return send({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } });
136
+ }
137
+ const response = await handleMessage(message, ctx, options.version);
138
+ if (response)
139
+ send(response);
140
+ })();
141
+ pending.add(task);
142
+ void task.finally(() => pending.delete(task));
143
+ });
144
+ await new Promise(resolve => lines.on('close', resolve));
145
+ await Promise.all(pending);
146
+ }
147
+ function send(message) {
148
+ process.stdout.write(`${JSON.stringify(message)}\n`);
149
+ }
@@ -0,0 +1,48 @@
1
+ import * as path from 'node:path';
2
+ import { readConfig, registryFor } from '../config.js';
3
+ import { UipkgeError } from '../errors.js';
4
+ import { color } from '../output.js';
5
+ import { findAppRoot } from '../project.js';
6
+ import { itemNameFromRef, refKind } from '../registry.js';
7
+ import { assertKnownNames } from '../resolve.js';
8
+ /** Everything an install would touch, without installing: metadata, dependencies, file contents. */
9
+ export function describeItem(item) {
10
+ const lines = [color.bold(item.title ? `${item.title} (${item.name})` : item.name)];
11
+ if (item.description)
12
+ lines.push(item.description);
13
+ const meta = [
14
+ ['type', item.type],
15
+ ['categories', item.categories?.join(', ')],
16
+ ['npm', item.dependencies?.join(', ')],
17
+ ['npm (dev)', item.devDependencies?.join(', ')],
18
+ ['requires', item.registryDependencies?.map(r => (refKind(r) === 'url' ? itemNameFromRef(r) : r)).join(', ')],
19
+ ['env', item.envVars ? Object.keys(item.envVars).join(', ') : undefined],
20
+ ];
21
+ for (const [label, value] of meta)
22
+ if (value)
23
+ lines.push(`${color.dim(label.padEnd(11))}${value}`);
24
+ if (item.docs)
25
+ lines.push('', color.bold('Notes'), item.docs.trim());
26
+ for (const f of item.files) {
27
+ const file = (f.target ?? f.path ?? '(no target)').replace(/^~\//, '');
28
+ lines.push('', color.bold(`── ${file}`), f.content ?? color.dim('(no content in this document)'));
29
+ }
30
+ return lines.join('\n');
31
+ }
32
+ export async function runView(options) {
33
+ if (!options.items.length)
34
+ throw new UipkgeError('Nothing to view.', 'Pass one or more names, e.g. `uipkge-ng view button`.');
35
+ const cwd = path.resolve(options.cwd);
36
+ const root = findAppRoot(cwd);
37
+ const config = root ? await readConfig(root) : null;
38
+ const registry = registryFor(root, config, cwd);
39
+ const bare = options.items.filter(i => refKind(i) === 'name');
40
+ if (bare.length)
41
+ assertKnownNames(bare, new Set((await registry.index()).items.map(i => i.name)));
42
+ const items = await Promise.all(options.items.map(ref => registry.item(ref)));
43
+ if (options.json) {
44
+ process.stdout.write(`${JSON.stringify(items.length === 1 ? items[0] : items, null, 2)}\n`);
45
+ return;
46
+ }
47
+ process.stdout.write(`${items.map(describeItem).join(`\n\n${'─'.repeat(40)}\n\n`)}\n`);
48
+ }
package/dist/config.js ADDED
@@ -0,0 +1,56 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { readFile, writeFile } from 'node:fs/promises';
3
+ import * as path from 'node:path';
4
+ import { projectEnv } from './env.js';
5
+ import { UipkgeError } from './errors.js';
6
+ import { createRegistry, resolveRegistryUrl } from './registry.js';
7
+ export const CONFIG_FILE = 'components.json';
8
+ export const DEFAULT_ALIASES = { ui: '@/ui', lib: '@/lib', blocks: '@/app/components/blocks' };
9
+ export function defaultConfig(registryUrl) {
10
+ return {
11
+ framework: 'angular',
12
+ registryUrl,
13
+ styles: 'src/styles.css',
14
+ tokens: 'src/uipkge.css',
15
+ aliases: { ...DEFAULT_ALIASES },
16
+ };
17
+ }
18
+ export async function readConfig(root) {
19
+ const file = path.join(root, CONFIG_FILE);
20
+ if (!existsSync(file))
21
+ return null;
22
+ let raw;
23
+ try {
24
+ raw = JSON.parse(await readFile(file, 'utf8'));
25
+ }
26
+ catch (error) {
27
+ throw new UipkgeError(`${CONFIG_FILE} is not valid JSON: ${error instanceof Error ? error.message : error}`);
28
+ }
29
+ // Tolerate hand-edited or older files: fill in what's missing.
30
+ const base = defaultConfig(raw.registryUrl ?? '');
31
+ const config = { ...base, ...raw, aliases: { ...DEFAULT_ALIASES, ...raw.aliases }, framework: 'angular' };
32
+ for (const key of Object.keys(config.registries ?? {})) {
33
+ if (!/^@[a-z0-9][\w-]*$/i.test(key)) {
34
+ throw new UipkgeError(`Registry names in ${CONFIG_FILE} must look like "@acme" (got "${key}").`);
35
+ }
36
+ }
37
+ return config;
38
+ }
39
+ export async function requireConfig(root) {
40
+ const config = await readConfig(root);
41
+ if (!config) {
42
+ throw new UipkgeError(`No ${CONFIG_FILE} found in ${root}.`, 'Run `uipkge-ng init` first.');
43
+ }
44
+ return config;
45
+ }
46
+ export async function writeConfig(root, config) {
47
+ await writeFile(path.join(root, CONFIG_FILE), JSON.stringify(config, null, 2) + '\n', 'utf8');
48
+ }
49
+ /** The registry client for a project: default URL, named registries, `.env`-backed tokens. */
50
+ export function registryFor(root, config, cwd = process.cwd()) {
51
+ return createRegistry(resolveRegistryUrl(config?.registryUrl), {
52
+ cwd,
53
+ registries: config?.registries,
54
+ env: projectEnv(root),
55
+ });
56
+ }
package/dist/env.js ADDED
@@ -0,0 +1,33 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import * as path from 'node:path';
3
+ /** `KEY=value` lines (quotes stripped, `export` allowed, comments ignored). */
4
+ export function parseDotenv(text) {
5
+ const out = {};
6
+ for (const line of text.split(/\r?\n/)) {
7
+ const match = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/);
8
+ if (!match)
9
+ continue;
10
+ let value = match[2];
11
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
12
+ value = value.slice(1, -1);
13
+ }
14
+ else {
15
+ value = value.replace(/\s+#.*$/, '');
16
+ }
17
+ out[match[1]] = value;
18
+ }
19
+ return out;
20
+ }
21
+ /**
22
+ * Variables for registry `${VAR}` expansion: `.env`, then `.env.local`, then the
23
+ * real environment (which always wins). Values are never written back.
24
+ */
25
+ export function projectEnv(root) {
26
+ const merged = {};
27
+ for (const file of ['.env', '.env.local']) {
28
+ const abs = root ? path.join(root, file) : '';
29
+ if (abs && existsSync(abs))
30
+ Object.assign(merged, parseDotenv(readFileSync(abs, 'utf8')));
31
+ }
32
+ return { ...merged, ...process.env };
33
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,9 @@
1
+ /** An expected failure: printed as a message (plus optional hint), exit code 1, no stack. */
2
+ export class UipkgeError extends Error {
3
+ hint;
4
+ constructor(message, hint) {
5
+ super(message);
6
+ this.hint = hint;
7
+ this.name = 'UipkgeError';
8
+ }
9
+ }
package/dist/extras.js ADDED
@@ -0,0 +1,111 @@
1
+ // Install-time additions a registry item can carry besides files:
2
+ // cssVars, css, envVars, docs (shadcn registry-item schema).
3
+ import { existsSync } from 'node:fs';
4
+ import { readFile, writeFile } from 'node:fs/promises';
5
+ export function hasCss(item) {
6
+ const vars = item.cssVars;
7
+ const anyVars = Boolean(vars && (Object.keys(vars.theme ?? {}).length || Object.keys(vars.light ?? {}).length || Object.keys(vars.dark ?? {}).length));
8
+ return anyVars || Boolean(item.css && Object.keys(item.css).length);
9
+ }
10
+ const decl = (name, value) => ` --${name.replace(/^--/, '')}: ${value};`;
11
+ /** CSS text for an item's cssVars + css rules (no markers). */
12
+ export function itemCss(item) {
13
+ const vars = item.cssVars ?? {};
14
+ const out = [];
15
+ const light = Object.entries(vars.light ?? {});
16
+ const dark = Object.entries(vars.dark ?? {});
17
+ if (light.length)
18
+ out.push(':root {', ...light.map(([k, v]) => decl(k, v)), '}');
19
+ if (dark.length)
20
+ out.push('.dark {', ...dark.map(([k, v]) => decl(k, v)), '}');
21
+ // Tailwind v4 only turns a variable into utilities (`bg-brand`) when it is
22
+ // registered as `--color-*` in @theme; do that for every light/dark colour var.
23
+ const colorNames = [...new Set([...light, ...dark].map(([k]) => k.replace(/^--/, '')))];
24
+ const theme = [
25
+ ...Object.entries(vars.theme ?? {}).map(([k, v]) => decl(k, v)),
26
+ ...colorNames.filter(n => !(vars.theme ?? {})[n]).map(n => ` --color-${n}: var(--${n});`),
27
+ ];
28
+ if (theme.length)
29
+ out.push('@theme inline {', ...theme, '}');
30
+ if (item.css && Object.keys(item.css).length)
31
+ out.push(serializeCss(item.css, 0));
32
+ return out.join('\n');
33
+ }
34
+ /** `{ "@layer base": { "h1": { "font-weight": "600" } } }` → nested CSS text. */
35
+ export function serializeCss(rules, depth) {
36
+ const pad = ' '.repeat(depth);
37
+ const lines = [];
38
+ for (const [key, value] of Object.entries(rules)) {
39
+ if (typeof value === 'string') {
40
+ lines.push(`${pad}${key}: ${value};`);
41
+ }
42
+ else if (Object.keys(value).length === 0) {
43
+ lines.push(`${pad}${key};`); // e.g. `@import "x"` expressed as an empty rule
44
+ }
45
+ else {
46
+ lines.push(`${pad}${key} {`, serializeCss(value, depth + 1), `${pad}}`);
47
+ }
48
+ }
49
+ return lines.join('\n');
50
+ }
51
+ const START = (name) => `/* uipkge:${name} — added by \`uipkge-ng add ${name}\`; edits here are replaced on reinstall */`;
52
+ const END = (name) => `/* /uipkge:${name} */`;
53
+ /**
54
+ * Puts the item's CSS at the end of `stylesheet` inside markers, replacing a
55
+ * previous block for the same item so reinstalling never duplicates it.
56
+ * Returns the new text, or null when unchanged.
57
+ */
58
+ export function upsertCssBlock(stylesheet, name, css) {
59
+ const block = `${START(name)}\n${css}\n${END(name)}`;
60
+ const start = stylesheet.indexOf(START(name));
61
+ const endMarker = END(name);
62
+ if (start !== -1) {
63
+ const end = stylesheet.indexOf(endMarker, start);
64
+ if (end !== -1) {
65
+ const next = stylesheet.slice(0, start) + block + stylesheet.slice(end + endMarker.length);
66
+ return next === stylesheet ? null : next;
67
+ }
68
+ }
69
+ const sep = stylesheet === '' || stylesheet.endsWith('\n\n') ? '' : stylesheet.endsWith('\n') ? '\n' : '\n\n';
70
+ return `${stylesheet}${sep}${block}\n`;
71
+ }
72
+ /** Keys already set in a dotenv file (`KEY=…`, `export KEY=…`). */
73
+ export function envKeys(text) {
74
+ const keys = new Set();
75
+ for (const line of text.split(/\r?\n/)) {
76
+ const match = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/);
77
+ if (match)
78
+ keys.add(match[1]);
79
+ }
80
+ return keys;
81
+ }
82
+ /** Appends variables the file doesn't define yet; never overwrites a value. */
83
+ export function appendEnv(text, vars) {
84
+ const existing = envKeys(text);
85
+ const added = Object.keys(vars).filter(k => !existing.has(k));
86
+ if (!added.length)
87
+ return { text, added };
88
+ const quote = (v) => (/[\s#"'=]/.test(v) ? JSON.stringify(v) : v);
89
+ const lines = added.map(k => `${k}=${quote(vars[k])}`);
90
+ const sep = text === '' || text.endsWith('\n') ? '' : '\n';
91
+ return { text: `${text}${sep}${lines.join('\n')}\n`, added };
92
+ }
93
+ export async function applyCss(tokensFile, item) {
94
+ if (!hasCss(item))
95
+ return false;
96
+ const current = existsSync(tokensFile) ? await readFile(tokensFile, 'utf8') : '';
97
+ const next = upsertCssBlock(current, item.name, itemCss(item));
98
+ if (next === null)
99
+ return false;
100
+ await writeFile(tokensFile, next, 'utf8');
101
+ return true;
102
+ }
103
+ export async function applyEnv(envFile, item) {
104
+ if (!item.envVars || !Object.keys(item.envVars).length)
105
+ return [];
106
+ const current = existsSync(envFile) ? await readFile(envFile, 'utf8') : '';
107
+ const { text, added } = appendEnv(current, item.envVars);
108
+ if (added.length)
109
+ await writeFile(envFile, text, 'utf8');
110
+ return added;
111
+ }
package/dist/files.js ADDED
@@ -0,0 +1,49 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { mkdir, writeFile } from 'node:fs/promises';
3
+ import * as path from 'node:path';
4
+ import { UipkgeError } from './errors.js';
5
+ import { resolveTarget } from './project.js';
6
+ /** Files of an item that have a destination. */
7
+ export function targetedFiles(item) {
8
+ return item.files.filter((f) => typeof f.target === 'string' && f.target !== '');
9
+ }
10
+ /** Installed = every one of the item's target files exists. Works on index items (no content needed). */
11
+ export function isItemInstalled(root, item) {
12
+ const files = targetedFiles(item);
13
+ return files.length > 0 && files.every(f => existsSync(resolveTarget(root, f.target)));
14
+ }
15
+ export const normalizeEol = (text) => text.replace(/\r\n?/g, '\n');
16
+ /**
17
+ * Files that already exist with different content (line endings aside). Writing
18
+ * the rest of the item around them would mix your file with the registry's, so
19
+ * `add` stops on these unless told to overwrite.
20
+ */
21
+ export function conflictingFiles(root, item) {
22
+ return targetedFiles(item).flatMap(f => {
23
+ const abs = resolveTarget(root, f.target);
24
+ if (!existsSync(abs) || typeof f.content !== 'string')
25
+ return [];
26
+ return normalizeEol(readFileSync(abs, 'utf8')) === normalizeEol(f.content) ? [] : [path.relative(root, abs)];
27
+ });
28
+ }
29
+ export async function writeItemFiles(root, item, overwrite) {
30
+ const result = { written: [], kept: [] };
31
+ const files = targetedFiles(item);
32
+ if (!files.length)
33
+ throw new UipkgeError(`Registry item \`${item.name}\` has no files to install.`);
34
+ for (const file of files) {
35
+ if (typeof file.content !== 'string') {
36
+ throw new UipkgeError(`Registry item \`${item.name}\` is missing the content of ${file.target}.`);
37
+ }
38
+ const abs = resolveTarget(root, file.target);
39
+ const rel = path.relative(root, abs);
40
+ if (existsSync(abs) && !overwrite) {
41
+ result.kept.push(rel);
42
+ continue;
43
+ }
44
+ await mkdir(path.dirname(abs), { recursive: true });
45
+ await writeFile(abs, file.content, 'utf8');
46
+ result.written.push(rel);
47
+ }
48
+ return result;
49
+ }
package/dist/index.js ADDED
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import { parseArgs } from 'node:util';
5
+ import { normalizeOptionalValues } from './args.js';
6
+ import { runAdd } from './commands/add.js';
7
+ import { runBuild } from './commands/build.js';
8
+ import { runDiff } from './commands/diff.js';
9
+ import { runInfo } from './commands/info.js';
10
+ import { runInit } from './commands/init.js';
11
+ import { runList } from './commands/list.js';
12
+ import { runMcp } from './commands/mcp.js';
13
+ import { runView } from './commands/view.js';
14
+ import { UipkgeError } from './errors.js';
15
+ import { color, log, setSilent } from './output.js';
16
+ const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
17
+ const COMMAND_HELP = [
18
+ ['init', 'set up Tailwind, the cn() helper, tokens and components.json'],
19
+ ['add [items...]', 'add components (and what they depend on)'],
20
+ ['list [@ns] [query]', 'list available components (alias: ls)'],
21
+ ['view <items...>', "show a component's details and files"],
22
+ ['diff [item]', 'compare installed components with the registry'],
23
+ ['info', 'show project, config and registry details'],
24
+ ['build [file]', 'build your own registry (registry.json → public/r)'],
25
+ ['mcp', 'run an MCP server so AI assistants can browse components'],
26
+ ];
27
+ /** Called as `uipkge` (the alias package), help says so. */
28
+ const CLI_NAMES = new Set(['uipkge-ng', 'uipkge']);
29
+ const invokedAs = path.basename(process.argv[1] ?? '').replace(/\.[cm]?js$/, '');
30
+ const NAME = CLI_NAMES.has(invokedAs) ? invokedAs : 'uipkge-ng';
31
+ const HELP = `${NAME} ${VERSION} — add uipkge Angular components to your project
32
+
33
+ Usage
34
+ ${COMMAND_HELP.map(([cmd, desc]) => ` ${`${NAME} ${cmd}`.padEnd(NAME.length + 21)}${desc}`).join('\n')}
35
+
36
+ Items are names (button), @registry/name, item URLs (https://…/x.json) or local files (./x.json).
37
+
38
+ Options
39
+ -c, --cwd <dir> run in another directory
40
+ -y, --yes don't ask for confirmation
41
+ -o, --overwrite replace existing files (init, add)
42
+ -a, --all add every component (add)
43
+ -p, --path <dir> put the requested components in <dir> (add)
44
+ --dry-run show what add would change, without changing it
45
+ --view[=<path>] print the files add would write (optionally only matching <path>)
46
+ --diff[=<path>] diff the files add would write against yours
47
+ -s, --silent only print errors
48
+ --category <c> only one category (list)
49
+ -i, --installed only installed components (list)
50
+ --json machine-readable output (list, view, info)
51
+ --output <dir> where build writes the registry (default public/r)
52
+ --debug show stack traces on errors
53
+ -h, --help show this help
54
+ -v, --version show the version
55
+
56
+ Registry: https://uipkge.dev/r/angular (override with components.json "registryUrl" or UIPKGE_REGISTRY_URL)
57
+ `;
58
+ const COMMANDS = new Set(['init', 'add', 'list', 'ls', 'view', 'diff', 'info', 'build', 'mcp']);
59
+ const OPTION_COMMANDS = {
60
+ overwrite: ['init', 'add'],
61
+ all: ['add'],
62
+ path: ['add'],
63
+ 'dry-run': ['add'],
64
+ view: ['add'],
65
+ diff: ['add'],
66
+ category: ['list', 'ls'],
67
+ installed: ['list', 'ls'],
68
+ json: ['list', 'ls', 'view', 'info'],
69
+ output: ['build'],
70
+ };
71
+ async function main(argv) {
72
+ const { values, positionals } = parseArgs({
73
+ args: normalizeOptionalValues(argv),
74
+ allowPositionals: true,
75
+ options: {
76
+ cwd: { type: 'string', short: 'c' },
77
+ yes: { type: 'boolean', short: 'y', default: false },
78
+ overwrite: { type: 'boolean', short: 'o', default: false },
79
+ all: { type: 'boolean', short: 'a', default: false },
80
+ path: { type: 'string', short: 'p' },
81
+ 'dry-run': { type: 'boolean', default: false },
82
+ view: { type: 'string' },
83
+ diff: { type: 'string' },
84
+ silent: { type: 'boolean', short: 's', default: false },
85
+ category: { type: 'string' },
86
+ installed: { type: 'boolean', short: 'i', default: false },
87
+ json: { type: 'boolean', default: false },
88
+ output: { type: 'string' },
89
+ debug: { type: 'boolean', default: false },
90
+ help: { type: 'boolean', short: 'h', default: false },
91
+ version: { type: 'boolean', short: 'v', default: false },
92
+ },
93
+ });
94
+ if (values.version)
95
+ return void process.stdout.write(`${VERSION}\n`);
96
+ const [command, ...rest] = positionals;
97
+ if (values.help || !command)
98
+ return void process.stdout.write(HELP);
99
+ if (!COMMANDS.has(command)) {
100
+ throw new UipkgeError(`Unknown command \`${command}\`.`, 'Run `uipkge-ng --help` to see the commands.');
101
+ }
102
+ // An option a command ignores would silently do the wrong thing (`init --dry-run` really initializing).
103
+ for (const [option, commands] of Object.entries(OPTION_COMMANDS)) {
104
+ const value = values[option];
105
+ if (value !== undefined && value !== false && !commands.includes(command)) {
106
+ throw new UipkgeError(`--${option} doesn't apply to \`${command}\`.`, `It works with: ${commands.filter(c => c !== 'ls').join(', ')}.`);
107
+ }
108
+ }
109
+ setSilent(values.silent);
110
+ const cwd = values.cwd ?? process.cwd();
111
+ switch (command) {
112
+ case 'init':
113
+ return runInit({ cwd, yes: values.yes, overwrite: values.overwrite });
114
+ case 'add':
115
+ return runAdd({
116
+ cwd,
117
+ names: rest,
118
+ all: values.all,
119
+ yes: values.yes,
120
+ overwrite: values.overwrite,
121
+ path: values.path,
122
+ dryRun: values['dry-run'],
123
+ view: values.view,
124
+ diff: values.diff,
125
+ });
126
+ case 'list':
127
+ case 'ls':
128
+ // `list @acme [query]` lists a named registry.
129
+ return rest[0]?.startsWith('@')
130
+ ? runList({ cwd, namespace: rest[0], query: rest[1], category: values.category, installedOnly: values.installed, json: values.json })
131
+ : runList({ cwd, query: rest[0], category: values.category, installedOnly: values.installed, json: values.json });
132
+ case 'view':
133
+ return runView({ cwd, items: rest, json: values.json });
134
+ case 'diff':
135
+ return runDiff({ cwd, name: rest[0] });
136
+ case 'info':
137
+ return runInfo({ cwd, json: values.json, version: VERSION });
138
+ case 'build':
139
+ return runBuild({ cwd, registry: rest[0] ?? 'registry.json', output: values.output ?? 'public/r' });
140
+ case 'mcp':
141
+ return runMcp({ cwd, version: VERSION });
142
+ }
143
+ }
144
+ main(process.argv.slice(2)).catch((error) => {
145
+ if (error instanceof UipkgeError) {
146
+ log.error(error.message);
147
+ if (error.hint)
148
+ process.stderr.write(` ${color.dim(error.hint)}\n`);
149
+ }
150
+ else if (error instanceof Error && error.code === 'ERR_PARSE_ARGS_UNKNOWN_OPTION') {
151
+ log.error(error.message);
152
+ process.stderr.write(` ${color.dim('Run `uipkge-ng --help` to see the options.')}\n`);
153
+ }
154
+ else {
155
+ log.error(error instanceof Error ? error.message : String(error));
156
+ if (process.argv.includes('--debug') && error instanceof Error)
157
+ process.stderr.write(`${error.stack}\n`);
158
+ else
159
+ process.stderr.write(` ${color.dim('Re-run with --debug for details.')}\n`);
160
+ }
161
+ process.exitCode = 1;
162
+ });