cascivo 0.2.0 → 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,14 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import { a as loadConfig, i as installCommand, o as __exportAll, r as detectPackageManager, t as DEFAULT_CONFIG } from "./config-CsnCR5eh.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
3
  import { n as resolveOutputPath, r as writeFileSafe, t as readFileSafe } from "./fs-m7ZvuBBm.mjs";
4
- import { t as fetchJson } from "./http-CLkdTpxD.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";
5
7
  import { argv, stdin, stdout } from "node:process";
6
8
  import { pathToFileURL } from "node:url";
9
+ import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
7
10
  import { basename, dirname, join, resolve } from "node:path";
8
11
  import { spawnSync } from "node:child_process";
9
- import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
10
- import { readFile, writeFile } from "node:fs/promises";
11
- import { createHash } from "node:crypto";
12
+ import { readFile } from "node:fs/promises";
12
13
  import { asTemplateMeta, buildRegistry, isTemplateItem, parseLegacyRegistry, validateIndex, validateTemplate } from "@cascivo/registry";
13
14
  import { createInterface } from "node:readline/promises";
14
15
  //#region src/utils/exec.ts
@@ -31,138 +32,6 @@ function installPackages(packages, cwd = process.cwd()) {
31
32
  return true;
32
33
  }
33
34
  //#endregion
