cascivo 0.1.6 → 0.3.4

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/dist/index.mjs CHANGED
@@ -1,70 +1,17 @@
1
1
  #!/usr/bin/env node
2
- import { t as fetchJson } from "./http-CLkdTpxD.mjs";
2
+ import { a as detectPackageManager, c as __exportAll, n as DEFAULT_CONFIG$1, o as installCommand, r as THEMES, s as loadConfig, t as CASCIVO_HOST } from "./config-BsyJbMvD.mjs";
3
+ import { n as resolveOutputPath, r as writeFileSafe, t as readFileSafe } from "./fs-m7ZvuBBm.mjs";
4
+ import { r as fetchTextRetry, t as fetchJson } from "./http-CJZ5W0Fa.mjs";
5
+ import { a as updateLockEntry, c as fileName, i as sha256, l as findComponent, n as createLock, o as writeLock, r as readLock, s as fetchRegistry, t as checkPeerVersions } from "./peer-versions-B2bCgko-.mjs";
6
+ import { createRequire } from "node:module";
3
7
  import { argv, stdin, stdout } from "node:process";
4
8
  import { pathToFileURL } from "node:url";
5
- import { spawnSync } from "node:child_process";
6
9
  import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
7
10
  import { basename, dirname, join, resolve } from "node:path";
8
- import { mkdir, readFile, writeFile } from "node:fs/promises";
9
- import { createHash } from "node:crypto";
11
+ import { spawnSync } from "node:child_process";
12
+ import { readFile } from "node:fs/promises";
13
+ import { asTemplateMeta, buildRegistry, isTemplateItem, parseLegacyRegistry, validateIndex, validateTemplate } from "@cascivo/registry";
10
14
  import { createInterface } from "node:readline/promises";
11
- import { buildRegistry, parseLegacyRegistry, validateIndex } from "@cascivo/registry";
12
- //#region src/utils/config.ts
13
- const DEFAULT_CONFIG = {
14
- registry: "https://cascivo.com/registry.json",
15
- outputDir: "src/components/ui",
16
- theme: "light"
17
- };
18
- const CONFIG_FILES = [
19
- "cascivo.config.ts",
20
- "cascivo.config.js",
21
- "cascivo.config.mjs"
22
- ];
23
- /** Apply defaults over a (possibly partial) user config object. */
24
- function resolveConfig(partial) {
25
- return {
26
- ...DEFAULT_CONFIG,
27
- ...partial
28
- };
29
- }
30
- /** Let env vars override config — used by the MCP server to pass `outputDir`. */
31
- function applyEnvOverrides(config, env = process.env) {
32
- return {
33
- ...config,
34
- ...env.CASCIVO_REGISTRY ? { registry: env.CASCIVO_REGISTRY } : {},
35
- ...env.CASCIVO_OUTPUT_DIR ? { outputDir: env.CASCIVO_OUTPUT_DIR } : {}
36
- };
37
- }
38
- /**
39
- * Locate and load `cascivo.config.{ts,js,mjs}` from `cwd`. Falls back to the
40
- * default config when no file is found or the file cannot be loaded. Env vars
41
- * (`CASCIVO_REGISTRY`, `CASCIVO_OUTPUT_DIR`) take precedence.
42
- */
43
- async function loadConfig(cwd = process.cwd()) {
44
- for (const file of CONFIG_FILES) {
45
- const path = join(cwd, file);
46
- if (!existsSync(path)) continue;
47
- try {
48
- const mod = await import(pathToFileURL(path).href);
49
- return applyEnvOverrides(resolveConfig(mod.default ?? mod));
50
- } catch {
51
- return applyEnvOverrides(resolveConfig(null));
52
- }
53
- }
54
- return applyEnvOverrides(resolveConfig(null));
55
- }
56
- /** Detect the package manager in use from lock files, defaulting to npm. */
57
- function detectPackageManager(cwd = process.cwd()) {
58
- if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
59
- if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
60
- if (existsSync(join(cwd, "bun.lockb"))) return "bun";
61
- return "npm";
62
- }
63
- /** The install subcommand each package manager uses to add dependencies. */
64
- function installCommand(pm, packages) {
65
- return [pm, [pm === "npm" ? "install" : "add", ...packages]];
66
- }
67
- //#endregion
68
15
  //#region src/utils/exec.ts
