cascivo 0.1.4 → 0.2.0

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,16 @@
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";
3
+ import { n as resolveOutputPath, r as writeFileSafe, t as readFileSafe } from "./fs-m7ZvuBBm.mjs";
2
4
  import { t as fetchJson } from "./http-CLkdTpxD.mjs";
3
5
  import { argv, stdin, stdout } from "node:process";
4
6
  import { pathToFileURL } from "node:url";
7
+ import { basename, dirname, join, resolve } from "node:path";
5
8
  import { spawnSync } from "node:child_process";
6
9
  import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
7
- import { basename, dirname, join, resolve } from "node:path";
8
- import { mkdir, readFile, writeFile } from "node:fs/promises";
10
+ import { readFile, writeFile } from "node:fs/promises";
9
11
  import { createHash } from "node:crypto";
12
+ import { asTemplateMeta, buildRegistry, isTemplateItem, parseLegacyRegistry, validateIndex, validateTemplate } from "@cascivo/registry";
10
13
  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
14
  //#region src/utils/exec.ts
69
15
  /**
70
16
  * Install npm packages using the detected package manager, inheriting stdio so
@@ -85,25 +31,6 @@ function installPackages(packages, cwd = process.cwd()) {
85
31
  return true;
86
32
  }
87
33
  //#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
34
  //#region src/utils/registry.ts
108
35
  function asStringArray(value) {
109
36
  return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
@@ -133,6 +60,10 @@ function parseRegistry(raw) {
133
60
  };
134
61
  if (type !== void 0) result.type = type;
135
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
+ }
136
67
  return result;
137
68
  });
138
69
  const blocks = Array.isArray(obj.blocks) ? obj.blocks.flatMap((entry) => {
@@ -365,6 +296,12 @@ async function resolveFromDirectory(ns, fetchFn) {
365
296
  }
366
297
  //#endregion
367
298
  //#region src/commands/add.ts
299
+ var add_exports = /* @__PURE__ */ __exportAll({
300
+ LAYOUT_ALIASES: () => LAYOUT_ALIASES,
301
+ add: () => add,
302
+ resolveBareClosure: () => resolveBareClosure,
303
+ resolveTemplateTarget: () => resolveTemplateTarget
304
+ });
368
305
  /** Dependencies always present in a cascade project — never auto-installed. */
369
306
  const ASSUMED_DEPS = new Set(["react", "react-dom"]);
