cascivo 0.8.0 → 0.8.1

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.
@@ -140,7 +140,7 @@ function detectPackageManager(cwd = process.cwd(), opts = {}) {
140
140
  const envPm = env.CASCIVO_PACKAGE_MANAGER;
141
141
  if (isPackageManager(envPm)) return envPm;
142
142
  const uaPm = pmFromUserAgent(env.npm_config_user_agent);
143
- if (uaPm) return uaPm;
143
+ if (uaPm && !opts.preferLockfileOverUserAgent) return uaPm;
144
144
  let current = cwd;
145
145
  for (;;) {
146
146
  for (const [file, pm] of PM_LOCKFILES) if (existsSync(join(current, file))) return pm;
@@ -151,7 +151,7 @@ function detectPackageManager(cwd = process.cwd(), opts = {}) {
151
151
  if (parent === current) break;
152
152
  current = parent;
153
153
  }
154
- return "npm";
154
+ return uaPm ?? "npm";
155
155
  }
156
156
  /**
157
157
  * Pin every `@cascivo/*` package to an explicit version specifier.
@@ -191,4 +191,4 @@ function installHint(pm, packages, opts = {}) {
191
191
  //#endregion
192
192
  export { detectPackageManager as a, isPackageManager as c, config_exports as i, loadConfig as l, DEFAULT_CONFIG as n, installCommand as o, THEMES as r, installHint as s, CASCIVO_HOST as t, __exportAll as u };
193
193
 
194
- //# sourceMappingURL=config-D7ddWN_9.mjs.map
194
+ //# sourceMappingURL=config-C8D_CmKI.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-C8D_CmKI.mjs","names":[],"sources":["../src/utils/config.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { pathToFileURL } from 'node:url'\n\n/**\n * Canonical (and only) host for hosted cascivo artifacts (registry.json, per-item\n * r/<name>.json, marketplace.json, /llms/*, /context/*). The legacy docs.cascivo.com\n * subdomain is retired and 301s here. Keep in sync with CASCIVO_HOST in packages/mcp.\n */\nexport const CASCIVO_HOST = 'https://cascivo.com'\n\n/** All first-party themes shipped by @cascivo/themes (selectable via data-theme). */\nexport const THEMES = [\n 'light',\n 'dark',\n 'warm',\n 'flat',\n 'minimal',\n 'midnight',\n 'pastel',\n 'brutalist',\n 'corporate',\n 'terminal',\n 'cyberpunk',\n 'arcade',\n] as const\n\nexport type ThemeName = (typeof THEMES)[number]\n\nexport type RegistryNamespaceConfig =\n | string\n | { url: string; headers?: Record<string, string>; params?: Record<string, string> }\n\nexport interface CascadeConfig {\n /** URL of the registry.json index. */\n registry: string\n /** Directory (relative to project root) where components are written. */\n outputDir: string\n /** Default theme imported by `cascade init`. */\n theme: ThemeName\n /** Namespace → registry URL template (with {name} placeholder) or auth config. */\n registries?: Record<string, RegistryNamespaceConfig>\n /** Whether to copy test files (*.contract.test.tsx) when adding components. */\n tests?: boolean\n}\n\nexport const DEFAULT_CONFIG: CascadeConfig = {\n // Canonical hosted registry index (served from the landing site, documented in\n // llms.txt). Prefer this over a branch's GitHub raw URL, which 404s for\n // unauthenticated/private-repo requests and breaks `cascivo list`/`add`.\n registry: `${CASCIVO_HOST}/registry.json`,\n outputDir: 'src/components/ui',\n theme: 'light',\n}\n\nconst CONFIG_FILES = ['cascivo.config.ts', 'cascivo.config.js', 'cascivo.config.mjs']\n\n/** Apply defaults over a (possibly partial) user config object. */\nexport function resolveConfig(partial: Partial<CascadeConfig> | null | undefined): CascadeConfig {\n return { ...DEFAULT_CONFIG, ...partial }\n}\n\n/** Let env vars override config — used by the MCP server to pass `outputDir`. */\nexport function applyEnvOverrides(\n config: CascadeConfig,\n env: NodeJS.ProcessEnv = process.env,\n): CascadeConfig {\n return {\n ...config,\n ...(env.CASCIVO_REGISTRY ? { registry: env.CASCIVO_REGISTRY } : {}),\n ...(env.CASCIVO_OUTPUT_DIR ? { outputDir: env.CASCIVO_OUTPUT_DIR } : {}),\n }\n}\n\n/**\n * Locate and load `cascivo.config.{ts,js,mjs}` from `cwd`. Falls back to the\n * default config when no file is found or the file cannot be loaded. Env vars\n * (`CASCIVO_REGISTRY`, `CASCIVO_OUTPUT_DIR`) take precedence.\n */\nexport async function loadConfig(cwd: string = process.cwd()): Promise<CascadeConfig> {\n for (const file of CONFIG_FILES) {\n const path = join(cwd, file)\n if (!existsSync(path)) continue\n try {\n const mod = (await import(pathToFileURL(path).href)) as {\n default?: Partial<CascadeConfig>\n }\n return applyEnvOverrides(resolveConfig(mod.default ?? (mod as Partial<CascadeConfig>)))\n } catch {\n // Unloadable config (e.g. unsupported TS syntax) — fall back to defaults.\n return applyEnvOverrides(resolveConfig(null))\n }\n }\n return applyEnvOverrides(resolveConfig(null))\n}\n\nexport type PackageManager = 'pnpm' | 'yarn' | 'npm' | 'bun'\n\n/** Narrow an arbitrary string to a known PackageManager. */\nexport function isPackageManager(value: string | undefined): value is PackageManager {\n return value === 'pnpm' || value === 'yarn' || value === 'npm' || value === 'bun'\n}\n\n/**\n * Lock files in probe order. `bun.lock` (bun ≥ 1.2 text lockfile) sits beside the\n * legacy binary `bun.lockb`; `package-lock.json` is npm's marker so a walk that\n * reaches an npm workspace root still resolves to npm rather than the default.\n */\nconst PM_LOCKFILES: readonly [string, PackageManager][] = [\n ['pnpm-lock.yaml', 'pnpm'],\n ['yarn.lock', 'yarn'],\n ['bun.lockb', 'bun'],\n ['bun.lock', 'bun'],\n ['package-lock.json', 'npm'],\n]\n\n/** The package manager that invoked this process, from `npm_config_user_agent`. */\nfunction pmFromUserAgent(ua: string | undefined): PackageManager | undefined {\n if (!ua) return undefined\n const name = ua.split('/')[0]\n return isPackageManager(name) ? name : undefined\n}\n\n/** The `packageManager` corepack field of a package.json, if it names a known PM. */\nfunction pmFromPackageJson(dir: string): PackageManager | undefined {\n try {\n const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')) as {\n packageManager?: string\n }\n const name = pkg.packageManager?.split('@')[0]\n return isPackageManager(name) ? name : undefined\n } catch {\n return undefined\n }\n}\n\n/**\n * Detect the package manager in use. Precedence, highest first:\n * 1. explicit override (the `--package-manager`/`--pm` flag)\n * 2. `CASCIVO_PACKAGE_MANAGER` env var\n * 3. `npm_config_user_agent` (the PM that spawned the CLI, e.g. `pnpm dlx`)\n * 4. an upward walk from `cwd` for a lock file or `packageManager` field —\n * this is what makes detection work inside a workspace, where the lock\n * file lives at the repo root, not in the app subdirectory the user runs\n * the CLI from. The walk stops after a directory containing `.git`.\n * 5. default `npm`.\n */\nexport function detectPackageManager(\n cwd: string = process.cwd(),\n opts: {\n override?: string\n env?: NodeJS.ProcessEnv\n /**\n * Let a lock file found by walking up from `cwd` outrank `npm_config_user_agent`.\n *\n * For `init`/`add`, which run *inside* an existing project, the user agent is the right\n * first signal: it is the tool the developer just invoked. For `create` it is the wrong\n * one. `npx cascivo create` always reports a `npm/…` agent regardless of what the\n * surrounding workspace uses, and it short-circuited before the walk-up ever ran — so a\n * 2026-08-14 adopter scaffolding into a pnpm workspace with a root `pnpm-lock.yaml` was\n * told to run `npm install`. Where the new project LANDS is a stronger signal than which\n * launcher started the CLI.\n */\n preferLockfileOverUserAgent?: boolean\n } = {},\n): PackageManager {\n const env = opts.env ?? process.env\n\n if (isPackageManager(opts.override)) return opts.override\n\n const envPm = env.CASCIVO_PACKAGE_MANAGER\n if (isPackageManager(envPm)) return envPm\n\n const uaPm = pmFromUserAgent(env.npm_config_user_agent)\n if (uaPm && !opts.preferLockfileOverUserAgent) return uaPm\n\n let current = cwd\n for (;;) {\n for (const [file, pm] of PM_LOCKFILES) {\n if (existsSync(join(current, file))) return pm\n }\n const fromField = pmFromPackageJson(current)\n if (fromField) return fromField\n // A `.git` directory marks the repo root — do not walk past it.\n if (existsSync(join(current, '.git'))) break\n const parent = dirname(current)\n if (parent === current) break\n current = parent\n }\n\n // No lock file anywhere up the tree: fall back to the launcher, which for a brand-new\n // project in an empty directory is the only signal there is.\n return uaPm ?? 'npm'\n}\n\n/**\n * Pin every `@cascivo/*` package to an explicit version specifier.\n *\n * A bare name lets the package manager reuse whatever it already has resolvable. In a pnpm\n * workspace a sibling package's lockfile entry won and `@cascivo/i18n` resolved to **0.2.14**\n * while latest was **0.16.0** — then cascivo warned the adopter about the version it had\n * just installed itself. `add name@latest` (or the registry's known floor) makes the intent\n * explicit instead of leaving it to resolution order.\n *\n * Non-cascivo packages (`@preact/signals-react`) keep their bare name: their version is the\n * app's business, and cascivo has no floor to assert.\n */\nexport function pinSpecifier(pkg: string, floors: Record<string, string> = {}): string {\n if (!pkg.startsWith('@cascivo/') && pkg !== 'cascivo') return pkg\n if (pkg.includes('@', 1)) return pkg // already carries an explicit version\n const floor = floors[pkg]\n // `>=x.y.z` is not an installable specifier on its own; widen it to a range the package\n // manager understands, so a known floor is honoured rather than silently ignored.\n if (floor?.startsWith('>=')) return `${pkg}@${floor.slice(2)} - x`\n return `${pkg}@latest`\n}\n\n/** The install subcommand each package manager uses to add dependencies. */\nexport function installCommand(\n pm: PackageManager,\n packages: string[],\n opts: { dev?: boolean; floors?: Record<string, string>; pin?: string } = {},\n): [string, string[]] {\n const verb = pm === 'npm' ? 'install' : 'add'\n const devFlag = opts.dev ? [pm === 'npm' ? '--save-dev' : '-D'] : []\n const specs = packages.map((p) =>\n opts.pin && (p.startsWith('@cascivo/') || p === 'cascivo')\n ? `${p}@${opts.pin}`\n : pinSpecifier(p, opts.floors ?? {}),\n )\n return [pm, [verb, ...devFlag, ...specs]]\n}\n\n/** Human-readable install command a user can copy-paste, e.g. `pnpm add -D cascivo`. */\nexport function installHint(\n pm: PackageManager,\n packages: string[],\n opts: { dev?: boolean } = {},\n): string {\n const [cmd, args] = installCommand(pm, packages, opts)\n return `${cmd} ${args.join(' ')}`\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.17.0",
2
+ "version": "0.17.1",
3
3
  "tokens": [
4
4
  { "name": "--cascivo-gray-0", "resolvedDefault": "oklch(1 0 0)" },
5
5
  { "name": "--cascivo-gray-50", "resolvedDefault": "oklch(0.985 0.002 264)" },
@@ -696,7 +696,8 @@
696
696
  {
697
697
  "name": "CodeSnippet",
698
698
  "props": [
699
- { "name": "code", "type": "string", "required": true },
699
+ { "name": "code", "type": "string", "required": false },
700
+ { "name": "children", "type": "string", "required": false },
700
701
  { "name": "variant", "type": "'inline' | 'single' | 'multi'", "required": false },
701
702
  { "name": "language", "type": "'bash' | 'css' | 'js' | 'ts'", "required": false },
702
703
  { "name": "terminal", "type": "boolean", "required": false },
@@ -2142,8 +2143,8 @@
2142
2143
  {
2143
2144
  "name": "PageHeader",
2144
2145
  "props": [
2145
- { "name": "title", "type": "string", "required": true },
2146
- { "name": "description", "type": "string", "required": false },
2146
+ { "name": "title", "type": "ReactNode", "required": true },
2147
+ { "name": "description", "type": "ReactNode", "required": false },
2147
2148
  { "name": "breadcrumb", "type": "ReactNode", "required": false },
2148
2149
  { "name": "actions", "type": "ReactNode", "required": false },
2149
2150
  { "name": "className", "type": "string", "required": false }
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-D7ddWN_9.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-C8D_CmKI.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";
@@ -514,11 +514,11 @@ function resolvePackageManagerFlag(args) {
514
514
  * and GETTING-STARTED.md tells adopters to pin exactly. Regenerate with `pnpm regen`.
515
515
  */
516
516
  const CASCIVO_VERSIONS = {
517
- "@cascivo/react": "0.17.0",
518
- "@cascivo/themes": "0.4.11",
519
- "@cascivo/charts": "0.17.0",
520
- "@cascivo/icons": "0.3.8",
521
- "@cascivo/eslint-config": "0.2.2"
517
+ "@cascivo/react": "0.17.1",
518
+ "@cascivo/themes": "0.4.12",
519
+ "@cascivo/charts": "0.17.1",
520
+ "@cascivo/icons": "0.3.9",
521
+ "@cascivo/eslint-config": "0.2.3"
522
522
  };
523
523
  /** `@cascivo/core`'s declared `@preact/signals-react` peer range. */
524
524
  const SIGNALS_PEER = ">=3.0.0";
@@ -683,7 +683,7 @@ function indexHtml(opts) {
683
683
  <head>
684
684
  <meta charset="UTF-8" />
685
685
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
686
- <title>${opts.name}</title>
686
+ <title>${brandName(opts.name)}</title>
687
687
  <style>
688
688
  @layer vendor, cascivo.reset, cascivo.base, cascivo.tokens, cascivo.component,
689
689
  cascivo.platform, cascivo.theme, cascivo.blocks, cascivo.example, cascivo.override;
@@ -737,7 +737,7 @@ if (root) {
737
737
  function viteEnv() {
738
738
  return `/// <reference types="vite/client" />\n`;
739
739
  }
740
- function appTsx(opts, sections) {
740
+ function appTsx(sections) {
741
741
  const sectionImports = sections.map((s) => `import { ${s.component} } from './sections/${s.component}'`).join("\n");
742
742
  const unionType = sections.map((s) => `'${s.key}'`).join(" | ");
743
743
  const navItems = sections.map((s) => ` {
@@ -750,19 +750,10 @@ function appTsx(opts, sections) {
750
750
  },`).join("\n");
751
751
  const renderedSections = sections.map((s) => ` {section.value === '${s.key}' && <${s.component} />}`).join("\n");
752
752
  return `'use client'
753
- import {
754
- AppShell,
755
- ShellHeader,
756
- SideNav,
757
- signal,
758
- useSignals,
759
- type SideNavItem,
760
- } from '@cascivo/react'
753
+ import { signal, useSignals, type SideNavItem } from '@cascivo/react'
754
+ import { Shell } from './Shell'
761
755
  ${sectionImports}
762
756
 
763
- import '@cascivo/themes/${opts.theme}.css'
764
- import '@cascivo/react/styles.css'
765
-
766
757
  type Section = ${unionType}
767
758
 
768
759
  const section = signal<Section>('${sections[0].key}')
@@ -774,12 +765,63 @@ export default function App() {
774
765
  ${navItems}
775
766
  ]
776
767
 
768
+ return (
769
+ <Shell navItems={navItems}>
770
+ ${renderedSections}
771
+ </Shell>
772
+ )
773
+ }
774
+ `;
775
+ }
776
+ /**
777
+ * The app shell, as its own component with a `children` slot.
778
+ *
779
+ * Split out of `App.tsx` because the shell composition — AppShell + ShellHeader + SideNav —
780
+ * is the valuable part of the scaffold, and it used to be welded to the signal-driven
781
+ * section switcher. A 2026-08-14 adopter prompted for "a dashboard with Vite and React
782
+ * Router", then deleted `App.tsx` and all of `src/sections/` — the majority of what `create`
783
+ * generated — and re-derived this wiring by hand.
784
+ *
785
+ * Now adding a router means deleting `App.tsx` + `src/sections/` and rendering `<Shell>` from
786
+ * the root route's layout, with `navItems` carrying `href` instead of `onClick`. Nothing in
787
+ * here needs to change.
788
+ */
789
+ function shellTsx(opts) {
790
+ return `'use client'
791
+ import { AppShell, ShellHeader, SideNav, type SideNavItem } from '@cascivo/react'
792
+ import type { ReactNode } from 'react'
793
+
794
+ import '@cascivo/themes/${opts.theme}.css'
795
+ // No '@cascivo/react/styles.css' here. On a bundler build each component imports its
796
+ // own CSS, so you ship exactly what you use — this app emits well under 100 kB of entry
797
+ // CSS instead of the ~273 kB aggregate sheet. Import the aggregate ONLY if you drop the
798
+ // bundler (CDN / single-file setup). See https://cascivo.com/docs/getting-started.md
799
+ // The theme import above is always required — themes are never automatic.
800
+
801
+ export interface ShellProps {
802
+ /** Side-nav entries. Use \`href\` for a routed app, \`onClick\` for local state. */
803
+ navItems: SideNavItem[]
804
+ children: ReactNode
805
+ }
806
+
807
+ /**
808
+ * App shell: header + side nav + a content slot.
809
+ *
810
+ * Adding a router? Keep this file. Delete \`App.tsx\` and \`src/sections/\`, render
811
+ * \`<Shell navItems={…}>\` from your root route's layout with your \`<Outlet />\` as children,
812
+ * and give each nav item an \`href\` instead of an \`onClick\`.
813
+ *
814
+ * For those hrefs to become real router links, call \`setLinkComponent\` ONCE at startup in
815
+ * \`main.tsx\` — see the "Adding a router" section of README.md for the exact snippet, or
816
+ * https://cascivo.com/docs/using-with-a-router.md for the full recipe.
817
+ */
818
+ export function Shell({ navItems, children }: ShellProps) {
777
819
  return (
778
820
  <AppShell
779
821
  header={<ShellHeader brand={{ name: '${brandName(opts.name).replace(/'/g, "\\'")}' }} />}
780
822
  nav={<SideNav items={navItems} />}
781
823
  >
782
- ${renderedSections}
824
+ {children}
783
825
  </AppShell>
784
826
  )
785
827
  }
@@ -887,10 +929,34 @@ ${runScriptCommand(pm, "dev")}
887
929
 
888
930
  ## Structure
889
931
 
890
- - \`src/App.tsx\` — app shell, navigation, and section routing
932
+ - \`src/Shell.tsx\` — the app shell (header + side nav + content slot). Router-agnostic.
933
+ - \`src/App.tsx\` — nav items and which section is showing
891
934
  - \`src/sections/\` — one component per nav item
892
935
 
893
936
  Add more components with \`npx cascivo add <component>\`.
937
+
938
+ ## Adding a router
939
+
940
+ This app switches sections with a signal, not a router. To add one (React Router,
941
+ TanStack Router, …):
942
+
943
+ 1. **Keep \`src/Shell.tsx\`.** Delete \`src/App.tsx\` and \`src/sections/\`.
944
+ 2. Render \`<Shell navItems={…}>\` from your root route's layout, with your \`<Outlet />\`
945
+ as its children.
946
+ 3. Give each nav item an \`href\` instead of \`onClick\`.
947
+ 4. Register your router's Link **once** at startup, in \`src/main.tsx\`:
948
+
949
+ \`\`\`tsx
950
+ import { setLinkComponent } from '@cascivo/react'
951
+ import type { LinkComponentProps } from '@cascivo/react'
952
+ import { Link } from 'react-router'
953
+
954
+ setLinkComponent(({ href, ...rest }: LinkComponentProps) => <Link to={href ?? '#'} {...rest} />)
955
+ \`\`\`
956
+
957
+ That one call makes \`SideNav\`, \`ShellHeader\` and \`Breadcrumb\` render real router links.
958
+ Links you write in page content use \`<Link asChild>\` instead — two kinds of link, two
959
+ mechanisms. Full recipe: https://cascivo.com/docs/using-with-a-router.md
894
960
  `;
895
961
  }
896
962
  function agentsMd(opts) {
@@ -938,6 +1004,28 @@ This app's declared layer order (in \`index.html\`):
938
1004
  }
939
1005
  \`\`\`
940
1006
 
1007
+ ## Routing
1008
+
1009
+ If you add a router, keep \`src/Shell.tsx\` and delete \`src/App.tsx\` + \`src/sections/\`.
1010
+
1011
+ cascivo links come in **two kinds**, wired two different ways. Do not intercept
1012
+ \`onClick\`, and do not hand-wrap nav items:
1013
+
1014
+ 1. **Config-driven navs** (\`SideNav\`, \`ShellHeader\`, \`Breadcrumb\`, \`Switcher\`) render
1015
+ through a module singleton. Register your router's Link once, in \`src/main.tsx\`:
1016
+ \`setLinkComponent(({ href, ...rest }: LinkComponentProps) => <Link to={href ?? '#'} {...rest} />)\`
1017
+ 2. **Links in page content** use \`asChild\`:
1018
+ \`<Link asChild><RouterLink to="/x">x</RouterLink></Link>\`
1019
+
1020
+ Full recipe: https://cascivo.com/docs/using-with-a-router.md
1021
+
1022
+ ## Types
1023
+
1024
+ The vocabulary types are on a subpath: \`import type { Tone } from '@cascivo/react/types'\`
1025
+ (also \`Progress\`, \`SpaceStep\`). \`Status.status\` and \`Badge.variant\` use them, so a
1026
+ \`Record<MyState, Tone>\` is the supported way to map domain states onto tones. **Never**
1027
+ add \`@cascivo/core\` to this app's dependencies — it is transitive here.
1028
+
941
1029
  More: cascivo's machine-readable guide is at https://cascivo.com/llms.txt.
942
1030
  `;
943
1031
  }
@@ -995,7 +1083,11 @@ function buildScaffold(opts) {
995
1083
  },
996
1084
  {
997
1085
  path: "src/App.tsx",
998
- contents: appTsx(opts, sections)
1086
+ contents: appTsx(sections)
1087
+ },
1088
+ {
1089
+ path: "src/Shell.tsx",
1090
+ contents: shellTsx(opts)
999
1091
  },
1000
1092
  ...sections.map((s) => ({
1001
1093
  path: `src/sections/${s.component}.tsx`,
@@ -1025,7 +1117,10 @@ async function create(args, cwd = process.cwd()) {
1025
1117
  process.exitCode = 1;
1026
1118
  return;
1027
1119
  }
1028
- const pm = detectPackageManager(cwd, pmFlag.pm ? { override: pmFlag.pm } : {});
1120
+ const pm = detectPackageManager(cwd, {
1121
+ preferLockfileOverUserAgent: true,
1122
+ ...pmFlag.pm ? { override: pmFlag.pm } : {}
1123
+ });
1029
1124
  const rl = !yes && stdin.isTTY ? createInterface({
1030
1125
  input: stdin,
1031
1126
  output: stdout
@@ -1058,7 +1153,7 @@ async function create(args, cwd = process.cwd()) {
1058
1153
  const templateSpec = flagValue(args, "template");
1059
1154
  if (templateSpec) {
1060
1155
  const { add } = await Promise.resolve().then(() => add_exports);
1061
- const { loadConfig } = await import("./config-D7ddWN_9.mjs").then((n) => n.i);
1156
+ const { loadConfig } = await import("./config-C8D_CmKI.mjs").then((n) => n.i);
1062
1157
  console.log(`\nInstalling template "${templateSpec}"…`);
1063
1158
  await add([templateSpec], await loadConfig(), {
1064
1159
  cwd: targetDir,
@@ -1069,6 +1164,13 @@ async function create(args, cwd = process.cwd()) {
1069
1164
  console.log(` cd ${name}`);
1070
1165
  console.log(` ${installAllCommand(pm)}`);
1071
1166
  console.log(` ${runScriptCommand(pm, "dev")}`);
1167
+ console.log("\nGood to know:");
1168
+ console.log(" No cascivo.config.ts is written — this app uses the prebuilt @cascivo/react");
1169
+ console.log(" packages and never copies source. `cascivo add <component>` writes the");
1170
+ console.log(" config itself the first time you vendor a component.");
1171
+ console.log("\n Adding a router? Keep src/Shell.tsx, delete src/App.tsx + src/sections/,");
1172
+ console.log(" and register your Link once with setLinkComponent — see");
1173
+ console.log(" https://cascivo.com/docs/using-with-a-router.md");
1072
1174
  } finally {
1073
1175
  rl?.close();
1074
1176
  }