69
16
  /**
70
17
  * Install npm packages using the detected package manager, inheriting stdio so
@@ -85,153 +32,6 @@ function installPackages(packages, cwd = process.cwd()) {
85
32
  return true;
86
33
  }
87
34
  //#endregion
88
- //#region src/utils/fs.ts
89
- /** Resolve where a component file should be written. */
90
- function resolveOutputPath(outputDir, component, file, cwd = process.cwd()) {
91
- return join(resolve(cwd, outputDir), component, file);
92
- }
93
- /** Write a file, creating parent directories as needed. */
94
- async function writeFileSafe(path, content) {
95
- await mkdir(dirname(path), { recursive: true });
96
- await writeFile(path, content, "utf8");
97
- }
98
- /** Read a file, returning null if it does not exist. */
99
- async function readFileSafe(path) {
100
- try {
101
- return await readFile(path, "utf8");
102
- } catch {
103
- return null;
104
- }
105
- }
106
- //#endregion
107
- //#region src/utils/registry.ts
108
- function asStringArray(value) {
109
- return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
110
- }
111
- /** Validate and normalize raw JSON into a typed Registry. Throws on bad shape. */
112
- function parseRegistry(raw) {
113
- if (typeof raw !== "object" || raw === null) throw new Error("Invalid registry: expected an object");
114
- const obj = raw;
115
- if (!Array.isArray(obj.components)) throw new Error("Invalid registry: \"components\" must be an array");
116
- const components = obj.components.map((entry, i) => {
117
- if (typeof entry !== "object" || entry === null) throw new Error(`Invalid registry: component at index ${i} is not an object`);
118
- const c = entry;
119
- if (typeof c.name !== "string") throw new Error(`Invalid registry: component at index ${i} is missing "name"`);
120
- const rawType = c.type;
121
- const type = rawType === "component" || rawType === "layout" || rawType === "block" || rawType === "chart" ? rawType : void 0;
122
- const rawMeta = c.meta;
123
- const metaName = typeof rawMeta === "object" && rawMeta !== null && typeof rawMeta.name === "string" ? rawMeta.name : c.name;
124
- const result = {
125
- name: c.name,
126
- description: typeof c.description === "string" ? c.description : "",
127
- category: typeof c.category === "string" ? c.category : "",
128
- version: typeof c.version === "string" ? c.version : "0.0.0",
129
- files: asStringArray(c.files),
130
- dependencies: asStringArray(c.dependencies),
131
- tags: asStringArray(c.tags),
132
- meta: { name: metaName }
133
- };
134
- if (type !== void 0) result.type = type;
135
- if (typeof c.install === "string") result.install = c.install;
136
- return result;
137
- });
138
- const blocks = Array.isArray(obj.blocks) ? obj.blocks.flatMap((entry) => {
139
- if (typeof entry !== "object" || entry === null) return [];
140
- const b = entry;
141
- if (typeof b.name !== "string") return [];
142
- const rawScreenshot = typeof b.screenshot === "object" && b.screenshot !== null ? b.screenshot : {};
143
- return [{
144
- name: b.name,
145
- type: "block",
146
- displayName: typeof b.displayName === "string" ? b.displayName : b.name,
147
- description: typeof b.description === "string" ? b.description : "",
148
- category: typeof b.category === "string" ? b.category : "",
149
- version: typeof b.version === "string" ? b.version : "0.0.0",
150
- files: asStringArray(b.files),
151
- dependencies: asStringArray(b.dependencies),
152
- tags: asStringArray(b.tags),
153
- screenshot: {
154
- light: typeof rawScreenshot.light === "string" ? rawScreenshot.light : "",
155
- dark: typeof rawScreenshot.dark === "string" ? rawScreenshot.dark : ""
156
- }
157
- }];
158
- }) : [];
159
- return {
160
- version: typeof obj.version === "string" ? obj.version : "0.0.0",
161
- generatedAt: typeof obj.generatedAt === "string" ? obj.generatedAt : "",
162
- components,
163
- blocks
164
- };
165
- }
166
- /** Fetch and parse the registry from a URL. */
167
- async function fetchRegistry(url) {
168
- const res = await fetch(url);
169
- if (!res.ok) throw new Error(`Failed to fetch registry from ${url}: ${res.status} ${res.statusText}`);
170
- return parseRegistry(await res.json());
171
- }
172
- /**
173
- * Find a component by name (case-insensitive, full name or unambiguous suffix).
174
- *
175
- * Examples:
176
- * "layout/app-shell" → exact match on full name
177
- * "app-shell" → suffix match if exactly one entry ends with "/app-shell"
178
- * "button" → exact match (no slash, no suffix ambiguity)
179
- */
180
- function findComponent(registry, name) {
181
- const target = name.toLowerCase();
182
- if (target.startsWith("block/")) {
183
- const blockName = target.slice(6);
184
- const block = (registry.blocks ?? []).find((b) => b.name.toLowerCase() === blockName);
185
- if (!block) throw new Error(`Block "${blockName}" not found in registry.`);
186
- return block;
187
- }
188
- const exact = registry.components.find((c) => c.name.toLowerCase() === target);
189
- if (exact) return exact;
190
- const suffix = `/${target}`;
191
- const matches = registry.components.filter((c) => c.name.toLowerCase().endsWith(suffix));
192
- if (matches.length === 1) return matches[0];
193
- }
194
- /** Extract the file name from a registry file URL. */
195
- function fileName(url) {
196
- const stripped = url.split(/[?#]/)[0] ?? url;
197
- return stripped.split("/").pop() || stripped;
198
- }
199
- //#endregion
200
- //#region src/utils/lock.ts
201
- function sha256(content) {
202
- return `sha256-${createHash("sha256").update(content).digest("hex")}`;
203
- }
204
- const LOCK_FILENAME = "cascade.lock";
205
- async function readLock(cwd = process.cwd()) {
206
- const path = join(cwd, LOCK_FILENAME);
207
- if (!existsSync(path)) return null;
208
- try {
209
- const raw = JSON.parse(await readFile(path, "utf8"));
210
- if (typeof raw !== "object" || raw === null) return null;
211
- return raw;
212
- } catch {
213
- return null;
214
- }
215
- }
216
- async function writeLock(lock, cwd = process.cwd()) {
217
- await writeFile(join(cwd, LOCK_FILENAME), `${JSON.stringify(lock, null, 2)}\n`, "utf8");
218
- }
219
- function createLock() {
220
- return {
221
- lockVersion: 1,
222
- items: {}
223
- };
224
- }
225
- function updateLockEntry(lock, name, entry) {
226
- return {
227
- ...lock,
228
- items: {
229
- ...lock.items,
230
- [name]: entry
231
- }
232
- };
233
- }
234
- //#endregion
235
35
  //#region src/utils/resolve.ts
236
36
  const GITHUB_RE = /^([^/]+)\/([^/]+)\/([^#]+)(?:#(.+))?$/;
237
37
  function expandEnv(str, env = process.env) {
@@ -309,7 +109,7 @@ async function resolveItemUrl(addr, config, env = process.env, resolveNamespaceH
309
109
  const hookResult = await resolveNamespaceHook(ns);
310
110
  if (hookResult) return { url: resolveNamespaceUrl(ns, name, hookResult, env) };
311
111
  }
312
- if (ns === "@cascade") return { url: `https://cascivo.com/r/${name}.json` };
112
+ if (ns === "@cascivo" || ns === "@cascade") return { url: `${CASCIVO_HOST}/r/${name}.json` };
313
113
  throw new Error(`Unknown namespace "${ns}". Add it to registries in cascivo.config.ts or check the directory at cascivo.com`);
314
114
  }
315
115
  }
@@ -345,7 +145,7 @@ async function resolveClosure(specs, config, env = process.env, resolveNamespace
345
145
  }
346
146
  //#endregion
347
147
  //#region src/utils/directory.ts
348
- const DIRECTORY_URL = "https://cascivo.com/r/registries.json";
148
+ const DIRECTORY_URL = `${CASCIVO_HOST}/r/registries.json`;
349
149
  async function resolveFromDirectory(ns, fetchFn) {
350
150
  try {
351
151
  let data;
@@ -365,12 +165,95 @@ async function resolveFromDirectory(ns, fetchFn) {
365
165
  }
366
166
  //#endregion
367
167
  //#region src/commands/add.ts
168
+ var add_exports = /* @__PURE__ */ __exportAll({
169
+ LAYOUT_ALIASES: () => LAYOUT_ALIASES,
170
+ add: () => add,
171
+ isThirdParty: () => isThirdParty,
172
+ resolveBareClosure: () => resolveBareClosure,
173
+ resolveTemplateTarget: () => resolveTemplateTarget
174
+ });
368
175
  /** Dependencies always present in a cascade project — never auto-installed. */
369
176
  const ASSUMED_DEPS = new Set(["react", "react-dom"]);
370
- async function fetchText$1(url) {
371
- const res = await fetch(url);
372
- if (!res.ok) throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`);
373
- return res.text();
177
+ /**
178
+ * Copied components read `--cascivo-*` custom properties; without
179
+ * @cascivo/tokens or @cascivo/themes imported somewhere, they render unstyled.
180
+ * Print the wiring once after a successful install if neither is a dependency.
181
+ */
182
+ function hintThemesIfMissing(cwd) {
183
+ let deps = {};
184
+ try {
185
+ const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
186
+ deps = {
187
+ ...pkg.dependencies,
188
+ ...pkg.devDependencies
189
+ };
190
+ } catch {}
191
+ if (deps["@cascivo/themes"] !== void 0 || deps["@cascivo/tokens"] !== void 0) return;
192
+ console.log("\nStyling: cascivo components read --cascivo-* tokens, which this project does not import yet.");
193
+ console.log(" npm install @cascivo/themes");
194
+ console.log(" import '@cascivo/themes/all' // once, in your entry file");
195
+ console.log(" <html data-theme=\"light\"> // or any container");
196
+ }
197
+ /** Resolve where a template's own file is written — relative to the project root. */
198
+ function resolveTemplateTarget(cwd, target) {
199
+ return resolve(cwd, target);
200
+ }
201
+ /**
202
+ * Names from other libraries → the cascivo layout primitive that fills the same
203
+ * role, so `cascivo add flex` installs `layout/stack`. Bare names that already
204
+ * resolve (e.g. `stack` → `layout/stack` via suffix match) need no alias.
205
+ */
206
+ const LAYOUT_ALIASES = {
207
+ flex: "stack",
208
+ box: "stack",
209
+ hstack: "stack",
210
+ vstack: "stack",
211
+ gap: "spacer"
212
+ };
213
+ /**
214
+ * Resolve the transitive closure of bare-name registry entries, following each
215
+ * entry's `registryDependencies`. De-duped by resolved registry name and
216
+ * cycle-safe (an entry is processed at most once). Returns resolved entries in
217
+ * install order plus the names that could not be found. Pure — no I/O.
218
+ */
219
+ function resolveBareClosure(registry, bareSpecs) {
220
+ const queue = bareSpecs.map((name) => ({
221
+ name,
222
+ requested: true
223
+ }));
224
+ const installed = /* @__PURE__ */ new Set();
225
+ const resolved = [];
226
+ const missing = [];
227
+ while (queue.length > 0) {
228
+ const { name, requested } = queue.shift();
229
+ const entry = findComponent(registry, LAYOUT_ALIASES[name.toLowerCase()] ?? name);
230
+ if (!entry) {
231
+ missing.push(name);
232
+ continue;
233
+ }
234
+ if (installed.has(entry.name)) continue;
235
+ installed.add(entry.name);
236
+ resolved.push({
237
+ entry,
238
+ requested
239
+ });
240
+ for (const dep of entry.registryDependencies ?? []) queue.push({
241
+ name: dep,
242
+ requested: false
243
+ });
244
+ }
245
+ return {
246
+ resolved,
247
+ missing
248
+ };
249
+ }
250
+ /**
251
+ * First-party items are hosted on cascivo.com or the cascivo/cascivo GitHub
252
+ * repo (raw file URLs). Anything else gets a review-before-use notice.
253
+ */
254
+ const FIRST_PARTY_URL = /^https:\/\/(cascivo\.com|raw\.githubusercontent\.com\/cascivo\/cascivo)\//;
255
+ function isThirdParty(itemUrl) {
256
+ return !FIRST_PARTY_URL.test(itemUrl);
374
257
  }
375
258
  function isMultiRegistrySpec(name) {
376
259
  return name.startsWith("@") || name.startsWith("http://") || name.startsWith("https://") || name.startsWith("./") || name.startsWith("/") || name.split("/").length >= 3;
@@ -380,81 +263,539 @@ async function add(names, config, opts = {}) {
380
263
  console.error("Usage: cascivo add <component...>");
381
264
  return;
382
265
  }
266
+ const cwd = opts.cwd ?? process.cwd();
383
267
  const multiSpecs = names.filter(isMultiRegistrySpec);
384
- const bareSpecs = names.filter((n) => !isMultiRegistrySpec(n));
385
- let lock = await readLock() ?? createLock();
268
+ let bareSpecs = names.filter((n) => !isMultiRegistrySpec(n));
269
+ let registry = null;
270
+ if (bareSpecs.length > 0) {
271
+ registry = await fetchRegistry(config.registry);
272
+ const templateNames = new Set(registry.templates.map((t) => t.toLowerCase()));
273
+ const templateSpecs = bareSpecs.filter((n) => templateNames.has(n.toLowerCase()));
274
+ if (templateSpecs.length > 0) {
275
+ bareSpecs = bareSpecs.filter((n) => !templateNames.has(n.toLowerCase()));
276
+ multiSpecs.push(...templateSpecs.map((n) => `@cascivo/${n.toLowerCase()}`));
277
+ }
278
+ }
279
+ let lock = await readLock(cwd) ?? createLock();
386
280
  if (multiSpecs.length > 0) {
387
281
  const plan = await resolveClosure(multiSpecs, config, process.env, resolveFromDirectory);
388
282
  if (opts.dryRun) {
389
283
  console.log("Dry run — would install:");
390
284
  for (const item of plan.items) {
391
- console.log(` ${item.spec} (${item.item.name}@${item.item.version}) from ${item.itemUrl}`);
392
- for (const f of item.item.files) console.log(` ${f.url}`);
285
+ const kind = isTemplateItem(item.item) ? " [template]" : "";
286
+ console.log(` ${item.spec} (${item.item.name}@${item.item.version})${kind} from ${item.itemUrl}`);
287
+ for (const f of item.item.files) {
288
+ const arrow = isTemplateItem(item.item) && f.target ? ` → ${f.target}` : "";
289
+ console.log(` ${f.url}${arrow}`);
290
+ }
291
+ if (item.item.registryDependencies?.length) console.log(` components: ${item.item.registryDependencies.join(", ")}`);
393
292
  }
394
293
  return;
395
294
  }
396
- const isThirdParty = (itemUrl) => !itemUrl.includes("cascivo.com");
397
295
  for (const { item, itemUrl, registryBase } of plan.items) {
398
296
  if (isThirdParty(itemUrl)) console.log(`\nReview before use — files from non-directory registry: ${registryBase}\n` + (item.files ?? []).map((f) => ` ${f.url}`).join("\n"));
399
- await installItem(item, config, lock, registryBase);
297
+ let fileHashes;
298
+ try {
299
+ fileHashes = await installItem(item, config, cwd);
300
+ } catch (e) {
301
+ console.error(`Failed to install ${item.name}: ${e instanceof Error ? e.message : e}`);
302
+ process.exitCode = 1;
303
+ continue;
304
+ }
400
305
  lock = updateLockEntry(lock, item.name, {
401
306
  registry: registryBase,
402
307
  version: item.version,
403
308
  installedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
404
- files: {}
309
+ files: fileHashes
405
310
  });
406
311
  }
407
- await writeLock(lock);
408
- return;
312
+ await writeLock(lock, cwd);
313
+ if (bareSpecs.length === 0) {
314
+ hintThemesIfMissing(cwd);
315
+ return;
316
+ }
409
317
  }
410
- const registry = await fetchRegistry(config.registry);
318
+ if (bareSpecs.length === 0) return;
319
+ registry ??= await fetchRegistry(config.registry);
411
320
  const missingDeps = /* @__PURE__ */ new Set();
412
- for (const name of bareSpecs) {
413
- const entry = findComponent(registry, name);
414
- if (!entry) {
415
- console.error(`Component "${name}" not found in registry. Run "cascivo list".`);
416
- continue;
417
- }
418
- if (entry.type === "chart") {
419
- const pkg = entry.install ?? "@cascivo/charts";
420
- console.log(`Chart "${entry.name}" is distributed as an npm package.`);
321
+ const registryBase = config.registry.replace(/\/registry\.json$/, "").replace(/\/r$/, "");
322
+ for (const spec of bareSpecs) {
323
+ const alias = LAYOUT_ALIASES[spec.toLowerCase()];
324
+ if (!alias) continue;
325
+ const target = findComponent(registry, alias);
326
+ if (target) console.log(`"${spec}" is not a cascivo component — installing ${target.name} (its ${spec} primitive).`);
327
+ }
328
+ const { resolved, missing } = resolveBareClosure(registry, bareSpecs);
329
+ for (const name of missing) console.error(`Component "${name}" not found in registry. Run "cascivo list".`);
330
+ const peerFloors = {};
331
+ for (const { entry } of resolved) for (const [pkg, floor] of Object.entries(entry.peerVersions ?? {})) peerFloors[pkg] = floor;
332
+ for (const { entry, requested } of resolved) {
333
+ if (entry.install) {
334
+ const pkg = entry.install;
335
+ console.log(`"${entry.name}" is distributed as the npm package ${pkg}.`);
421
336
  console.log(`Install it with: npm install ${pkg}`);
422
337
  console.log(`Then import: import { ${entry.meta.name} } from '${pkg}'`);
338
+ if (entry.styles) console.log(`And import its stylesheet once: import '${entry.styles}'`);
423
339
  continue;
424
340
  }
425
341
  const outputName = entry.name.includes("/") ? entry.name.split("/").pop() : entry.name;
426
342
  const fileHashes = {};
427
- for (const url of entry.files) {
428
- const content = await fetchText$1(url);
429
- const dest = resolveOutputPath(config.outputDir, outputName, fileName(url));
343
+ let fetched;
344
+ try {
345
+ fetched = await Promise.all(entry.files.map(async (url) => ({
346
+ url,
347
+ content: await fetchTextRetry(url)
348
+ })));
349
+ } catch (e) {
350
+ console.error(`Failed to install ${entry.name}: ${e instanceof Error ? e.message : e}`);
351
+ process.exitCode = 1;
352
+ continue;
353
+ }
354
+ for (const { url, content } of fetched) {
355
+ const dest = resolveOutputPath(config.outputDir, outputName, fileName(url), cwd);
430
356
  await writeFileSafe(dest, content);
431
357
  fileHashes[dest] = sha256(content);
432
358
  }
433
359
  for (const dep of entry.dependencies) if (!ASSUMED_DEPS.has(dep)) missingDeps.add(dep);
434
- const registryBase = config.registry.replace(/\/registry\.json$/, "").replace(/\/r$/, "");
435
360
  lock = updateLockEntry(lock, entry.name, {
436
361
  registry: registryBase,
437
362
  version: entry.version,
438
363
  installedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
439
364
  files: fileHashes
440
365
  });
441
- console.log(`Added ${entry.name} to ${config.outputDir}/${outputName}/`);
366
+ const suffix = requested ? "" : " (dependency)";
367
+ console.log(`Added ${entry.name} to ${config.outputDir}/${outputName}/${suffix}`);
442
368
  }
443
369
  if (missingDeps.size > 0) installPackages([...missingDeps]);
444
- await writeLock(lock);
370
+ if (Object.keys(peerFloors).length > 0) {
371
+ const violations = await checkPeerVersions(cwd, peerFloors);
372
+ for (const v of violations) {
373
+ console.error(`\nWarning: ${v.pkg} ${v.installed ? `${v.installed} is` : "is not"} installed, but the copied component source needs ${v.pkg} ${v.required}. Run: npm install ${v.pkg}@latest`);
374
+ process.exitCode = 1;
375
+ }
376
+ }
377
+ await writeLock(lock, cwd);
378
+ hintThemesIfMissing(cwd);
445
379
  }
446
- async function installItem(item, config, _lock, _registryBase) {
447
- const outputName = item.name.includes("/") ? item.name.split("/").pop() : item.name;
380
+ /**
381
+ * Install a single resolved item, returning a map of written path → content
382
+ * hash for the lockfile. A template writes its own files to their `target`
383
+ * paths (relative to the project root); every other item writes its files under
384
+ * the components output directory. A template's components are installed
385
+ * separately as their own plan entries (via `registryDependencies`).
386
+ */
387
+ async function installItem(item, config, cwd) {
388
+ const fileHashes = {};
448
389
  const missingDeps = /* @__PURE__ */ new Set();
449
- for (const file of item.files ?? []) try {
450
- const content = await fetchText$1(file.url);
451
- await writeFileSafe(resolveOutputPath(config.outputDir, outputName, fileName(file.url)), content);
452
- } catch {
453
- console.warn(` Could not fetch ${file.url}`);
390
+ const template = isTemplateItem(item);
391
+ const outputName = item.name.includes("/") ? item.name.split("/").pop() : item.name;
392
+ const fetched = await Promise.all((item.files ?? []).map(async (file) => ({
393
+ file,
394
+ content: await fetchTextRetry(file.url)
395
+ })));
396
+ for (const { file, content } of fetched) {
397
+ const dest = template && file.target ? resolveTemplateTarget(cwd, file.target) : resolveOutputPath(config.outputDir, outputName, fileName(file.url), cwd);
398
+ await writeFileSafe(dest, content);
399
+ fileHashes[dest] = sha256(content);
454
400
  }
455
401
  for (const dep of item.dependencies ?? []) if (!ASSUMED_DEPS.has(dep)) missingDeps.add(dep);
456
402
  if (missingDeps.size > 0) installPackages([...missingDeps]);
457
- console.log(`Added ${item.name} to ${config.outputDir}/${outputName}/`);
403
+ if (template) {
404
+ const pages = item.files?.filter((f) => f.target).map((f) => f.target) ?? [];
405
+ console.log(`Installed template ${item.name} (${pages.length} files)`);
406
+ } else console.log(`Added ${item.name} to ${config.outputDir}/${outputName}/`);
407
+ return fileHashes;
408
+ }
409
+ //#endregion
410
+ //#region src/commands/create.ts
411
+ /** Version specifier used for every `@cascivo/*` dependency in generated apps. */
412
+ const CASCIVO_DEP = "latest";
413
+ /** Slug suitable for a union-member string literal: lower-kebab, alnum only. */
414
+ function slug(label) {
415
+ return label.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "section";
416
+ }
417
+ /** PascalCase identifier derived from a free-text label. */
418
+ function pascalCase(label) {
419
+ const safe = label.trim().split(/[^a-zA-Z0-9]+/).filter(Boolean).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("") || "Section";
420
+ return /^[0-9]/.test(safe) ? `Section${safe}` : safe;
421
+ }
422
+ /** Normalize the project name into a valid npm package name. */
423
+ function packageName(name) {
424
+ return name.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "cascivo-app";
425
+ }
426
+ /** Turn raw section labels into unique keyed/component-named sections. */
427
+ function resolveSections(labels) {
428
+ const seenKeys = /* @__PURE__ */ new Set();
429
+ const seenComponents = /* @__PURE__ */ new Set();
430
+ const sections = [];
431
+ for (const label of labels) {
432
+ const trimmed = label.trim();
433
+ if (!trimmed) continue;
434
+ let key = slug(trimmed);
435
+ let component = pascalCase(trimmed);
436
+ let n = 2;
437
+ while (seenKeys.has(key) || seenComponents.has(component)) {
438
+ key = `${slug(trimmed)}-${n}`;
439
+ component = `${pascalCase(trimmed)}${n}`;
440
+ n++;
441
+ }
442
+ seenKeys.add(key);
443
+ seenComponents.add(component);
444
+ sections.push({
445
+ key,
446
+ label: trimmed,
447
+ component
448
+ });
449
+ }
450
+ return sections.length > 0 ? sections : [{
451
+ key: "home",
452
+ label: "Home",
453
+ component: "Home"
454
+ }];
455
+ }
456
+ function packageJson(opts) {
457
+ const pkg = {
458
+ name: packageName(opts.name),
459
+ private: true,
460
+ version: "0.0.0",
461
+ type: "module",
462
+ scripts: {
463
+ dev: "vite",
464
+ build: "tsc && vite build",
465
+ preview: "vite preview"
466
+ },
467
+ dependencies: {
468
+ "@cascivo/core": CASCIVO_DEP,
469
+ "@cascivo/react": CASCIVO_DEP,
470
+ "@cascivo/themes": CASCIVO_DEP,
471
+ "@cascivo/tokens": CASCIVO_DEP,
472
+ react: "^19.0.0",
473
+ "react-dom": "^19.0.0"
474
+ },
475
+ devDependencies: {
476
+ "@types/react": "^19.0.0",
477
+ "@types/react-dom": "^19.0.0",
478
+ "@vitejs/plugin-react": "^5.0.0",
479
+ typescript: "^5.7.0",
480
+ vite: "^7.0.0"
481
+ }
482
+ };
483
+ return JSON.stringify(pkg, null, 2) + "\n";
484
+ }
485
+ function tsconfig() {
486
+ return JSON.stringify({
487
+ compilerOptions: {
488
+ target: "ES2022",
489
+ useDefineForClassFields: true,
490
+ lib: [
491
+ "ES2022",
492
+ "DOM",
493
+ "DOM.Iterable"
494
+ ],
495
+ module: "ESNext",
496
+ skipLibCheck: true,
497
+ moduleResolution: "bundler",
498
+ allowImportingTsExtensions: true,
499
+ resolveJsonModule: true,
500
+ isolatedModules: true,
501
+ moduleDetection: "force",
502
+ noEmit: true,
503
+ jsx: "react-jsx",
504
+ strict: true,
505
+ noUnusedLocals: true,
506
+ noUnusedParameters: true,
507
+ noFallthroughCasesInSwitch: true
508
+ },
509
+ include: ["src"]
510
+ }, null, 2) + "\n";
511
+ }
512
+ function viteConfig() {
513
+ return `import react from '@vitejs/plugin-react'
514
+ import { defineConfig } from 'vite'
515
+
516
+ export default defineConfig({
517
+ plugins: [react()],
518
+ })
519
+ `;
520
+ }
521
+ function indexHtml(opts) {
522
+ return `<!doctype html>
523
+ <html lang="en" data-theme="${opts.theme}">
524
+ <head>
525
+ <meta charset="UTF-8" />
526
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
527
+ <title>${opts.name}</title>
528
+ <style>
529
+ @layer cascivo.reset, cascivo.tokens, cascivo.component, cascivo.theme;
530
+ @layer cascivo.reset {
531
+ *,
532
+ *::before,
533
+ *::after {
534
+ box-sizing: border-box;
535
+ margin: 0;
536
+ padding: 0;
537
+ }
538
+ }
539
+ html,
540
+ body,
541
+ #root {
542
+ height: 100%;
543
+ }
544
+ </style>
545
+ </head>
546
+ <body>
547
+ <div id="root"></div>
548
+ <script type="module" src="/src/main.tsx"><\/script>
549
+ </body>
550
+ </html>
551
+ `;
552
+ }
553
+ function mainTsx() {
554
+ return `import React from 'react'
555
+ import ReactDOM from 'react-dom/client'
556
+ import App from './App'
557
+
558
+ const root = document.getElementById('root')
559
+ if (root) {
560
+ ReactDOM.createRoot(root).render(
561
+ <React.StrictMode>
562
+ <App />
563
+ </React.StrictMode>,
564
+ )
565
+ }
566
+ `;
567
+ }
568
+ function viteEnv() {
569
+ return `/// <reference types="vite/client" />\n`;
570
+ }
571
+ function appTsx(opts, sections) {
572
+ const sectionImports = sections.map((s) => `import { ${s.component} } from './sections/${s.component}'`).join("\n");
573
+ const unionType = sections.map((s) => `'${s.key}'`).join(" | ");
574
+ const navItems = sections.map((s) => ` {
575
+ label: '${s.label.replace(/'/g, "\\'")}',
576
+ active: section.value === '${s.key}',
577
+ onClick: (e) => {
578
+ e.preventDefault()
579
+ section.value = '${s.key}'
580
+ },
581
+ },`).join("\n");
582
+ const renderedSections = sections.map((s) => ` {section.value === '${s.key}' && <${s.component} />}`).join("\n");
583
+ return `'use client'
584
+ import { signal, useSignals } from '@cascivo/core'
585
+ import { AppShell, ShellHeader, SideNav, type SideNavItem } from '@cascivo/react'
586
+ ${sectionImports}
587
+
588
+ import '@cascivo/tokens'
589
+ import '@cascivo/themes/${opts.theme}.css'
590
+ import '@cascivo/react/styles.css'
591
+
592
+ type Section = ${unionType}
593
+
594
+ const section = signal<Section>('${sections[0].key}')
595
+
596
+ export default function App() {
597
+ useSignals()
598
+
599
+ const navItems: SideNavItem[] = [
600
+ ${navItems}
601
+ ]
602
+
603
+ return (
604
+ <AppShell
605
+ header={<ShellHeader brand={{ name: '${opts.name.replace(/'/g, "\\'")}' }} />}
606
+ nav={<SideNav items={navItems} />}
607
+ >
608
+ ${renderedSections}
609
+ </AppShell>
610
+ )
611
+ }
612
+ `;
613
+ }
614
+ function sectionTsx(section) {
615
+ return `import { Card, CardContent, CardHeader, CardTitle, Heading, Text } from '@cascivo/react'
616
+
617
+ export function ${section.component}() {
618
+ return (
619
+ <div
620
+ style={{
621
+ display: 'grid',
622
+ gap: 'var(--cascivo-space-6)',
623
+ padding: 'var(--cascivo-space-6)',
624
+ maxWidth: '64rem',
625
+ }}
626
+ >
627
+ <div style={{ display: 'grid', gap: 'var(--cascivo-space-2)' }}>
628
+ <Heading level={1}>${section.label}</Heading>
629
+ <Text muted>
630
+ Edit <code>src/sections/${section.component}.tsx</code> to build out this page.
631
+ </Text>
632
+ </div>
633
+
634
+ <Card>
635
+ <CardHeader>
636
+ <CardTitle>Get started</CardTitle>
637
+ </CardHeader>
638
+ <CardContent>
639
+ <Text>
640
+ This page is wired into the app shell. Add components with{' '}
641
+ <code>npx cascivo add &lt;component&gt;</code>.
642
+ </Text>
643
+ </CardContent>
644
+ </Card>
645
+ </div>
646
+ )
647
+ }
648
+ `;
649
+ }
650
+ function cascivoConfig(opts) {
651
+ return `import type { CascadeConfig } from 'cascivo'
652
+
653
+ const config: CascadeConfig = {
654
+ registry: '${CASCIVO_HOST}/registry.json',
655
+ outputDir: 'src/components/ui',
656
+ theme: '${opts.theme}',
657
+ }
658
+
659
+ export default config
660
+ `;
661
+ }
662
+ function gitignore() {
663
+ return `node_modules
664
+ dist
665
+ *.local
666
+ .DS_Store
667
+ `;
668
+ }
669
+ function readme(opts) {
670
+ return `# ${opts.name}
671
+
672
+ A [cascivo](https://cascivo.com) app — Vite + React + TypeScript, pre-wired with
673
+ the cascivo app shell, side navigation, and the \`${opts.theme}\` theme.
674
+
675
+ ## Develop
676
+
677
+ \`\`\`sh
678
+ npm install
679
+ npm run dev
680
+ \`\`\`
681
+
682
+ ## Structure
683
+
684
+ - \`src/App.tsx\` — app shell, navigation, and section routing
685
+ - \`src/sections/\` — one component per nav item
686
+
687
+ Add more components with \`npx cascivo add <component>\`.
688
+ `;
689
+ }
690
+ /** Build the full set of files for a new cascivo app. Pure — no filesystem I/O. */
691
+ function buildScaffold(opts) {
692
+ const sections = resolveSections(opts.sections);
693
+ return [
694
+ {
695
+ path: "package.json",
696
+ contents: packageJson(opts)
697
+ },
698
+ {
699
+ path: "tsconfig.json",
700
+ contents: tsconfig()
701
+ },
702
+ {
703
+ path: "vite.config.ts",
704
+ contents: viteConfig()
705
+ },
706
+ {
707
+ path: "index.html",
708
+ contents: indexHtml(opts)
709
+ },
710
+ {
711
+ path: "cascivo.config.ts",
712
+ contents: cascivoConfig(opts)
713
+ },
714
+ {
715
+ path: ".gitignore",
716
+ contents: gitignore()
717
+ },
718
+ {
719
+ path: "README.md",
720
+ contents: readme(opts)
721
+ },
722
+ {
723
+ path: "src/main.tsx",
724
+ contents: mainTsx()
725
+ },
726
+ {
727
+ path: "src/vite-env.d.ts",
728
+ contents: viteEnv()
729
+ },
730
+ {
731
+ path: "src/App.tsx",
732
+ contents: appTsx(opts, sections)
733
+ },
734
+ ...sections.map((s) => ({
735
+ path: `src/sections/${s.component}.tsx`,
736
+ contents: sectionTsx(s)
737
+ }))
738
+ ];
739
+ }
740
+ function flagValue$1(args, key) {
741
+ const eq = args.find((a) => a.startsWith(`--${key}=`));
742
+ if (eq) return eq.slice(key.length + 3);
743
+ const idx = args.indexOf(`--${key}`);
744
+ const next = idx !== -1 ? args[idx + 1] : void 0;
745
+ return next && !next.startsWith("--") ? next : void 0;
746
+ }
747
+ const DEFAULT_SECTIONS = [
748
+ "Dashboard",
749
+ "Reports",
750
+ "Settings"
751
+ ];
752
+ async function create(args, cwd = process.cwd()) {
753
+ const yes = args.includes("--yes") || args.includes("-y");
754
+ const nameArg = args.find((a) => !a.startsWith("-"));
755
+ const themeArg = flagValue$1(args, "theme");
756
+ const sectionsArg = flagValue$1(args, "sections");
757
+ const rl = !yes && stdin.isTTY ? createInterface({
758
+ input: stdin,
759
+ output: stdout
760
+ }) : null;
761
+ try {
762
+ let name = nameArg;
763
+ if (!name && rl) name = (await rl.question("Project name? [my-cascivo-app]: ")).trim();
764
+ name = name || "my-cascivo-app";
765
+ let theme = (themeArg ?? "").toLowerCase();
766
+ if (!THEMES.includes(theme) && rl) theme = (await rl.question(`Theme? (${THEMES.join("/")}) [light]: `)).trim().toLowerCase();
767
+ const resolvedTheme = THEMES.includes(theme) ? theme : "light";
768
+ let sectionsInput = sectionsArg;
769
+ if (!sectionsInput && rl) sectionsInput = (await rl.question(`Nav sections? (comma-separated) [${DEFAULT_SECTIONS.join(", ")}]: `)).trim();
770
+ const sections = (sectionsInput ?? "").split(",").map((s) => s.trim()).filter(Boolean);
771
+ const opts = {
772
+ name,
773
+ theme: resolvedTheme,
774
+ sections: sections.length > 0 ? sections : DEFAULT_SECTIONS
775
+ };
776
+ const targetDir = join(cwd, name);
777
+ if (existsSync(targetDir) && readdirSync(targetDir).length > 0) {
778
+ console.error(`Target directory "${name}" already exists and is not empty.`);
779
+ process.exitCode = 1;
780
+ return;
781
+ }
782
+ const files = buildScaffold(opts);
783
+ for (const file of files) await writeFileSafe(join(targetDir, file.path), file.contents);
784
+ console.log(`\nCreated ${name} with the ${resolvedTheme} theme (${files.length} files).`);
785
+ const templateSpec = flagValue$1(args, "template");
786
+ if (templateSpec) {
787
+ const { add } = await Promise.resolve().then(() => add_exports);
788
+ const { loadConfig } = await import("./config-BsyJbMvD.mjs").then((n) => n.i);
789
+ console.log(`\nInstalling template "${templateSpec}"…`);
790
+ await add([templateSpec], await loadConfig(), { cwd: targetDir });
791
+ }
792
+ console.log("\nNext steps:");
793
+ console.log(` cd ${name}`);
794
+ console.log(" npm install");
795
+ console.log(" npm run dev");
796
+ } finally {
797
+ rl?.close();
798
+ }
458
799
  }
459
800
  //#endregion
460
801
  //#region src/commands/doctor.ts
@@ -465,6 +806,47 @@ const BANNED_HOOKS = [
465
806
  "useContext",
466
807
  "useReducer"
467
808
  ];
809
+ /**
810
+ * Blank out comments and string/template-literal contents so identifier scans
811
+ * only match real code. A mention of `useEffect` in a comment must not trip
812
+ * the no-react-hooks rule. Single pass — a `//` inside a string (URLs) is not
813
+ * treated as a comment.
814
+ */
815
+ function stripCommentsAndStrings(source) {
816
+ let out = "";
817
+ let i = 0;
818
+ const n = source.length;
819
+ while (i < n) {
820
+ const c = source[i];
821
+ const next = source[i + 1];
822
+ if (c === "/" && next === "/") while (i < n && source[i] !== "\n") i++;
823
+ else if (c === "/" && next === "*") {
824
+ i += 2;
825
+ while (i < n && !(source[i] === "*" && source[i + 1] === "/")) i++;
826
+ i += 2;
827
+ } else if (c === "'" || c === "\"") {
828
+ i++;
829
+ while (i < n && source[i] !== c) {
830
+ if (source[i] === "\\") i++;
831
+ i++;
832
+ }
833
+ i++;
834
+ out += c + c;
835
+ } else if (c === "`") {
836
+ i++;
837
+ while (i < n && source[i] !== "`") {
838
+ if (source[i] === "\\") i++;
839
+ i++;
840
+ }
841
+ i++;
842
+ out += "``";
843
+ } else {
844
+ out += c;
845
+ i++;
846
+ }
847
+ }
848
+ return out;
849
+ }
468
850
  async function runDoctor(root) {
469
851
  const violations = [];
470
852
  const componentsDir = join(root, "packages", "components", "src");
@@ -479,7 +861,8 @@ async function runDoctor(root) {
479
861
  const tsxPath = join(componentsDir, name, `${name}.tsx`);
480
862
  if (!existsSync(tsxPath)) continue;
481
863
  const content = readFileSync(tsxPath, "utf8");
482
- for (const hook of BANNED_HOOKS) if (new RegExp(`\\b${hook}\\b`).test(content)) violations.push({
864
+ const code = stripCommentsAndStrings(content);
865
+ for (const hook of BANNED_HOOKS) if (new RegExp(`\\b${hook}\\b`).test(code)) violations.push({
483
866
  file: tsxPath,
484
867
  rule: "no-react-hooks",
485
868
  detail: `Banned hook '${hook}' in ${name}.tsx`
@@ -623,19 +1006,21 @@ async function generate(args, config) {
623
1006
  }
624
1007
  //#endregion
625
1008
  //#region src/commands/init.ts
626
- const THEMES$1 = [
627
- "light",
628
- "dark",
629
- "warm"
630
- ];
1009
+ function flagValue(args, key) {
1010
+ const eq = args.find((a) => a.startsWith(`--${key}=`));
1011
+ if (eq) return eq.slice(key.length + 3);
1012
+ const idx = args.indexOf(`--${key}`);
1013
+ const next = idx !== -1 ? args[idx + 1] : void 0;
1014
+ return next && !next.startsWith("--") ? next : void 0;
1015
+ }
631
1016
  async function promptTheme() {
632
1017
  const rl = createInterface({
633
1018
  input: stdin,
634
1019
  output: stdout
635
1020
  });
636
1021
  try {
637
- const answer = (await rl.question("Theme? (light/dark/warm) [light]: ")).trim().toLowerCase();
638
- return THEMES$1.includes(answer) ? answer : "light";
1022
+ const answer = (await rl.question(`Theme? (${THEMES.join("/")}) [light]: `)).trim().toLowerCase();
1023
+ return THEMES.includes(answer) ? answer : "light";
639
1024
  } finally {
640
1025
  rl.close();
641
1026
  }
@@ -644,17 +1029,25 @@ function configFileContents(theme) {
644
1029
  return `import type { CascadeConfig } from 'cascivo'
645
1030
 
646
1031
  const config: CascadeConfig = {
647
- registry: '${DEFAULT_CONFIG.registry}',
648
- outputDir: '${DEFAULT_CONFIG.outputDir}',
1032
+ registry: '${DEFAULT_CONFIG$1.registry}',
1033
+ outputDir: '${DEFAULT_CONFIG$1.outputDir}',
649
1034
  theme: '${theme}',
650
1035
  }
651
1036
 
652
1037
  export default config
653
1038
  `;
654
1039
  }
655
- async function init(cwd = process.cwd()) {
1040
+ async function init(args = [], cwd = process.cwd()) {
1041
+ const yes = args.includes("--yes") || args.includes("-y");
1042
+ const themeArg = flagValue(args, "theme")?.toLowerCase();
1043
+ if (themeArg !== void 0 && !THEMES.includes(themeArg)) {
1044
+ console.error(`Unknown theme "${themeArg}". Available: ${THEMES.join(", ")}`);
1045
+ process.exitCode = 1;
1046
+ return;
1047
+ }
656
1048
  installPackages(["@cascivo/core", "@cascivo/tokens"], cwd);
657
- const theme = await promptTheme();
1049
+ const interactive = themeArg === void 0 && !yes && stdin.isTTY;
1050
+ const theme = themeArg ?? (interactive ? await promptTheme() : "light");
658
1051
  await writeFileSafe(join(cwd, "cascivo.config.ts"), configFileContents(theme));
659
1052
  console.log(`\nCreated cascivo.config.ts (theme: ${theme})`);
660
1053
  console.log("Import the theme in your root CSS or entry file:");
@@ -667,9 +1060,12 @@ async function init(cwd = process.cwd()) {
667
1060
  //#region src/commands/list.ts
668
1061
  const TYPE_LABELS = {
669
1062
  component: "Components",
670
- layout: "Layouts",
671
- block: "Blocks",
672
- section: "Sections"
1063
+ layout: "Layouts (copy-paste)",
1064
+ block: "Blocks (copy-paste)",
1065
+ section: "Sections (copy-paste)",
1066
+ chart: "Charts (npm: @cascivo/charts)",
1067
+ flow: "Flow (npm: @cascivo/flow)",
1068
+ editor: "Editor (npm: @cascivo/editor)"
673
1069
  };
674
1070
  /** Render a group of entries as an aligned text table (no section header). */
675
1071
  function formatGroup(entries) {
@@ -716,9 +1112,30 @@ async function list(config, options = {}) {
716
1112
  return existsSync(join(process.cwd(), config.outputDir, outputName));
717
1113
  });
718
1114
  console.log(formatList(components));
1115
+ console.log("\nTip: install any entry by its bare name — `cascivo add stack` resolves to `layout/stack`. Layout primitives (Stack, Grid, AutoGrid, Columns, Spacer, Center) replace inline-style layout; see docs/cookbooks/layout-and-spacing.md.");
719
1116
  }
720
1117
  //#endregion
721
1118
  //#region src/commands/registry.ts
1119
+ /**
1120
+ * Validate every template item in an index: each must pass `validateTemplate`,
1121
+ * and any bare `registryDependencies` not present in the same index produce a
1122
+ * warning (they are expected to resolve from another registry at install time).
1123
+ * Returns `{ errors, warnings }`.
1124
+ */
1125
+ function validateTemplates(index) {
1126
+ const errors = [];
1127
+ const warnings = [];
1128
+ const localNames = new Set(index.items.map((i) => i.name));
1129
+ for (const item of index.items) {
1130
+ if (!isTemplateItem(item)) continue;
1131
+ for (const e of validateTemplate(item)) errors.push(`template "${item.name}": ${e}`);
1132
+ for (const dep of item.registryDependencies ?? []) if (!(dep.includes("/") || dep.startsWith("@") || dep.startsWith("http")) && !localNames.has(dep)) warnings.push(`template "${item.name}": component "${dep}" is not in this index — it must resolve from another registry at install time`);
1133
+ }
1134
+ return {
1135
+ errors,
1136
+ warnings
1137
+ };
1138
+ }
722
1139
  async function registryBuild(args) {
723
1140
  let inFile = "cascade-registry.json";
724
1141
  let outDir = "public/r";
@@ -748,6 +1165,13 @@ async function registryBuild(args) {
748
1165
  process.exitCode = 1;
749
1166
  return;
750
1167
  }
1168
+ const templateResult = validateTemplates(index);
1169
+ for (const w of templateResult.warnings) console.warn(`warn: ${w}`);
1170
+ if (templateResult.errors.length > 0) {
1171
+ for (const e of templateResult.errors) console.error(`error: ${e}`);
1172
+ process.exitCode = 1;
1173
+ return;
1174
+ }
751
1175
  const outPath = resolve(outDir);
752
1176
  await buildRegistry(index, outPath);
753
1177
  console.log(`cascade registry build: wrote static output to ${outPath}`);
@@ -763,7 +1187,7 @@ async function search(args, config) {
763
1187
  const registryFilter = nsFlag !== -1 ? args[nsFlag + 1] : void 0;
764
1188
  const query = args.filter((a, i) => a !== "--registry" && args[i - 1] !== "--registry").join(" ").trim();
765
1189
  if (!query) {
766
- console.error("Usage: cascade search <query> [--registry @ns]");
1190
+ console.error("Usage: cascivo search <query> [--registry @ns]");
767
1191
  return;
768
1192
  }
769
1193
  const results = [];
@@ -800,25 +1224,224 @@ async function search(args, config) {
800
1224
  }
801
1225
  }
802
1226
  //#endregion
1227
+ //#region ../theme-kit/src/config.ts
1228
+ const DEFAULT_CONFIG = {
1229
+ baseMode: "light",
1230
+ accentHue: 250,
1231
+ accentChroma: .2,
1232
+ radiusBase: .375,
1233
+ fontFamily: "system",
1234
+ presetId: "light"
1235
+ };
1236
+ //#endregion
1237
+ //#region ../theme-kit/src/hash.ts
1238
+ /** Decode a hash produced by {@link configToHash}. Returns null on malformed
1239
+ * input; unknown/missing fields fall back to {@link DEFAULT_CONFIG}. */
1240
+ function hashToConfig(hash) {
1241
+ if (!hash) return null;
1242
+ try {
1243
+ const base64 = hash.replace(/-/g, "+").replace(/_/g, "/");
1244
+ const json = atob(base64);
1245
+ const parsed = JSON.parse(json);
1246
+ return {
1247
+ baseMode: parsed.baseMode === "dark" ? "dark" : "light",
1248
+ accentHue: typeof parsed.accentHue === "number" ? parsed.accentHue : DEFAULT_CONFIG.accentHue,
1249
+ accentChroma: typeof parsed.accentChroma === "number" ? parsed.accentChroma : DEFAULT_CONFIG.accentChroma,
1250
+ radiusBase: typeof parsed.radiusBase === "number" ? parsed.radiusBase : DEFAULT_CONFIG.radiusBase,
1251
+ fontFamily: parsed.fontFamily ?? DEFAULT_CONFIG.fontFamily,
1252
+ presetId: parsed.presetId ?? null
1253
+ };
1254
+ } catch {
1255
+ return null;
1256
+ }
1257
+ }
1258
+ //#endregion
1259
+ //#region ../theme-kit/src/css.ts
1260
+ const FONT_STACKS = {
1261
+ geometric: "'Futura', 'Century Gothic', 'Trebuchet MS', ui-sans-serif, sans-serif",
1262
+ humanist: "'Gill Sans', 'Trebuchet MS', 'Calibri', ui-sans-serif, sans-serif",
1263
+ transitional: "'Optima', 'Candara', 'Palatino Linotype', ui-sans-serif, sans-serif",
1264
+ mono: "ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace"
1265
+ };
1266
+ const BASE_LIGHT = `
1267
+ color-scheme: light;
1268
+ --cascivo-color-background: oklch(1 0 0);
1269
+ --cascivo-color-surface: oklch(0.985 0.002 264);
1270
+ --cascivo-color-surface-2: oklch(0.967 0.003 264);
1271
+ --cascivo-color-bg: var(--cascivo-color-background);
1272
+ --cascivo-color-bg-subtle: var(--cascivo-color-surface);
1273
+ --cascivo-color-surface-raised: var(--cascivo-color-surface);
1274
+ --cascivo-color-surface-overlay: var(--cascivo-color-background);
1275
+ --cascivo-color-border: oklch(0.928 0.006 264);
1276
+ --cascivo-color-border-strong: oklch(0.872 0.008 264);
1277
+ --cascivo-border-subtle: oklch(0.928 0.006 264);
1278
+ --cascivo-border-default: oklch(0.872 0.008 264);
1279
+ --cascivo-border-strong: oklch(0.707 0.015 264);
1280
+ --cascivo-color-foreground: oklch(0.145 0.005 264);
1281
+ --cascivo-color-foreground-muted: oklch(0.554 0.018 264);
1282
+ --cascivo-color-text: var(--cascivo-color-foreground);
1283
+ --cascivo-color-text-subtle: oklch(0.446 0.018 264);
1284
+ --cascivo-color-text-muted: oklch(0.707 0.015 264);
1285
+ --cascivo-color-text-on-accent: oklch(1 0 0);
1286
+ --cascivo-color-text-on-destructive: oklch(1 0 0);
1287
+ --cascivo-color-primary: oklch(0.205 0 0);
1288
+ --cascivo-color-primary-fg: oklch(0.985 0 0);
1289
+ --cascivo-color-primary-hover: oklch(0.27 0 0);
1290
+ --cascivo-color-primary-active: oklch(0.32 0 0);
1291
+ --cascivo-color-active-bg: oklch(0.145 0.005 264 / 6%);
1292
+ --cascivo-color-destructive: oklch(0.54 0.188 22);
1293
+ --cascivo-color-destructive-hover: oklch(0.448 0.17 22);
1294
+ --cascivo-color-destructive-subtle: oklch(0.971 0.013 22);
1295
+ --cascivo-color-error: oklch(0.628 0.188 22);
1296
+ --cascivo-color-warning: oklch(0.768 0.145 75);
1297
+ --cascivo-color-success: oklch(0.648 0.15 145);
1298
+ --cascivo-color-success-subtle: oklch(0.982 0.018 145);
1299
+ --cascivo-color-warning-subtle: oklch(0.98 0.02 75);
1300
+ --cascivo-color-info: oklch(0.546 0.224 250);
1301
+ --cascivo-color-info-subtle: oklch(0.97 0.025 250);
1302
+ --cascivo-color-success-foreground: oklch(0.45 0.14 145);
1303
+ --cascivo-color-warning-foreground: oklch(0.5 0.14 75);
1304
+ --cascivo-shadow-xs: 0 1px 2px oklch(0 0 0 / 0.05);
1305
+ --cascivo-shadow-sm: 0 1px 3px oklch(0 0 0 / 0.07), 0 1px 2px oklch(0 0 0 / 0.04);
1306
+ --cascivo-shadow-md: 0 2px 8px oklch(0 0 0 / 0.07), 0 1px 2px oklch(0 0 0 / 0.04);
1307
+ --cascivo-shadow-overlay: 0 4px 32px oklch(0 0 0 / 0.16), 0 0 0 1px oklch(0 0 0 / 0.04);
1308
+ --cascivo-shadow-lg: var(--cascivo-shadow-overlay);
1309
+ --cascivo-ring-width: 2px;
1310
+ --cascivo-ring-offset: 0px;
1311
+ --cascivo-ring-color: color-mix(in oklch, var(--cascivo-color-accent) 55%, transparent);
1312
+ --cascivo-focus-ring: 0 0 0 var(--cascivo-ring-width) var(--cascivo-ring-color);`;
1313
+ const BASE_DARK = `
1314
+ color-scheme: dark;
1315
+ --cascivo-color-background: oklch(0.145 0.005 250);
1316
+ --cascivo-color-surface: oklch(0.185 0.007 250);
1317
+ --cascivo-color-surface-2: oklch(0.22 0.008 250);
1318
+ --cascivo-color-bg: var(--cascivo-color-background);
1319
+ --cascivo-color-bg-subtle: var(--cascivo-color-surface);
1320
+ --cascivo-color-surface-raised: var(--cascivo-color-surface-2);
1321
+ --cascivo-color-surface-overlay: var(--cascivo-color-surface);
1322
+ --cascivo-color-border: oklch(1 0 0 / 10%);
1323
+ --cascivo-color-border-strong: oklch(1 0 0 / 16%);
1324
+ --cascivo-border-subtle: oklch(1 0 0 / 0.06);
1325
+ --cascivo-border-default: oklch(1 0 0 / 0.1);
1326
+ --cascivo-border-strong: oklch(1 0 0 / 0.2);
1327
+ --cascivo-color-foreground: oklch(0.985 0.002 264);
1328
+ --cascivo-color-foreground-muted: oklch(0.707 0.015 264);
1329
+ --cascivo-color-text: var(--cascivo-color-foreground);
1330
+ --cascivo-color-text-subtle: oklch(0.707 0.015 264);
1331
+ --cascivo-color-text-muted: oklch(0.554 0.018 264);
1332
+ --cascivo-color-text-on-accent: oklch(0.145 0.005 250);
1333
+ --cascivo-color-text-on-destructive: oklch(1 0 0);
1334
+ --cascivo-color-primary: oklch(0.922 0 0);
1335
+ --cascivo-color-primary-fg: oklch(0.205 0 0);
1336
+ --cascivo-color-primary-hover: oklch(0.86 0 0);
1337
+ --cascivo-color-primary-active: oklch(0.8 0 0);
1338
+ --cascivo-color-active-bg: oklch(1 0 0 / 8%);
1339
+ --cascivo-color-destructive: oklch(0.72 0.16 22);
1340
+ --cascivo-color-destructive-hover: oklch(0.628 0.188 22);
1341
+ --cascivo-color-destructive-subtle: oklch(0.628 0.188 22 / 0.1);
1342
+ --cascivo-color-error: oklch(0.72 0.16 22);
1343
+ --cascivo-color-warning: oklch(0.82 0.13 75);
1344
+ --cascivo-color-success: oklch(0.72 0.13 145);
1345
+ --cascivo-color-success-subtle: oklch(0.72 0.13 145 / 0.1);
1346
+ --cascivo-color-warning-subtle: oklch(0.82 0.13 75 / 0.1);
1347
+ --cascivo-color-info: oklch(0.65 0.2 250);
1348
+ --cascivo-color-info-subtle: oklch(0.65 0.2 250 / 0.1);
1349
+ --cascivo-color-success-foreground: oklch(0.72 0.13 145);
1350
+ --cascivo-color-warning-foreground: oklch(0.82 0.13 75);
1351
+ --cascivo-shadow-xs: none;
1352
+ --cascivo-shadow-sm: 0 1px 3px oklch(0 0 0 / 0.4);
1353
+ --cascivo-shadow-md: 0 2px 8px oklch(0 0 0 / 0.45);
1354
+ --cascivo-shadow-overlay: 0 4px 32px oklch(0 0 0 / 0.6), 0 0 0 1px oklch(1 0 0 / 0.06);
1355
+ --cascivo-shadow-lg: var(--cascivo-shadow-overlay);
1356
+ --cascivo-ring-width: 2px;
1357
+ --cascivo-ring-offset: 0px;
1358
+ --cascivo-ring-color: color-mix(in oklch, var(--cascivo-color-accent) 65%, transparent);
1359
+ --cascivo-focus-ring: 0 0 0 var(--cascivo-ring-width) var(--cascivo-ring-color);`;
1360
+ function configToCSS(config, options = {}) {
1361
+ const { previewMode = false, themeName = "create-custom" } = options;
1362
+ const light = config.baseMode === "light";
1363
+ const h = config.accentHue;
1364
+ const c = config.accentChroma;
1365
+ const acc = (l, chroma = c) => `oklch(${l} ${chroma.toFixed(3)} ${h})`;
1366
+ const lines = [
1367
+ `/* Add to your project CSS after importing the base theme: */`,
1368
+ `/* @import '@cascivo/themes/${config.baseMode}.css'; */`,
1369
+ `/* @import './theme.css'; */`,
1370
+ ``,
1371
+ `@layer cascivo.theme {`,
1372
+ ` [data-theme="${themeName}"] {`
1373
+ ];
1374
+ if (previewMode) {
1375
+ lines.push(light ? BASE_LIGHT : BASE_DARK);
1376
+ lines.push(``);
1377
+ }
1378
+ lines.push(` /* ── Accent ─────────────────────────────────── */`, ` --cascivo-color-accent: ${acc(light ? .52 : .65)};`, ` --cascivo-color-accent-foreground: ${light ? "oklch(1 0 0)" : `oklch(0.145 0.005 ${h})`};`, ` --cascivo-color-accent-hover: ${acc(light ? .45 : .707)};`, ` --cascivo-color-accent-active: ${acc(light ? .373 : .808, Math.min(c, .155))};`, ` --cascivo-color-accent-subtle: ${acc(light ? .97 : .623, .025)};`, ` --cascivo-color-accent-muted: ${acc(light ? .932 : .623, light ? .055 : c * .2)};`, ``, ` /* ── Radius ─────────────────────────────────── */`, ` --cascivo-radius-base: ${config.radiusBase}rem;`, ` --cascivo-radius-control: var(--cascivo-radius-base);`, ` --cascivo-radius-surface: calc(var(--cascivo-radius-base) * 1.66);`, ` --cascivo-radius-indicator: calc(var(--cascivo-radius-base) / 2);`, ` --cascivo-radius-full: 9999px;`, ` --cascivo-radius-component: var(--cascivo-radius-base);`, ` --cascivo-radius-button: var(--cascivo-radius-base);`, ` --cascivo-radius-input: var(--cascivo-radius-base);`, ` --cascivo-radius-card: calc(var(--cascivo-radius-base) * 1.66);`, ` --cascivo-radius-badge: var(--cascivo-radius-full);`, ` --cascivo-radius-modal: calc(var(--cascivo-radius-base) * 2);`);
1379
+ if (config.fontFamily !== "system") {
1380
+ lines.push(``);
1381
+ lines.push(` /* ── Font ───────────────────────────────────── */`);
1382
+ lines.push(` --cascivo-font-sans: ${FONT_STACKS[config.fontFamily] ?? FONT_STACKS["humanist"]};`);
1383
+ }
1384
+ lines.push(` }`);
1385
+ lines.push(`}`);
1386
+ return lines.join("\n");
1387
+ }
1388
+ //#endregion
803
1389
  //#region src/commands/theme.ts
804
- const THEMES = [
805
- "light",
806
- "dark",
807
- "warm",
808
- "flat",
809
- "minimal",
810
- "midnight",
811
- "pastel",
812
- "brutalist",
813
- "corporate",
814
- "terminal",
815
- "cyberpunk",
816
- "arcade"
817
- ];
1390
+ const THEME_NAME_RE = /^[a-z][a-z0-9-]*$/;
1391
+ /** Read a `--key value` or `--key=value` flag from an argv slice. */
1392
+ function flag(args, key) {
1393
+ const eq = args.find((a) => a.startsWith(`--${key}=`));
1394
+ if (eq) return eq.slice(key.length + 3);
1395
+ const idx = args.indexOf(`--${key}`);
1396
+ const next = idx !== -1 ? args[idx + 1] : void 0;
1397
+ return next && !next.startsWith("--") ? next : void 0;
1398
+ }
1399
+ /**
1400
+ * `cascivo theme create <name> --from <hash>` — turn a /create theme-builder
1401
+ * config (the `#hash` in the builder URL, or its "Copy CLI command" output) into
1402
+ * an owned `<name>.theme.css` in the project. Uses the same generator the builder
1403
+ * previews with, so the file matches the preview exactly.
1404
+ */
1405
+ async function themeCreate(args, cwd) {
1406
+ const name = args.find((a) => !a.startsWith("--"));
1407
+ const hash = flag(args, "from");
1408
+ const dryRun = args.includes("--dry-run");
1409
+ if (!name || !hash) {
1410
+ console.error("Usage: cascivo theme create <name> --from <hash> [--out <path>]");
1411
+ console.error("Get <hash> from the theme builder at https://cascivo.com/create (Copy CLI command).");
1412
+ return;
1413
+ }
1414
+ if (!THEME_NAME_RE.test(name)) {
1415
+ console.error(`Invalid theme name "${name}". Use lowercase letters, digits, and dashes.`);
1416
+ return;
1417
+ }
1418
+ const config = hashToConfig(hash);
1419
+ if (!config) {
1420
+ console.error("Could not decode --from. Copy the command again from the theme builder.");
1421
+ return;
1422
+ }
1423
+ const css = configToCSS(config, { themeName: name });
1424
+ if (dryRun) {
1425
+ console.log(css);
1426
+ return;
1427
+ }
1428
+ await writeFileSafe(resolve(cwd, flag(args, "out") ?? `${name}.theme.css`), css);
1429
+ console.log(`\n✓ Wrote ${name}.theme.css`);
1430
+ console.log(`\nImport the base theme and your overlay in your entry CSS:`);
1431
+ console.log(` @import '@cascivo/themes/${config.baseMode}.css';`);
1432
+ console.log(` @import './${name}.theme.css';`);
1433
+ console.log(`Then set it on a container:`);
1434
+ console.log(` <div data-theme="${name}">…</div>`);
1435
+ }
818
1436
  async function theme(args) {
819
1437
  const [sub, name] = args;
1438
+ if (sub === "create") {
1439
+ await themeCreate(args.slice(1), process.cwd());
1440
+ return;
1441
+ }
820
1442
  if (sub !== "add" || !name) {
821
- console.error(`Usage: cascade theme add <${THEMES.join("|")}>`);
1443
+ console.error(`Usage: cascivo theme add <${THEMES.join("|")}>`);
1444
+ console.error(` cascivo theme create <name> --from <hash>`);
822
1445
  return;
823
1446
  }
824
1447
  if (!THEMES.includes(name)) {
@@ -1051,11 +1674,6 @@ async function confirm(question) {
1051
1674
  rl.close();
1052
1675
  }
1053
1676
  }
1054
- async function fetchText(url) {
1055
- const res = await fetch(url);
1056
- if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${url}`);
1057
- return res.text();
1058
- }
1059
1677
  async function update(name, config, opts = {}) {
1060
1678
  if (opts.check) return updateCheck(config);
1061
1679
  if (!name) {
@@ -1093,9 +1711,9 @@ async function threeWayUpdate(name, upstreamEntry, baseItem, config, opts) {
1093
1711
  const fname = fileName(url);
1094
1712
  const baseFile = baseItem.files.find((f) => fileName(f.url) === fname);
1095
1713
  const dest = resolveOutputPath(config.outputDir, outputName, fname);
1096
- const upstream = await fetchText(url);
1714
+ const upstream = await fetchTextRetry(url);
1097
1715
  const local = existsSync(dest) ? await readFile(dest, "utf8") : "";
1098
- const base = baseFile ? await fetchText(baseFile.url) : "";
1716
+ const base = baseFile ? await fetchTextRetry(baseFile.url) : "";
1099
1717
  if (base === "" && local === "") {
1100
1718
  summaries.push({
1101
1719
  file: fname,
@@ -1165,12 +1783,13 @@ async function twoWayUpdate(name, entry, config, opts) {
1165
1783
  if (!entry) return;
1166
1784
  const pending = [];
1167
1785
  for (const url of entry.files) {
1168
- const res = await fetch(url);
1169
- if (!res.ok) {
1170
- console.error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`);
1786
+ let latest;
1787
+ try {
1788
+ latest = await fetchTextRetry(url);
1789
+ } catch (e) {
1790
+ console.error(e instanceof Error ? e.message : String(e));
1171
1791
  return;
1172
1792
  }
1173
- const latest = await res.text();
1174
1793
  const dest = resolveOutputPath(config.outputDir, entry.name, fileName(url));
1175
1794
  const current = await readFileSafe(dest);
1176
1795
  if (current === null) {
@@ -1208,7 +1827,14 @@ async function updateCheck(config) {
1208
1827
  for (const [name, entry] of Object.entries(lock.items)) {
1209
1828
  const current = findComponent(registry, name);
1210
1829
  if (!current) continue;
1211
- if (current.version !== entry.version) {
1830
+ if (current.fileHashes && Object.keys(current.fileHashes).length > 0) {
1831
+ const lockByBase = new Map(Object.entries(entry.files).map(([dest, hash]) => [dest.split("/").pop(), hash]));
1832
+ const changed = Object.entries(current.fileHashes).filter(([file, hash]) => lockByBase.get(file) !== hash);
1833
+ if (changed.length > 0) {
1834
+ console.log(`${name}: ${changed.length} file(s) changed upstream (${entry.version} → ${current.version})`);
1835
+ outdated++;
1836
+ }
1837
+ } else if (current.version !== entry.version) {
1212
1838
  console.log(`${name}: ${entry.version} → ${current.version}`);
1213
1839
  outdated++;
1214
1840
  }
@@ -1220,10 +1846,48 @@ async function updateCheck(config) {
1220
1846
  }
1221
1847
  //#endregion
1222
1848
  //#region src/commands/view.ts
1849
+ /** Render an item's details as text. Pure, so it can be unit-tested. */
1850
+ function formatItem(item) {
1851
+ const lines = [];
1852
+ lines.push(`\n${item.name} v${item.version}`);
1853
+ lines.push(`Type: ${item.type}${item.category ? ` / ${item.category}` : ""}`);
1854
+ lines.push(item.description);
1855
+ if (item.author) lines.push(`Author: ${item.author}`);
1856
+ if (item.license) lines.push(`License: ${item.license}`);
1857
+ if (item.homepage) lines.push(`Homepage: ${item.homepage}`);
1858
+ if (item.install) {
1859
+ lines.push(`Distribution: npm package — pnpm add ${item.install}`);
1860
+ if (item.styles) lines.push(`Stylesheet: import '${item.styles}' (required)`);
1861
+ } else if (!isTemplateItem(item)) lines.push(`Distribution: copy-paste — npx cascivo add ${item.name}`);
1862
+ if (isTemplateItem(item)) try {
1863
+ const meta = asTemplateMeta(item.meta);
1864
+ lines.push(`\nTemplate: ${meta.intent}`);
1865
+ lines.push(`Framework: ${meta.framework}`);
1866
+ if (meta.demoUrl) lines.push(`Demo: ${meta.demoUrl}`);
1867
+ if (meta.pages?.length) {
1868
+ lines.push("Pages:");
1869
+ for (const p of meta.pages) lines.push(` ${p.name} → ${p.target}`);
1870
+ }
1871
+ } catch {}
1872
+ if (item.changelog?.length) {
1873
+ lines.push("\nChangelog:");
1874
+ for (const c of item.changelog.slice(0, 5)) lines.push(` ${c.version}: ${c.note}`);
1875
+ }
1876
+ if (item.files?.length) {
1877
+ lines.push("\nFiles:");
1878
+ for (const f of item.files) lines.push(` ${f.url}${f.target ? ` → ${f.target}` : ""}`);
1879
+ }
1880
+ if (item.dependencies?.length) lines.push(`\nDependencies: ${item.dependencies.join(", ")}`);
1881
+ if (item.registryDependencies?.length) {
1882
+ const label = isTemplateItem(item) ? "Components" : "Registry dependencies";
1883
+ lines.push(`${label}: ${item.registryDependencies.join(", ")}`);
1884
+ }
1885
+ return lines.join("\n");
1886
+ }
1223
1887
  async function view(args, config) {
1224
1888
  const spec = args[0];
1225
1889
  if (!spec) {
1226
- console.error("Usage: cascade view <component-spec>");
1890
+ console.error("Usage: cascivo view <component-spec>");
1227
1891
  return;
1228
1892
  }
1229
1893
  const { url, headers, params } = await resolveItemUrl(parseAddress(spec), config);
@@ -1231,53 +1895,164 @@ async function view(args, config) {
1231
1895
  if (headers) fetchOpts.headers = headers;
1232
1896
  if (params) fetchOpts.params = params;
1233
1897
  const item = await fetchJson(url, fetchOpts);
1234
- console.log(`\n${item.name} v${item.version}`);
1235
- console.log(`Type: ${item.type}${item.category ? ` / ${item.category}` : ""}`);
1236
- console.log(`${item.description}`);
1237
- if (item.author) console.log(`Author: ${item.author}`);
1238
- if (item.license) console.log(`License: ${item.license}`);
1239
- if (item.homepage) console.log(`Homepage: ${item.homepage}`);
1240
- if (item.changelog?.length) {
1241
- console.log("\nChangelog:");
1242
- for (const c of item.changelog.slice(0, 5)) console.log(` ${c.version}: ${c.note}`);
1243
- }
1244
- if (item.files?.length) {
1245
- console.log("\nFiles:");
1246
- for (const f of item.files) {
1247
- const target = f.target ? ` → ${f.target}` : "";
1248
- console.log(` ${f.url}${target}`);
1249
- }
1250
- }
1251
- if (item.dependencies?.length) console.log(`\nDependencies: ${item.dependencies.join(", ")}`);
1252
- if (item.registryDependencies?.length) console.log(`Registry dependencies: ${item.registryDependencies.join(", ")}`);
1898
+ console.log(formatItem(item));
1253
1899
  }
1254
1900
  //#endregion
1255
1901
  //#region src/index.ts
1256
- const VERSION = "0.0.0";
1902
+ const VERSION = createRequire(import.meta.url)("../package.json").version;
1257
1903
  const HELP = `cascivo ${VERSION} — the CSS-native, signal-driven, AI-first design system
1258
1904
 
1259
1905
  Usage: cascivo <command> [options]
1260
1906
 
1261
1907
  Commands:
1908
+ create [name] Scaffold a new ready-to-run app (shell + nav + theme)
1909
+ (--template <spec>: start from a marketplace template)
1262
1910
  init Set up cascivo in the current project
1263
- add <component...> Add one or more components to your project
1911
+ add <component...> Add components or a template to your project
1264
1912
  list [--installed] List available components
1265
1913
  update [component] Update installed components (--check: list outdated)
1266
1914
  search <query> Search components across registries
1267
1915
  view <spec> View a component before installing
1268
- theme add <name> Install a theme (light | dark | warm)
1916
+ theme <add|create> Install a first-party theme, or build a custom one from /create
1269
1917
  eject <component> Eject specific tokens into a scoped local override file
1270
1918
  generate <config.json> Generate TSX from a ViewConfig JSON file
1271
1919
  doctor [--ci] Check components for rule violations
1272
1920
  audit --ai <paths...> Audit AI-generated code against the cascivo contract
1273
1921
  registry build Build a static registry from a cascivo-registry.json file
1922
+ template init <name> Scaffold a new template (source + manifest + registry entry)
1923
+ tokens import <file> Import external design tokens as cascivo overrides
1274
1924
 
1275
1925
  Run "cascivo <command> --help" for details.`;
1926
+ const THEME_LIST = THEMES.join(" | ");
1927
+ const COMMAND_HELP = {
1928
+ create: `Usage: cascivo create [name] [options]
1929
+
1930
+ Scaffold a new ready-to-run app — Vite + React + TypeScript, pre-wired with the
1931
+ cascivo app shell, side navigation, header, and a theme.
1932
+
1933
+ Options:
1934
+ --template <spec> Start from a marketplace template (@ns/name or owner/repo/name)
1935
+ --theme <name> Theme to install (${THEME_LIST})
1936
+ --sections "<a, b>" Comma-separated nav section labels (one component each)
1937
+ --yes, -y Accept defaults, never prompt`,
1938
+ init: `Usage: cascivo init [options]
1939
+
1940
+ Set up cascivo in the current project: installs @cascivo/core + @cascivo/tokens
1941
+ and writes cascivo.config.ts.
1942
+
1943
+ Options:
1944
+ --theme <name> Theme to configure (${THEME_LIST})
1945
+ --yes, -y Accept defaults, never prompt (implied when stdin is not a TTY)`,
1946
+ add: `Usage: cascivo add <component...> [options]
1947
+
1948
+ Copy component source (TSX + CSS module) from the registry into your project,
1949
+ resolving component dependencies. Also installs templates (@ns/name) and
1950
+ third-party components (owner/repo/name).
1951
+
1952
+ Options:
1953
+ --dry-run Show what would be written without writing
1954
+ --yes, -y Skip confirmation prompts`,
1955
+ list: `Usage: cascivo list [options]
1956
+
1957
+ List components available in the configured registry.
1958
+
1959
+ Options:
1960
+ --installed Only list components already installed in this project`,
1961
+ update: `Usage: cascivo update [component] [options]
1962
+
1963
+ Update installed components to the current registry version. Without a
1964
+ component argument, updates everything in the lockfile.
1965
+
1966
+ Options:
1967
+ --check List outdated components without writing
1968
+ --yes, -y Skip confirmation prompts`,
1969
+ search: `Usage: cascivo search <query> [options]
1970
+
1971
+ Search components across the configured registries.
1972
+
1973
+ Options:
1974
+ --registry <@ns> Restrict the search to one registry namespace`,
1975
+ view: `Usage: cascivo view <spec>
1976
+
1977
+ Preview a component or template (files, dependencies, description) before
1978
+ installing. <spec> is a bare name, @ns/name, or owner/repo/name.`,
1979
+ theme: `Usage: cascivo theme add <name>
1980
+ cascivo theme create <name> --from <hash> [--out <path>]
1981
+
1982
+ add Install @cascivo/themes and print the import + data-theme wiring.
1983
+ create Turn a theme-builder config into an owned <name>.theme.css. Get <hash>
1984
+ from https://cascivo.com/create ("Copy CLI command"). --dry-run prints
1985
+ the CSS without writing.
1986
+
1987
+ Themes: ${THEME_LIST}`,
1988
+ eject: `Usage: cascivo eject <component> [options]
1989
+
1990
+ Eject specific tokens into a scoped local override file so you can restyle a
1991
+ component without forking it.
1992
+
1993
+ Options:
1994
+ --tokens <a,b> Comma-separated token names to eject (default: all)
1995
+ --scope <sel> CSS selector the overrides are scoped to
1996
+ --out <file> Output file path
1997
+ --dry-run Print the override file without writing`,
1998
+ generate: `Usage: cascivo generate <config.json> [options]
1999
+
2000
+ Generate TSX from a ViewConfig JSON file (see the MCP scaffold_view tool).
2001
+
2002
+ Options:
2003
+ --out <file> Output file (default: stdout)
2004
+ --components-dir <dir> Components import base (default: ./src/components/ui)`,
2005
+ doctor: `Usage: cascivo doctor [options]
2006
+
2007
+ Check components in this repo for cascivo rule violations (banned React hooks,
2008
+ hardcoded strings, missing @cascivo/react exports).
2009
+
2010
+ Options:
2011
+ --ci Exit non-zero when violations are found
2012
+ --drift Compare installed components against the registry (copy-paste drift)`,
2013
+ audit: `Usage: cascivo audit --ai <paths...> [options]
2014
+
2015
+ Audit AI-generated code against the cascivo contract: hard-coded values that
2016
+ should be tokens, invented props, missing required props, raw strings where
2017
+ i18n is expected.
2018
+
2019
+ Options:
2020
+ --fix Rewrite unambiguous CSS literals to their token equivalents
2021
+ --json Machine-readable output
2022
+ --level <name> Minimum finding level to report (error | warn; default error)`,
2023
+ registry: `Usage: cascivo registry build [dir]
2024
+
2025
+ Build a static registry (registry.json + file payloads) from a
2026
+ cascivo-registry.json manifest, ready to host on any static file server.`,
2027
+ template: `Usage: cascivo template init <name> [options]
2028
+
2029
+ Scaffold a new template: source, manifest, and registry entry.
2030
+
2031
+ Options:
2032
+ --category <name> Template category (e.g. dashboard)
2033
+ --framework <name> Target framework (default react-vite)
2034
+ --components <a,b> Registry components the template composes
2035
+ --repo <owner/repo> Repository the template will be hosted in`,
2036
+ tokens: `Usage: cascivo tokens import <file>
2037
+
2038
+ Import external design tokens (W3C design-tokens JSON) as cascivo token
2039
+ overrides.`
2040
+ };
1276
2041
  async function run(args) {
1277
2042
  const [command, ...rest] = args;
2043
+ if (command !== void 0 && (rest.includes("--help") || rest.includes("-h"))) {
2044
+ const commandHelp = COMMAND_HELP[command];
2045
+ if (commandHelp !== void 0) {
2046
+ console.log(commandHelp);
2047
+ return;
2048
+ }
2049
+ }
1278
2050
  switch (command) {
2051
+ case "create":
2052
+ await create(rest);
2053
+ break;
1279
2054
  case "init":
1280
- await init();
2055
+ await init(rest);
1281
2056
  break;
1282
2057
  case "add":
1283
2058
  await add(rest, await loadConfig(), {
@@ -1304,7 +2079,7 @@ async function run(args) {
1304
2079
  await theme(rest);
1305
2080
  break;
1306
2081
  case "eject": {
1307
- const { eject } = await import("./eject-D_mC1lsq.mjs");
2082
+ const { eject } = await import("./eject-Br8V7I3m.mjs");
1308
2083
  const flag = (key) => {
1309
2084
  const eq = rest.find((r) => r.startsWith(`--${key}=`));
1310
2085
  if (eq) return eq.slice(key.length + 3);
@@ -1331,11 +2106,20 @@ async function run(args) {
1331
2106
  process.exitCode = 1;
1332
2107
  }
1333
2108
  break;
2109
+ case "template":
2110
+ if (rest[0] === "init") {
2111
+ const { templateInit } = await import("./template-init-C46fRXo-.mjs");
2112
+ await templateInit(rest.slice(1));
2113
+ } else {
2114
+ console.error(`Unknown template subcommand: ${rest[0]}`);
2115
+ process.exitCode = 1;
2116
+ }
2117
+ break;
1334
2118
  case "doctor": {
1335
2119
  const ci = rest.includes("--ci");
1336
2120
  if (rest.includes("--drift")) {
1337
- const { runDoctorDrift } = await import("./drift-D7JFzpP_.mjs");
1338
- await runDoctorDrift(rest);
2121
+ const { runDoctorDrift } = await import("./drift-Ciw5qeEt.mjs");
2122
+ await runDoctorDrift(await loadConfig());
1339
2123
  } else {
1340
2124
  const result = await runDoctor(process.cwd());
1341
2125
  if (result.passed) console.log("No violations found.");
@@ -1347,7 +2131,7 @@ async function run(args) {
1347
2131
  break;
1348
2132
  }
1349
2133
  case "audit": {
1350
- const { audit } = await import("./audit-AHr4MDMK.mjs");
2134
+ const { audit } = await import("./audit-InQqdY1s.mjs");
1351
2135
  await audit(rest, await loadConfig());
1352
2136
  break;
1353
2137
  }