370
307
  async function fetchText$1(url) {
@@ -372,6 +309,59 @@ async function fetchText$1(url) {
372
309
  if (!res.ok) throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`);
373
310
  return res.text();
374
311
  }
312
+ /** Resolve where a template's own file is written — relative to the project root. */
313
+ function resolveTemplateTarget(cwd, target) {
314
+ return resolve(cwd, target);
315
+ }
316
+ /**
317
+ * Names from other libraries → the cascivo layout primitive that fills the same
318
+ * role, so `cascivo add flex` installs `layout/stack`. Bare names that already
319
+ * resolve (e.g. `stack` → `layout/stack` via suffix match) need no alias.
320
+ */
321
+ const LAYOUT_ALIASES = {
322
+ flex: "stack",
323
+ box: "stack",
324
+ hstack: "stack",
325
+ vstack: "stack",
326
+ gap: "spacer"
327
+ };
328
+ /**
329
+ * Resolve the transitive closure of bare-name registry entries, following each
330
+ * entry's `registryDependencies`. De-duped by resolved registry name and
331
+ * cycle-safe (an entry is processed at most once). Returns resolved entries in
332
+ * install order plus the names that could not be found. Pure — no I/O.
333
+ */
334
+ function resolveBareClosure(registry, bareSpecs) {
335
+ const queue = bareSpecs.map((name) => ({
336
+ name,
337
+ requested: true
338
+ }));
339
+ const installed = /* @__PURE__ */ new Set();
340
+ const resolved = [];
341
+ const missing = [];
342
+ while (queue.length > 0) {
343
+ const { name, requested } = queue.shift();
344
+ const entry = findComponent(registry, LAYOUT_ALIASES[name.toLowerCase()] ?? name);
345
+ if (!entry) {
346
+ missing.push(name);
347
+ continue;
348
+ }
349
+ if (installed.has(entry.name)) continue;
350
+ installed.add(entry.name);
351
+ resolved.push({
352
+ entry,
353
+ requested
354
+ });
355
+ for (const dep of entry.registryDependencies ?? []) queue.push({
356
+ name: dep,
357
+ requested: false
358
+ });
359
+ }
360
+ return {
361
+ resolved,
362
+ missing
363
+ };
364
+ }
375
365
  function isMultiRegistrySpec(name) {
376
366
  return name.startsWith("@") || name.startsWith("http://") || name.startsWith("https://") || name.startsWith("./") || name.startsWith("/") || name.split("/").length >= 3;
377
367
  }
@@ -380,41 +370,51 @@ async function add(names, config, opts = {}) {
380
370
  console.error("Usage: cascivo add <component...>");
381
371
  return;
382
372
  }
373
+ const cwd = opts.cwd ?? process.cwd();
383
374
  const multiSpecs = names.filter(isMultiRegistrySpec);
384
375
  const bareSpecs = names.filter((n) => !isMultiRegistrySpec(n));
385
- let lock = await readLock() ?? createLock();
376
+ let lock = await readLock(cwd) ?? createLock();
386
377
  if (multiSpecs.length > 0) {
387
378
  const plan = await resolveClosure(multiSpecs, config, process.env, resolveFromDirectory);
388
379
  if (opts.dryRun) {
389
380
  console.log("Dry run — would install:");
390
381
  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}`);
382
+ const kind = isTemplateItem(item.item) ? " [template]" : "";
383
+ console.log(` ${item.spec} (${item.item.name}@${item.item.version})${kind} from ${item.itemUrl}`);
384
+ for (const f of item.item.files) {
385
+ const arrow = isTemplateItem(item.item) && f.target ? ` → ${f.target}` : "";
386
+ console.log(` ${f.url}${arrow}`);
387
+ }
388
+ if (item.item.registryDependencies?.length) console.log(` components: ${item.item.registryDependencies.join(", ")}`);
393
389
  }
394
390
  return;
395
391
  }
396
392
  const isThirdParty = (itemUrl) => !itemUrl.includes("cascivo.com");
397
393
  for (const { item, itemUrl, registryBase } of plan.items) {
398
394
  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);
395
+ const fileHashes = await installItem(item, config, cwd);
400
396
  lock = updateLockEntry(lock, item.name, {
401
397
  registry: registryBase,
402
398
  version: item.version,
403
399
  installedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
404
- files: {}
400
+ files: fileHashes
405
401
  });
406
402
  }
407
- await writeLock(lock);
403
+ await writeLock(lock, cwd);
408
404
  return;
409
405
  }
410
406
  const registry = await fetchRegistry(config.registry);
411
407
  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
