nimbus-docs 0.0.2

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,692 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { dirname, join, relative } from "node:path";
4
+ import mri from "mri";
5
+ import * as p from "@clack/prompts";
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
+ import { determineAgent } from "@vercel/detect-agent";
8
+
9
+ //#region src/cli/_registry.generated.ts
10
+ const REGISTRY_BASE_URL = "https://nimbus-docs.com/registry";
11
+ const BUNDLED_INDEX = {
12
+ "version": 1,
13
+ "items": {
14
+ "cn": {
15
+ "name": "cn",
16
+ "type": "registry:lib",
17
+ "title": "cn",
18
+ "description": "Tailwind-aware className merger built on clsx + tailwind-merge."
19
+ },
20
+ "accordion": {
21
+ "name": "accordion",
22
+ "type": "registry:ui",
23
+ "title": "Accordion",
24
+ "description": "Vertically stacked collapsible sections."
25
+ },
26
+ "aside": {
27
+ "name": "aside",
28
+ "type": "registry:ui",
29
+ "title": "Aside",
30
+ "description": "Generic boxed callout. Building block for Callout, Note, Warning."
31
+ },
32
+ "badge": {
33
+ "name": "badge",
34
+ "type": "registry:ui",
35
+ "title": "Badge",
36
+ "description": "Small status / category pill."
37
+ },
38
+ "banner": {
39
+ "name": "banner",
40
+ "type": "registry:ui",
41
+ "title": "Banner",
42
+ "description": "Site-wide dismissible announcement bar."
43
+ },
44
+ "breadcrumbs": {
45
+ "name": "breadcrumbs",
46
+ "type": "registry:ui",
47
+ "title": "Breadcrumbs",
48
+ "description": "Page-context navigation crumbs."
49
+ },
50
+ "callout": {
51
+ "name": "callout",
52
+ "type": "registry:ui",
53
+ "title": "Callout",
54
+ "description": "Inline note / tip / warning / danger / info card."
55
+ },
56
+ "card": {
57
+ "name": "card",
58
+ "type": "registry:ui",
59
+ "title": "Card",
60
+ "description": "Generic content card with optional title and footer."
61
+ },
62
+ "card-grid": {
63
+ "name": "card-grid",
64
+ "type": "registry:ui",
65
+ "title": "CardGrid",
66
+ "description": "Responsive grid layout for cards."
67
+ },
68
+ "code": {
69
+ "name": "code",
70
+ "type": "registry:ui",
71
+ "title": "Code",
72
+ "description": "Inline / block code wrapper. Re-exports Astro's built-in <Code> (Shiki)."
73
+ },
74
+ "code-group": {
75
+ "name": "code-group",
76
+ "type": "registry:ui",
77
+ "title": "CodeGroup",
78
+ "description": "Tabbed group of code blocks."
79
+ },
80
+ "collapsible": {
81
+ "name": "collapsible",
82
+ "type": "registry:ui",
83
+ "title": "Collapsible",
84
+ "description": "Headless show/hide primitive — building block for Accordion and Sidebar groups."
85
+ },
86
+ "dialog": {
87
+ "name": "dialog",
88
+ "type": "registry:ui",
89
+ "title": "Dialog",
90
+ "description": "Modal dialog with focus-trap and body-scroll lock."
91
+ },
92
+ "embed": {
93
+ "name": "embed",
94
+ "type": "registry:ui",
95
+ "title": "Embed",
96
+ "description": "Responsive iframe / video / external content wrapper."
97
+ },
98
+ "file-tree": {
99
+ "name": "file-tree",
100
+ "type": "registry:ui",
101
+ "title": "FileTree",
102
+ "description": "Render a directory tree as nested markup."
103
+ },
104
+ "frame": {
105
+ "name": "frame",
106
+ "type": "registry:ui",
107
+ "title": "Frame",
108
+ "description": "Decorative outer frame for screenshots and demos."
109
+ },
110
+ "layer-card": {
111
+ "name": "layer-card",
112
+ "type": "registry:ui",
113
+ "title": "LayerCard",
114
+ "description": "Stacked-card container with sticky header. Base for CodeGroup and PackageManagers."
115
+ },
116
+ "link-button": {
117
+ "name": "link-button",
118
+ "type": "registry:ui",
119
+ "title": "LinkButton",
120
+ "description": "Anchor styled as a button."
121
+ },
122
+ "link-card": {
123
+ "name": "link-card",
124
+ "type": "registry:ui",
125
+ "title": "LinkCard",
126
+ "description": "Card whose entire surface is a link."
127
+ },
128
+ "package-managers": {
129
+ "name": "package-managers",
130
+ "type": "registry:ui",
131
+ "title": "PackageManagers",
132
+ "description": "Tabbed install command block translated across npm / pnpm / yarn / bun."
133
+ },
134
+ "pagination": {
135
+ "name": "pagination",
136
+ "type": "registry:ui",
137
+ "title": "Pagination",
138
+ "description": "Prev / next page navigation."
139
+ },
140
+ "popover": {
141
+ "name": "popover",
142
+ "type": "registry:ui",
143
+ "title": "Popover",
144
+ "description": "Floating panel anchored to a trigger element."
145
+ },
146
+ "search": {
147
+ "name": "search",
148
+ "type": "registry:ui",
149
+ "title": "Search",
150
+ "description": "Command-palette search dialog with a provider seam. Defaults to Pagefind."
151
+ },
152
+ "sidebar": {
153
+ "name": "sidebar",
154
+ "type": "registry:ui",
155
+ "title": "Sidebar",
156
+ "description": "Docs sidebar with nested groups and active-link tracking."
157
+ },
158
+ "steps": {
159
+ "name": "steps",
160
+ "type": "registry:ui",
161
+ "title": "Steps",
162
+ "description": "Numbered ordered-list with vertical connectors."
163
+ },
164
+ "tabs": {
165
+ "name": "tabs",
166
+ "type": "registry:ui",
167
+ "title": "Tabs",
168
+ "description": "Tabbed content panels (manual + Starlight-compatible modes)."
169
+ },
170
+ "theme-toggle": {
171
+ "name": "theme-toggle",
172
+ "type": "registry:ui",
173
+ "title": "ThemeToggle",
174
+ "description": "Light / dark theme switcher button."
175
+ },
176
+ "toc": {
177
+ "name": "toc",
178
+ "type": "registry:ui",
179
+ "title": "TOC",
180
+ "description": "On-page table of contents with active-heading tracking."
181
+ },
182
+ "404-page": {
183
+ "name": "404-page",
184
+ "type": "registry:feature",
185
+ "title": "Custom 404 page",
186
+ "description": "Generate a brand-matched 404 page for the docs site."
187
+ },
188
+ "ai-native": {
189
+ "name": "ai-native",
190
+ "type": "registry:feature",
191
+ "title": "AI-native static surface",
192
+ "description": "Add llms.txt, markdown variants, robots.txt, and an AgentDirective to a Nimbus docs site."
193
+ },
194
+ "pagefind-search": {
195
+ "name": "pagefind-search",
196
+ "type": "registry:feature",
197
+ "title": "Pagefind search",
198
+ "description": "Add static Pagefind indexing and the Nimbus search dialog to an existing docs site."
199
+ }
200
+ }
201
+ };
202
+
203
+ //#endregion
204
+ //#region src/cli/pm.ts
205
+ /**
206
+ * Package-manager detection + install command helpers.
207
+ *
208
+ * Detection prefers lockfile presence in the user's cwd, then falls back
209
+ * to the `npm_config_user_agent` env var the active package manager sets
210
+ * when invoking the CLI. Finally falls back to `npm`.
211
+ */
212
+ const LOCKFILES = [
213
+ ["pnpm-lock.yaml", "pnpm"],
214
+ ["yarn.lock", "yarn"],
215
+ ["bun.lockb", "bun"],
216
+ ["bun.lock", "bun"],
217
+ ["package-lock.json", "npm"]
218
+ ];
219
+ function detectPackageManager(cwd) {
220
+ for (const [lockfile, pm] of LOCKFILES) if (existsSync(join(cwd, lockfile))) return pm;
221
+ const ua = process.env.npm_config_user_agent ?? "";
222
+ if (ua.startsWith("pnpm")) return "pnpm";
223
+ if (ua.startsWith("yarn")) return "yarn";
224
+ if (ua.startsWith("bun")) return "bun";
225
+ return "npm";
226
+ }
227
+ /**
228
+ * Command + args to install one or more new npm deps. Each PM picks the
229
+ * verb that both adds to package.json AND installs:
230
+ *
231
+ * npm install <deps...>
232
+ * pnpm add <deps...>
233
+ * yarn add <deps...>
234
+ * bun add <deps...>
235
+ */
236
+ function addCommand$1(pm, deps) {
237
+ if (deps.length === 0) throw new Error("addCommand called with empty deps");
238
+ switch (pm) {
239
+ case "npm": return {
240
+ bin: "npm",
241
+ args: ["install", ...deps]
242
+ };
243
+ case "pnpm": return {
244
+ bin: "pnpm",
245
+ args: ["add", ...deps]
246
+ };
247
+ case "yarn": return {
248
+ bin: "yarn",
249
+ args: ["add", ...deps]
250
+ };
251
+ case "bun": return {
252
+ bin: "bun",
253
+ args: ["add", ...deps]
254
+ };
255
+ }
256
+ }
257
+
258
+ //#endregion
259
+ //#region src/cli/component.ts
260
+ /**
261
+ * Component / utility installer.
262
+ *
263
+ * Walks the resolved list of items, writes each file (per-file overwrite
264
+ * prompt on conflict), then collects all npm `dependencies` across the
265
+ * tree and runs `<pm> add` once for the dedup'd set.
266
+ *
267
+ * File destination: `<cwd>/src/<path>`. The `path` field already encodes
268
+ * the directory layout (e.g. `components/ui/dialog/Dialog.astro`).
269
+ */
270
+ async function installComponents(items, options) {
271
+ const report = {
272
+ written: [],
273
+ skipped: [],
274
+ npmDepsInstalled: []
275
+ };
276
+ const srcDir = join(options.cwd, "src");
277
+ for (const item of items) {
278
+ const filePlans = item.files.map((file) => {
279
+ const targetAbs = join(srcDir, file.path);
280
+ return {
281
+ targetAbs,
282
+ targetRel: relative(options.cwd, targetAbs),
283
+ content: file.content,
284
+ exists: existsSync(targetAbs)
285
+ };
286
+ });
287
+ const conflicts = filePlans.filter((f) => f.exists);
288
+ if (conflicts.length > 0 && !options.yes) {
289
+ const total = filePlans.length;
290
+ const message = conflicts.length === total ? `${item.name} is already installed (${total} file${total === 1 ? "" : "s"}). Overwrite?` : `${item.name} is partially installed (${conflicts.length} of ${total} file${total === 1 ? "" : "s"} present). Overwrite all?`;
291
+ const choice = await p.select({
292
+ message,
293
+ options: [
294
+ {
295
+ value: "overwrite",
296
+ label: "Overwrite — replace existing files"
297
+ },
298
+ {
299
+ value: "skip",
300
+ label: "Skip — leave files as-is"
301
+ },
302
+ {
303
+ value: "cancel",
304
+ label: "Cancel install"
305
+ }
306
+ ],
307
+ initialValue: "overwrite"
308
+ });
309
+ if (p.isCancel(choice) || choice === "cancel") {
310
+ p.cancel("Cancelled.");
311
+ process.exit(0);
312
+ }
313
+ if (choice === "skip") {
314
+ report.skipped.push(item.name);
315
+ continue;
316
+ }
317
+ }
318
+ for (const plan of filePlans) {
319
+ mkdirSync(dirname(plan.targetAbs), { recursive: true });
320
+ writeFileSync(plan.targetAbs, plan.content);
321
+ report.written.push(plan.targetRel);
322
+ }
323
+ }
324
+ const allDeps = /* @__PURE__ */ new Set();
325
+ for (const item of items) for (const dep of item.dependencies) allDeps.add(dep);
326
+ if (allDeps.size > 0) {
327
+ const newDeps = filterAlreadyInstalled(options.cwd, [...allDeps]);
328
+ if (newDeps.length > 0) {
329
+ const pm = detectPackageManager(options.cwd);
330
+ const { bin, args } = addCommand$1(pm, newDeps);
331
+ const spinner = p.spinner();
332
+ spinner.start(`${pm} add ${newDeps.join(" ")}`);
333
+ try {
334
+ await runCommand(bin, args, options.cwd);
335
+ spinner.stop(`Installed ${newDeps.length} dep${newDeps.length === 1 ? "" : "s"}.`);
336
+ report.npmDepsInstalled = newDeps;
337
+ } catch (err) {
338
+ spinner.stop("Dependency install failed.");
339
+ p.log.warn(`Could not install ${newDeps.join(", ")}. Run \`${bin} ${args.join(" ")}\` manually.`);
340
+ }
341
+ }
342
+ }
343
+ return report;
344
+ }
345
+ /**
346
+ * Filter out deps already present in `dependencies` or `devDependencies`
347
+ * of the user's package.json. If package.json is missing, returns all.
348
+ */
349
+ function filterAlreadyInstalled(cwd, deps) {
350
+ const pkgPath = join(cwd, "package.json");
351
+ if (!existsSync(pkgPath)) return deps;
352
+ try {
353
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
354
+ const installed = new Set([...Object.keys(pkg.dependencies ?? {}), ...Object.keys(pkg.devDependencies ?? {})]);
355
+ return deps.filter((d) => !installed.has(d));
356
+ } catch {
357
+ return deps;
358
+ }
359
+ }
360
+ function runCommand(bin, args, cwd) {
361
+ return new Promise((resolveP, rejectP) => {
362
+ const child = spawn(bin, args, {
363
+ cwd,
364
+ stdio: [
365
+ "ignore",
366
+ "ignore",
367
+ "inherit"
368
+ ]
369
+ });
370
+ child.on("close", (code) => code === 0 ? resolveP() : rejectP(/* @__PURE__ */ new Error(`${bin} ${args.join(" ")} exited ${code}`)));
371
+ child.on("error", rejectP);
372
+ });
373
+ }
374
+
375
+ //#endregion
376
+ //#region src/cli/dotenv.ts
377
+ /**
378
+ * Tiny .env loader — no dependency.
379
+ *
380
+ * Reads `.env` from the user's cwd at CLI startup and sets any KEY=VALUE
381
+ * pairs into `process.env` IF the variable isn't already set (so a shell-
382
+ * provided env always wins over the file). Supports the basic cases:
383
+ *
384
+ * KEY=value
385
+ * KEY="quoted value"
386
+ * KEY='quoted value'
387
+ * # comments
388
+ *
389
+ * Used so `examples/local/.env` can carry `NIMBUS_REGISTRY_URL=...` without
390
+ * the user having to prefix every CLI invocation.
391
+ */
392
+ function loadDotenv(cwd) {
393
+ const path = join(cwd, ".env");
394
+ if (!existsSync(path)) return;
395
+ let raw;
396
+ try {
397
+ raw = readFileSync(path, "utf8");
398
+ } catch {
399
+ return;
400
+ }
401
+ for (const rawLine of raw.split(/\r?\n/)) {
402
+ const line = rawLine.trim();
403
+ if (!line || line.startsWith("#")) continue;
404
+ const eq = line.indexOf("=");
405
+ if (eq <= 0) continue;
406
+ const key = line.slice(0, eq).trim();
407
+ if (!/^[A-Z_][A-Z0-9_]*$/i.test(key)) continue;
408
+ let value = line.slice(eq + 1).trim();
409
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
410
+ if (process.env[key] === void 0) process.env[key] = value;
411
+ }
412
+ }
413
+
414
+ //#endregion
415
+ //#region src/cli/resolver.ts
416
+ /**
417
+ * Registry resolver.
418
+ *
419
+ * Two entry points:
420
+ *
421
+ * - `resolveComponentTree(slug)` walks `registryDependencies` transitively
422
+ * and returns a flat ordered list of components/utilities to install
423
+ * (dependencies first, root last). Cycles are detected as repeated
424
+ * visits and skipped.
425
+ *
426
+ * - `fetchFeatureMarkdown(slug)` returns the raw markdown for an
427
+ * agent-handoff feature; the caller decides what to do with it.
428
+ *
429
+ * The base URL for hosted artifacts is read from the bundled index, with
430
+ * an `NIMBUS_REGISTRY_URL` env override for local development.
431
+ */
432
+ /**
433
+ * Read the registry base URL on every call so `.env` files loaded after
434
+ * module-import time (see cli/dotenv.ts) are picked up. The cost is
435
+ * negligible — string interpolation of an env var.
436
+ */
437
+ function getBaseUrl() {
438
+ return (process.env.NIMBUS_REGISTRY_URL ?? REGISTRY_BASE_URL).replace(/\/$/, "");
439
+ }
440
+ function getIndexEntry(slug) {
441
+ return BUNDLED_INDEX.items[slug];
442
+ }
443
+ function listEntries(filter) {
444
+ const all = Object.values(BUNDLED_INDEX.items);
445
+ if (!filter?.type) return all;
446
+ return all.filter((e) => e.type === filter.type);
447
+ }
448
+ async function httpGet(url) {
449
+ let res;
450
+ try {
451
+ res = await fetch(url);
452
+ } catch (err) {
453
+ const cause = err.message;
454
+ throw new Error(`Could not reach the registry at ${url}.\n Underlying error: ${cause}\n\n Things to try:\n - Is the registry server running? Start it with \`pnpm local\` (in the monorepo root).\n - Override the URL: NIMBUS_REGISTRY_URL=https://example.com nimbus add ...\n - Check the value in your project's .env file.`);
455
+ }
456
+ if (!res.ok) throw new Error(`Registry returned ${res.status} ${res.statusText} for ${url}. The server is up but doesn't know about this slug — check \`nimbus list\` for valid names.`);
457
+ return res;
458
+ }
459
+ async function fetchComponent(slug) {
460
+ return await (await httpGet(`${getBaseUrl()}/components/${slug}.json`)).json();
461
+ }
462
+ async function fetchFeatureMarkdown(slug) {
463
+ return await (await httpGet(`${getBaseUrl()}/features/${slug}.md`)).text();
464
+ }
465
+ /**
466
+ * Depth-first walk of registryDependencies. Returns items in install order
467
+ * (deps before dependents), deduplicated by slug.
468
+ */
469
+ async function resolveComponentTree(rootSlug) {
470
+ const visited = /* @__PURE__ */ new Set();
471
+ const ordered = [];
472
+ async function visit(slug) {
473
+ if (visited.has(slug)) return;
474
+ visited.add(slug);
475
+ const item = await fetchComponent(slug);
476
+ for (const dep of item.registryDependencies) await visit(dep);
477
+ ordered.push(item);
478
+ }
479
+ await visit(rootSlug);
480
+ return ordered;
481
+ }
482
+
483
+ //#endregion
484
+ //#region src/cli/feature.ts
485
+ /**
486
+ * Feature installer — Flue-style agent-handoff.
487
+ *
488
+ * Same rule Flue uses for `flue add`: if `--print` is set OR
489
+ * `determineAgent()` says the CLI is running inside a known coding agent,
490
+ * the markdown is piped to stdout for the agent to consume. Otherwise we
491
+ * print human-friendly instructions on stderr telling the user exactly
492
+ * how to pipe the output to their agent of choice.
493
+ *
494
+ * No picker, no clipboard mode — the printed pipe commands cover both.
495
+ */
496
+ async function installFeature(slug, options) {
497
+ const markdown = await fetchFeatureMarkdown(slug);
498
+ const detected = await determineAgent().catch(() => ({ isAgent: false }));
499
+ if (options.print || detected.isAgent === true) {
500
+ process.stdout.write(markdown);
501
+ if (!markdown.endsWith("\n")) process.stdout.write("\n");
502
+ return;
503
+ }
504
+ printHumanInstructions(slug);
505
+ }
506
+ /**
507
+ * Stderr-only. We don't put this on stdout because if the user pipes our
508
+ * output anywhere by accident, only the markdown should reach the agent.
509
+ *
510
+ * Formatting mirrors Flue's `printHumanInstructions` 1:1 — agents listed
511
+ * with a blank line between the "first-tier" CLIs (claude/codex/cursor-agent)
512
+ * and the rest (opencode/pi).
513
+ */
514
+ function printHumanInstructions(slug) {
515
+ const cmd = `nimbus add ${slug}`;
516
+ const stream = process.stderr;
517
+ stream.write(`${cmd}\n\n`);
518
+ stream.write("To install this feature, pipe it to your coding agent:\n\n");
519
+ stream.write(` ${cmd} --print | claude\n`);
520
+ stream.write(` ${cmd} --print | codex\n`);
521
+ stream.write(` ${cmd} --print | cursor-agent\n\n`);
522
+ stream.write(` ${cmd} --print | opencode\n`);
523
+ stream.write(` ${cmd} --print | pi\n`);
524
+ stream.write("Or paste this prompt into any agent:\n\n");
525
+ stream.write(` Run "${cmd} --print" and follow the instructions.\n`);
526
+ }
527
+
528
+ //#endregion
529
+ //#region src/cli/index.ts
530
+ /**
531
+ * `nimbus` CLI entry.
532
+ *
533
+ * Surface:
534
+ *
535
+ * nimbus → list (table of installable items)
536
+ * nimbus list → list
537
+ * nimbus list --type ui|lib|feature
538
+ * nimbus add → list
539
+ * nimbus add <slug> → install (component path or feature path)
540
+ * nimbus add <slug> --yes → component: skip overwrite prompts
541
+ * nimbus add <slug> --print → feature: print markdown to stdout (skip detect)
542
+ *
543
+ * Feature behavior mirrors Flue's `add` command: print markdown to stdout
544
+ * iff `--print` OR an agent is detected; otherwise print human-friendly
545
+ * pipe instructions to stderr.
546
+ *
547
+ * The bundled index makes `list` (and `add` with no slug) work offline.
548
+ * Per-item content is fetched from `REGISTRY_BASE_URL` only when actually
549
+ * installing a slug — override via `NIMBUS_REGISTRY_URL` for local dev.
550
+ */
551
+ loadDotenv(process.cwd());
552
+ const HELP = `
553
+ Usage: nimbus <command> [args]
554
+
555
+ Commands:
556
+ list [--type ui|lib|feature] List available registry items
557
+ add Same as \`list\`
558
+ add <slug> Install a component or hand off a feature
559
+
560
+ Flags:
561
+ --yes, -y Component: overwrite conflicts without prompting
562
+ --print Feature: print markdown to stdout (skip agent detect)
563
+ --type <ui|lib|feature> \`list\`: filter by type
564
+ --help, -h
565
+ --version, -v
566
+
567
+ Examples:
568
+ nimbus add dialog # component: resolve + install
569
+ nimbus add 404-page # feature: detect agent or print
570
+ # pipe instructions for humans
571
+ nimbus add 404-page --print | claude # explicit pipe to claude
572
+ nimbus add 404-page --print | codex # …or any other agent
573
+ `;
574
+ async function main() {
575
+ const args = mri(process.argv.slice(2), {
576
+ boolean: [
577
+ "yes",
578
+ "print",
579
+ "help",
580
+ "version"
581
+ ],
582
+ string: ["type"],
583
+ alias: {
584
+ y: "yes",
585
+ h: "help",
586
+ v: "version"
587
+ }
588
+ });
589
+ if (args.help) {
590
+ process.stdout.write(HELP);
591
+ return;
592
+ }
593
+ if (args.version) {
594
+ process.stdout.write(`0.0.2\n`);
595
+ return;
596
+ }
597
+ const [command, slug] = args._;
598
+ if (command === "list" || command === "add" && !slug || !command) {
599
+ listCommand(args.type);
600
+ return;
601
+ }
602
+ if (command === "add") {
603
+ await addCommand(slug, {
604
+ yes: args.yes,
605
+ print: args.print
606
+ });
607
+ return;
608
+ }
609
+ p.log.error(`Unknown command: \`${command}\`. Try \`nimbus --help\`.`);
610
+ process.exit(1);
611
+ }
612
+ function listCommand(typeFilter) {
613
+ const typeMap = {
614
+ ui: "registry:ui",
615
+ lib: "registry:lib",
616
+ feature: "registry:feature"
617
+ };
618
+ const filter = typeFilter && typeFilter in typeMap ? { type: typeMap[typeFilter] } : void 0;
619
+ if (typeFilter && !(typeFilter in typeMap)) {
620
+ p.log.error(`Unknown --type "${typeFilter}". Valid: ui, lib, feature.`);
621
+ process.exit(1);
622
+ }
623
+ const items = listEntries(filter);
624
+ if (items.length === 0) {
625
+ p.log.info("No items match the filter.");
626
+ return;
627
+ }
628
+ const grouped = {
629
+ "registry:ui": [],
630
+ "registry:lib": [],
631
+ "registry:feature": []
632
+ };
633
+ for (const item of items) grouped[item.type].push(item);
634
+ const labels = {
635
+ "registry:ui": "Components",
636
+ "registry:lib": "Utilities",
637
+ "registry:feature": "Features"
638
+ };
639
+ const widths = items.reduce((m, i) => Math.max(m, i.name.length), 0);
640
+ process.stdout.write("\n");
641
+ for (const [type, label] of Object.entries(labels)) {
642
+ const group = grouped[type];
643
+ if (!group || group.length === 0) continue;
644
+ process.stdout.write(` ${label}\n`);
645
+ for (const item of group) process.stdout.write(` ${item.name.padEnd(widths + 2)}${item.description}\n`);
646
+ process.stdout.write("\n");
647
+ }
648
+ process.stdout.write(` Install: nimbus add <name> · ${items.length} item${items.length === 1 ? "" : "s"}\n\n`);
649
+ }
650
+ async function addCommand(slug, flags) {
651
+ const entry = getIndexEntry(slug);
652
+ if (!entry) {
653
+ p.log.error(`Unknown registry item: \`${slug}\`. Try \`nimbus list\` to see what's available.`);
654
+ process.exit(1);
655
+ }
656
+ if (entry.type === "registry:feature") {
657
+ await installFeature(slug, { print: flags.print });
658
+ return;
659
+ }
660
+ p.intro(`nimbus add ${slug}`);
661
+ p.log.info(`${entry.title} — ${entry.description}`);
662
+ const spinner = p.spinner();
663
+ spinner.start("Resolving dependencies");
664
+ let items;
665
+ try {
666
+ items = await resolveComponentTree(slug);
667
+ spinner.stop(`Resolved ${items.length} item${items.length === 1 ? "" : "s"}.`);
668
+ } catch (err) {
669
+ spinner.stop("Failed to resolve.");
670
+ p.log.error(err.message);
671
+ process.exit(1);
672
+ }
673
+ if (items.length > 1) p.log.message("Install order:\n " + items.map((i) => i.name).join(" → "));
674
+ const report = await installComponents(items, {
675
+ cwd: process.cwd(),
676
+ yes: flags.yes
677
+ });
678
+ const lines = [];
679
+ if (report.written.length > 0) lines.push(`✓ Wrote ${report.written.length} file${report.written.length === 1 ? "" : "s"}`);
680
+ if (report.skipped.length > 0) lines.push(`↷ Skipped: ${report.skipped.join(", ")}`);
681
+ if (report.npmDepsInstalled.length > 0) lines.push(`+ Installed ${report.npmDepsInstalled.length} npm dep${report.npmDepsInstalled.length === 1 ? "" : "s"}: ${report.npmDepsInstalled.join(", ")}`);
682
+ if (lines.length === 0) p.outro("Nothing to do.");
683
+ else p.outro(lines.join("\n"));
684
+ }
685
+ main().catch((err) => {
686
+ p.log.error(`${err.message}`);
687
+ process.exit(1);
688
+ });
689
+
690
+ //#endregion
691
+ export { };
692
+ //# sourceMappingURL=index.js.map