cascivo 0.5.0 → 0.5.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.
@@ -176,69 +176,6 @@ function updateLockEntry(lock, name, entry) {
176
176
  };
177
177
  }
178
178
  //#endregion
179
- //#region src/utils/semver.ts
180
- /**
181
- * Minimal semver comparison — just enough to evaluate the `>=x.y.z` floors
182
- * the registry generator emits for `peerVersions` (scripts/registry/generate.ts).
183
- * Not a general-purpose range parser; unsupported input is treated as
184
- * unsatisfied so callers surface it rather than silently pass.
185
- */
186
- function parseVersion(v) {
187
- const m = /^(\d+)\.(\d+)\.(\d+)/.exec(v.trim());
188
- if (!m) return null;
189
- return [
190
- Number(m[1]),
191
- Number(m[2]),
192
- Number(m[3])
193
- ];
194
- }
195
- /** Compares two `x.y.z` versions: -1 if a<b, 0 if equal, 1 if a>b. Throws on unparsable input. */
196
- function compareVersions(a, b) {
197
- const pa = parseVersion(a);
198
- const pb = parseVersion(b);
199
- if (!pa || !pb) throw new Error(`Cannot compare versions: "${a}" vs "${b}"`);
200
- for (let i = 0; i < 3; i++) if (pa[i] !== pb[i]) return pa[i] < pb[i] ? -1 : 1;
201
- return 0;
202
- }
203
- /** Evaluates whether `installed` satisfies a floor spec of the form `>=x.y.z`. */
204
- function satisfiesFloor(installed, floor) {
205
- const m = /^>=\s*(\d+\.\d+\.\d+)/.exec(floor.trim());
206
- if (!m) return true;
207
- try {
208
- return compareVersions(installed, m[1]) >= 0;
209
- } catch {
210
- return true;
211
- }
212
- }
213
- //#endregion
214
- //#region src/utils/peer-versions.ts
215
- /** Reads the installed version of a package from the project's node_modules, or null if absent/unreadable. */
216
- async function readInstalledPackageVersion(cwd, pkgName) {
217
- try {
218
- const pkg = JSON.parse(await readFile(join(cwd, "node_modules", pkgName, "package.json"), "utf8"));
219
- return typeof pkg.version === "string" ? pkg.version : null;
220
- } catch {
221
- return null;
222
- }
223
- }
224
- /**
225
- * Checks installed peer package versions against the floors a registry entry
226
- * declares (`peerVersions`, e.g. `{ "@cascivo/i18n": ">=0.2.1" }`). Returns
227
- * only the packages that fail — either missing or older than required.
228
- */
229
- async function checkPeerVersions(cwd, floors) {
230
- const violations = [];
231
- for (const [pkg, required] of Object.entries(floors)) {
232
- const installed = await readInstalledPackageVersion(cwd, pkg);
233
- if (installed === null || !satisfiesFloor(installed, required)) violations.push({
234
- pkg,
235
- required,
236
- installed
237
- });
238
- }
239
- return violations;
240
- }
241
- //#endregion
242
- export { updateLockEntry as a, fileName as c, sha256 as i, findComponent as l, createLock as n, writeLock as o, readLock as r, fetchRegistry as s, checkPeerVersions as t };
179
+ export { writeLock as a, findComponent as c, updateLockEntry as i, readLock as n, fetchRegistry as o, sha256 as r, fileName as s, createLock as t };
243
180
 
