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.
- package/LICENSE +21 -0
- package/README.md +25 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +692 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/client.d.ts +167 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +367 -0
- package/dist/client.js.map +1 -0
- package/dist/content.d.ts +126 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.js +57 -0
- package/dist/content.js.map +1 -0
- package/dist/index.d.ts +255 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1478 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/pkgm.d.ts +41 -0
- package/dist/lib/pkgm.d.ts.map +1 -0
- package/dist/lib/pkgm.js +76 -0
- package/dist/lib/pkgm.js.map +1 -0
- package/dist/schemas.d.ts +164 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +110 -0
- package/dist/schemas.js.map +1 -0
- package/dist/types.d.ts +274 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +81 -0
- package/src/components/NimbusHead.astro +161 -0
|
@@ -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
|