- }
408
+ const registryBase = config.registry.replace(/\/registry\.json$/, "").replace(/\/r$/, "");
409
+ for (const spec of bareSpecs) {
410
+ const alias = LAYOUT_ALIASES[spec.toLowerCase()];
411
+ if (!alias) continue;
412
+ const target = findComponent(registry, alias);
413
+ if (target) console.log(`"${spec}" is not a cascivo component — installing ${target.name} (its ${spec} primitive).`);
414
+ }
415
+ const { resolved, missing } = resolveBareClosure(registry, bareSpecs);
416
+ for (const name of missing) console.error(`Component "${name}" not found in registry. Run "cascivo list".`);
417
+ for (const { entry, requested } of resolved) {
418
418
  if (entry.type === "chart") {
419
419
  const pkg = entry.install ?? "@cascivo/charts";
420
420
  console.log(`Chart "${entry.name}" is distributed as an npm package.`);
@@ -426,35 +426,446 @@ async function add(names, config, opts = {}) {
426
426
  const fileHashes = {};
427
427
  for (const url of entry.files) {
428
428
  const content = await fetchText$1(url);
429
- const dest = resolveOutputPath(config.outputDir, outputName, fileName(url));
429
+ const dest = resolveOutputPath(config.outputDir, outputName, fileName(url), cwd);
430
430
  await writeFileSafe(dest, content);
431
431
  fileHashes[dest] = sha256(content);
432
432
  }
433
433
  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
434
  lock = updateLockEntry(lock, entry.name, {
436
435
  registry: registryBase,
437
436
  version: entry.version,
438
437
  installedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
439
438
  files: fileHashes
440
439
  });
441
- console.log(`Added ${entry.name} to ${config.outputDir}/${outputName}/`);
440
+ const suffix = requested ? "" : " (dependency)";
441
+ console.log(`Added ${entry.name} to ${config.outputDir}/${outputName}/${suffix}`);
442
442
  }
443
443
  if (missingDeps.size > 0) installPackages([...missingDeps]);
444
- await writeLock(lock);
444
+ await writeLock(lock, cwd);
445
445
  }
446
- async function installItem(item, config, _lock, _registryBase) {
447
- const outputName = item.name.includes("/") ? item.name.split("/").pop() : item.name;
446
+ /**
447
+ * Install a single resolved item, returning a map of written path → content
448
+ * hash for the lockfile. A template writes its own files to their `target`
449
+ * paths (relative to the project root); every other item writes its files under
450
+ * the components output directory. A template's components are installed
451
+ * separately as their own plan entries (via `registryDependencies`).
452
+ */
453
+ async function installItem(item, config, cwd) {
454
+ const fileHashes = {};
448
455
  const missingDeps = /* @__PURE__ */ new Set();
456
+ const template = isTemplateItem(item);
457
+ const outputName = item.name.includes("/") ? item.name.split("/").pop() : item.name;
449
458
  for (const file of item.files ?? []) try {
450
459
  const content = await fetchText$1(file.url);
451
- await writeFileSafe(resolveOutputPath(config.outputDir, outputName, fileName(file.url)), content);
460
+ const dest = template && file.target ? resolveTemplateTarget(cwd, file.target) : resolveOutputPath(config.outputDir, outputName, fileName(file.url), cwd);
461
+ await writeFileSafe(dest, content);
462
+ fileHashes[dest] = sha256(content);
452
463
  } catch {
453
464
  console.warn(` Could not fetch ${file.url}`);
454
465
  }
455
466
  for (const dep of item.dependencies ?? []) if (!ASSUMED_DEPS.has(dep)) missingDeps.add(dep);
456
467
  if (missingDeps.size > 0) installPackages([...missingDeps]);
457
- console.log(`Added ${item.name} to ${config.outputDir}/${outputName}/`);
468
+ if (template) {
469
+ const pages = item.files?.filter((f) => f.target).map((f) => f.target) ?? [];
470
+ console.log(`Installed template ${item.name} (${pages.length} files)`);
471
+ } else console.log(`Added ${item.name} to ${config.outputDir}/${outputName}/`);
472
+ return fileHashes;
473
+ }
474
+ //#endregion
475
+ //#region src/commands/create.ts
476
+ const THEMES$2 = [
477
+ "light",
478
+ "dark",
479
+ "warm"
480
+ ];
481
+ /** Version specifier used for every `@cascivo/*` dependency in generated apps. */
482
+ const CASCIVO_DEP = "latest";
483
+ /** Slug suitable for a union-member string literal: lower-kebab, alnum only. */
484
+ function slug(label) {
485
+ return label.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "section";
486
+ }
487
+ /** PascalCase identifier derived from a free-text label. */
488
+ function pascalCase(label) {
489
+ const safe = label.trim().split(/[^a-zA-Z0-9]+/).filter(Boolean).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("") || "Section";
490
+ return /^[0-9]/.test(safe) ? `Section${safe}` : safe;
491
+ }
492
+ /** Normalize the project name into a valid npm package name. */
493
+ function packageName(name) {
494
+ return name.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "cascivo-app";
495
+ }
496
+ /** Turn raw section labels into unique keyed/component-named sections. */
497
+ function resolveSections(labels) {
498
+ const seenKeys = /* @__PURE__ */ new Set();
499
+ const seenComponents = /* @__PURE__ */ new Set();
500
+ const sections = [];
501
+ for (const label of labels) {
502
+ const trimmed = label.trim();
503
+ if (!trimmed) continue;
504
+ let key = slug(trimmed);
505
+ let component = pascalCase(trimmed);
506
+ let n = 2;
507
+ while (seenKeys.has(key) || seenComponents.has(component)) {
508
+ key = `${slug(trimmed)}-${n}`;
509
+ component = `${pascalCase(trimmed)}${n}`;
510
+ n++;
511
+ }
512
+ seenKeys.add(key);
513
+ seenComponents.add(component);
514
+ sections.push({
515
+ key,
516
+ label: trimmed,
517
+ component
518
+ });
519
+ }
520
+ return sections.length > 0 ? sections : [{
521
+ key: "home",
522
+ label: "Home",
523
+ component: "Home"
524
+ }];
525
+ }
526
+ function packageJson(opts) {
527
+ const pkg = {
528
+ name: packageName(opts.name),
529
+ private: true,
530
+ version: "0.0.0",
531
+ type: "module",
532
+ scripts: {
533
+ dev: "vite",
534
+ build: "tsc && vite build",
535
+ preview: "vite preview"
536
+ },
537
+ dependencies: {
538
+ "@cascivo/core": CASCIVO_DEP,
539
+ "@cascivo/react": CASCIVO_DEP,
540
+ "@cascivo/themes": CASCIVO_DEP,
541
+ "@cascivo/tokens": CASCIVO_DEP,
542
+ react: "^19.0.0",
543
+ "react-dom": "^19.0.0"
544
+ },
545
+ devDependencies: {
546
+ "@types/react": "^19.0.0",
547
+ "@types/react-dom": "^19.0.0",
548
+ "@vitejs/plugin-react": "^5.0.0",
549
+ typescript: "^5.7.0",
550
+ vite: "^7.0.0"
551
+ }
552
+ };
553
+ return JSON.stringify(pkg, null, 2) + "\n";
554
+ }
555
+ function tsconfig() {
556
+ return JSON.stringify({
557
+ compilerOptions: {
558
+ target: "ES2022",
559
+ useDefineForClassFields: true,
560
+ lib: [
561
+ "ES2022",
562
+ "DOM",
563
+ "DOM.Iterable"
564
+ ],
565
+ module: "ESNext",
566
+ skipLibCheck: true,
567
+ moduleResolution: "bundler",
568
+ allowImportingTsExtensions: true,
569
+ resolveJsonModule: true,
570
+ isolatedModules: true,
571
+ moduleDetection: "force",
572
+ noEmit: true,
573
+ jsx: "react-jsx",
574
+ strict: true,
575
+ noUnusedLocals: true,
576
+ noUnusedParameters: true,
577
+ noFallthroughCasesInSwitch: true
578
+ },
579
+ include: ["src"]
580
+ }, null, 2) + "\n";
581
+ }
582
+ function viteConfig() {
583
+ return `import react from '@vitejs/plugin-react'
584
+ import { defineConfig } from 'vite'
585
+
586
+ export default defineConfig({
587
+ plugins: [react()],
588
+ })
589
+ `;
590
+ }
591
+ function indexHtml(opts) {
592
+ return `<!doctype html>
593
+ <html lang="en" data-theme="${opts.theme}">
594
+ <head>
595
+ <meta charset="UTF-8" />
596
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
597
+ <title>${opts.name}</title>
598
+ <style>
599
+ @layer cascivo.reset, cascivo.tokens, cascivo.component, cascivo.theme;
600
+ @layer cascivo.reset {
601
+ *,
602
+ *::before,
603
+ *::after {
604
+ box-sizing: border-box;
605
+ margin: 0;
606
+ padding: 0;
607
+ }
608
+ }
609
+ html,
610
+ body,
611
+ #root {
612
+ height: 100%;
613
+ }
614
+ </style>
615
+ </head>
616
+ <body>
617
+ <div id="root"></div>
618
+ <script type="module" src="/src/main.tsx"><\/script>
619
+ </body>
620
+ </html>
621
+ `;
622
+ }
623
+ function mainTsx() {
624
+ return `import React from 'react'
625
+ import ReactDOM from 'react-dom/client'
626
+ import App from './App'
627
+
628
+ const root = document.getElementById('root')
629
+ if (root) {
630
+ ReactDOM.createRoot(root).render(
631
+ <React.StrictMode>
632
+ <App />
633
+ </React.StrictMode>,
634
+ )
635
+ }
636
+ `;
637
+ }
638
+ function viteEnv() {
639
+ return `/// <reference types="vite/client" />\n`;
640
+ }
641
+ function appTsx(opts, sections) {
642
+ const sectionImports = sections.map((s) => `import { ${s.component} } from './sections/${s.component}'`).join("\n");
643
+ const unionType = sections.map((s) => `'${s.key}'`).join(" | ");
644
+ const navItems = sections.map((s) => ` {
645
+ label: '${s.label.replace(/'/g, "\\'")}',
646
+ active: section.value === '${s.key}',
647
+ onClick: (e) => {
648
+ e.preventDefault()
649
+ section.value = '${s.key}'
650
+ },
651
+ },`).join("\n");
652
+ const renderedSections = sections.map((s) => ` {section.value === '${s.key}' && <${s.component} />}`).join("\n");
653
+ return `'use client'
654
+ import { signal, useSignals } from '@cascivo/core'
655
+ import { AppShell, ShellHeader, SideNav, type SideNavItem } from '@cascivo/react'
656
+ ${sectionImports}
657
+
658
+ import '@cascivo/tokens'
659
+ import '@cascivo/themes/${opts.theme}.css'
660
+ import '@cascivo/react/styles.css'
661
+
662
+ type Section = ${unionType}
663
+
664
+ const section = signal<Section>('${sections[0].key}')
665
+
666
+ export default function App() {
667
+ useSignals()
668
+
669
+ const navItems: SideNavItem[] = [
670
+ ${navItems}
671
+ ]
672
+
673
+ return (
674
+ <AppShell
675
+ header={<ShellHeader brand={{ name: '${opts.name.replace(/'/g, "\\'")}' }} />}
676
+ nav={<SideNav items={navItems} />}
677
+ >
678
+ ${renderedSections}
679
+ </AppShell>
680
+ )
681
+ }
682
+ `;
683
+ }
684
+ function sectionTsx(section) {
685
+ return `import { Card, CardContent, CardHeader, CardTitle, Heading, Text } from '@cascivo/react'
686
+
687
+ export function ${section.component}() {
688
+ return (
689
+ <div
690
+ style={{
691
+ display: 'grid',
692
+ gap: 'var(--cascivo-space-6)',
693
+ padding: 'var(--cascivo-space-6)',
694
+ maxWidth: '64rem',
695
+ }}
696
+ >
697
+ <div style={{ display: 'grid', gap: 'var(--cascivo-space-2)' }}>
698
+ <Heading level={1}>${section.label}</Heading>
699
+ <Text muted>
700
+ Edit <code>src/sections/${section.component}.tsx</code> to build out this page.
701
+ </Text>
702
+ </div>
703
+
704
+ <Card>
705
+ <CardHeader>
706
+ <CardTitle>Get started</CardTitle>
707
+ </CardHeader>
708
+ <CardContent>
709
+ <Text>
710
+ This page is wired into the app shell. Add components with{' '}
711
+ <code>npx cascivo add &lt;component&gt;</code>.
712
+ </Text>
713
+ </CardContent>
714
+ </Card>
715
+ </div>
716
+ )
717
+ }
718
+ `;
719
+ }
720
+ function cascivoConfig(opts) {
721
+ return `import type { CascadeConfig } from 'cascivo'
722
+
723
+ const config: CascadeConfig = {
724
+ registry: 'https://cascivo.com/registry.json',
725
+ outputDir: 'src/components/ui',
726
+ theme: '${opts.theme}',
727
+ }
728
+
729
+ export default config
730
+ `;
731
+ }
732
+ function gitignore() {
733
+ return `node_modules
734
+ dist
735
+ *.local
736
+ .DS_Store
737
+ `;
738
+ }
739
+ function readme(opts) {
740
+ return `# ${opts.name}
741
+
742
+ A [cascivo](https://cascivo.com) app — Vite + React + TypeScript, pre-wired with
743
+ the cascivo app shell, side navigation, and the \`${opts.theme}\` theme.
744
+
745
+ ## Develop
746
+
747
+ \`\`\`sh
748
+ npm install
749
+ npm run dev
750
+ \`\`\`
751
+
752
+ ## Structure
753
+
754
+ - \`src/App.tsx\` — app shell, navigation, and section routing
755
+ - \`src/sections/\` — one component per nav item
756
+
757
+ Add more components with \`npx cascivo add <component>\`.
758
+ `;
759
+ }
760
+ /** Build the full set of files for a new cascivo app. Pure — no filesystem I/O. */
761
+ function buildScaffold(opts) {
762
+ const sections = resolveSections(opts.sections);
763
+ return [
764
+ {
765
+ path: "package.json",
766
+ contents: packageJson(opts)
767
+ },
768
+ {
769
+ path: "tsconfig.json",
770
+ contents: tsconfig()
771
+ },
772
+ {
773
+ path: "vite.config.ts",
774
+ contents: viteConfig()
775
+ },
776
+ {
777
+ path: "index.html",
778
+ contents: indexHtml(opts)
779
+ },
780
+ {
781
+ path: "cascivo.config.ts",
782
+ contents: cascivoConfig(opts)
783
+ },
784
+ {
785
+ path: ".gitignore",
786
+ contents: gitignore()
787
+ },
788
+ {
789
+ path: "README.md",
790
+ contents: readme(opts)
791
+ },
792
+ {
793
+ path: "src/main.tsx",
794
+ contents: mainTsx()
795
+ },
796
+ {
797
+ path: "src/vite-env.d.ts",
798
+ contents: viteEnv()
799
+ },
800
+ {
801
+ path: "src/App.tsx",
802
+ contents: appTsx(opts, sections)
803
+ },
804
+ ...sections.map((s) => ({
805
+ path: `src/sections/${s.component}.tsx`,
806
+ contents: sectionTsx(s)
807
+ }))
808
+ ];
809
+ }
810
+ function flagValue(args, key) {
811
+ const eq = args.find((a) => a.startsWith(`--${key}=`));
812
+ if (eq) return eq.slice(key.length + 3);
813
+ const idx = args.indexOf(`--${key}`);
814
+ const next = idx !== -1 ? args[idx + 1] : void 0;
815
+ return next && !next.startsWith("--") ? next : void 0;
816
+ }
817
+ const DEFAULT_SECTIONS = [
818
+ "Dashboard",
819
+ "Reports",
820
+ "Settings"
821
+ ];
822
+ async function create(args, cwd = process.cwd()) {
823
+ const yes = args.includes("--yes") || args.includes("-y");
824
+ const nameArg = args.find((a) => !a.startsWith("-"));
825
+ const themeArg = flagValue(args, "theme");
826
+ const sectionsArg = flagValue(args, "sections");
827
+ const rl = !yes && stdin.isTTY ? createInterface({
828
+ input: stdin,
829
+ output: stdout
830
+ }) : null;
831
+ try {
832
+ let name = nameArg;
833
+ if (!name && rl) name = (await rl.question("Project name? [my-cascivo-app]: ")).trim();
834
+ name = name || "my-cascivo-app";
835
+ 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";
838
+ let sectionsInput = sectionsArg;
839
+ if (!sectionsInput && rl) sectionsInput = (await rl.question(`Nav sections? (comma-separated) [${DEFAULT_SECTIONS.join(", ")}]: `)).trim();
840
+ const sections = (sectionsInput ?? "").split(",").map((s) => s.trim()).filter(Boolean);
841
+ const opts = {
842
+ name,
843
+ theme: resolvedTheme,
844
+ sections: sections.length > 0 ? sections : DEFAULT_SECTIONS
845
+ };
846
+ const targetDir = join(cwd, name);
847
+ if (existsSync(targetDir) && readdirSync(targetDir).length > 0) {
848
+ console.error(`Target directory "${name}" already exists and is not empty.`);
849
+ process.exitCode = 1;
850
+ return;
851
+ }
852
+ const files = buildScaffold(opts);
853
+ for (const file of files) await writeFileSafe(join(targetDir, file.path), file.contents);
854
+ console.log(`\nCreated ${name} with the ${resolvedTheme} theme (${files.length} files).`);
855
+ const templateSpec = flagValue(args, "template");
856
+ if (templateSpec) {
857
+ const { add } = await Promise.resolve().then(() => add_exports);
858
+ const { loadConfig } = await import("./config-CsnCR5eh.mjs").then((n) => n.n);
859
+ console.log(`\nInstalling template "${templateSpec}"…`);
860
+ await add([templateSpec], await loadConfig(), { cwd: targetDir });
861
+ }
862
+ console.log("\nNext steps:");
863
+ console.log(` cd ${name}`);
864
+ console.log(" npm install");
865
+ console.log(" npm run dev");
866
+ } finally {
867
+ rl?.close();
868
+ }
458
869
  }
459
870
  //#endregion
460
871
  //#region src/commands/doctor.ts
@@ -716,9 +1127,30 @@ async function list(config, options = {}) {
716
1127
  return existsSync(join(process.cwd(), config.outputDir, outputName));
717
1128
  });
718
1129
  console.log(formatList(components));
1130
+ 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
1131
  }