34
- //#region src/utils/registry.ts
35
- function asStringArray(value) {
36
- return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
37
- }
38
- /** Validate and normalize raw JSON into a typed Registry. Throws on bad shape. */
39
- function parseRegistry(raw) {
40
- if (typeof raw !== "object" || raw === null) throw new Error("Invalid registry: expected an object");
41
- const obj = raw;
42
- if (!Array.isArray(obj.components)) throw new Error("Invalid registry: \"components\" must be an array");
43
- const components = obj.components.map((entry, i) => {
44
- if (typeof entry !== "object" || entry === null) throw new Error(`Invalid registry: component at index ${i} is not an object`);
45
- const c = entry;
46
- if (typeof c.name !== "string") throw new Error(`Invalid registry: component at index ${i} is missing "name"`);
47
- const rawType = c.type;
48
- const type = rawType === "component" || rawType === "layout" || rawType === "block" || rawType === "chart" ? rawType : void 0;
49
- const rawMeta = c.meta;
50
- const metaName = typeof rawMeta === "object" && rawMeta !== null && typeof rawMeta.name === "string" ? rawMeta.name : c.name;
51
- const result = {
52
- name: c.name,
53
- description: typeof c.description === "string" ? c.description : "",
54
- category: typeof c.category === "string" ? c.category : "",
55
- version: typeof c.version === "string" ? c.version : "0.0.0",
56
- files: asStringArray(c.files),
57
- dependencies: asStringArray(c.dependencies),
58
- tags: asStringArray(c.tags),
59
- meta: { name: metaName }
60
- };
61
- if (type !== void 0) result.type = type;
62
- if (typeof c.install === "string") result.install = c.install;
63
- if (Array.isArray(c.registryDependencies)) {
64
- const deps = asStringArray(c.registryDependencies);
65
- if (deps.length > 0) result.registryDependencies = deps;
66
- }
67
- return result;
68
- });
69
- const blocks = Array.isArray(obj.blocks) ? obj.blocks.flatMap((entry) => {
70
- if (typeof entry !== "object" || entry === null) return [];
71
- const b = entry;
72
- if (typeof b.name !== "string") return [];
73
- const rawScreenshot = typeof b.screenshot === "object" && b.screenshot !== null ? b.screenshot : {};
74
- return [{
75
- name: b.name,
76
- type: "block",
77
- displayName: typeof b.displayName === "string" ? b.displayName : b.name,
78
- description: typeof b.description === "string" ? b.description : "",
79
- category: typeof b.category === "string" ? b.category : "",
80
- version: typeof b.version === "string" ? b.version : "0.0.0",
81
- files: asStringArray(b.files),
82
- dependencies: asStringArray(b.dependencies),
83
- tags: asStringArray(b.tags),
84
- screenshot: {
85
- light: typeof rawScreenshot.light === "string" ? rawScreenshot.light : "",
86
- dark: typeof rawScreenshot.dark === "string" ? rawScreenshot.dark : ""
87
- }
88
- }];
89
- }) : [];
90
- return {
91
- version: typeof obj.version === "string" ? obj.version : "0.0.0",
92
- generatedAt: typeof obj.generatedAt === "string" ? obj.generatedAt : "",
93
- components,
94
- blocks
95
- };
96
- }
97
- /** Fetch and parse the registry from a URL. */
98
- async function fetchRegistry(url) {
99
- const res = await fetch(url);
100
- if (!res.ok) throw new Error(`Failed to fetch registry from ${url}: ${res.status} ${res.statusText}`);
101
- return parseRegistry(await res.json());
102
- }
103
- /**
104
- * Find a component by name (case-insensitive, full name or unambiguous suffix).
105
- *
106
- * Examples:
107
- * "layout/app-shell" → exact match on full name
108
- * "app-shell" → suffix match if exactly one entry ends with "/app-shell"
109
- * "button" → exact match (no slash, no suffix ambiguity)
110
- */
111
- function findComponent(registry, name) {
112
- const target = name.toLowerCase();
113
- if (target.startsWith("block/")) {
114
- const blockName = target.slice(6);
115
- const block = (registry.blocks ?? []).find((b) => b.name.toLowerCase() === blockName);
116
- if (!block) throw new Error(`Block "${blockName}" not found in registry.`);
117
- return block;
118
- }
119
- const exact = registry.components.find((c) => c.name.toLowerCase() === target);
120
- if (exact) return exact;
121
- const suffix = `/${target}`;
122
- const matches = registry.components.filter((c) => c.name.toLowerCase().endsWith(suffix));
123
- if (matches.length === 1) return matches[0];
124
- }
125
- /** Extract the file name from a registry file URL. */
126
- function fileName(url) {
127
- const stripped = url.split(/[?#]/)[0] ?? url;
128
- return stripped.split("/").pop() || stripped;
129
- }
130
- //#endregion
131
- //#region src/utils/lock.ts
132
- function sha256(content) {
133
- return `sha256-${createHash("sha256").update(content).digest("hex")}`;
134
- }
135
- const LOCK_FILENAME = "cascade.lock";
136
- async function readLock(cwd = process.cwd()) {
137
- const path = join(cwd, LOCK_FILENAME);
138
- if (!existsSync(path)) return null;
139
- try {
140
- const raw = JSON.parse(await readFile(path, "utf8"));
141
- if (typeof raw !== "object" || raw === null) return null;
142
- return raw;
143
- } catch {
144
- return null;
145
- }
146
- }
147
- async function writeLock(lock, cwd = process.cwd()) {
148
- await writeFile(join(cwd, LOCK_FILENAME), `${JSON.stringify(lock, null, 2)}\n`, "utf8");
149
- }
150
- function createLock() {
151
- return {
152
- lockVersion: 1,
153
- items: {}
154
- };
155
- }
156
- function updateLockEntry(lock, name, entry) {
157
- return {
158
- ...lock,
159
- items: {
160
- ...lock.items,
161
- [name]: entry
162
- }
163
- };
164
- }
165
- //#endregion
166
35
  //#region src/utils/resolve.ts
167
36
  const GITHUB_RE = /^([^/]+)\/([^/]+)\/([^#]+)(?:#(.+))?$/;
168
37
  function expandEnv(str, env = process.env) {
@@ -240,7 +109,7 @@ async function resolveItemUrl(addr, config, env = process.env, resolveNamespaceH
240
109
  const hookResult = await resolveNamespaceHook(ns);
241
110
  if (hookResult) return { url: resolveNamespaceUrl(ns, name, hookResult, env) };
242
111
  }
243
- if (ns === "@cascade") return { url: `https://cascivo.com/r/${name}.json` };
112
+ if (ns === "@cascivo" || ns === "@cascade") return { url: `${CASCIVO_HOST}/r/${name}.json` };
244
113
  throw new Error(`Unknown namespace "${ns}". Add it to registries in cascivo.config.ts or check the directory at cascivo.com`);
245
114
  }
246
115
  }
@@ -276,7 +145,7 @@ async function resolveClosure(specs, config, env = process.env, resolveNamespace
276
145
  }
277
146
  //#endregion
278
147
  //#region src/utils/directory.ts
279
- const DIRECTORY_URL = "https://cascivo.com/r/registries.json";
148
+ const DIRECTORY_URL = `${CASCIVO_HOST}/r/registries.json`;
280
149
  async function resolveFromDirectory(ns, fetchFn) {
281
150
  try {
282
151
  let data;
@@ -299,15 +168,31 @@ async function resolveFromDirectory(ns, fetchFn) {
299
168
  var add_exports = /* @__PURE__ */ __exportAll({
300
169
  LAYOUT_ALIASES: () => LAYOUT_ALIASES,
301
170
  add: () => add,
171
+ isThirdParty: () => isThirdParty,
302
172
  resolveBareClosure: () => resolveBareClosure,
303
173
  resolveTemplateTarget: () => resolveTemplateTarget
304
174
  });
305
175
  /** Dependencies always present in a cascade project — never auto-installed. */
306
176
  const ASSUMED_DEPS = new Set(["react", "react-dom"]);
307
- async function fetchText$1(url) {
308
- const res = await fetch(url);
309
- if (!res.ok) throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`);
310
- 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");
311
196
  }
312
197
  /** Resolve where a template's own file is written — relative to the project root. */
313
198
  function resolveTemplateTarget(cwd, target) {
@@ -362,6 +247,14 @@ function resolveBareClosure(registry, bareSpecs) {
362
247
  missing
363
248
  };
364
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);
257
+ }
365
258
  function isMultiRegistrySpec(name) {
366
259
  return name.startsWith("@") || name.startsWith("http://") || name.startsWith("https://") || name.startsWith("./") || name.startsWith("/") || name.split("/").length >= 3;
367
260
  }
@@ -372,7 +265,17 @@ async function add(names, config, opts = {}) {
372
265
  }
373
266
  const cwd = opts.cwd ?? process.cwd();
374
267
  const multiSpecs = names.filter(isMultiRegistrySpec);
375
- const bareSpecs = names.filter((n) => !isMultiRegistrySpec(n));
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
+ }
376
279
  let lock = await readLock(cwd) ?? createLock();
377
280
  if (multiSpecs.length > 0) {
378
281
  const plan = await resolveClosure(multiSpecs, config, process.env, resolveFromDirectory);
@@ -389,10 +292,16 @@ async function add(names, config, opts = {}) {
389
292
  }
390
293
  return;
391
294
  }
392
- const isThirdParty = (itemUrl) => !itemUrl.includes("cascivo.com");
393
295
  for (const { item, itemUrl, registryBase } of plan.items) {
394
296
  if (isThirdParty(itemUrl)) console.log(`\nReview before use — files from non-directory registry: ${registryBase}\n` + (item.files ?? []).map((f) => ` ${f.url}`).join("\n"));
395
- const fileHashes = await installItem(item, config, cwd);
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
+ }
396
305
  lock = updateLockEntry(lock, item.name, {
397
306
  registry: registryBase,
398
307
  version: item.version,
@@ -401,9 +310,13 @@ async function add(names, config, opts = {}) {
401
310
  });
402
311
  }
403
312
  await writeLock(lock, cwd);
404
- return;
313
+ if (bareSpecs.length === 0) {
314
+ hintThemesIfMissing(cwd);
315
+ return;
316
+ }
405
317
  }
406
- const registry = await fetchRegistry(config.registry);
318
+ if (bareSpecs.length === 0) return;
319
+ registry ??= await fetchRegistry(config.registry);
407
320
  const missingDeps = /* @__PURE__ */ new Set();
408
321
  const registryBase = config.registry.replace(/\/registry\.json$/, "").replace(/\/r$/, "");
409
322
  for (const spec of bareSpecs) {
@@ -414,18 +327,31 @@ async function add(names, config, opts = {}) {
414
327
  }
415
328
  const { resolved, missing } = resolveBareClosure(registry, bareSpecs);
416
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;
417
332
  for (const { entry, requested } of resolved) {
418
- if (entry.type === "chart") {
419
- const pkg = entry.install ?? "@cascivo/charts";
420
- console.log(`Chart "${entry.name}" is distributed as an npm package.`);
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);
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) {
429
355
  const dest = resolveOutputPath(config.outputDir, outputName, fileName(url), cwd);
430
356
  await writeFileSafe(dest, content);
431
357
  fileHashes[dest] = sha256(content);
@@ -441,7 +367,15 @@ async function add(names, config, opts = {}) {
441
367
  console.log(`Added ${entry.name} to ${config.outputDir}/${outputName}/${suffix}`);
442
368
  }
443
369
  if (missingDeps.size > 0) installPackages([...missingDeps]);
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
+ }
444
377
  await writeLock(lock, cwd);
378
+ hintThemesIfMissing(cwd);
445
379
  }
446
380
  /**
447
381
  * Install a single resolved item, returning a map of written path → content
@@ -455,13 +389,14 @@ async function installItem(item, config, cwd) {
455
389
  const missingDeps = /* @__PURE__ */ new Set();
456
390
  const template = isTemplateItem(item);
457
391
  const outputName = item.name.includes("/") ? item.name.split("/").pop() : item.name;
458
- for (const file of item.files ?? []) try {
459
- const content = await fetchText$1(file.url);
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) {
460
397
  const dest = template && file.target ? resolveTemplateTarget(cwd, file.target) : resolveOutputPath(config.outputDir, outputName, fileName(file.url), cwd);
461
398
  await writeFileSafe(dest, content);
462
399
  fileHashes[dest] = sha256(content);
463
- } catch {
464
- console.warn(` Could not fetch ${file.url}`);
465
400
  }
466
401
  for (const dep of item.dependencies ?? []) if (!ASSUMED_DEPS.has(dep)) missingDeps.add(dep);
467
402
  if (missingDeps.size > 0) installPackages([...missingDeps]);
@@ -473,11 +408,6 @@ async function installItem(item, config, cwd) {
473
408
  }
474
409
  //#endregion
475
410
  //#region src/commands/create.ts
476
- const THEMES$2 = [
477
- "light",
478
- "dark",
479
- "warm"
480
- ];
481
411
  /** Version specifier used for every `@cascivo/*` dependency in generated apps. */
482
412
  const CASCIVO_DEP = "latest";
483
413
  /** Slug suitable for a union-member string literal: lower-kebab, alnum only. */
@@ -721,7 +651,7 @@ function cascivoConfig(opts) {
721
651
  return `import type { CascadeConfig } from 'cascivo'
722
652
 
723
653
  const config: CascadeConfig = {
724
- registry: 'https://cascivo.com/registry.json',
654
+ registry: '${CASCIVO_HOST}/registry.json',
725
655
  outputDir: 'src/components/ui',
726
656
  theme: '${opts.theme}',
727
657
  }
@@ -807,7 +737,7 @@ function buildScaffold(opts) {
807
737
  }))
808
738
  ];
809
739
  }
810
- function flagValue(args, key) {
740
+ function flagValue$1(args, key) {
811
741
  const eq = args.find((a) => a.startsWith(`--${key}=`));
812
742
  if (eq) return eq.slice(key.length + 3);
813
743
  const idx = args.indexOf(`--${key}`);
@@ -822,8 +752,8 @@ const DEFAULT_SECTIONS = [
822
752
  async function create(args, cwd = process.cwd()) {
823
753
  const yes = args.includes("--yes") || args.includes("-y");
824
754
  const nameArg = args.find((a) => !a.startsWith("-"));
825
- const themeArg = flagValue(args, "theme");
826
- const sectionsArg = flagValue(args, "sections");
755
+ const themeArg = flagValue$1(args, "theme");
756
+ const sectionsArg = flagValue$1(args, "sections");
827
757
  const rl = !yes && stdin.isTTY ? createInterface({
828
758
  input: stdin,
829
759
  output: stdout
@@ -833,8 +763,8 @@ async function create(args, cwd = process.cwd()) {
833
763
  if (!name && rl) name = (await rl.question("Project name? [my-cascivo-app]: ")).trim();
834
764
  name = name || "my-cascivo-app";
835
765
  let theme = (themeArg ?? "").toLowerCase();
836
- if (!THEMES$2.includes(theme) && rl) theme = (await rl.question("Theme? (light/dark/warm) [light]: ")).trim().toLowerCase();
837
- const resolvedTheme = THEMES$2.includes(theme) ? theme : "light";
766
+ if (!THEMES.includes(theme) && rl) theme = (await rl.question(`Theme? (${THEMES.join("/")}) [light]: `)).trim().toLowerCase();
767
+ const resolvedTheme = THEMES.includes(theme) ? theme : "light";
838
768
  let sectionsInput = sectionsArg;
839
769
  if (!sectionsInput && rl) sectionsInput = (await rl.question(`Nav sections? (comma-separated) [${DEFAULT_SECTIONS.join(", ")}]: `)).trim();
840
770
  const sections = (sectionsInput ?? "").split(",").map((s) => s.trim()).filter(Boolean);
@@ -852,10 +782,10 @@ async function create(args, cwd = process.cwd()) {
852
782
  const files = buildScaffold(opts);
853
783
  for (const file of files) await writeFileSafe(join(targetDir, file.path), file.contents);
854
784
  console.log(`\nCreated ${name} with the ${resolvedTheme} theme (${files.length} files).`);
855
- const templateSpec = flagValue(args, "template");
785
+ const templateSpec = flagValue$1(args, "template");
856
786
  if (templateSpec) {
857
787
  const { add } = await Promise.resolve().then(() => add_exports);
858
- const { loadConfig } = await import("./config-CsnCR5eh.mjs").then((n) => n.n);
788
+ const { loadConfig } = await import("./config-BsyJbMvD.mjs").then((n) => n.i);
859
789
  console.log(`\nInstalling template "${templateSpec}"…`);
860
790
  await add([templateSpec], await loadConfig(), { cwd: targetDir });
861
791
  }
@@ -876,6 +806,47 @@ const BANNED_HOOKS = [
876
806
  "useContext",
877
807
  "useReducer"
878
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
+ }
879
850
  async function runDoctor(root) {
880
851
  const violations = [];
881
852
  const componentsDir = join(root, "packages", "components", "src");
@@ -890,7 +861,8 @@ async function runDoctor(root) {
890
861
  const tsxPath = join(componentsDir, name, `${name}.tsx`);
891
862
  if (!existsSync(tsxPath)) continue;
892
863
  const content = readFileSync(tsxPath, "utf8");
893
- 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({
894
866
  file: tsxPath,
895
867
  rule: "no-react-hooks",
896
868
  detail: `Banned hook '${hook}' in ${name}.tsx`
@@ -1034,19 +1006,21 @@ async function generate(args, config) {
1034
1006
  }
1035
1007
  //#endregion
1036
1008
  //#region src/commands/init.ts
1037
- const THEMES$1 = [
1038
- "light",
1039
- "dark",
1040
- "warm"
1041
- ];
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
+ }
1042
1016
  async function promptTheme() {
1043
1017
  const rl = createInterface({
1044
1018
  input: stdin,
1045
1019
  output: stdout
1046
1020
  });
1047
1021
  try {
1048
- const answer = (await rl.question("Theme? (light/dark/warm) [light]: ")).trim().toLowerCase();
1049
- 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";
1050
1024
  } finally {
1051
1025
  rl.close();
1052
1026
  }
@@ -1055,17 +1029,25 @@ function configFileContents(theme) {
1055
1029
  return `import type { CascadeConfig } from 'cascivo'
1056
1030
 
1057
1031
  const config: CascadeConfig = {
1058
- registry: '${DEFAULT_CONFIG.registry}',
1059
- outputDir: '${DEFAULT_CONFIG.outputDir}',
1032
+ registry: '${DEFAULT_CONFIG$1.registry}',
1033
+ outputDir: '${DEFAULT_CONFIG$1.outputDir}',
1060
1034
  theme: '${theme}',
1061
1035
  }
1062
1036
 
1063
1037
  export default config
1064
1038
  `;
1065
1039
  }
1066
- 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
+ }
1067
1048
  installPackages(["@cascivo/core", "@cascivo/tokens"], cwd);
1068
- const theme = await promptTheme();
1049
+ const interactive = themeArg === void 0 && !yes && stdin.isTTY;
1050
+ const theme = themeArg ?? (interactive ? await promptTheme() : "light");
1069
1051
  await writeFileSafe(join(cwd, "cascivo.config.ts"), configFileContents(theme));
1070
1052
  console.log(`\nCreated cascivo.config.ts (theme: ${theme})`);
1071
1053
  console.log("Import the theme in your root CSS or entry file:");
@@ -1078,9 +1060,12 @@ async function init(cwd = process.cwd()) {
1078
1060
  //#region src/commands/list.ts
1079
1061
  const TYPE_LABELS = {
1080
1062
  component: "Components",
1081
- layout: "Layouts",
1082
- block: "Blocks",
1083
- 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)"
1084
1069
  };
1085
1070
  /** Render a group of entries as an aligned text table (no section header). */
1086
1071
  function formatGroup(entries) {
@@ -1202,7 +1187,7 @@ async function search(args, config) {
1202
1187
  const registryFilter = nsFlag !== -1 ? args[nsFlag + 1] : void 0;
1203
1188
  const query = args.filter((a, i) => a !== "--registry" && args[i - 1] !== "--registry").join(" ").trim();
1204
1189
  if (!query) {
1205
- console.error("Usage: cascade search <query> [--registry @ns]");
1190
+ console.error("Usage: cascivo search <query> [--registry @ns]");
1206
1191
  return;
1207
1192
  }
1208
1193
  const results = [];
@@ -1239,25 +1224,224 @@ async function search(args, config) {
1239
1224
  }
1240
1225
  }
1241
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
1242
1389
  //#region src/commands/theme.ts
1243
- const THEMES = [
1244
- "light",
1245
- "dark",
1246
- "warm",
1247
- "flat",
1248
- "minimal",
1249
- "midnight",
1250
- "pastel",
1251
- "brutalist",
1252
- "corporate",
1253
- "terminal",
1254
- "cyberpunk",
1255
- "arcade"
1256
- ];
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
+ }
1257
1436
  async function theme(args) {
1258
1437
  const [sub, name] = args;
1438
+ if (sub === "create") {
1439
+ await themeCreate(args.slice(1), process.cwd());
1440
+ return;
1441
+ }
1259
1442
  if (sub !== "add" || !name) {
1260
- 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>`);
1261
1445
  return;
1262
1446
  }
1263
1447
  if (!THEMES.includes(name)) {
@@ -1490,11 +1674,6 @@ async function confirm(question) {
1490
1674
  rl.close();
1491
1675
  }
1492
1676
  }
1493
- async function fetchText(url) {
1494
- const res = await fetch(url);
1495
- if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${url}`);
1496
- return res.text();
1497
- }
1498
1677
  async function update(name, config, opts = {}) {
1499
1678
  if (opts.check) return updateCheck(config);
1500
1679
  if (!name) {
@@ -1532,9 +1711,9 @@ async function threeWayUpdate(name, upstreamEntry, baseItem, config, opts) {
1532
1711
  const fname = fileName(url);
1533
1712
  const baseFile = baseItem.files.find((f) => fileName(f.url) === fname);
1534
1713
  const dest = resolveOutputPath(config.outputDir, outputName, fname);
1535
- const upstream = await fetchText(url);
1714
+ const upstream = await fetchTextRetry(url);
1536
1715
  const local = existsSync(dest) ? await readFile(dest, "utf8") : "";
1537
- const base = baseFile ? await fetchText(baseFile.url) : "";
1716
+ const base = baseFile ? await fetchTextRetry(baseFile.url) : "";
1538
1717
  if (base === "" && local === "") {
1539
1718
  summaries.push({
1540
1719
  file: fname,
@@ -1604,12 +1783,13 @@ async function twoWayUpdate(name, entry, config, opts) {
1604
1783
  if (!entry) return;
1605
1784
  const pending = [];
1606
1785
  for (const url of entry.files) {
1607
- const res = await fetch(url);
1608
- if (!res.ok) {
1609
- 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));
1610
1791
  return;
1611
1792
  }
1612
- const latest = await res.text();
1613
1793
  const dest = resolveOutputPath(config.outputDir, entry.name, fileName(url));
1614
1794
  const current = await readFileSafe(dest);
1615
1795
  if (current === null) {
@@ -1647,7 +1827,14 @@ async function updateCheck(config) {
1647
1827
  for (const [name, entry] of Object.entries(lock.items)) {
1648
1828
  const current = findComponent(registry, name);
1649
1829
  if (!current) continue;
1650
- 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) {
1651
1838
  console.log(`${name}: ${entry.version} → ${current.version}`);
1652
1839
  outdated++;
1653
1840
  }
@@ -1668,6 +1855,10 @@ function formatItem(item) {
1668
1855
  if (item.author) lines.push(`Author: ${item.author}`);
1669
1856
  if (item.license) lines.push(`License: ${item.license}`);
1670
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}`);
1671
1862
  if (isTemplateItem(item)) try {
1672
1863
  const meta = asTemplateMeta(item.meta);
1673
1864
  lines.push(`\nTemplate: ${meta.intent}`);
@@ -1696,7 +1887,7 @@ function formatItem(item) {
1696
1887
  async function view(args, config) {
1697
1888
  const spec = args[0];
1698
1889
  if (!spec) {
1699
- console.error("Usage: cascade view <component-spec>");
1890
+ console.error("Usage: cascivo view <component-spec>");
1700
1891
  return;
1701
1892
  }
1702
1893
  const { url, headers, params } = await resolveItemUrl(parseAddress(spec), config);
@@ -1708,7 +1899,7 @@ async function view(args, config) {
1708
1899
  }
1709
1900
  //#endregion
1710
1901
  //#region src/index.ts
1711
- const VERSION = "0.0.0";
1902
+ const VERSION = createRequire(import.meta.url)("../package.json").version;
1712
1903
  const HELP = `cascivo ${VERSION} — the CSS-native, signal-driven, AI-first design system
1713
1904
 
1714
1905
  Usage: cascivo <command> [options]
@@ -1722,23 +1913,146 @@ Commands:
1722
1913
  update [component] Update installed components (--check: list outdated)
1723
1914
  search <query> Search components across registries
1724
1915
  view <spec> View a component before installing
1725
- 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
1726
1917
  eject <component> Eject specific tokens into a scoped local override file
1727
1918
  generate <config.json> Generate TSX from a ViewConfig JSON file
1728
1919
  doctor [--ci] Check components for rule violations
1729
1920
  audit --ai <paths...> Audit AI-generated code against the cascivo contract
1730
1921
  registry build Build a static registry from a cascivo-registry.json file
1731
1922
  template init <name> Scaffold a new template (source + manifest + registry entry)
1923
+ tokens import <file> Import external design tokens as cascivo overrides
1732
1924
 
1733
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
+ };
1734
2041
  async function run(args) {
1735
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
+ }
1736
2050
  switch (command) {
1737
2051
  case "create":
1738
2052
  await create(rest);
1739
2053
  break;
1740
2054
  case "init":
1741
- await init();
2055
+ await init(rest);
1742
2056
  break;
1743
2057
  case "add":
1744
2058
  await add(rest, await loadConfig(), {
@@ -1765,7 +2079,7 @@ async function run(args) {
1765
2079
  await theme(rest);
1766
2080
  break;
1767
2081
  case "eject": {
1768
- const { eject } = await import("./eject-B92gZhLX.mjs");
2082
+ const { eject } = await import("./eject-Br8V7I3m.mjs");
1769
2083
  const flag = (key) => {
1770
2084
  const eq = rest.find((r) => r.startsWith(`--${key}=`));
1771
2085
  if (eq) return eq.slice(key.length + 3);
@@ -1794,7 +2108,7 @@ async function run(args) {
1794
2108
  break;
1795
2109
  case "template":
1796
2110
  if (rest[0] === "init") {
1797
- const { templateInit } = await import("./template-init-DGEgatij.mjs");
2111
+ const { templateInit } = await import("./template-init-C46fRXo-.mjs");
1798
2112
  await templateInit(rest.slice(1));
1799
2113
  } else {
1800
2114
  console.error(`Unknown template subcommand: ${rest[0]}`);
@@ -1804,8 +2118,8 @@ async function run(args) {
1804
2118
  case "doctor": {
1805
2119
  const ci = rest.includes("--ci");
1806
2120
  if (rest.includes("--drift")) {
1807
- const { runDoctorDrift } = await import("./drift-D7JFzpP_.mjs");
1808
- await runDoctorDrift(rest);
2121
+ const { runDoctorDrift } = await import("./drift-Ciw5qeEt.mjs");
2122
+ await runDoctorDrift(await loadConfig());
1809
2123
  } else {
1810
2124
  const result = await runDoctor(process.cwd());
1811
2125
  if (result.passed) console.log("No violations found.");
@@ -1817,7 +2131,7 @@ async function run(args) {
1817
2131
  break;
1818
2132
  }
1819
2133
  case "audit": {
1820
- const { audit } = await import("./audit-BRn4MeXD.mjs");
2134
+ const { audit } = await import("./audit-InQqdY1s.mjs");
1821
2135
  await audit(rest, await loadConfig());
1822
2136
  break;
1823
2137
  }