244
- //# sourceMappingURL=peer-versions-B2bCgko-.mjs.map
181
+ //# sourceMappingURL=lock-CW8UuEPJ.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lock-CW8UuEPJ.mjs","names":[],"sources":["../src/utils/registry.ts","../src/utils/lock.ts"],"sourcesContent":["import { fetchJsonFresh } from './http.js'\n\nexport interface RegistryComponent {\n name: string\n type?: 'component' | 'layout' | 'block' | 'chart' | 'section' | 'flow' | 'editor'\n description: string\n category: string\n version: string\n files: string[]\n /** filename → sha256 of upstream content — used by `update --check`. */\n fileHashes?: Record<string, string>\n /** npm package to install (charts/flow/editor). */\n install?: string\n /** stylesheet the npm package requires, e.g. `@cascivo/charts/styles.css`. */\n styles?: string\n dependencies: string[]\n /**\n * Minimum published version required for each `@cascivo/*` dependency, e.g.\n * `{ \"@cascivo/i18n\": \">=0.2.1\" }`. Used by `add`/`doctor --drift` to detect\n * an installed peer package that is older than what this copied source needs.\n */\n peerVersions?: Record<string, string>\n /** Other registry components to install transitively (shared hooks/siblings). */\n registryDependencies?: string[]\n tags: string[]\n meta: { name: string }\n}\n\nexport interface BlockRegistryEntry {\n name: string\n type: 'block'\n displayName: string\n description: string\n category: string\n version: string\n files: string[]\n dependencies: string[]\n tags: string[]\n screenshot: { light: string; dark: string }\n}\n\nexport interface Registry {\n version: string\n generatedAt: string\n components: RegistryComponent[]\n blocks?: BlockRegistryEntry[]\n /** Names of first-party templates — installed via the per-item path (r/<name>.json). */\n templates: string[]\n}\n\nfunction asStringArray(value: unknown): string[] {\n return Array.isArray(value) ? value.filter((v): v is string => typeof v === 'string') : []\n}\n\n/** Validate and normalize raw JSON into a typed Registry. Throws on bad shape. */\nexport function parseRegistry(raw: unknown): Registry {\n if (typeof raw !== 'object' || raw === null) {\n throw new Error('Invalid registry: expected an object')\n }\n const obj = raw as Record<string, unknown>\n if (!Array.isArray(obj.components)) {\n throw new Error('Invalid registry: \"components\" must be an array')\n }\n\n const components: RegistryComponent[] = obj.components.map((entry, i) => {\n if (typeof entry !== 'object' || entry === null) {\n throw new Error(`Invalid registry: component at index ${i} is not an object`)\n }\n const c = entry as Record<string, unknown>\n if (typeof c.name !== 'string') {\n throw new Error(`Invalid registry: component at index ${i} is missing \"name\"`)\n }\n const rawType = c.type\n const KNOWN_TYPES = [\n 'component',\n 'layout',\n 'block',\n 'chart',\n 'section',\n 'flow',\n 'editor',\n ] as const\n const type = (KNOWN_TYPES as readonly string[]).includes(rawType as string)\n ? (rawType as RegistryComponent['type'])\n : undefined\n const rawMeta = c.meta\n const metaName =\n typeof rawMeta === 'object' &&\n rawMeta !== null &&\n typeof (rawMeta as Record<string, unknown>).name === 'string'\n ? ((rawMeta as Record<string, unknown>).name as string)\n : c.name\n const result: RegistryComponent = {\n name: c.name,\n description: typeof c.description === 'string' ? c.description : '',\n category: typeof c.category === 'string' ? c.category : '',\n version: typeof c.version === 'string' ? c.version : '0.0.0',\n files: asStringArray(c.files),\n dependencies: asStringArray(c.dependencies),\n tags: asStringArray(c.tags),\n meta: { name: metaName },\n }\n if (type !== undefined) result.type = type\n if (typeof c.install === 'string') result.install = c.install\n if (typeof c.styles === 'string') result.styles = c.styles\n if (Array.isArray(c.registryDependencies)) {\n const deps = asStringArray(c.registryDependencies)\n if (deps.length > 0) result.registryDependencies = deps\n }\n if (typeof c.fileHashes === 'object' && c.fileHashes !== null) {\n const hashes: Record<string, string> = {}\n for (const [file, hash] of Object.entries(c.fileHashes as Record<string, unknown>)) {\n if (typeof hash === 'string') hashes[file] = hash\n }\n if (Object.keys(hashes).length > 0) result.fileHashes = hashes\n }\n if (typeof c.peerVersions === 'object' && c.peerVersions !== null) {\n const floors: Record<string, string> = {}\n for (const [dep, floor] of Object.entries(c.peerVersions as Record<string, unknown>)) {\n if (typeof floor === 'string') floors[dep] = floor\n }\n if (Object.keys(floors).length > 0) result.peerVersions = floors\n }\n return result\n })\n\n const blocks: BlockRegistryEntry[] = Array.isArray(obj.blocks)\n ? obj.blocks.flatMap((entry) => {\n if (typeof entry !== 'object' || entry === null) return []\n const b = entry as Record<string, unknown>\n if (typeof b.name !== 'string') return []\n const rawScreenshot =\n typeof b.screenshot === 'object' && b.screenshot !== null\n ? (b.screenshot as Record<string, unknown>)\n : {}\n return [\n {\n name: b.name,\n type: 'block' as const,\n displayName: typeof b.displayName === 'string' ? b.displayName : b.name,\n description: typeof b.description === 'string' ? b.description : '',\n category: typeof b.category === 'string' ? b.category : '',\n version: typeof b.version === 'string' ? b.version : '0.0.0',\n files: asStringArray(b.files),\n dependencies: asStringArray(b.dependencies),\n tags: asStringArray(b.tags),\n screenshot: {\n light: typeof rawScreenshot.light === 'string' ? rawScreenshot.light : '',\n dark: typeof rawScreenshot.dark === 'string' ? rawScreenshot.dark : '',\n },\n },\n ]\n })\n : []\n\n const templates: string[] = Array.isArray(obj.templates)\n ? obj.templates.flatMap((entry) => {\n if (typeof entry !== 'object' || entry === null) return []\n const name = (entry as Record<string, unknown>).name\n return typeof name === 'string' ? [name] : []\n })\n : []\n\n return {\n version: typeof obj.version === 'string' ? obj.version : '0.0.0',\n generatedAt: typeof obj.generatedAt === 'string' ? obj.generatedAt : '',\n components,\n blocks,\n templates,\n }\n}\n\n/**\n * Fetch and parse the registry from a URL. Retries transient failures and\n * falls back to the last cached copy when offline (via fetchJsonFresh).\n */\nexport async function fetchRegistry(url: string): Promise<Registry> {\n try {\n return parseRegistry(await fetchJsonFresh(url))\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e)\n throw new Error(`Failed to fetch registry from ${url}: ${msg}`)\n }\n}\n\n/**\n * Find a component by name (case-insensitive, full name or unambiguous suffix).\n *\n * Examples:\n * \"layout/app-shell\" → exact match on full name\n * \"app-shell\" → suffix match if exactly one entry ends with \"/app-shell\"\n * \"button\" → exact match (no slash, no suffix ambiguity)\n */\nexport function findComponent(registry: Registry, name: string): RegistryComponent | undefined {\n const target = name.toLowerCase()\n\n // Handle block/ prefix: look up in the blocks array.\n if (target.startsWith('block/')) {\n const blockName = target.slice('block/'.length)\n const block = (registry.blocks ?? []).find((b) => b.name.toLowerCase() === blockName)\n if (!block) throw new Error(`Block \"${blockName}\" not found in registry.`)\n return block as unknown as RegistryComponent\n }\n\n // 1. Exact full-name match (case-insensitive).\n const exact = registry.components.find((c) => c.name.toLowerCase() === target)\n if (exact) return exact\n\n // 2. Suffix match: resolve \"app-shell\" to \"layout/app-shell\" when unambiguous.\n const suffix = `/${target}`\n const matches = registry.components.filter((c) => c.name.toLowerCase().endsWith(suffix))\n if (matches.length === 1) return matches[0]\n\n return undefined\n}\n\n/** Fuzzy-ish search over name, tags and description. */\nexport function searchComponents(registry: Registry, query: string): RegistryComponent[] {\n const q = query.toLowerCase()\n return registry.components.filter(\n (c) =>\n c.name.toLowerCase().includes(q) ||\n c.description.toLowerCase().includes(q) ||\n c.tags.some((t) => t.toLowerCase().includes(q)),\n )\n}\n\n/** Extract the file name from a registry file URL. */\nexport function fileName(url: string): string {\n const stripped = url.split(/[?#]/)[0] ?? url\n return stripped.split('/').pop() || stripped\n}\n","import { createHash } from 'node:crypto'\nimport { existsSync } from 'node:fs'\nimport { readFile, unlink, writeFile } from 'node:fs/promises'\nimport { join } from 'node:path'\n\nexport interface LockEntry {\n registry: string\n version: string\n installedAt: string\n files: Record<string, string>\n conflicted?: true\n}\n\nexport interface LockFile {\n lockVersion: 1\n items: Record<string, LockEntry>\n}\n\nexport function sha256(content: string): string {\n return `sha256-${createHash('sha256').update(content).digest('hex')}`\n}\n\nconst LOCK_FILENAME = 'cascivo.lock'\n/** Pre-rename lockfile name — read as a fallback, migrated away on next write. */\nconst LEGACY_LOCK_FILENAME = 'cascade.lock'\n\nexport async function readLock(cwd: string = process.cwd()): Promise<LockFile | null> {\n for (const filename of [LOCK_FILENAME, LEGACY_LOCK_FILENAME]) {\n const path = join(cwd, filename)\n if (!existsSync(path)) continue\n try {\n const raw = JSON.parse(await readFile(path, 'utf8')) as unknown\n if (typeof raw !== 'object' || raw === null) return null\n return raw as LockFile\n } catch {\n return null\n }\n }\n return null\n}\n\nexport async function writeLock(lock: LockFile, cwd: string = process.cwd()): Promise<void> {\n const path = join(cwd, LOCK_FILENAME)\n await writeFile(path, `${JSON.stringify(lock, null, 2)}\\n`, 'utf8')\n // Migrate away from the legacy name on the first write (read-only commands\n // like --dry-run never touch it).\n const legacyPath = join(cwd, LEGACY_LOCK_FILENAME)\n if (existsSync(legacyPath)) {\n try {\n await unlink(legacyPath)\n console.log('Migrated cascade.lock → cascivo.lock')\n } catch {\n // Leaving the legacy file behind is harmless; cascivo.lock now wins.\n }\n }\n}\n\nexport function createLock(): LockFile {\n return { lockVersion: 1, items: {} }\n}\n\nexport function updateLockEntry(lock: LockFile, name: string, entry: LockEntry): LockFile {\n return {\n ...lock,\n items: { ...lock.items, [name]: entry },\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ import { join } from "node:path";
2
+ import { readFile } from "node:fs/promises";
3
+ //#region src/utils/semver.ts
4
+ /**
5
+ * Minimal semver comparison — just enough to evaluate the `>=x.y.z` floors
6
+ * the registry generator emits for `peerVersions` (scripts/registry/generate.ts).
7
+ * Not a general-purpose range parser; unsupported input is treated as
8
+ * unsatisfied so callers surface it rather than silently pass.
9
+ */
10
+ function parseVersion(v) {
11
+ const m = /^(\d+)\.(\d+)\.(\d+)/.exec(v.trim());
12
+ if (!m) return null;
13
+ return [
14
+ Number(m[1]),
15
+ Number(m[2]),
16
+ Number(m[3])
17
+ ];
18
+ }
19
+ /** Compares two `x.y.z` versions: -1 if a<b, 0 if equal, 1 if a>b. Throws on unparsable input. */
20
+ function compareVersions(a, b) {
21
+ const pa = parseVersion(a);
22
+ const pb = parseVersion(b);
23
+ if (!pa || !pb) throw new Error(`Cannot compare versions: "${a}" vs "${b}"`);
24
+ for (let i = 0; i < 3; i++) if (pa[i] !== pb[i]) return pa[i] < pb[i] ? -1 : 1;
25
+ return 0;
26
+ }
27
+ /** Evaluates whether `installed` satisfies a floor spec of the form `>=x.y.z`. */
28
+ function satisfiesFloor(installed, floor) {
29
+ const m = /^>=\s*(\d+\.\d+\.\d+)/.exec(floor.trim());
30
+ if (!m) return true;
31
+ try {
32
+ return compareVersions(installed, m[1]) >= 0;
33
+ } catch {
34
+ return true;
35
+ }
36
+ }
37
+ //#endregion
38
+ //#region src/utils/peer-versions.ts
39
+ /** Reads the installed version of a package from the project's node_modules, or null if absent/unreadable. */
40
+ async function readInstalledPackageVersion(cwd, pkgName) {
41
+ try {
42
+ const pkg = JSON.parse(await readFile(join(cwd, "node_modules", pkgName, "package.json"), "utf8"));
43
+ return typeof pkg.version === "string" ? pkg.version : null;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+ /**
49
+ * Checks installed peer package versions against the floors a registry entry
50
+ * declares (`peerVersions`, e.g. `{ "@cascivo/i18n": ">=0.2.1" }`). Returns
51
+ * only the packages that fail — either missing or older than required.
52
+ */
53
+ async function checkPeerVersions(cwd, floors) {
54
+ const violations = [];
55
+ for (const [pkg, required] of Object.entries(floors)) {
56
+ const installed = await readInstalledPackageVersion(cwd, pkg);
57
+ if (installed === null || !satisfiesFloor(installed, required)) violations.push({
58
+ pkg,
59
+ required,
60
+ installed
61
+ });
62
+ }
63
+ return violations;
64
+ }
65
+ //#endregion
66
+ export { readInstalledPackageVersion as n, compareVersions as r, checkPeerVersions as t };
67
+
68
+ //# sourceMappingURL=peer-versions-Cep8Brn3.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"peer-versions-Cep8Brn3.mjs","names":[],"sources":["../src/utils/semver.ts","../src/utils/peer-versions.ts"],"sourcesContent":["/**\n * Minimal semver comparison — just enough to evaluate the `>=x.y.z` floors\n * the registry generator emits for `peerVersions` (scripts/registry/generate.ts).\n * Not a general-purpose range parser; unsupported input is treated as\n * unsatisfied so callers surface it rather than silently pass.\n */\n\nfunction parseVersion(v: string): [number, number, number] | null {\n const m = /^(\\d+)\\.(\\d+)\\.(\\d+)/.exec(v.trim())\n if (!m) return null\n return [Number(m[1]), Number(m[2]), Number(m[3])]\n}\n\n/** Compares two `x.y.z` versions: -1 if a<b, 0 if equal, 1 if a>b. Throws on unparsable input. */\nexport function compareVersions(a: string, b: string): number {\n const pa = parseVersion(a)\n const pb = parseVersion(b)\n if (!pa || !pb) throw new Error(`Cannot compare versions: \"${a}\" vs \"${b}\"`)\n for (let i = 0; i < 3; i++) {\n if (pa[i] !== pb[i]) return pa[i]! < pb[i]! ? -1 : 1\n }\n return 0\n}\n\n/** Evaluates whether `installed` satisfies a floor spec of the form `>=x.y.z`. */\nexport function satisfiesFloor(installed: string, floor: string): boolean {\n const m = /^>=\\s*(\\d+\\.\\d+\\.\\d+)/.exec(floor.trim())\n if (!m) return true // unrecognized floor shape — don't block on it\n try {\n return compareVersions(installed, m[1]!) >= 0\n } catch {\n return true // unparsable installed version — don't block on it\n }\n}\n","import { readFile } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { satisfiesFloor } from './semver.js'\n\n/** Reads the installed version of a package from the project's node_modules, or null if absent/unreadable. */\nexport async function readInstalledPackageVersion(\n cwd: string,\n pkgName: string,\n): Promise<string | null> {\n try {\n const pkg = JSON.parse(\n await readFile(join(cwd, 'node_modules', pkgName, 'package.json'), 'utf8'),\n ) as { version?: string }\n return typeof pkg.version === 'string' ? pkg.version : null\n } catch {\n return null\n }\n}\n\nexport interface PeerVersionViolation {\n pkg: string\n required: string\n installed: string | null\n}\n\n/**\n * Checks installed peer package versions against the floors a registry entry\n * declares (`peerVersions`, e.g. `{ \"@cascivo/i18n\": \">=0.2.1\" }`). Returns\n * only the packages that fail — either missing or older than required.\n */\nexport async function checkPeerVersions(\n cwd: string,\n floors: Record<string, string>,\n): Promise<PeerVersionViolation[]> {\n const violations: PeerVersionViolation[] = []\n for (const [pkg, required] of Object.entries(floors)) {\n const installed = await readInstalledPackageVersion(cwd, pkg)\n if (installed === null || !satisfiesFloor(installed, required)) {\n violations.push({ pkg, required, installed })\n }\n }\n return violations\n}\n"],"mappings":";;;;;;;;;AAOA,SAAS,aAAa,GAA4C;CAChE,MAAM,IAAI,uBAAuB,KAAK,EAAE,KAAK,CAAC;CAC9C,IAAI,CAAC,GAAG,OAAO;CACf,OAAO;EAAC,OAAO,EAAE,EAAE;EAAG,OAAO,EAAE,EAAE;EAAG,OAAO,EAAE,EAAE;CAAC;AAClD;;AAGA,SAAgB,gBAAgB,GAAW,GAAmB;CAC5D,MAAM,KAAK,aAAa,CAAC;CACzB,MAAM,KAAK,aAAa,CAAC;CACzB,IAAI,CAAC,MAAM,CAAC,IAAI,MAAM,IAAI,MAAM,6BAA6B,EAAE,QAAQ,EAAE,EAAE;CAC3E,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KACrB,IAAI,GAAG,OAAO,GAAG,IAAI,OAAO,GAAG,KAAM,GAAG,KAAM,KAAK;CAErD,OAAO;AACT;;AAGA,SAAgB,eAAe,WAAmB,OAAwB;CACxE,MAAM,IAAI,wBAAwB,KAAK,MAAM,KAAK,CAAC;CACnD,IAAI,CAAC,GAAG,OAAO;CACf,IAAI;EACF,OAAO,gBAAgB,WAAW,EAAE,EAAG,KAAK;CAC9C,QAAQ;EACN,OAAO;CACT;AACF;;;;AC5BA,eAAsB,4BACpB,KACA,SACwB;CACxB,IAAI;EACF,MAAM,MAAM,KAAK,MACf,MAAM,SAAS,KAAK,KAAK,gBAAgB,SAAS,cAAc,GAAG,MAAM,CAC3E;EACA,OAAO,OAAO,IAAI,YAAY,WAAW,IAAI,UAAU;CACzD,QAAQ;EACN,OAAO;CACT;AACF;;;;;;AAaA,eAAsB,kBACpB,KACA,QACiC;CACjC,MAAM,aAAqC,CAAC;CAC5C,KAAK,MAAM,CAAC,KAAK,aAAa,OAAO,QAAQ,MAAM,GAAG;EACpD,MAAM,YAAY,MAAM,4BAA4B,KAAK,GAAG;EAC5D,IAAI,cAAc,QAAQ,CAAC,eAAe,WAAW,QAAQ,GAC3D,WAAW,KAAK;GAAE;GAAK;GAAU;EAAU,CAAC;CAEhD;CACA,OAAO;AACT"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cascivo",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "private": false,
5
5
  "description": "CLI — npx cascivo init / add / list / update",
6
6
  "keywords": [
@@ -31,9 +31,10 @@
31
31
  "type": "module",
32
32
  "types": "./dist/index.d.mts",
33
33
  "exports": {
34
+ "./package.json": "./package.json",
34
35
  ".": {
35
- "import": "./dist/index.mjs",
36
36
  "types": "./dist/index.d.mts",
37
+ "import": "./dist/index.mjs",
37
38
  "default": "./dist/index.mjs"
38
39
  }
39
40
  },
@@ -42,7 +43,7 @@
42
43
  "provenance": true
43
44
  },
44
45
  "dependencies": {
45
- "@cascivo/registry": "^0.2.0"
46
+ "@cascivo/registry": "^0.2.1"
46
47
  },
47
48
  "devDependencies": {
48
49
  "@types/node": "^25",
@@ -1 +0,0 @@
1
- {"version":3,"file":"config-CYlTGdmE.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 host for hosted cascivo artifacts (registry.json, per-item\n * r/<name>.json, marketplace.json). docs.cascivo.com mirrors the same tree.\n * 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: { override?: string; env?: NodeJS.ProcessEnv } = {},\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) 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 return 'npm'\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 } = {},\n): [string, string[]] {\n const verb = pm === 'npm' ? 'install' : 'add'\n const devFlag = opts.dev ? [pm === 'npm' ? '--save-dev' : '-D'] : []\n return [pm, [verb, ...devFlag, ...packages]]\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AASA,MAAa,eAAe;;AAG5B,MAAa,SAAS;CACpB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAqBA,MAAa,iBAAgC;CAI3C,UAAU,GAAG,aAAa;CAC1B,WAAW;CACX,OAAO;AACT;AAEA,MAAM,eAAe;CAAC;CAAqB;CAAqB;AAAoB;;AAGpF,SAAgB,cAAc,SAAmE;CAC/F,OAAO;EAAE,GAAG;EAAgB,GAAG;CAAQ;AACzC;;AAGA,SAAgB,kBACd,QACA,MAAyB,QAAQ,KAClB;CACf,OAAO;EACL,GAAG;EACH,GAAI,IAAI,mBAAmB,EAAE,UAAU,IAAI,iBAAiB,IAAI,CAAC;EACjE,GAAI,IAAI,qBAAqB,EAAE,WAAW,IAAI,mBAAmB,IAAI,CAAC;CACxE;AACF;;;;;;AAOA,eAAsB,WAAW,MAAc,QAAQ,IAAI,GAA2B;CACpF,KAAK,MAAM,QAAQ,cAAc;EAC/B,MAAM,OAAO,KAAK,KAAK,IAAI;EAC3B,IAAI,CAAC,WAAW,IAAI,GAAG;EACvB,IAAI;GACF,MAAM,MAAO,MAAM,OAAO,cAAc,IAAI,CAAC,CAAC;GAG9C,OAAO,kBAAkB,cAAc,IAAI,WAAY,GAA8B,CAAC;EACxF,QAAQ;GAEN,OAAO,kBAAkB,cAAc,IAAI,CAAC;EAC9C;CACF;CACA,OAAO,kBAAkB,cAAc,IAAI,CAAC;AAC9C;;AAKA,SAAgB,iBAAiB,OAAoD;CACnF,OAAO,UAAU,UAAU,UAAU,UAAU,UAAU,SAAS,UAAU;AAC9E;;;;;;AAOA,MAAM,eAAoD;CACxD,CAAC,kBAAkB,MAAM;CACzB,CAAC,aAAa,MAAM;CACpB,CAAC,aAAa,KAAK;CACnB,CAAC,YAAY,KAAK;CAClB,CAAC,qBAAqB,KAAK;AAC7B;;AAGA,SAAS,gBAAgB,IAAoD;CAC3E,IAAI,CAAC,IAAI,OAAO,KAAA;CAChB,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,CAAC;CAC3B,OAAO,iBAAiB,IAAI,IAAI,OAAO,KAAA;AACzC;;AAGA,SAAS,kBAAkB,KAAyC;CAClE,IAAI;EAIF,MAAM,OAHM,KAAK,MAAM,aAAa,KAAK,KAAK,cAAc,GAAG,MAAM,CAGtD,CAAC,CAAC,gBAAgB,MAAM,GAAG,CAAC,CAAC;EAC5C,OAAO,iBAAiB,IAAI,IAAI,OAAO,KAAA;CACzC,QAAQ;EACN;CACF;AACF;;;;;;;;;;;;AAaA,SAAgB,qBACd,MAAc,QAAQ,IAAI,GAC1B,OAAuD,CAAC,GACxC;CAChB,MAAM,MAAM,KAAK,OAAO,QAAQ;CAEhC,IAAI,iBAAiB,KAAK,QAAQ,GAAG,OAAO,KAAK;CAEjD,MAAM,QAAQ,IAAI;CAClB,IAAI,iBAAiB,KAAK,GAAG,OAAO;CAEpC,MAAM,OAAO,gBAAgB,IAAI,qBAAqB;CACtD,IAAI,MAAM,OAAO;CAEjB,IAAI,UAAU;CACd,SAAS;EACP,KAAK,MAAM,CAAC,MAAM,OAAO,cACvB,IAAI,WAAW,KAAK,SAAS,IAAI,CAAC,GAAG,OAAO;EAE9C,MAAM,YAAY,kBAAkB,OAAO;EAC3C,IAAI,WAAW,OAAO;EAEtB,IAAI,WAAW,KAAK,SAAS,MAAM,CAAC,GAAG;EACvC,MAAM,SAAS,QAAQ,OAAO;EAC9B,IAAI,WAAW,SAAS;EACxB,UAAU;CACZ;CAEA,OAAO;AACT;;AAGA,SAAgB,eACd,IACA,UACA,OAA0B,CAAC,GACP;CAGpB,OAAO,CAAC,IAAI;EAFC,OAAO,QAAQ,YAAY;EAErB,GADH,KAAK,MAAM,CAAC,OAAO,QAAQ,eAAe,IAAI,IAAI,CAAC;EACpC,GAAG;CAAQ,CAAC;AAC7C;;AAGA,SAAgB,YACd,IACA,UACA,OAA0B,CAAC,GACnB;CACR,MAAM,CAAC,KAAK,QAAQ,eAAe,IAAI,UAAU,IAAI;CACrD,OAAO,GAAG,IAAI,GAAG,KAAK,KAAK,GAAG;AAChC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"peer-versions-B2bCgko-.mjs","names":[],"sources":["../src/utils/registry.ts","../src/utils/lock.ts","../src/utils/semver.ts","../src/utils/peer-versions.ts"],"sourcesContent":["import { fetchJsonFresh } from './http.js'\n\nexport interface RegistryComponent {\n name: string\n type?: 'component' | 'layout' | 'block' | 'chart' | 'section' | 'flow' | 'editor'\n description: string\n category: string\n version: string\n files: string[]\n /** filename → sha256 of upstream content — used by `update --check`. */\n fileHashes?: Record<string, string>\n /** npm package to install (charts/flow/editor). */\n install?: string\n /** stylesheet the npm package requires, e.g. `@cascivo/charts/styles.css`. */\n styles?: string\n dependencies: string[]\n /**\n * Minimum published version required for each `@cascivo/*` dependency, e.g.\n * `{ \"@cascivo/i18n\": \">=0.2.1\" }`. Used by `add`/`doctor --drift` to detect\n * an installed peer package that is older than what this copied source needs.\n */\n peerVersions?: Record<string, string>\n /** Other registry components to install transitively (shared hooks/siblings). */\n registryDependencies?: string[]\n tags: string[]\n meta: { name: string }\n}\n\nexport interface BlockRegistryEntry {\n name: string\n type: 'block'\n displayName: string\n description: string\n category: string\n version: string\n files: string[]\n dependencies: string[]\n tags: string[]\n screenshot: { light: string; dark: string }\n}\n\nexport interface Registry {\n version: string\n generatedAt: string\n components: RegistryComponent[]\n blocks?: BlockRegistryEntry[]\n /** Names of first-party templates — installed via the per-item path (r/<name>.json). */\n templates: string[]\n}\n\nfunction asStringArray(value: unknown): string[] {\n return Array.isArray(value) ? value.filter((v): v is string => typeof v === 'string') : []\n}\n\n/** Validate and normalize raw JSON into a typed Registry. Throws on bad shape. */\nexport function parseRegistry(raw: unknown): Registry {\n if (typeof raw !== 'object' || raw === null) {\n throw new Error('Invalid registry: expected an object')\n }\n const obj = raw as Record<string, unknown>\n if (!Array.isArray(obj.components)) {\n throw new Error('Invalid registry: \"components\" must be an array')\n }\n\n const components: RegistryComponent[] = obj.components.map((entry, i) => {\n if (typeof entry !== 'object' || entry === null) {\n throw new Error(`Invalid registry: component at index ${i} is not an object`)\n }\n const c = entry as Record<string, unknown>\n if (typeof c.name !== 'string') {\n throw new Error(`Invalid registry: component at index ${i} is missing \"name\"`)\n }\n const rawType = c.type\n const KNOWN_TYPES = [\n 'component',\n 'layout',\n 'block',\n 'chart',\n 'section',\n 'flow',\n 'editor',\n ] as const\n const type = (KNOWN_TYPES as readonly string[]).includes(rawType as string)\n ? (rawType as RegistryComponent['type'])\n : undefined\n const rawMeta = c.meta\n const metaName =\n typeof rawMeta === 'object' &&\n rawMeta !== null &&\n typeof (rawMeta as Record<string, unknown>).name === 'string'\n ? ((rawMeta as Record<string, unknown>).name as string)\n : c.name\n const result: RegistryComponent = {\n name: c.name,\n description: typeof c.description === 'string' ? c.description : '',\n category: typeof c.category === 'string' ? c.category : '',\n version: typeof c.version === 'string' ? c.version : '0.0.0',\n files: asStringArray(c.files),\n dependencies: asStringArray(c.dependencies),\n tags: asStringArray(c.tags),\n meta: { name: metaName },\n }\n if (type !== undefined) result.type = type\n if (typeof c.install === 'string') result.install = c.install\n if (typeof c.styles === 'string') result.styles = c.styles\n if (Array.isArray(c.registryDependencies)) {\n const deps = asStringArray(c.registryDependencies)\n if (deps.length > 0) result.registryDependencies = deps\n }\n if (typeof c.fileHashes === 'object' && c.fileHashes !== null) {\n const hashes: Record<string, string> = {}\n for (const [file, hash] of Object.entries(c.fileHashes as Record<string, unknown>)) {\n if (typeof hash === 'string') hashes[file] = hash\n }\n if (Object.keys(hashes).length > 0) result.fileHashes = hashes\n }\n if (typeof c.peerVersions === 'object' && c.peerVersions !== null) {\n const floors: Record<string, string> = {}\n for (const [dep, floor] of Object.entries(c.peerVersions as Record<string, unknown>)) {\n if (typeof floor === 'string') floors[dep] = floor\n }\n if (Object.keys(floors).length > 0) result.peerVersions = floors\n }\n return result\n })\n\n const blocks: BlockRegistryEntry[] = Array.isArray(obj.blocks)\n ? obj.blocks.flatMap((entry) => {\n if (typeof entry !== 'object' || entry === null) return []\n const b = entry as Record<string, unknown>\n if (typeof b.name !== 'string') return []\n const rawScreenshot =\n typeof b.screenshot === 'object' && b.screenshot !== null\n ? (b.screenshot as Record<string, unknown>)\n : {}\n return [\n {\n name: b.name,\n type: 'block' as const,\n displayName: typeof b.displayName === 'string' ? b.displayName : b.name,\n description: typeof b.description === 'string' ? b.description : '',\n category: typeof b.category === 'string' ? b.category : '',\n version: typeof b.version === 'string' ? b.version : '0.0.0',\n files: asStringArray(b.files),\n dependencies: asStringArray(b.dependencies),\n tags: asStringArray(b.tags),\n screenshot: {\n light: typeof rawScreenshot.light === 'string' ? rawScreenshot.light : '',\n dark: typeof rawScreenshot.dark === 'string' ? rawScreenshot.dark : '',\n },\n },\n ]\n })\n : []\n\n const templates: string[] = Array.isArray(obj.templates)\n ? obj.templates.flatMap((entry) => {\n if (typeof entry !== 'object' || entry === null) return []\n const name = (entry as Record<string, unknown>).name\n return typeof name === 'string' ? [name] : []\n })\n : []\n\n return {\n version: typeof obj.version === 'string' ? obj.version : '0.0.0',\n generatedAt: typeof obj.generatedAt === 'string' ? obj.generatedAt : '',\n components,\n blocks,\n templates,\n }\n}\n\n/**\n * Fetch and parse the registry from a URL. Retries transient failures and\n * falls back to the last cached copy when offline (via fetchJsonFresh).\n */\nexport async function fetchRegistry(url: string): Promise<Registry> {\n try {\n return parseRegistry(await fetchJsonFresh(url))\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e)\n throw new Error(`Failed to fetch registry from ${url}: ${msg}`)\n }\n}\n\n/**\n * Find a component by name (case-insensitive, full name or unambiguous suffix).\n *\n * Examples:\n * \"layout/app-shell\" → exact match on full name\n * \"app-shell\" → suffix match if exactly one entry ends with \"/app-shell\"\n * \"button\" → exact match (no slash, no suffix ambiguity)\n */\nexport function findComponent(registry: Registry, name: string): RegistryComponent | undefined {\n const target = name.toLowerCase()\n\n // Handle block/ prefix: look up in the blocks array.\n if (target.startsWith('block/')) {\n const blockName = target.slice('block/'.length)\n const block = (registry.blocks ?? []).find((b) => b.name.toLowerCase() === blockName)\n if (!block) throw new Error(`Block \"${blockName}\" not found in registry.`)\n return block as unknown as RegistryComponent\n }\n\n // 1. Exact full-name match (case-insensitive).\n const exact = registry.components.find((c) => c.name.toLowerCase() === target)\n if (exact) return exact\n\n // 2. Suffix match: resolve \"app-shell\" to \"layout/app-shell\" when unambiguous.\n const suffix = `/${target}`\n const matches = registry.components.filter((c) => c.name.toLowerCase().endsWith(suffix))\n if (matches.length === 1) return matches[0]\n\n return undefined\n}\n\n/** Fuzzy-ish search over name, tags and description. */\nexport function searchComponents(registry: Registry, query: string): RegistryComponent[] {\n const q = query.toLowerCase()\n return registry.components.filter(\n (c) =>\n c.name.toLowerCase().includes(q) ||\n c.description.toLowerCase().includes(q) ||\n c.tags.some((t) => t.toLowerCase().includes(q)),\n )\n}\n\n/** Extract the file name from a registry file URL. */\nexport function fileName(url: string): string {\n const stripped = url.split(/[?#]/)[0] ?? url\n return stripped.split('/').pop() || stripped\n}\n","import { createHash } from 'node:crypto'\nimport { existsSync } from 'node:fs'\nimport { readFile, unlink, writeFile } from 'node:fs/promises'\nimport { join } from 'node:path'\n\nexport interface LockEntry {\n registry: string\n version: string\n installedAt: string\n files: Record<string, string>\n conflicted?: true\n}\n\nexport interface LockFile {\n lockVersion: 1\n items: Record<string, LockEntry>\n}\n\nexport function sha256(content: string): string {\n return `sha256-${createHash('sha256').update(content).digest('hex')}`\n}\n\nconst LOCK_FILENAME = 'cascivo.lock'\n/** Pre-rename lockfile name — read as a fallback, migrated away on next write. */\nconst LEGACY_LOCK_FILENAME = 'cascade.lock'\n\nexport async function readLock(cwd: string = process.cwd()): Promise<LockFile | null> {\n for (const filename of [LOCK_FILENAME, LEGACY_LOCK_FILENAME]) {\n const path = join(cwd, filename)\n if (!existsSync(path)) continue\n try {\n const raw = JSON.parse(await readFile(path, 'utf8')) as unknown\n if (typeof raw !== 'object' || raw === null) return null\n return raw as LockFile\n } catch {\n return null\n }\n }\n return null\n}\n\nexport async function writeLock(lock: LockFile, cwd: string = process.cwd()): Promise<void> {\n const path = join(cwd, LOCK_FILENAME)\n await writeFile(path, `${JSON.stringify(lock, null, 2)}\\n`, 'utf8')\n // Migrate away from the legacy name on the first write (read-only commands\n // like --dry-run never touch it).\n const legacyPath = join(cwd, LEGACY_LOCK_FILENAME)\n if (existsSync(legacyPath)) {\n try {\n await unlink(legacyPath)\n console.log('Migrated cascade.lock → cascivo.lock')\n } catch {\n // Leaving the legacy file behind is harmless; cascivo.lock now wins.\n }\n }\n}\n\nexport function createLock(): LockFile {\n return { lockVersion: 1, items: {} }\n}\n\nexport function updateLockEntry(lock: LockFile, name: string, entry: LockEntry): LockFile {\n return {\n ...lock,\n items: { ...lock.items, [name]: entry },\n }\n}\n","/**\n * Minimal semver comparison — just enough to evaluate the `>=x.y.z` floors\n * the registry generator emits for `peerVersions` (scripts/registry/generate.ts).\n * Not a general-purpose range parser; unsupported input is treated as\n * unsatisfied so callers surface it rather than silently pass.\n */\n\nfunction parseVersion(v: string): [number, number, number] | null {\n const m = /^(\\d+)\\.(\\d+)\\.(\\d+)/.exec(v.trim())\n if (!m) return null\n return [Number(m[1]), Number(m[2]), Number(m[3])]\n}\n\n/** Compares two `x.y.z` versions: -1 if a<b, 0 if equal, 1 if a>b. Throws on unparsable input. */\nexport function compareVersions(a: string, b: string): number {\n const pa = parseVersion(a)\n const pb = parseVersion(b)\n if (!pa || !pb) throw new Error(`Cannot compare versions: \"${a}\" vs \"${b}\"`)\n for (let i = 0; i < 3; i++) {\n if (pa[i] !== pb[i]) return pa[i]! < pb[i]! ? -1 : 1\n }\n return 0\n}\n\n/** Evaluates whether `installed` satisfies a floor spec of the form `>=x.y.z`. */\nexport function satisfiesFloor(installed: string, floor: string): boolean {\n const m = /^>=\\s*(\\d+\\.\\d+\\.\\d+)/.exec(floor.trim())\n if (!m) return true // unrecognized floor shape — don't block on it\n try {\n return compareVersions(installed, m[1]!) >= 0\n } catch {\n return true // unparsable installed version — don't block on it\n }\n}\n","import { readFile } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { satisfiesFloor } from './semver.js'\n\n/** Reads the installed version of a package from the project's node_modules, or null if absent/unreadable. */\nexport async function readInstalledPackageVersion(\n cwd: string,\n pkgName: string,\n): Promise<string | null> {\n try {\n const pkg = JSON.parse(\n await readFile(join(cwd, 'node_modules', pkgName, 'package.json'), 'utf8'),\n ) as { version?: string }\n return typeof pkg.version === 'string' ? pkg.version : null\n } catch {\n return null\n }\n}\n\nexport interface PeerVersionViolation {\n pkg: string\n required: string\n installed: string | null\n}\n\n/**\n * Checks installed peer package versions against the floors a registry entry\n * declares (`peerVersions`, e.g. `{ \"@cascivo/i18n\": \">=0.2.1\" }`). Returns\n * only the packages that fail — either missing or older than required.\n */\nexport async function checkPeerVersions(\n cwd: string,\n floors: Record<string, string>,\n): Promise<PeerVersionViolation[]> {\n const violations: PeerVersionViolation[] = []\n for (const [pkg, required] of Object.entries(floors)) {\n const installed = await readInstalledPackageVersion(cwd, pkg)\n if (installed === null || !satisfiesFloor(installed, required)) {\n violations.push({ pkg, required, installed })\n }\n }\n return violations\n}\n"],"mappings":"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"}