cascivo 0.6.2 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { a as detectPackageManager, c as isPackageManager, l as loadConfig, n as DEFAULT_CONFIG$1, o as installCommand, r as THEMES, s as installHint, t as CASCIVO_HOST, u as __exportAll } from "./config-C6GdrbvF.mjs";
2
+ import { a as detectPackageManager, c as isPackageManager, l as loadConfig, n as DEFAULT_CONFIG$1, o as installCommand, r as THEMES, s as installHint, t as CASCIVO_HOST, u as __exportAll } from "./config-D7ddWN_9.mjs";
3
3
  import { n as resolveOutputPath, r as writeFileSafe, t as readFileSafe } from "./fs-m7ZvuBBm.mjs";
4
4
  import { r as fetchTextRetry, t as fetchJson } from "./http-CJZ5W0Fa.mjs";
5
5
  import { a as writeLock, c as findComponent, i as updateLockEntry, n as readLock, o as fetchRegistry, r as sha256, s as fileName, t as createLock } from "./lock-CW8UuEPJ.mjs";
@@ -24,7 +24,11 @@ import { createInterface } from "node:readline/promises";
24
24
  function installPackages(packages, cwd = process.cwd(), opts = {}) {
25
25
  if (packages.length === 0) return true;
26
26
  const pm = opts.pm ?? detectPackageManager(cwd);
27
- const [cmd, args] = installCommand(pm, packages, { dev: opts.dev ?? false });
27
+ const [cmd, args] = installCommand(pm, packages, {
28
+ dev: opts.dev ?? false,
29
+ ...opts.floors ? { floors: opts.floors } : {},
30
+ ...opts.pin ? { pin: opts.pin } : {}
31
+ });
28
32
  console.log(`Installing ${packages.join(", ")} with ${cmd}…`);
29
33
  if (spawnSync(cmd, args, {
30
34
  cwd,
@@ -351,6 +355,17 @@ async function add(names, config, opts = {}) {
351
355
  }
352
356
  const { resolved, missing } = resolveBareClosure(registry, bareSpecs);
353
357
  for (const name of missing) console.error(`Component "${name}" not found in registry. Run "cascivo list".`);
358
+ const announced = /* @__PURE__ */ new Set();
359
+ for (const { entry, requested } of resolved) {
360
+ if (!requested || !entry.install || announced.has(entry.install)) continue;
361
+ announced.add(entry.install);
362
+ console.log(`\n"${entry.name}" ships in the ${entry.install} npm package — no source is copied into your project. Updates come from your package manager, not \`cascivo update\`.`);
363
+ }
364
+ for (const { entry, requested } of resolved) {
365
+ if (!requested || !entry.deprecated) continue;
366
+ const { replacement, since, note } = entry.deprecated;
367
+ console.warn(`\n⚠ "${entry.name}" is deprecated since ${since}. Use "${replacement}" instead:\n cascivo add ${replacement}\n` + (note ? ` ${note}\n` : "") + ` Adding "${entry.name}" anyway — it still works.\n`);
368
+ }
354
369
  const peerFloors = {};
355
370
  for (const { entry } of resolved) for (const [pkg, floor] of Object.entries(entry.peerVersions ?? {})) peerFloors[pkg] = floor;
356
371
  const npmPackages = /* @__PURE__ */ new Set();
@@ -490,9 +505,35 @@ function resolvePackageManagerFlag(args) {
490
505
  return { error: `Unknown package manager "${raw}". Use one of: pnpm, yarn, npm, bun.` };
491
506
  }
492
507
  //#endregion
508
+ //#region src/generated/versions.ts
509
+ /**
510
+ * Exact published versions, baked in at build time.
511
+ *
512
+ * Scaffolded apps pin these rather than `"latest"`: the cascivo packages version
513
+ * independently on 0.x, so a floating specifier can resolve a mutually incompatible set,
514
+ * and GETTING-STARTED.md tells adopters to pin exactly. Regenerate with `pnpm regen`.
515
+ */
516
+ const CASCIVO_VERSIONS = {
517
+ "@cascivo/react": "0.16.1",
518
+ "@cascivo/themes": "0.4.11",
519
+ "@cascivo/charts": "0.16.1",
520
+ "@cascivo/icons": "0.3.8",
521
+ "@cascivo/eslint-config": "0.2.2"
522
+ };
523
+ /** `@cascivo/core`'s declared `@preact/signals-react` peer range. */
524
+ const SIGNALS_PEER = ">=3.0.0";
525
+ //#endregion
493
526
  //#region src/commands/create.ts
494
- /** Version specifier used for every `@cascivo/*` dependency in generated apps. */
495
- const CASCIVO_DEP = "latest";
527
+ /**
528
+ * Exact published versions, baked in at build time by `scripts/registry/cli-versions.ts`.
529
+ *
530
+ * This used to be the literal `'latest'` for every cascivo dependency — the loosest
531
+ * possible specifier on a set of independently-versioned 0.x packages, and the direct
532
+ * opposite of GETTING-STARTED.md's "pin **exact** versions (no `^`)". A scaffold that
533
+ * contradicts the docs on the very first file an adopter opens undermines every other rule
534
+ * those docs state, so the pins are generated rather than hand-written.
535
+ */
536
+ const V = CASCIVO_VERSIONS;
496
537
  /** Install-everything command for a package manager (`pnpm install`, `yarn`, …). */
497
538
  function installAllCommand(pm) {
498
539
  return pm === "yarn" ? "yarn" : `${pm} install`;
@@ -510,6 +551,24 @@ function pascalCase(label) {
510
551
  const safe = label.trim().split(/[^a-zA-Z0-9]+/).filter(Boolean).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("") || "Section";
511
552
  return /^[0-9]/.test(safe) ? `Section${safe}` : safe;
512
553
  }
554
+ /**
555
+ * Short, human-readable brand for the shell header.
556
+ *
557
+ * The directory name is the wrong thing to render verbatim: a dated demo directory
558
+ * (`vercel-dashboard-2026-07-30-take2`) produced a 45-character brand in the top-left of
559
+ * every page. Take the leading words, title-case them, and stop — a brand is a label, not a
560
+ * slug. Bare numbers and `v2`-style segments are dropped rather than counted.
561
+ */
562
+ function brandName(name) {
563
+ const words = name.trim().split(/[^a-zA-Z0-9]+/).filter((w) => w !== "" && !/^\d+$/.test(w) && !/^v\d+$/i.test(w));
564
+ const kept = [];
565
+ for (const word of words) {
566
+ if (kept.length >= 3) break;
567
+ if (kept.length > 0 && kept.join(" ").length + 1 + word.length > 24) break;
568
+ kept.push(word.charAt(0).toUpperCase() + word.slice(1));
569
+ }
570
+ return kept.join(" ") || "App";
571
+ }
513
572
  /** Normalize the project name into a valid npm package name. */
514
573
  function packageName(name) {
515
574
  return name.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "cascivo-app";
@@ -553,20 +612,25 @@ function packageJson(opts) {
553
612
  scripts: {
554
613
  dev: "vite",
555
614
  build: "tsc && vite build",
556
- preview: "vite preview"
615
+ preview: "vite preview",
616
+ typecheck: "tsc --noEmit",
617
+ lint: "eslint ."
557
618
  },
558
619
  dependencies: {
559
- "@cascivo/core": CASCIVO_DEP,
560
- "@cascivo/react": CASCIVO_DEP,
561
- "@cascivo/themes": CASCIVO_DEP,
562
- "@cascivo/tokens": CASCIVO_DEP,
620
+ "@cascivo/react": V["@cascivo/react"],
621
+ "@cascivo/themes": V["@cascivo/themes"],
622
+ "@preact/signals-react": SIGNALS_PEER,
563
623
  react: "^19.0.0",
564
624
  "react-dom": "^19.0.0"
565
625
  },
566
626
  devDependencies: {
627
+ "@cascivo/eslint-config": V["@cascivo/eslint-config"],
628
+ "@eslint/js": "^9.0.0",
567
629
  "@types/react": "^19.0.0",
568
630
  "@types/react-dom": "^19.0.0",
569
631
  "@vitejs/plugin-react": "^5.0.0",
632
+ eslint: "^9.0.0",
633
+ "eslint-plugin-react-hooks": "^7.0.0",
570
634
  typescript: "^5.7.0",
571
635
  vite: "^7.0.0"
572
636
  }
@@ -617,7 +681,13 @@ function indexHtml(opts) {
617
681
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
618
682
  <title>${opts.name}</title>
619
683
  <style>
620
- @layer vendor, cascivo.reset, cascivo.base, cascivo.tokens, cascivo.component, cascivo.theme, cascivo.blocks, cascivo.override;
684
+ @layer vendor, cascivo.reset, cascivo.base, cascivo.tokens, cascivo.component,
685
+ cascivo.platform, cascivo.theme, cascivo.blocks, cascivo.example, cascivo.override;
686
+ /* cascivo.example is this app's own slot — above the component/blocks layers so your
687
+ styles win, below cascivo.override which stays free for one-off hotfixes. The
688
+ generated AGENTS.md tells the agent to write there, and this statement is what makes
689
+ that legal: a layer used but never declared falls to the end of the cascade and
690
+ beats everything, which is the opposite of what the ordering is for. */
621
691
  /* Third-party CSS goes in the low-priority vendor layer so it can't beat cascivo:
622
692
  @import url('some-lib/styles.css') layer(vendor); — see docs/THIRD-PARTY-CSS.md */
623
693
  @layer cascivo.reset {
@@ -674,11 +744,16 @@ function appTsx(opts, sections) {
674
744
  },`).join("\n");
675
745
  const renderedSections = sections.map((s) => ` {section.value === '${s.key}' && <${s.component} />}`).join("\n");
676
746
  return `'use client'
677
- import { signal, useSignals } from '@cascivo/core'
678
- import { AppShell, ShellHeader, SideNav, type SideNavItem } from '@cascivo/react'
747
+ import {
748
+ AppShell,
749
+ ShellHeader,
750
+ SideNav,
751
+ signal,
752
+ useSignals,
753
+ type SideNavItem,
754
+ } from '@cascivo/react'
679
755
  ${sectionImports}
680
756
 
681
- import '@cascivo/tokens'
682
757
  import '@cascivo/themes/${opts.theme}.css'
683
758
  import '@cascivo/react/styles.css'
684
759
 
@@ -695,7 +770,7 @@ ${navItems}
695
770
 
696
771
  return (
697
772
  <AppShell
698
- header={<ShellHeader brand={{ name: '${opts.name.replace(/'/g, "\\'")}' }} />}
773
+ header={<ShellHeader brand={{ name: '${brandName(opts.name).replace(/'/g, "\\'")}' }} />}
699
774
  nav={<SideNav items={navItems} />}
700
775
  >
701
776
  ${renderedSections}
@@ -740,16 +815,31 @@ export function ${section.component}() {
740
815
  }
741
816
  `;
742
817
  }
743
- function cascivoConfig(opts) {
744
- return `import type { CascadeConfig } from 'cascivo'
745
-
746
- const config: CascadeConfig = {
747
- registry: '${CASCIVO_HOST}/registry.json',
748
- outputDir: 'src/components/ui',
749
- theme: '${opts.theme}',
750
- }
818
+ /**
819
+ * ESLint flat config for the scaffold.
820
+ *
821
+ * `@cascivo/eslint-config` is wired in from the start because
822
+ * `eslint-plugin-react-hooks@7`'s `recommended-latest` reports every `signal.value = next`
823
+ * — the idiom AI-RULES.md mandates and the generated `App.tsx` below uses — as
824
+ * `Error: This value cannot be modified`. Without this, `pnpm lint` on a freshly scaffolded
825
+ * app errors on every piece of state it ships with.
826
+ */
827
+ function eslintConfig() {
828
+ return `import js from '@eslint/js'
829
+ import reactHooks from 'eslint-plugin-react-hooks'
830
+ import cascivo from '@cascivo/eslint-config'
751
831
 
752
- export default config
832
+ export default [
833
+ js.configs.recommended,
834
+ // NOTE the \`.flat\` — the plugin exports both \`configs['recommended-latest']\` (the legacy
835
+ // eslintrc shape, which applies NOTHING here and reports no error) and this one.
836
+ reactHooks.configs.flat['recommended-latest'],
837
+ // Spread LAST — flat config is last-wins. This turns off \`react-hooks/immutability\`,
838
+ // which reports cascivo's signal writes (\`signal.value = next\`) as errors.
839
+ // See https://cascivo.com/docs/using-with-strict-eslint.md
840
+ ...cascivo,
841
+ { ignores: ['dist/**'] },
842
+ ]
753
843
  `;
754
844
  }
755
845
  function gitignore() {
@@ -807,7 +897,7 @@ This app's declared layer order (in \`index.html\`):
807
897
 
808
898
  \`\`\`css
809
899
  @layer vendor, cascivo.reset, cascivo.base, cascivo.tokens, cascivo.component,
810
- cascivo.theme, cascivo.blocks, cascivo.override;
900
+ cascivo.platform, cascivo.theme, cascivo.blocks, cascivo.example, cascivo.override;
811
901
  \`\`\`
812
902
 
813
903
  ### Worked example — nesting, not new layers
@@ -850,8 +940,8 @@ function buildScaffold(opts) {
850
940
  contents: indexHtml(opts)
851
941
  },
852
942
  {
853
- path: "cascivo.config.ts",
854
- contents: cascivoConfig(opts)
943
+ path: "eslint.config.js",
944
+ contents: eslintConfig()
855
945
  },
856
946
  {
857
947
  path: ".gitignore",
@@ -938,7 +1028,7 @@ async function create(args, cwd = process.cwd()) {
938
1028
  const templateSpec = flagValue(args, "template");
939
1029
  if (templateSpec) {
940
1030
  const { add } = await Promise.resolve().then(() => add_exports);
941
- const { loadConfig } = await import("./config-C6GdrbvF.mjs").then((n) => n.i);
1031
+ const { loadConfig } = await import("./config-D7ddWN_9.mjs").then((n) => n.i);
942
1032
  console.log(`\nInstalling template "${templateSpec}"…`);
943
1033
  await add([templateSpec], await loadConfig(), {
944
1034
  cwd: targetDir,
@@ -956,20 +1046,36 @@ async function create(args, cwd = process.cwd()) {
956
1046
  //#endregion
957
1047
  //#region src/commands/doctor.ts
958
1048
  var doctor_exports = /* @__PURE__ */ __exportAll({
1049
+ checkDuplicateCore: () => checkDuplicateCore,
1050
+ checkFormatterIgnore: () => checkFormatterIgnore,
959
1051
  checkProjectDependencies: () => checkProjectDependencies,
960
1052
  checkSignalsCompat: () => checkSignalsCompat,
961
1053
  checkSsrConfig: () => checkSsrConfig,
962
- isAdopterProject: () => isAdopterProject,
1054
+ detectInstallPath: () => detectInstallPath,
963
1055
  runDoctor: () => runDoctor,
964
1056
  stripCommentsAndStrings: () => stripCommentsAndStrings
965
1057
  });
966
- /** Runtime packages copied cascivo source needs; the last is @cascivo/core's peer. */
1058
+ /** Runtime packages **copied** cascivo source needs; the last is @cascivo/core's peer. */
967
1059
  const REQUIRED_RUNTIME_DEPS = [
968
1060
  "@cascivo/core",
969
1061
  "@cascivo/tokens",
970
1062
  "@cascivo/themes",
971
1063
  "@preact/signals-react"
972
1064
  ];
1065
+ /** Runtime packages a **prebuilt** (Path B) app needs. */
1066
+ const REQUIRED_PREBUILT_DEPS = [
1067
+ "@cascivo/react",
1068
+ "@cascivo/themes",
1069
+ "@preact/signals-react"
1070
+ ];
1071
+ /**
1072
+ * Packages a prebuilt app must NOT declare directly, with the rule each one breaks. They
1073
+ * are transitive there, and everything they export is re-exported from `@cascivo/react`.
1074
+ */
1075
+ const FORBIDDEN_PREBUILT_DEPS = {
1076
+ "@cascivo/core": "AI-RULES.md: never add @cascivo/core to a prebuilt-path app — it is transitive, and everything is re-exported from @cascivo/react",
1077
+ "@cascivo/tokens": "GETTING-STARTED.md: @cascivo/tokens comes with @cascivo/themes as a direct dependency — never install it by hand"
1078
+ };
973
1079
  /** Installed on demand by `cascivo add` when a component/chart declares them. */
974
1080
  const ON_DEMAND_DEPS = ["@cascivo/i18n", "@cascivo/charts"];
975
1081
  const CONFIG_FILES = [
@@ -977,17 +1083,74 @@ const CONFIG_FILES = [
977
1083
  "cascivo.config.js",
978
1084
  "cascivo.config.mjs"
979
1085
  ];
980
- /** Whether cwd looks like a cascivo adopter project (has a generated config). */
981
- function isAdopterProject(cwd) {
982
- return CONFIG_FILES.some((f) => existsSync(join(cwd, f)));
1086
+ /**
1087
+ * Infer the install path from evidence.
1088
+ *
1089
+ * This used to be `isAdopterProject()` — "does a `cascivo.config.*` exist?" — and
1090
+ * `cascivo create` wrote that config into every scaffold, including prebuilt-path ones. So
1091
+ * every scaffolded app was judged copy-paste and told to install `@cascivo/core` and
1092
+ * `@cascivo/tokens`, which the docs explicitly forbid on that path. `doctor --ci` exited 1
1093
+ * on a correctly-installed app, which made the CI gate the docs recommend
1094
+ * (`cascivo doctor --ci && cascivo audit --ai src`) red on day one.
1095
+ *
1096
+ * A config file now only contributes evidence when it points at a directory that actually
1097
+ * contains copied source, which is what it was ever meant to signal.
1098
+ */
1099
+ function detectInstallPath(cwd) {
1100
+ let deps = {};
1101
+ let hasPackageJson = true;
1102
+ try {
1103
+ const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
1104
+ deps = {
1105
+ ...pkg.dependencies,
1106
+ ...pkg.devDependencies
1107
+ };
1108
+ } catch {
1109
+ hasPackageJson = false;
1110
+ }
1111
+ if (!hasPackageJson) return "unknown";
1112
+ const usesPackage = deps["@cascivo/react"] !== void 0;
1113
+ const copied = hasCopiedSource(cwd);
1114
+ if (usesPackage && copied) return "hybrid";
1115
+ if (usesPackage) return "prebuilt";
1116
+ if (copied) return "copied";
1117
+ return "unknown";
1118
+ }
1119
+ /** Whether the configured output directory holds vendored component source. */
1120
+ function hasCopiedSource(cwd) {
1121
+ for (const dir of outputDirCandidates(cwd)) {
1122
+ const full = join(cwd, dir);
1123
+ if (!existsSync(full)) continue;
1124
+ try {
1125
+ if (readdirSync(full).some((f) => f.endsWith(".tsx"))) return true;
1126
+ } catch {}
1127
+ }
1128
+ return false;
1129
+ }
1130
+ /** `outputDir` from the config if it declares one, plus the documented default. */
1131
+ function outputDirCandidates(cwd) {
1132
+ const dirs = new Set(["src/components/ui"]);
1133
+ for (const file of CONFIG_FILES) {
1134
+ const full = join(cwd, file);
1135
+ if (!existsSync(full)) continue;
1136
+ try {
1137
+ const declared = /outputDir\s*:\s*['"]([^'"]+)['"]/.exec(readFileSync(full, "utf8"))?.[1];
1138
+ if (declared !== void 0) dirs.add(declared);
1139
+ } catch {}
1140
+ }
1141
+ return [...dirs];
983
1142
  }
984
1143
  /**
985
- * Advisory check that the runtime dependencies copied source needs are declared
986
- * in the project's package.json. Turns the opaque "cannot find module
987
- * '@preact/signals-react'" build failure — the report's #4, where the peer was
988
- * invisible — into a diagnosed condition with a fix. Adopter-only (gated on a
989
- * cascivo.config by the caller); `@cascivo/i18n`/`@cascivo/charts` are advisory
990
- * since not every project uses them.
1144
+ * Check that the runtime dependencies this project's install path needs are declared in its
1145
+ * package.json — and, on the prebuilt path, that it declares none it must not.
1146
+ *
1147
+ * Turns the opaque "cannot find module '@preact/signals-react'" build failure into a
1148
+ * diagnosed condition with a fix. What it demands depends on `detectInstallPath`: requiring
1149
+ * `@cascivo/core` of a prebuilt app is not merely unhelpful, it is the opposite of what the
1150
+ * docs say, and following the advice makes a correct project wrong.
1151
+ *
1152
+ * `@cascivo/i18n`/`@cascivo/charts` stay advisory since not every project uses them, and an
1153
+ * `unknown` path emits nothing at all rather than guessing.
991
1154
  */
992
1155
  function checkProjectDependencies(cwd) {
993
1156
  let deps = {};
@@ -1000,13 +1163,24 @@ function checkProjectDependencies(cwd) {
1000
1163
  } catch {
1001
1164
  return [];
1002
1165
  }
1166
+ const path = detectInstallPath(cwd);
1167
+ if (path === "unknown") return [];
1003
1168
  const pm = detectPackageManager(cwd);
1004
1169
  const findings = [];
1005
- for (const pkg of REQUIRED_RUNTIME_DEPS) if (deps[pkg] === void 0) findings.push({
1170
+ const required = path === "prebuilt" ? REQUIRED_PREBUILT_DEPS : REQUIRED_RUNTIME_DEPS;
1171
+ for (const pkg of required) if (deps[pkg] === void 0) findings.push({
1006
1172
  package: pkg,
1007
1173
  required: true,
1008
- hint: installHint(pm, [pkg])
1174
+ hint: `detected ${path} install path — ${installHint(pm, [pkg])}`
1009
1175
  });
1176
+ if (path === "prebuilt") {
1177
+ for (const [pkg, reason] of Object.entries(FORBIDDEN_PREBUILT_DEPS)) if (deps[pkg] !== void 0) findings.push({
1178
+ package: pkg,
1179
+ required: true,
1180
+ kind: "forbidden",
1181
+ hint: reason
1182
+ });
1183
+ }
1010
1184
  for (const pkg of ON_DEMAND_DEPS) if (deps[pkg] === void 0) findings.push({
1011
1185
  package: pkg,
1012
1186
  required: false,
@@ -1014,6 +1188,29 @@ function checkProjectDependencies(cwd) {
1014
1188
  });
1015
1189
  return findings;
1016
1190
  }
1191
+ /** Packages that carry their own `@cascivo/core` dependency. */
1192
+ const CORE_DEPENDENTS = [
1193
+ "@cascivo/react",
1194
+ "@cascivo/charts",
1195
+ "@cascivo/flow",
1196
+ "@cascivo/editor"
1197
+ ];
1198
+ async function checkDuplicateCore(cwd) {
1199
+ const root = await readInstalledPackageVersion(cwd, "@cascivo/core");
1200
+ const nested = [];
1201
+ for (const owner of CORE_DEPENDENTS) {
1202
+ const ownerRoot = join(cwd, "node_modules", owner);
1203
+ if (!existsSync(ownerRoot)) continue;
1204
+ const version = await readInstalledPackageVersion(ownerRoot, "@cascivo/core");
1205
+ if (version !== null && version !== root) nested.push(`${owner} → ${version}`);
1206
+ }
1207
+ if (nested.length === 0) return null;
1208
+ return {
1209
+ root,
1210
+ nested,
1211
+ hint: "Align the @cascivo/* versions (they are released together — see breaking-changes.json) and reinstall. Two copies of @cascivo/core means two signal registries: writes through one are invisible to components subscribed through the other, with no error."
1212
+ };
1213
+ }
1017
1214
  /**
1018
1215
  * Checks the installed `@preact/signals-react` against the installed React.
1019
1216
  * React 19 removed the `__SECRET_INTERNALS…` export that signals-react 2.x
@@ -1071,6 +1268,38 @@ const VITE_CONFIG_FILES = [
1071
1268
  * cliff the 2026-07-20 report hit (blocker #1). A text match, not a gate. Returns
1072
1269
  * null when there's no Vite SSR framework or the config already handles it.
1073
1270
  */
1271
+ /**
1272
+ * Warn when the vendored components dir is not excluded from the project's formatter.
1273
+ *
1274
+ * Owning the code means your formatter rewrites it, and `cascivo update` then reports drift
1275
+ * on files you never edited. `cascivo init` writes the exclusion for you, but a project that
1276
+ * adopted cascivo before that existed — or that added Prettier afterwards — never got it.
1277
+ */
1278
+ function checkFormatterIgnore(cwd, outputDir) {
1279
+ const pairs = [{
1280
+ configs: [
1281
+ ".prettierrc",
1282
+ ".prettierrc.json",
1283
+ ".prettierrc.js",
1284
+ "prettier.config.js"
1285
+ ],
1286
+ ignore: ".prettierignore"
1287
+ }, {
1288
+ configs: [".oxfmtrc", ".oxfmtrc.json"],
1289
+ ignore: ".oxfmtignore"
1290
+ }];
1291
+ const dir = outputDir.replace(/\/+$/, "");
1292
+ for (const { configs, ignore } of pairs) {
1293
+ if (!configs.some((f) => existsSync(join(cwd, f)))) continue;
1294
+ let content = "";
1295
+ try {
1296
+ content = readFileSync(join(cwd, ignore), "utf8");
1297
+ } catch {}
1298
+ if (content.split("\n").some((l) => l.trim().replace(/\/+$/, "") === dir)) continue;
1299
+ return `"${dir}/" is not excluded from your formatter. Running it will rewrite code you own, and \`cascivo update\` will then report drift on files you never edited. Add "${dir}/" to ${ignore}.`;
1300
+ }
1301
+ return null;
1302
+ }
1074
1303
  function checkSsrConfig(cwd) {
1075
1304
  let deps = {};
1076
1305
  try {
@@ -1299,9 +1528,7 @@ async function generate(args, config) {
1299
1528
  }
1300
1529
  const { readFileSync } = await import("node:fs");
1301
1530
  const configJson = readFileSync(inputArg, "utf-8");
1302
- const viewConfig = JSON.parse(configJson);
1303
- await fetchRegistry(config.registry);
1304
- const tsx = generateTsx(viewConfig, /* @__PURE__ */ new Map(), componentsDirArg ?? config.outputDir ?? "./src/components/ui");
1531
+ const tsx = generateTsx(JSON.parse(configJson), /* @__PURE__ */ new Map(), componentsDirArg ?? config.outputDir ?? "./src/components/ui");
1305
1532
  const outPath = outArg ?? join(dirname(inputArg), `${basename(inputArg, ".json")}.tsx`);
1306
1533
  writeFileSync(outPath, tsx, "utf-8");
1307
1534
  console.log(`Generated ${outPath}`);
@@ -1391,6 +1618,85 @@ function hintEslintIfPresent(cwd) {
1391
1618
  console.log("\nESLint: copied cascivo source may trip a strict host config on stylistic rules.");
1392
1619
  console.log(" Scope them off your components dir — see docs/USING-WITH-STRICT-ESLINT.md");
1393
1620
  }
1621
+ /**
1622
+ * Write dependency entries into `package.json` so a failed install still leaves a
1623
+ * DECLARATIVE-complete project, one `install` away from working.
1624
+ *
1625
+ * The reported failure: one unrelated bad version range elsewhere in the adopter's
1626
+ * `package.json` made `pnpm add` exit non-zero. cascivo had already written
1627
+ * `cascivo.config.ts`, so the project claimed to be cascivo-configured with none of the
1628
+ * runtime present, and the printed advice ("install them yourself") was the same command
1629
+ * that had just failed. Recording the dependencies is the difference between "recoverable
1630
+ * with one command" and "figure out what was supposed to be here".
1631
+ *
1632
+ * Never overwrites an entry that already exists — the app's own pin wins.
1633
+ */
1634
+ function recordDependencies(cwd, packages, opts) {
1635
+ const pkgPath = join(cwd, "package.json");
1636
+ if (!existsSync(pkgPath)) return [];
1637
+ let pkg;
1638
+ try {
1639
+ pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
1640
+ } catch {
1641
+ return [];
1642
+ }
1643
+ const field = opts.dev ? "devDependencies" : "dependencies";
1644
+ const deps = pkg[field] ??= {};
1645
+ const added = [];
1646
+ for (const name of packages) {
1647
+ if (deps[name] !== void 0) continue;
1648
+ deps[name] = "latest";
1649
+ added.push(name);
1650
+ }
1651
+ if (added.length === 0) return [];
1652
+ writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8");
1653
+ return added;
1654
+ }
1655
+ /** Formatter ignore files, in the order a project is likely to use them. */
1656
+ const FORMATTER_IGNORES = [{
1657
+ config: [
1658
+ ".prettierrc",
1659
+ ".prettierrc.json",
1660
+ ".prettierrc.js",
1661
+ "prettier.config.js"
1662
+ ],
1663
+ ignore: ".prettierignore"
1664
+ }, {
1665
+ config: [".oxfmtrc", ".oxfmtrc.json"],
1666
+ ignore: ".oxfmtignore"
1667
+ }];
1668
+ /**
1669
+ * Exclude the vendored components dir from the project's formatter.
1670
+ *
1671
+ * Owning the code means your formatter reformats it, and then `cascivo update` reports drift
1672
+ * on files you never touched. This ACTS rather than hints: the ESLint equivalent below only
1673
+ * prints a pointer, and an adopter ran `prettier --write .` before ever reading it.
1674
+ *
1675
+ * Idempotent, and never rewrites an existing line.
1676
+ */
1677
+ function ensureFormatterIgnore(cwd, outputDir) {
1678
+ const line = `${outputDir.replace(/\/+$/, "")}/`;
1679
+ for (const { config, ignore } of FORMATTER_IGNORES) {
1680
+ if (!(config.some((f) => existsSync(join(cwd, f))) || hasPrettierKeyInPackageJson(cwd, ignore))) continue;
1681
+ const path = join(cwd, ignore);
1682
+ const current = existsSync(path) ? readFileSync(path, "utf8") : "";
1683
+ if (current.split("\n").some((l) => l.trim() === line)) continue;
1684
+ const banner = "# cascivo: vendored component source — you own it, so do not reformat it\n";
1685
+ writeFileSync(path, current === "" ? banner + line + "\n" : `${current.replace(/\n*$/, "\n")}\n${banner}${line}\n`, "utf8");
1686
+ console.log(`\nAdded "${line}" to ${ignore} (so your formatter does not rewrite copied source).`);
1687
+ }
1688
+ }
1689
+ /** `prettier` key in package.json counts as a Prettier config — only relevant to .prettierignore. */
1690
+ function hasPrettierKeyInPackageJson(cwd, ignore) {
1691
+ if (ignore !== ".prettierignore") return false;
1692
+ const pkgPath = join(cwd, "package.json");
1693
+ if (!existsSync(pkgPath)) return false;
1694
+ try {
1695
+ return "prettier" in JSON.parse(readFileSync(pkgPath, "utf8"));
1696
+ } catch {
1697
+ return false;
1698
+ }
1699
+ }
1394
1700
  /** The "here's everything you need" summary, printed once at the end of init. */
1395
1701
  function printDependencySummary() {
1396
1702
  console.log("\nDependencies");
@@ -1428,13 +1734,36 @@ async function init(args = [], cwd = process.cwd()) {
1428
1734
  pm,
1429
1735
  dev: true
1430
1736
  });
1431
- if (!runtimeOk || !devOk) process.exitCode = 1;
1737
+ if (!runtimeOk || !devOk) {
1738
+ const recorded = [...runtimeOk ? [] : recordDependencies(cwd, RUNTIME_DEPS, { dev: false }), ...devOk ? [] : recordDependencies(cwd, DEV_DEPS, { dev: true })];
1739
+ console.error("\nInstall failed — cascivo.config.ts was written but the packages are not installed.");
1740
+ if (recorded.length > 0) {
1741
+ console.error(`Wrote ${recorded.length} dependency entries to package.json: ${recorded.join(", ")}`);
1742
+ console.error(`Recover with:\n ${pm} install`);
1743
+ } else console.error(`Recover with:\n ${installHint(pm, RUNTIME_DEPS)}\n ${installHint(pm, DEV_DEPS, { dev: true })}`);
1744
+ process.exitCode = 1;
1745
+ }
1432
1746
  }
1433
- console.log("\nImport the theme in your root CSS or entry file:");
1434
- console.log(` import '@cascivo/themes/${theme}.css'`);
1435
- console.log("Then set the theme on your root element:");
1747
+ console.log("\nStylesheets — import these once, in this order, in your entry file:");
1748
+ console.log(` import '@cascivo/tokens' // primitive tokens — every --cascivo-* value`);
1749
+ console.log(` import '@cascivo/themes/${theme}.css'${" ".repeat(Math.max(1, 16 - theme.length))}// the ${theme} theme's semantic values`);
1750
+ console.log(` // …then your component CSS (\`cascivo add\` writes .module.css beside each component)`);
1751
+ console.log("\nThen set the theme on your root element:");
1436
1752
  console.log(` <html data-theme="${theme}">`);
1753
+ console.log("\nSwitching themes at runtime? Use a bundle instead of the single theme:");
1754
+ console.log(" import '@cascivo/themes/light-dark.css' // light + dark — the common case");
1755
+ console.log(" import '@cascivo/themes/all.css' // all twelve themes");
1756
+ console.log(" import { ThemeProvider } from '@cascivo/core'");
1757
+ console.log("\nIn YOUR components, when you read a signal during render:");
1758
+ console.log(" import { useSignals } from '@cascivo/core'");
1759
+ console.log(" function MyComponent() {");
1760
+ console.log(" useSignals() // ← first statement, or the component never re-renders");
1761
+ console.log(" return <span>{count.value}</span>");
1762
+ console.log(" }");
1763
+ console.log("\nAdding a chart later? Charts ship as an npm package with their own stylesheet:");
1764
+ console.log(" import '@cascivo/charts/styles.css' // `cascivo add <chart>` reminds you");
1437
1765
  printDependencySummary();
1766
+ ensureFormatterIgnore(cwd, DEFAULT_CONFIG$1.outputDir);
1438
1767
  hintEslintIfPresent(cwd);
1439
1768
  console.log("\nAdd components with: cascivo add <name>");
1440
1769
  }
@@ -1449,12 +1778,22 @@ const TYPE_LABELS = {
1449
1778
  flow: "Flow (npm: @cascivo/flow)",
1450
1779
  editor: "Editor (npm: @cascivo/editor)"
1451
1780
  };
1781
+ /**
1782
+ * Deprecation marker for the listing.
1783
+ *
1784
+ * Shown at DISCOVERY time on purpose. `overflow-menu` carried a `@deprecated` JSDoc in its
1785
+ * source for months, which an adopter only meets after they have already vendored the file —
1786
+ * and it pointed at an import path that cannot resolve on either install path.
1787
+ */
1788
+ function deprecationSuffix(c) {
1789
+ return c.deprecated ? ` ⚠ deprecated → ${c.deprecated.replacement}` : "";
1790
+ }
1452
1791
  /** Render a group of entries as an aligned text table (no section header). */
1453
1792
  function formatGroup(entries) {
1454
1793
  const rows = entries.map((c) => [
1455
1794
  c.name,
1456
1795
  c.category,
1457
- c.description
1796
+ c.description + deprecationSuffix(c)
1458
1797
  ]);
1459
1798
  const headers = [
1460
1799
  "Name",
@@ -2518,32 +2857,42 @@ async function run(args) {
2518
2857
  case "doctor": {
2519
2858
  const ci = rest.includes("--ci");
2520
2859
  if (rest.includes("--drift")) {
2521
- const { runDoctorDrift } = await import("./drift-qTzJ75Ds.mjs");
2860
+ const { runDoctorDrift } = await import("./drift-B02BbFN_.mjs");
2522
2861
  await runDoctorDrift(await loadConfig());
2523
2862
  } else {
2524
2863
  const cwd = process.cwd();
2525
2864
  const result = await runDoctor(cwd);
2526
- const { checkProjectDependencies, checkSignalsCompat, checkSsrConfig, isAdopterProject } = await Promise.resolve().then(() => doctor_exports);
2527
- const adopter = isAdopterProject(cwd);
2865
+ const { checkDuplicateCore, checkFormatterIgnore, checkProjectDependencies, checkSignalsCompat, checkSsrConfig, detectInstallPath } = await Promise.resolve().then(() => doctor_exports);
2866
+ const adopter = detectInstallPath(cwd) !== "unknown";
2528
2867
  const deps = adopter ? checkProjectDependencies(cwd) : [];
2529
2868
  const missingRequired = deps.filter((d) => d.required);
2530
2869
  const signalsCompat = adopter ? await checkSignalsCompat(cwd) : null;
2531
2870
  const signalsError = signalsCompat?.severity === "error";
2532
2871
  const ssrHint = adopter ? checkSsrConfig(cwd) : null;
2533
- if (result.passed && deps.length === 0 && signalsCompat === null && ssrHint === null) console.log("No violations found.");
2872
+ const duplicateCore = adopter ? await checkDuplicateCore(cwd) : null;
2873
+ const formatterHint = adopter ? checkFormatterIgnore(cwd, (await loadConfig()).outputDir) : null;
2874
+ const { runDoctorDrift } = await import("./drift-B02BbFN_.mjs");
2875
+ const driftOutcome = adopter ? await runDoctorDrift(await loadConfig(), cwd) : {
2876
+ ran: false,
2877
+ reason: "no cascivo install detected",
2878
+ issues: 0
2879
+ };
2880
+ if (result.passed && deps.length === 0 && signalsCompat === null && ssrHint === null && duplicateCore === null && formatterHint === null && driftOutcome.issues === 0) console.log(driftOutcome.ran ? "No violations found." : `No violations found (drift: not checked — ${driftOutcome.reason}).`);
2534
2881
  else {
2535
2882
  for (const v of result.violations) console.error(`[${v.rule}] ${v.detail}\n ${v.file}`);
2536
- for (const d of missingRequired) console.error(`[missing-dependency] ${d.package} is not in package.json — copied cascivo source needs it. Install: ${d.hint}`);
2883
+ for (const d of missingRequired) console.error(d.kind === "forbidden" ? `[forbidden-dependency] ${d.package} must not be a direct dependency here. Remove it — ${d.hint}` : `[missing-dependency] ${d.package} is not in package.json. Install: ${d.hint}`);
2537
2884
  if (signalsCompat) (signalsError ? console.error : console.log)(`[${signalsError ? "signals-incompatible" : "signals-outdated"}] ${signalsCompat.detail} Upgrade: ${signalsCompat.hint}`);
2538
2885
  if (ssrHint) console.log(`[ssr-config] ${ssrHint}`);
2886
+ if (formatterHint) console.log(`[formatter-drift] ${formatterHint}`);
2887
+ if (duplicateCore) console.error(`[duplicate-core] More than one @cascivo/core is installed (root: ${duplicateCore.root ?? "none"}; ${duplicateCore.nested.join("; ")}). ` + duplicateCore.hint);
2539
2888
  for (const d of deps.filter((x) => !x.required)) console.log(`[optional] ${d.package} is not installed; add it when a component or chart needs it: ${d.hint}`);
2540
- if (ci && (result.violations.length > 0 || missingRequired.length > 0 || signalsError)) process.exitCode = 1;
2889
+ if (ci && (result.violations.length > 0 || missingRequired.length > 0 || signalsError || duplicateCore !== null || driftOutcome.issues > 0)) process.exitCode = 1;
2541
2890
  }
2542
2891
  }
2543
2892
  break;
2544
2893
  }
2545
2894
  case "audit": {
2546
- const { audit } = await import("./audit-Cn6HJkuo.mjs");
2895
+ const { audit } = await import("./audit-C4ul-2Ix.mjs");
2547
2896
  await audit(rest, await loadConfig());
2548
2897
  break;
2549
2898
  }