uipkge-ng 0.1.0 → 0.1.1

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/CHANGELOG.md ADDED
@@ -0,0 +1,32 @@
1
+ # Changelog
2
+
3
+ All notable changes to `uipkge-ng` (and its aliases `@uipkge/ng` and `uipkge`). This project follows [Semantic Versioning](https://semver.org); while it's 0.x, minor versions may change behaviour.
4
+
5
+ ## 0.1.1 — 2026-09-26
6
+
7
+ ### Added
8
+
9
+ - `doctor`: checks the setup (Node, Angular app, `components.json`, registries and their tokens, aliases, Tailwind v4 and its build step, design tokens actually loaded, `cn()`, incomplete components, undeclared component packages) and prints the fix for each problem. Exits with 1 on failure; `--json`.
10
+
11
+ ### Fixed
12
+
13
+ - `add` no longer stops half-way on a bundle item — an item with no files of its own, only dependencies, such as `charts`. Bundles now install their dependencies.
14
+ - `add` checks every item before writing anything, so a broken item can't leave a project half-installed.
15
+ - `init` works when the default registry (`registryUrl`) ships only its own components: `utils` and `tailwind` come from uipkge instead, and it says so.
16
+
17
+ ### Changed
18
+
19
+ - Resolving dependencies fetches in parallel: adding all 200 uipkge components resolves in about 3 seconds instead of about 70.
20
+ - `build` writes bare names in `registryDependencies` as uipkge URLs, so they resolve the same in every project whatever its default registry is. A bare name that matches one of the registry's own items is now an error — use `@your-registry/name` or a URL for those.
21
+
22
+ ## 0.1.0 — 2026-09-25
23
+
24
+ First release.
25
+
26
+ - `init`: Tailwind CSS v4, the `cn()` helper, design tokens and tsconfig paths; Sass/Less apps get the tokens registered in `angular.json`.
27
+ - `add`: components with their dependencies, `@angular/*` pinned to the app's version; `--dry-run`, `--view`, `--diff`, `--path`, `--overwrite`; stops before writing when a file you have differs.
28
+ - `list`, `view`, `diff`, `info`.
29
+ - Custom folders through `aliases`, with import rewriting.
30
+ - Named and private registries (`@name/item`, `{name}` URLs, `${ENV}` headers from the environment, `.env.local` and `.env`).
31
+ - `build` for publishing your own registry; `mcp` server for AI assistants.
32
+ - Multi-project Angular workspaces and npm/pnpm/yarn/bun monorepos.
package/README.md CHANGED
@@ -92,6 +92,7 @@ Dark mode follows a `dark` class on `<html>`.
92
92
  | [`view <items...>`](#view) | show a component's details and source |
93
93
  | [`diff [item]`](#diff) | compare your copies with the registry |
94
94
  | [`info`](#info) | project, config and registry details |
95
+ | [`doctor`](#doctor) | check the setup and explain how to fix problems |
95
96
  | [`build [registry.json]`](#publishing-your-own-registry) | build your own registry |
96
97
  | [`mcp`](#ai-assistants-mcp) | MCP server for AI assistants |
97
98
 
@@ -168,6 +169,30 @@ Checks what you installed from the default registry and from every named one. Ta
168
169
 
169
170
  Prints the CLI and Node versions, the project (app, Angular, package manager, Tailwind, PostCSS), `components.json`, the folders your aliases resolve to, whether the registry is reachable and what's installed. Paste it into bug reports; `--json` for machines.
170
171
 
172
+ ### doctor
173
+
174
+ ```bash
175
+ npx uipkge-ng doctor
176
+ ```
177
+
178
+ Checks the whole setup — like `react-native doctor` — and says how to fix anything wrong:
179
+
180
+ ```text
181
+ Styling
182
+ ✔ Tailwind CSS ^4.3.3
183
+ ✔ Tailwind build .postcssrc.json uses @tailwindcss/postcss
184
+ ✖ Design tokens src/uipkge.css isn't loaded
185
+ → Add `@import './uipkge.css';` to src/styles.css, or list src/uipkge.css under "styles" in angular.json.
186
+
187
+ Components
188
+ ! Incomplete components files missing from 1: card
189
+ → Reinstall with `uipkge-ng add card --overwrite`.
190
+ ✖ Component packages not in package.json: class-variance-authority
191
+ → Run `npm install class-variance-authority`.
192
+ ```
193
+
194
+ It checks Node, the Angular app (and workspace), `components.json`, that the registry and every named registry answer (and their tokens are set), that your aliases resolve, Tailwind v4 and its build step, that the design tokens are actually loaded, the `cn()` helper, components with missing files, and npm packages your components need but `package.json` doesn't list. It changes nothing. It exits with 1 when something fails, so it can run in CI; `--json` for scripts.
195
+
171
196
  ## Existing projects
172
197
 
173
198
  uipkge-ng is built to be run on apps that already have code in them:
@@ -237,7 +262,7 @@ Every command then uses those folders, and the `@/ui/…` and `@/lib/…` import
237
262
 
238
263
  ## Registries
239
264
 
240
- Components come from `https://uipkge.dev/r/angular` unless you configure otherwise. Any shadcn-style registry works — a public one, your company's private one, or one you [build yourself](#publishing-your-own-registry).
265
+ Out of the box everything comes from the uipkge registry, `https://uipkge.dev/r/angular` — no setup needed. Any shadcn-style registry works — a public one, your company's private one, or one you [build yourself](#publishing-your-own-registry).
241
266
 
242
267
  ### Changing the default registry
243
268
 
@@ -360,7 +385,7 @@ It never writes files — installing stays a command you run. It reads the `comp
360
385
  | `Registry "@acme" needs the environment variable ACME_TOKEN` | set it in your shell, `.env.local` or `.env` |
361
386
  | `Nx workspaces are not supported yet` | use a standalone Angular CLI app for now |
362
387
 
363
- Still stuck? Include the output of `npx uipkge-ng info` when you [open an issue](https://github.com/uday-a/uipkge-cli/issues).
388
+ Start with `npx uipkge-ng doctor` — it finds most setup problems and tells you the fix. Still stuck? Include the output of `npx uipkge-ng info` when you [open an issue](https://github.com/uday-a/uipkge-cli/issues).
364
389
 
365
390
  ## Credits
366
391
 
@@ -368,4 +393,6 @@ Created by **Uday Adaka** as part of **[uipkge](https://uipkge.dev)**. It uses t
368
393
 
369
394
  ## License
370
395
 
371
- [MIT](./LICENSE) © 2026 Uday Adaka — uipkge. You can use, change and redistribute it freely; keep the copyright and license notice in copies of the CLI.
396
+ [MIT](./LICENSE) © 2026 Uday Adaka — uipkge.
397
+
398
+ In plain words: you can use it for anything — personal or commercial — fork it, change it, and sell or redistribute it, including your modified version. The one condition: keep the copyright line (Uday Adaka, uipkge) and the license text in every copy.
@@ -3,7 +3,7 @@ import * as path from 'node:path';
3
3
  import { registryFor, requireConfig } from '../config.js';
4
4
  import { UipkgeError } from '../errors.js';
5
5
  import { applyCss, applyEnv, envKeys, hasCss } from '../extras.js';
6
- import { conflictingFiles, isItemInstalled, targetedFiles, writeItemFiles } from '../files.js';
6
+ import { assertWritable, conflictingFiles, isItemInstalled, targetedFiles, writeItemFiles } from '../files.js';
7
7
  import { adaptItem, resolveLayout } from '../layout.js';
8
8
  import { color, log, plural } from '../output.js';
9
9
  import { installPackages, missingPackages, pinAngular } from '../packages.js';
@@ -70,6 +70,9 @@ export async function runAdd(options) {
70
70
  log.info(`Already installed: ${plan.skipped.join(', ')}. ${color.dim('Use --overwrite to reinstall.')}`);
71
71
  return;
72
72
  }
73
+ // Every item must be writable before anything is written or installed.
74
+ for (const item of plan.install)
75
+ assertWritable(root, item);
73
76
  // Existing files that differ: never write the rest of an item around them.
74
77
  let overwrite = options.overwrite;
75
78
  const conflicts = overwrite ? [] : plan.install.flatMap(item => conflictingFiles(root, item).map(file => `${file} (${item.name})`));
@@ -115,7 +118,8 @@ export async function runAdd(options) {
115
118
  log.warn(`${item.name} ships a Tailwind v3 config; Tailwind v4 reads config from CSS, so it was not applied.`);
116
119
  if (item.docs)
117
120
  docs.push([item.name, item.docs]);
118
- log.success(`${item.name} ${color.dim(`(${[plural(result.written.length, 'file'), ...extras].join(', ')})`)}`);
121
+ const summary = targetedFiles(item).length ? plural(result.written.length, 'file') : 'bundle';
122
+ log.success(`${item.name} ${color.dim(`(${[summary, ...extras].join(', ')})`)}`);
119
123
  }
120
124
  log.blank();
121
125
  log.info(`${color.green('✔')} Added ${plural(plan.install.length, 'component')}.`);
@@ -0,0 +1,251 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { readConfig, registryFor } from '../config.js';
4
+ import { targetedFiles } from '../files.js';
5
+ import { adaptItem, resolveLayout } from '../layout.js';
6
+ import { color } from '../output.js';
7
+ import { installArgs, missingPackages } from '../packages.js';
8
+ import { angularApps, declaredDependencies, detectPackageManager, findApp, readPackageJson, resolveTarget } from '../project.js';
9
+ import { FOUNDATION_ITEMS, namedRegistryFrom } from '../registry.js';
10
+ import { projectEnv } from '../env.js';
11
+ import { TAILWIND_POSTCSS, readPostcssConfig, relativeImport } from '../setup.js';
12
+ const MIN_NODE = [20, 12];
13
+ /** `3: a, b, c` or `202: a, b, c, d, e, f … and 196 more`. */
14
+ const summarize = (names, shown = 6) => `${names.length}: ${names.slice(0, shown).join(', ')}${names.length > shown ? ` … and ${names.length - shown} more` : ''}`;
15
+ /**
16
+ * Checks a project's uipkge setup, like `react-native doctor`. Never throws:
17
+ * every problem becomes a failed check with a fix, and later checks that
18
+ * depend on it are skipped rather than failing with noise.
19
+ */
20
+ export async function runChecks(cwd) {
21
+ const checks = [];
22
+ const push = (c) => (checks.push(c), c);
23
+ // ── Environment ──
24
+ const [major = 0, minor = 0] = process.versions.node.split('.').map(Number);
25
+ const nodeOk = major > MIN_NODE[0] || (major === MIN_NODE[0] && minor >= MIN_NODE[1]);
26
+ push({
27
+ section: 'Environment',
28
+ title: 'Node.js',
29
+ status: nodeOk ? 'pass' : 'fail',
30
+ detail: process.version,
31
+ fix: nodeOk ? undefined : `uipkge-ng needs Node ${MIN_NODE.join('.')} or newer.`,
32
+ });
33
+ // ── Project ──
34
+ const location = findApp(path.resolve(cwd));
35
+ if (!location) {
36
+ push({ section: 'Project', title: 'package.json', status: 'fail', detail: 'none found', fix: 'Run this inside your Angular project.' });
37
+ return checks;
38
+ }
39
+ if (location.ambiguous) {
40
+ push({
41
+ section: 'Project',
42
+ title: 'Angular app',
43
+ status: 'fail',
44
+ detail: `this workspace has ${location.ambiguous.length} apps: ${location.ambiguous.map(a => a.name).join(', ')}`,
45
+ fix: `Run it inside one app's folder, or pass it with \`-c ${path.relative(cwd, location.ambiguous[0].root) || '.'}\`.`,
46
+ });
47
+ return checks;
48
+ }
49
+ const { root, packageRoot } = location;
50
+ let pkg;
51
+ try {
52
+ pkg = await readPackageJson(packageRoot);
53
+ }
54
+ catch (error) {
55
+ push({ section: 'Project', title: 'package.json', status: 'fail', detail: error.message });
56
+ return checks;
57
+ }
58
+ const deps = declaredDependencies(pkg);
59
+ const angular = deps['@angular/core'];
60
+ push({
61
+ section: 'Project',
62
+ title: 'Angular',
63
+ status: angular ? 'pass' : 'fail',
64
+ detail: angular ? `${angular}${location.app ? ` (app: ${location.app.name})` : ''}` : 'no @angular/core in package.json',
65
+ fix: angular ? undefined : 'uipkge-ng works with Angular CLI apps.',
66
+ });
67
+ if (!angular)
68
+ return checks;
69
+ const nx = Boolean(deps['nx'] || existsSync(path.join(packageRoot, 'nx.json')));
70
+ if (nx)
71
+ push({ section: 'Project', title: 'Nx', status: 'fail', detail: 'Nx workspaces are not supported yet' });
72
+ const pm = detectPackageManager(packageRoot, pkg);
73
+ push({ section: 'Project', title: 'Package manager', status: 'pass', detail: pm });
74
+ // ── Configuration ──
75
+ let config = null;
76
+ try {
77
+ config = await readConfig(root);
78
+ }
79
+ catch (error) {
80
+ push({ section: 'Configuration', title: 'components.json', status: 'fail', detail: error.message, fix: 'Fix the JSON, or run `uipkge-ng init --overwrite`.' });
81
+ return checks;
82
+ }
83
+ if (!config) {
84
+ push({ section: 'Configuration', title: 'components.json', status: 'fail', detail: `not found in ${root}`, fix: 'Run `uipkge-ng init`.' });
85
+ return checks;
86
+ }
87
+ push({ section: 'Configuration', title: 'components.json', status: 'pass', detail: path.relative(cwd, path.join(root, 'components.json')) || 'components.json' });
88
+ const registry = registryFor(root, config, cwd);
89
+ let index = null;
90
+ try {
91
+ index = (await registry.index()).items;
92
+ push({ section: 'Configuration', title: 'Registry', status: 'pass', detail: `${registry.baseUrl} (${index.length} items)` });
93
+ }
94
+ catch (error) {
95
+ push({
96
+ section: 'Configuration',
97
+ title: 'Registry',
98
+ status: 'fail',
99
+ detail: `${registry.baseUrl}: ${error.message}`,
100
+ fix: 'Check your connection, "registryUrl" in components.json, or UIPKGE_REGISTRY_URL.',
101
+ });
102
+ }
103
+ const env = projectEnv(root);
104
+ for (const [ns, raw] of Object.entries(config.registries ?? {})) {
105
+ try {
106
+ namedRegistryFrom(ns, raw, env);
107
+ await registry.index(ns);
108
+ push({ section: 'Configuration', title: `Registry ${ns}`, status: 'pass' });
109
+ }
110
+ catch (error) {
111
+ push({
112
+ section: 'Configuration',
113
+ title: `Registry ${ns}`,
114
+ status: 'fail',
115
+ detail: error.message,
116
+ fix: `Check "${ns}" under "registries" in components.json; tokens come from the environment, .env.local or .env.`,
117
+ });
118
+ }
119
+ }
120
+ let layout = null;
121
+ try {
122
+ layout = resolveLayout(root, config.aliases);
123
+ push({ section: 'Configuration', title: 'Folders (aliases)', status: 'pass', detail: `ui → ${layout.dirs.ui}, lib → ${layout.dirs.lib}` });
124
+ }
125
+ catch (error) {
126
+ push({ section: 'Configuration', title: 'Folders (aliases)', status: 'fail', detail: error.message, fix: 'Map the aliases in tsconfig "paths", or run `uipkge-ng init --overwrite`.' });
127
+ }
128
+ // ── Styling ──
129
+ const tailwind = deps['tailwindcss'];
130
+ const tailwindMajor = Number(tailwind?.match(/\d+/)?.[0] ?? 0);
131
+ push({
132
+ section: 'Styling',
133
+ title: 'Tailwind CSS',
134
+ status: tailwindMajor >= 4 ? 'pass' : 'fail',
135
+ detail: tailwind ?? 'not installed',
136
+ fix: tailwindMajor >= 4 ? undefined : tailwind ? 'uipkge components need Tailwind CSS v4.' : 'Run `uipkge-ng init`.',
137
+ });
138
+ const analog = Boolean(deps['@analogjs/platform'] || deps['@analogjs/vite-plugin-angular']);
139
+ if (analog) {
140
+ push({ section: 'Styling', title: 'Tailwind build', status: 'warn', detail: 'Analog builds with Vite', fix: "Check vite.config.ts has `tailwindcss()` from '@tailwindcss/vite' in plugins." });
141
+ }
142
+ else {
143
+ const postcss = await readPostcssConfig(packageRoot);
144
+ const wired = postcss?.content.includes(TAILWIND_POSTCSS);
145
+ push({
146
+ section: 'Styling',
147
+ title: 'Tailwind build',
148
+ status: wired ? 'pass' : 'fail',
149
+ detail: wired ? `${postcss.file} uses ${TAILWIND_POSTCSS}` : postcss ? `${postcss.file} doesn't use ${TAILWIND_POSTCSS}` : 'no PostCSS config',
150
+ fix: wired ? undefined : `Add "${TAILWIND_POSTCSS}" to your PostCSS plugins, or run \`uipkge-ng init --overwrite\`.`,
151
+ });
152
+ }
153
+ const tokensFile = path.join(root, config.tokens);
154
+ if (!existsSync(tokensFile)) {
155
+ push({ section: 'Styling', title: 'Design tokens', status: 'fail', detail: `${config.tokens} is missing`, fix: 'Run `uipkge-ng init --overwrite`.' });
156
+ }
157
+ else {
158
+ // Loaded either by an @import in the global stylesheet or as its own entry in angular.json "styles".
159
+ const importPath = relativeImport(config.styles, config.tokens);
160
+ const stylesFile = path.join(root, config.styles);
161
+ const imported = existsSync(stylesFile) && new RegExp(`@import\\s+(url\\()?['"]${importPath.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&')}['"]`).test(readFileSync(stylesFile, 'utf8'));
162
+ const app = location.app ?? angularApps(packageRoot).find(a => a.root === root);
163
+ const registered = Boolean(app?.styles.some(s => path.resolve(root, s) === tokensFile));
164
+ push({
165
+ section: 'Styling',
166
+ title: 'Design tokens',
167
+ status: imported || registered ? 'pass' : 'fail',
168
+ detail: imported ? `${config.styles} imports ${importPath}` : registered ? `${config.tokens} is in angular.json "styles"` : `${config.tokens} isn't loaded`,
169
+ fix: imported || registered ? undefined : `Add \`@import '${importPath}';\` to ${config.styles}, or list ${config.tokens} under "styles" in angular.json.`,
170
+ });
171
+ }
172
+ if (layout) {
173
+ const utils = path.join(root, layout.dirs.lib, 'utils.ts');
174
+ const hasCn = existsSync(utils) && /export\s+(?:function|const|let)\s+cn\b|export\s*\{[^}]*\bcn\b[^}]*\}/.test(readFileSync(utils, 'utf8'));
175
+ push({
176
+ section: 'Styling',
177
+ title: 'cn() helper',
178
+ status: hasCn ? 'pass' : 'fail',
179
+ detail: hasCn ? path.relative(root, utils) : existsSync(utils) ? `${path.relative(root, utils)} doesn't export cn()` : `${path.relative(root, utils)} is missing`,
180
+ fix: hasCn ? undefined : 'Every component imports it. Run `uipkge-ng init --overwrite`, or add it (`uipkge-ng view utils`).',
181
+ });
182
+ }
183
+ // ── Components ──
184
+ if (!index || !layout) {
185
+ push({ section: 'Components', title: 'Installed components', status: 'skip', detail: 'needs the registry and folders above' });
186
+ return checks;
187
+ }
188
+ const installed = [];
189
+ const partial = [];
190
+ for (const item of index.filter(i => !FOUNDATION_ITEMS.has(i.name))) {
191
+ const files = targetedFiles(adaptItem(item, layout));
192
+ if (!files.length)
193
+ continue;
194
+ const present = files.filter(f => existsSync(resolveTarget(root, f.target))).length;
195
+ if (present === files.length)
196
+ installed.push(item);
197
+ else if (present > 0)
198
+ partial.push(item.name);
199
+ }
200
+ push({ section: 'Components', title: 'Installed components', status: 'pass', detail: installed.length ? summarize(installed.map(i => i.name)) : 'none yet' });
201
+ if (partial.length) {
202
+ push({
203
+ section: 'Components',
204
+ title: 'Incomplete components',
205
+ status: 'warn',
206
+ detail: `files missing from ${summarize(partial)}`,
207
+ fix: `Reinstall with \`uipkge-ng add ${partial.join(' ')} --overwrite\`.`,
208
+ });
209
+ }
210
+ const missing = missingPackages(installed.flatMap(i => i.dependencies ?? []), deps);
211
+ push({
212
+ section: 'Components',
213
+ title: 'Component packages',
214
+ status: missing.length ? 'fail' : 'pass',
215
+ detail: missing.length ? `not in package.json: ${missing.join(', ')}` : 'all declared',
216
+ fix: missing.length ? `Run \`${pm} ${installArgs(pm, missing, false).join(' ')}\`.` : undefined,
217
+ });
218
+ return checks;
219
+ }
220
+ const MARK = {
221
+ pass: color.green('✔'),
222
+ warn: color.yellow('!'),
223
+ fail: color.red('✖'),
224
+ skip: color.dim('-'),
225
+ };
226
+ export async function runDoctor(options) {
227
+ const checks = await runChecks(options.cwd);
228
+ const failed = checks.filter(c => c.status === 'fail').length;
229
+ const warned = checks.filter(c => c.status === 'warn').length;
230
+ if (failed)
231
+ process.exitCode = 1;
232
+ if (options.json) {
233
+ process.stdout.write(`${JSON.stringify({ ok: failed === 0, checks }, null, 2)}\n`);
234
+ return;
235
+ }
236
+ const width = Math.max(...checks.map(c => c.title.length)) + 2;
237
+ const out = [color.bold('uipkge-ng doctor')];
238
+ let section = '';
239
+ for (const c of checks) {
240
+ if (c.section !== section)
241
+ out.push('', color.bold(c.section)), (section = c.section);
242
+ out.push(` ${MARK[c.status]} ${c.title.padEnd(width)}${c.detail ? color.dim(c.detail) : ''}`);
243
+ if (c.fix && (c.status === 'fail' || c.status === 'warn'))
244
+ out.push(` ${' '.repeat(width)}${color.cyan('→')} ${c.fix}`);
245
+ }
246
+ const passed = checks.filter(c => c.status === 'pass').length;
247
+ out.push('', failed
248
+ ? color.red(`${failed} problem${failed === 1 ? '' : 's'} found`) + color.dim(` · ${passed} passed${warned ? ` · ${warned} warning${warned === 1 ? '' : 's'}` : ''}`)
249
+ : color.green('Everything looks good.') + color.dim(` ${passed} checks passed${warned ? ` · ${warned} warning${warned === 1 ? '' : 's'}` : ''}`), '');
250
+ process.stdout.write(out.join('\n'));
251
+ }
package/dist/files.js CHANGED
@@ -26,15 +26,24 @@ export function conflictingFiles(root, item) {
26
26
  return normalizeEol(readFileSync(abs, 'utf8')) === normalizeEol(f.content) ? [] : [path.relative(root, abs)];
27
27
  });