720
1132
  //#endregion
721
1133
  //#region src/commands/registry.ts
1134
+ /**
1135
+ * Validate every template item in an index: each must pass `validateTemplate`,
1136
+ * and any bare `registryDependencies` not present in the same index produce a
1137
+ * warning (they are expected to resolve from another registry at install time).
1138
+ * Returns `{ errors, warnings }`.
1139
+ */
1140
+ function validateTemplates(index) {
1141
+ const errors = [];
1142
+ const warnings = [];
1143
+ const localNames = new Set(index.items.map((i) => i.name));
1144
+ for (const item of index.items) {
1145
+ if (!isTemplateItem(item)) continue;
1146
+ for (const e of validateTemplate(item)) errors.push(`template "${item.name}": ${e}`);
1147
+ 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`);
1148
+ }
1149
+ return {
1150
+ errors,
1151
+ warnings
1152
+ };
1153
+ }
722
1154
  async function registryBuild(args) {
723
1155
  let inFile = "cascade-registry.json";
724
1156
  let outDir = "public/r";
@@ -748,6 +1180,13 @@ async function registryBuild(args) {
748
1180
  process.exitCode = 1;
749
1181
  return;
750
1182
  }
1183
+ const templateResult = validateTemplates(index);
1184
+ for (const w of templateResult.warnings) console.warn(`warn: ${w}`);
1185
+ if (templateResult.errors.length > 0) {
1186
+ for (const e of templateResult.errors) console.error(`error: ${e}`);
1187
+ process.exitCode = 1;
1188
+ return;
1189
+ }
751
1190
  const outPath = resolve(outDir);
752
1191
  await buildRegistry(index, outPath);
753
1192
  console.log(`cascade registry build: wrote static output to ${outPath}`);
@@ -1220,6 +1659,40 @@ async function updateCheck(config) {
1220
1659
  }
1221
1660
  //#endregion
1222
1661
  //#region src/commands/view.ts
1662
+ /** Render an item's details as text. Pure, so it can be unit-tested. */
1663
+ function formatItem(item) {
1664
+ const lines = [];
1665
+ lines.push(`\n${item.name} v${item.version}`);
1666
+ lines.push(`Type: ${item.type}${item.category ? ` / ${item.category}` : ""}`);
1667
+ lines.push(item.description);
1668
+ if (item.author) lines.push(`Author: ${item.author}`);
1669
+ if (item.license) lines.push(`License: ${item.license}`);
1670
+ if (item.homepage) lines.push(`Homepage: ${item.homepage}`);
1671
+ if (isTemplateItem(item)) try {
1672
+ const meta = asTemplateMeta(item.meta);
1673
+ lines.push(`\nTemplate: ${meta.intent}`);
1674
+ lines.push(`Framework: ${meta.framework}`);
1675
+ if (meta.demoUrl) lines.push(`Demo: ${meta.demoUrl}`);
1676
+ if (meta.pages?.length) {
1677
+ lines.push("Pages:");
1678
+ for (const p of meta.pages) lines.push(` ${p.name} → ${p.target}`);
1679
+ }
1680
+ } catch {}
1681
+ if (item.changelog?.length) {
1682
+ lines.push("\nChangelog:");
1683
+ for (const c of item.changelog.slice(0, 5)) lines.push(` ${c.version}: ${c.note}`);
1684
+ }
1685
+ if (item.files?.length) {
1686
+ lines.push("\nFiles:");
1687
+ for (const f of item.files) lines.push(` ${f.url}${f.target ? ` → ${f.target}` : ""}`);
1688
+ }
1689
+ if (item.dependencies?.length) lines.push(`\nDependencies: ${item.dependencies.join(", ")}`);
1690
+ if (item.registryDependencies?.length) {
1691
+ const label = isTemplateItem(item) ? "Components" : "Registry dependencies";
1692
+ lines.push(`${label}: ${item.registryDependencies.join(", ")}`);
1693
+ }
1694
+ return lines.join("\n");
1695
+ }
1223
1696
  async function view(args, config) {
1224
1697
  const spec = args[0];
1225
1698
  if (!spec) {
@@ -1231,25 +1704,7 @@ async function view(args, config) {
1231
1704
  if (headers) fetchOpts.headers = headers;
1232
1705
  if (params) fetchOpts.params = params;
1233
1706
  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(", ")}`);
1707
+ console.log(formatItem(item));
1253
1708
  }
1254
1709
  //#endregion
1255
1710
  //#region src/index.ts
@@ -1259,8 +1714,10 @@ const HELP = `cascivo ${VERSION} — the CSS-native, signal-driven, AI-first des
1259
1714
  Usage: cascivo <command> [options]
1260
1715
 
1261
1716
  Commands:
1717
+ create [name] Scaffold a new ready-to-run app (shell + nav + theme)
1718
+ (--template <spec>: start from a marketplace template)
1262
1719
  init Set up cascivo in the current project
1263
- add <component...> Add one or more components to your project
1720
+ add <component...> Add components or a template to your project
1264
1721
  list [--installed] List available components
1265
1722
  update [component] Update installed components (--check: list outdated)
1266
1723
  search <query> Search components across registries
@@ -1271,11 +1728,15 @@ Commands:
1271
1728
  doctor [--ci] Check components for rule violations
1272
1729
  audit --ai <paths...> Audit AI-generated code against the cascivo contract
1273
1730
  registry build Build a static registry from a cascivo-registry.json file
1731
+ template init <name> Scaffold a new template (source + manifest + registry entry)
1274
1732
 
1275
1733
  Run "cascivo <command> --help" for details.`;
1276
1734
  async function run(args) {
1277
1735
  const [command, ...rest] = args;
1278
1736
  switch (command) {
1737
+ case "create":
1738
+ await create(rest);
1739
+ break;
1279
1740
  case "init":
1280
1741
  await init();
1281
1742
  break;
@@ -1304,7 +1765,7 @@ async function run(args) {
1304
1765
  await theme(rest);
1305
1766
  break;
1306
1767
  case "eject": {
1307
- const { eject } = await import("./eject-D_mC1lsq.mjs");
1768
+ const { eject } = await import("./eject-B92gZhLX.mjs");
1308
1769
  const flag = (key) => {
1309
1770
  const eq = rest.find((r) => r.startsWith(`--${key}=`));
1310
1771
  if (eq) return eq.slice(key.length + 3);
@@ -1331,6 +1792,15 @@ async function run(args) {
1331
1792
  process.exitCode = 1;
1332
1793
  }
1333
1794
  break;
1795
+ case "template":
1796
+ if (rest[0] === "init") {
1797
+ const { templateInit } = await import("./template-init-DGEgatij.mjs");
1798
+ await templateInit(rest.slice(1));
1799
+ } else {
1800
+ console.error(`Unknown template subcommand: ${rest[0]}`);
1801
+ process.exitCode = 1;
1802
+ }
1803
+ break;
1334
1804
  case "doctor": {
1335
1805
  const ci = rest.includes("--ci");
1336
1806
  if (rest.includes("--drift")) {
@@ -1347,7 +1817,7 @@ async function run(args) {
1347
1817
  break;
1348
1818
  }
1349
1819
  case "audit": {
1350
- const { audit } = await import("./audit-AHr4MDMK.mjs");
1820
+ const { audit } = await import("./audit-BRn4MeXD.mjs");
1351
1821
  await audit(rest, await loadConfig());
1352
1822
  break;
1353
1823
  }