28
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) {
29
+ /**
30
+ * Throws if the item can't be written as-is. `add` checks every item before
31
+ * writing any, so a bad item never leaves a project half-installed. An item
32
+ * without files (a bundle such as `charts`) is fine: it only brings its
33
+ * dependencies.
34
+ */
35
+ export function assertWritable(root, item) {
36
+ for (const file of targetedFiles(item)) {
35
37
  if (typeof file.content !== 'string') {
36
- throw new UipkgeError(`Registry item \`${item.name}\` is missing the content of ${file.target}.`);
38
+ throw new UipkgeError(`Registry item \`${item.name}\` is missing the content of ${file.target}.`, 'Nothing was installed.');
37
39
  }
40
+ resolveTarget(root, file.target);
41
+ }
42
+ }
43
+ export async function writeItemFiles(root, item, overwrite) {
44
+ const result = { written: [], kept: [] };
45
+ assertWritable(root, item);
46
+ for (const file of targetedFiles(item)) {
38
47
  const abs = resolveTarget(root, file.target);
39
48
  const rel = path.relative(root, abs);
40
49
  if (existsSync(abs) && !overwrite) {
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { normalizeOptionalValues } from './args.js';
6
6
  import { runAdd } from './commands/add.js';
7
7
  import { runBuild } from './commands/build.js';
8
8
  import { runDiff } from './commands/diff.js';
9
+ import { runDoctor } from './commands/doctor.js';
9
10
  import { runInfo } from './commands/info.js';
10
11
  import { runInit } from './commands/init.js';
11
12
  import { runList } from './commands/list.js';
@@ -21,6 +22,7 @@ const COMMAND_HELP = [
21
22
  ['view <items...>', "show a component's details and files"],
22
23
  ['diff [item]', 'compare installed components with the registry'],
23
24
  ['info', 'show project, config and registry details'],
25
+ ['doctor', 'check the setup and explain how to fix problems'],
24
26
  ['build [file]', 'build your own registry (registry.json → public/r)'],
25
27
  ['mcp', 'run an MCP server so AI assistants can browse components'],
26
28
  ];
@@ -47,7 +49,7 @@ Options
47
49
  -s, --silent only print errors
48
50
  --category <c> only one category (list)
49
51
  -i, --installed only installed components (list)
50
- --json machine-readable output (list, view, info)
52
+ --json machine-readable output (list, view, info, doctor)
51
53
  --output <dir> where build writes the registry (default public/r)
52
54
  --debug show stack traces on errors
53
55
  -h, --help show this help
@@ -55,7 +57,7 @@ Options
55
57
 
56
58
  Registry: https://uipkge.dev/r/angular (override with components.json "registryUrl" or UIPKGE_REGISTRY_URL)
57
59
  `;
58
- const COMMANDS = new Set(['init', 'add', 'list', 'ls', 'view', 'diff', 'info', 'build', 'mcp']);
60
+ const COMMANDS = new Set(['init', 'add', 'list', 'ls', 'view', 'diff', 'info', 'doctor', 'build', 'mcp']);
59
61
  const OPTION_COMMANDS = {
60
62
  overwrite: ['init', 'add'],
61
63
  all: ['add'],
@@ -65,7 +67,7 @@ const OPTION_COMMANDS = {
65
67
  diff: ['add'],
66
68
  category: ['list', 'ls'],
67
69
  installed: ['list', 'ls'],
68
- json: ['list', 'ls', 'view', 'info'],
70
+ json: ['list', 'ls', 'view', 'info', 'doctor'],
69
71
  output: ['build'],
70
72
  };
71
73
  async function main(argv) {
@@ -135,6 +137,8 @@ async function main(argv) {
135
137
  return runDiff({ cwd, name: rest[0] });
136
138
  case 'info':
137
139
  return runInfo({ cwd, json: values.json, version: VERSION });
140
+ case 'doctor':
141
+ return runDoctor({ cwd, json: values.json });
138
142
  case 'build':
139
143
  return runBuild({ cwd, registry: rest[0] ?? 'registry.json', output: values.output ?? 'public/r' });
140
144
  case 'mcp':
package/dist/resolve.js CHANGED
@@ -28,17 +28,28 @@ transform = item => item) {
28
28
  const skipped = [];
29
29
  const dependencies = new Set();
30
30
  const devDependencies = new Set();
31
- async function visit(ref, direct) {
32
- // The default registry's foundation is init's job; a component depending on
33
- // `utils` must not reinstall it. `@acme/utils` is someone else's item.
31
+ // The default registry's foundation is init's job; a component depending on
32
+ // `utils` must not reinstall it. `@acme/utils` is someone else's item.
33
+ const isFoundation = (ref) => {
34
34
  const kind = refKind(ref);
35
35
  const ownRegistry = kind === 'name' || (kind === 'url' && ref.startsWith(`${registry.baseUrl}/`));
36
- if (ownRegistry && FOUNDATION_ITEMS.has(itemNameFromRef(ref)))
36
+ return ownRegistry && FOUNDATION_ITEMS.has(itemNameFromRef(ref));
37
+ };
38
+ // Start fetches early and in parallel; the registry caches them, so the ordered
39
+ // walk below just awaits ones already in flight. Errors surface there, not here.
40
+ const prefetch = (refs) => {
41
+ for (const ref of refs)
42
+ if (!isFoundation(ref))
43
+ registry.item(ref).catch(() => undefined);
44
+ };
45
+ async function visit(ref, direct) {
46
+ if (isFoundation(ref))
37
47
  return undefined;
38
48
  const item = transform(await registry.item(ref), direct);
39
49
  if (visited.has(item.name))
40
50
  return item.name;
41
51
  visited.add(item.name);
52
+ prefetch(item.registryDependencies ?? []);
42
53
  for (const dep of item.registryDependencies ?? [])
43
54
  await visit(dep, false);
44
55
  if (isInstalled(item) && !overwrite) {
@@ -51,6 +62,7 @@ transform = item => item) {
51
62
  return item.name;
52
63
  }
53
64
  const requested = [];
65
+ prefetch(names);
54
66
  for (const name of names) {
55
67
  const resolved = await visit(name, true);
56
68
  if (resolved && !requested.includes(resolved))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uipkge-ng",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Add uipkge Angular components to your project: copy the source, own the code.",
5
5
  "license": "MIT",
6
6
  "author": "Uday Adaka (https://uipkge.dev)",
@@ -25,7 +25,8 @@
25
25
  "uipkge-ng": "dist/index.js"
26
26
  },
27
27
  "files": [
28
- "dist"
28
+ "dist",
29
+ "CHANGELOG.md"
29
30
  ],
30
31
  "engines": {
31
32
  "node": ">=20.12"
@@ -34,7 +35,9 @@
34
35
  "build": "tsc -p tsconfig.build.json",
35
36
  "dev": "tsx src/index.ts",
36
37
  "test": "vitest run",
37
- "typecheck": "tsc --noEmit -p tsconfig.json"
38
+ "typecheck": "tsc --noEmit -p tsconfig.json",
39
+ "prepublishOnly": "npm run typecheck && npm test && npm run build",
40
+ "test:coverage": "vitest run --coverage"
38
41
  },
39
42
  "dependencies": {
40
43
  "@clack/prompts": "^1.8.1",
@@ -43,6 +46,7 @@
43
46
  },
44
47
  "devDependencies": {
45
48
  "@types/node": "^22.0.0",
49
+ "@vitest/coverage-v8": "3.2.7",
46
50
  "tsx": "^4.20.0",
47
51
  "typescript": "^5.9.3",
48
52
  "vitest": "^3.2.7"