cascivo 0.2.0 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +10 -1
- package/dist/{audit-BRn4MeXD.mjs → audit-InQqdY1s.mjs} +3 -3
- package/dist/{audit-BRn4MeXD.mjs.map → audit-InQqdY1s.mjs.map} +1 -1
- package/dist/{config-CsnCR5eh.mjs → config-BsyJbMvD.mjs} +28 -4
- package/dist/config-BsyJbMvD.mjs.map +1 -0
- package/dist/drift-Ciw5qeEt.mjs +56 -0
- package/dist/drift-Ciw5qeEt.mjs.map +1 -0
- package/dist/{eject-B92gZhLX.mjs → eject-Br8V7I3m.mjs} +3 -3
- package/dist/{eject-B92gZhLX.mjs.map → eject-Br8V7I3m.mjs.map} +1 -1
- package/dist/{http-CLkdTpxD.mjs → http-CJZ5W0Fa.mjs} +56 -5
- package/dist/http-CJZ5W0Fa.mjs.map +1 -0
- package/dist/index.d.mts +4 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +536 -222
- package/dist/index.mjs.map +1 -1
- package/dist/peer-versions-B2bCgko-.mjs +244 -0
- package/dist/peer-versions-B2bCgko-.mjs.map +1 -0
- package/dist/{template-init-DGEgatij.mjs → template-init-C46fRXo-.mjs} +2 -2
- package/dist/{template-init-DGEgatij.mjs.map → template-init-C46fRXo-.mjs.map} +1 -1
- package/package.json +4 -3
- package/readme.body.md +10 -1
- package/dist/config-CsnCR5eh.mjs.map +0 -1
- package/dist/drift-D7JFzpP_.mjs +0 -8
- package/dist/drift-D7JFzpP_.mjs.map +0 -1
- package/dist/http-CLkdTpxD.mjs.map +0 -1
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { n as fetchJsonFresh } from "./http-CJZ5W0Fa.mjs";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { readFile, unlink, writeFile } from "node:fs/promises";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
//#region src/utils/registry.ts
|
|
7
|
+
function asStringArray(value) {
|
|
8
|
+
return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
|
|
9
|
+
}
|
|
10
|
+
/** Validate and normalize raw JSON into a typed Registry. Throws on bad shape. */
|
|
11
|
+
function parseRegistry(raw) {
|
|
12
|
+
if (typeof raw !== "object" || raw === null) throw new Error("Invalid registry: expected an object");
|
|
13
|
+
const obj = raw;
|
|
14
|
+
if (!Array.isArray(obj.components)) throw new Error("Invalid registry: \"components\" must be an array");
|
|
15
|
+
const components = obj.components.map((entry, i) => {
|
|
16
|
+
if (typeof entry !== "object" || entry === null) throw new Error(`Invalid registry: component at index ${i} is not an object`);
|
|
17
|
+
const c = entry;
|
|
18
|
+
if (typeof c.name !== "string") throw new Error(`Invalid registry: component at index ${i} is missing "name"`);
|
|
19
|
+
const rawType = c.type;
|
|
20
|
+
const type = [
|
|
21
|
+
"component",
|
|
22
|
+
"layout",
|
|
23
|
+
"block",
|
|
24
|
+
"chart",
|
|
25
|
+
"section",
|
|
26
|
+
"flow",
|
|
27
|
+
"editor"
|
|
28
|
+
].includes(rawType) ? rawType : void 0;
|
|
29
|
+
const rawMeta = c.meta;
|
|
30
|
+
const metaName = typeof rawMeta === "object" && rawMeta !== null && typeof rawMeta.name === "string" ? rawMeta.name : c.name;
|
|
31
|
+
const result = {
|
|
32
|
+
name: c.name,
|
|
33
|
+
description: typeof c.description === "string" ? c.description : "",
|
|
34
|
+
category: typeof c.category === "string" ? c.category : "",
|
|
35
|
+
version: typeof c.version === "string" ? c.version : "0.0.0",
|
|
36
|
+
files: asStringArray(c.files),
|
|
37
|
+
dependencies: asStringArray(c.dependencies),
|
|
38
|
+
tags: asStringArray(c.tags),
|
|
39
|
+
meta: { name: metaName }
|
|
40
|
+
};
|
|
41
|
+
if (type !== void 0) result.type = type;
|
|
42
|
+
if (typeof c.install === "string") result.install = c.install;
|
|
43
|
+
if (typeof c.styles === "string") result.styles = c.styles;
|
|
44
|
+
if (Array.isArray(c.registryDependencies)) {
|
|
45
|
+
const deps = asStringArray(c.registryDependencies);
|
|
46
|
+
if (deps.length > 0) result.registryDependencies = deps;
|
|
47
|
+
}
|
|
48
|
+
if (typeof c.fileHashes === "object" && c.fileHashes !== null) {
|
|
49
|
+
const hashes = {};
|
|
50
|
+
for (const [file, hash] of Object.entries(c.fileHashes)) if (typeof hash === "string") hashes[file] = hash;
|
|
51
|
+
if (Object.keys(hashes).length > 0) result.fileHashes = hashes;
|
|
52
|
+
}
|
|
53
|
+
if (typeof c.peerVersions === "object" && c.peerVersions !== null) {
|
|
54
|
+
const floors = {};
|
|
55
|
+
for (const [dep, floor] of Object.entries(c.peerVersions)) if (typeof floor === "string") floors[dep] = floor;
|
|
56
|
+
if (Object.keys(floors).length > 0) result.peerVersions = floors;
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
});
|
|
60
|
+
const blocks = Array.isArray(obj.blocks) ? obj.blocks.flatMap((entry) => {
|
|
61
|
+
if (typeof entry !== "object" || entry === null) return [];
|
|
62
|
+
const b = entry;
|
|
63
|
+
if (typeof b.name !== "string") return [];
|
|
64
|
+
const rawScreenshot = typeof b.screenshot === "object" && b.screenshot !== null ? b.screenshot : {};
|
|
65
|
+
return [{
|
|
66
|
+
name: b.name,
|
|
67
|
+
type: "block",
|
|
68
|
+
displayName: typeof b.displayName === "string" ? b.displayName : b.name,
|
|
69
|
+
description: typeof b.description === "string" ? b.description : "",
|
|
70
|
+
category: typeof b.category === "string" ? b.category : "",
|
|
71
|
+
version: typeof b.version === "string" ? b.version : "0.0.0",
|
|
72
|
+
files: asStringArray(b.files),
|
|
73
|
+
dependencies: asStringArray(b.dependencies),
|
|
74
|
+
tags: asStringArray(b.tags),
|
|
75
|
+
screenshot: {
|
|
76
|
+
light: typeof rawScreenshot.light === "string" ? rawScreenshot.light : "",
|
|
77
|
+
dark: typeof rawScreenshot.dark === "string" ? rawScreenshot.dark : ""
|
|
78
|
+
}
|
|
79
|
+
}];
|
|
80
|
+
}) : [];
|
|
81
|
+
const templates = Array.isArray(obj.templates) ? obj.templates.flatMap((entry) => {
|
|
82
|
+
if (typeof entry !== "object" || entry === null) return [];
|
|
83
|
+
const name = entry.name;
|
|
84
|
+
return typeof name === "string" ? [name] : [];
|
|
85
|
+
}) : [];
|
|
86
|
+
return {
|
|
87
|
+
version: typeof obj.version === "string" ? obj.version : "0.0.0",
|
|
88
|
+
generatedAt: typeof obj.generatedAt === "string" ? obj.generatedAt : "",
|
|
89
|
+
components,
|
|
90
|
+
blocks,
|
|
91
|
+
templates
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Fetch and parse the registry from a URL. Retries transient failures and
|
|
96
|
+
* falls back to the last cached copy when offline (via fetchJsonFresh).
|
|
97
|
+
*/
|
|
98
|
+
async function fetchRegistry(url) {
|
|
99
|
+
try {
|
|
100
|
+
return parseRegistry(await fetchJsonFresh(url));
|
|
101
|
+
} catch (e) {
|
|
102
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
103
|
+
throw new Error(`Failed to fetch registry from ${url}: ${msg}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Find a component by name (case-insensitive, full name or unambiguous suffix).
|
|
108
|
+
*
|
|
109
|
+
* Examples:
|
|
110
|
+
* "layout/app-shell" → exact match on full name
|
|
111
|
+
* "app-shell" → suffix match if exactly one entry ends with "/app-shell"
|
|
112
|
+
* "button" → exact match (no slash, no suffix ambiguity)
|
|
113
|
+
*/
|
|
114
|
+
function findComponent(registry, name) {
|
|
115
|
+
const target = name.toLowerCase();
|
|
116
|
+
if (target.startsWith("block/")) {
|
|
117
|
+
const blockName = target.slice(6);
|
|
118
|
+
const block = (registry.blocks ?? []).find((b) => b.name.toLowerCase() === blockName);
|
|
119
|
+
if (!block) throw new Error(`Block "${blockName}" not found in registry.`);
|
|
120
|
+
return block;
|
|
121
|
+
}
|
|
122
|
+
const exact = registry.components.find((c) => c.name.toLowerCase() === target);
|
|
123
|
+
if (exact) return exact;
|
|
124
|
+
const suffix = `/${target}`;
|
|
125
|
+
const matches = registry.components.filter((c) => c.name.toLowerCase().endsWith(suffix));
|
|
126
|
+
if (matches.length === 1) return matches[0];
|
|
127
|
+
}
|
|
128
|
+
/** Extract the file name from a registry file URL. */
|
|
129
|
+
function fileName(url) {
|
|
130
|
+
const stripped = url.split(/[?#]/)[0] ?? url;
|
|
131
|
+
return stripped.split("/").pop() || stripped;
|
|
132
|
+
}
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/utils/lock.ts
|
|
135
|
+
function sha256(content) {
|
|
136
|
+
return `sha256-${createHash("sha256").update(content).digest("hex")}`;
|
|
137
|
+
}
|
|
138
|
+
const LOCK_FILENAME = "cascivo.lock";
|
|
139
|
+
/** Pre-rename lockfile name — read as a fallback, migrated away on next write. */
|
|
140
|
+
const LEGACY_LOCK_FILENAME = "cascade.lock";
|
|
141
|
+
async function readLock(cwd = process.cwd()) {
|
|
142
|
+
for (const filename of [LOCK_FILENAME, LEGACY_LOCK_FILENAME]) {
|
|
143
|
+
const path = join(cwd, filename);
|
|
144
|
+
if (!existsSync(path)) continue;
|
|
145
|
+
try {
|
|
146
|
+
const raw = JSON.parse(await readFile(path, "utf8"));
|
|
147
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
148
|
+
return raw;
|
|
149
|
+
} catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
async function writeLock(lock, cwd = process.cwd()) {
|
|
156
|
+
await writeFile(join(cwd, LOCK_FILENAME), `${JSON.stringify(lock, null, 2)}\n`, "utf8");
|
|
157
|
+
const legacyPath = join(cwd, LEGACY_LOCK_FILENAME);
|
|
158
|
+
if (existsSync(legacyPath)) try {
|
|
159
|
+
await unlink(legacyPath);
|
|
160
|
+
console.log("Migrated cascade.lock → cascivo.lock");
|
|
161
|
+
} catch {}
|
|
162
|
+
}
|
|
163
|
+
function createLock() {
|
|
164
|
+
return {
|
|
165
|
+
lockVersion: 1,
|
|
166
|
+
items: {}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function updateLockEntry(lock, name, entry) {
|
|
170
|
+
return {
|
|
171
|
+
...lock,
|
|
172
|
+
items: {
|
|
173
|
+
...lock.items,
|
|
174
|
+
[name]: entry
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
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 };
|
|
243
|
+
|
|
244
|
+
//# sourceMappingURL=peer-versions-B2bCgko-.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
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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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { r as writeFileSafe } from "./fs-m7ZvuBBm.mjs";
|
|
2
|
-
import { join, resolve } from "node:path";
|
|
3
2
|
import { existsSync } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
5
|
//#region src/commands/template-init.ts
|
|
6
6
|
function pascal(name) {
|
|
@@ -176,4 +176,4 @@ async function templateInit(args, cwd = process.cwd()) {
|
|
|
176
176
|
//#endregion
|
|
177
177
|
export { templateInit };
|
|
178
178
|
|
|
179
|
-
//# sourceMappingURL=template-init-
|
|
179
|
+
//# sourceMappingURL=template-init-C46fRXo-.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template-init-DGEgatij.mjs","names":[],"sources":["../src/commands/template-init.ts"],"sourcesContent":["import { existsSync } from 'node:fs'\nimport { readFile } from 'node:fs/promises'\nimport { join, resolve } from 'node:path'\nimport type { RegistryItem, TemplateMeta } from '@cascivo/registry'\nimport { writeFileSafe } from '../utils/fs.js'\n\nexport interface TemplateInitOptions {\n /** kebab-case template name (also the source folder + registry item name). */\n name: string\n /** Discovery category, e.g. \"dashboard\". */\n category: string\n /** Target framework for the page source. */\n framework: TemplateMeta['framework']\n /** Components the template composes (registry names). */\n components: string[]\n /** Repo owner/name used to build raw file URLs and the homepage. */\n repo?: string\n /** License SPDX id. */\n license: string\n /** Author. */\n author?: string\n}\n\nexport interface ScaffoldFile {\n /** Path relative to the template repo root. */\n path: string\n contents: string\n}\n\nfunction pascal(name: string): string {\n return name\n .split(/[-_/]/)\n .filter(Boolean)\n .map((s) => s.charAt(0).toUpperCase() + s.slice(1))\n .join('')\n}\n\nfunction rawUrl(repo: string | undefined, path: string): string {\n if (!repo) return path\n return `https://raw.githubusercontent.com/${repo}/main/${path}`\n}\n\nfunction pageSource(name: string, components: string[]): string {\n const comp = pascal(name)\n const importLine =\n components.length > 0\n ? `// Components installed via the template's registryDependencies:\\n// ${components.join(', ')}\\n`\n : ''\n return `${importLine}import './${name}.module.css'\n\n/**\n * ${comp} template page. Owned, copy-paste source — adapt freely.\n * Presentational only: no useState/useEffect (cascade house rules).\n */\nexport function ${comp}Page() {\n return (\n <main className=\"${name}-page\">\n <h1>${comp}</h1>\n {/* Compose the template's components here. */}\n </main>\n )\n}\n`\n}\n\nfunction cssSource(name: string): string {\n return `@layer components {\n .${name}-page {\n display: grid;\n gap: var(--cascivo-space-4, 1rem);\n padding: var(--cascivo-space-4, 1rem);\n }\n}\n`\n}\n\nfunction fixturesSource(name: string): string {\n return `/** Mock data for the ${name} template. Replace with your real source. */\nexport const ${name.replace(/-/g, '_')}Fixtures = {\n items: [],\n}\n`\n}\n\nfunction templateMetaSource(meta: TemplateMeta): string {\n return `import type { TemplateMeta } from '@cascivo/registry'\n\nexport const meta: TemplateMeta = ${JSON.stringify(meta, null, 2)}\n`\n}\n\n/**\n * Build the files + registry item for a new template. Pure — no I/O — so it can\n * be unit-tested. The caller writes the files and merges the item into\n * `cascade-registry.json`.\n */\nexport function buildTemplateScaffold(opts: TemplateInitOptions): {\n files: ScaffoldFile[]\n item: RegistryItem\n} {\n const { name, category, framework, components, repo, license, author } = opts\n const dir = `src/${name}`\n const pageTarget = `src/pages/${name}.tsx`\n const cssTarget = `src/pages/${name}.module.css`\n const fixturesTarget = `src/fixtures/${name}.ts`\n\n const files: ScaffoldFile[] = [\n { path: `${dir}/${name}.tsx`, contents: pageSource(name, components) },\n { path: `${dir}/${name}.module.css`, contents: cssSource(name) },\n { path: `${dir}/fixtures.ts`, contents: fixturesSource(name) },\n ]\n\n const meta: TemplateMeta = {\n intent: `${pascal(name)} template`,\n framework,\n category,\n screenshots: [\n { light: rawUrl(repo, `screenshots/${name}-light.png`), alt: `${name} preview (light)` },\n ],\n fileRoles: {\n [pageTarget]: 'page',\n [cssTarget]: 'asset',\n [fixturesTarget]: 'fixture',\n },\n pages: [{ name: pascal(name), target: pageTarget }],\n }\n\n const item: RegistryItem = {\n schemaVersion: 2,\n name,\n type: 'template',\n description: `${pascal(name)} template`,\n category,\n version: '0.1.0',\n license,\n ...(author ? { author } : {}),\n ...(repo ? { homepage: `https://github.com/${repo}` } : {}),\n files: [\n { url: rawUrl(repo, `${dir}/${name}.tsx`), target: pageTarget },\n { url: rawUrl(repo, `${dir}/${name}.module.css`), target: cssTarget },\n { url: rawUrl(repo, `${dir}/fixtures.ts`), target: fixturesTarget },\n ],\n dependencies: [],\n registryDependencies: components,\n tags: [category, 'template'],\n meta,\n }\n\n files.push({ path: `${dir}/${name}.template.ts`, contents: templateMetaSource(meta) })\n\n return { files, item }\n}\n\nfunction flag(args: string[], key: string): string | undefined {\n const eq = args.find((a) => a.startsWith(`--${key}=`))\n if (eq) return eq.slice(key.length + 3)\n const idx = args.indexOf(`--${key}`)\n const next = idx !== -1 ? args[idx + 1] : undefined\n return next && !next.startsWith('--') ? next : undefined\n}\n\nexport async function templateInit(args: string[], cwd: string = process.cwd()): Promise<void> {\n const name = args.find((a) => !a.startsWith('-'))\n if (!name) {\n console.error(\n 'Usage: cascivo template init <name> [--category dashboard] [--framework react-vite] [--components card,line-chart] [--repo owner/repo]',\n )\n process.exitCode = 1\n return\n }\n\n const componentsArg = flag(args, 'components')\n const opts: TemplateInitOptions = {\n name,\n category: flag(args, 'category') ?? 'misc',\n framework: (flag(args, 'framework') as TemplateMeta['framework']) ?? 'react-vite',\n components: componentsArg\n ? componentsArg\n .split(',')\n .map((c) => c.trim())\n .filter(Boolean)\n : [],\n license: flag(args, 'license') ?? 'MIT',\n ...(flag(args, 'repo') ? { repo: flag(args, 'repo')! } : {}),\n ...(flag(args, 'author') ? { author: flag(args, 'author')! } : {}),\n }\n\n const { files, item } = buildTemplateScaffold(opts)\n\n for (const file of files) {\n await writeFileSafe(join(cwd, file.path), file.contents)\n }\n\n // Merge into cascivo-registry.json (create if missing).\n const registryPath = resolve(cwd, 'cascivo-registry.json')\n let index: { schemaVersion: 2; name: string; items: RegistryItem[] }\n if (existsSync(registryPath)) {\n index = JSON.parse(await readFile(registryPath, 'utf8')) as typeof index\n index.items = (index.items ?? []).filter((i) => i.name !== item.name)\n index.items.push(item)\n } else {\n index = { schemaVersion: 2, name: opts.repo ?? name, items: [item] }\n }\n await writeFileSafe(registryPath, `${JSON.stringify(index, null, 2)}\\n`)\n\n console.log(`Scaffolded template \"${name}\" (${files.length} files + cascivo-registry.json).`)\n console.log('Next: fill in the page, capture screenshots, then `cascivo registry build`.')\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"template-init-C46fRXo-.mjs","names":[],"sources":["../src/commands/template-init.ts"],"sourcesContent":["import { existsSync } from 'node:fs'\nimport { readFile } from 'node:fs/promises'\nimport { join, resolve } from 'node:path'\nimport type { RegistryItem, TemplateMeta } from '@cascivo/registry'\nimport { writeFileSafe } from '../utils/fs.js'\n\nexport interface TemplateInitOptions {\n /** kebab-case template name (also the source folder + registry item name). */\n name: string\n /** Discovery category, e.g. \"dashboard\". */\n category: string\n /** Target framework for the page source. */\n framework: TemplateMeta['framework']\n /** Components the template composes (registry names). */\n components: string[]\n /** Repo owner/name used to build raw file URLs and the homepage. */\n repo?: string\n /** License SPDX id. */\n license: string\n /** Author. */\n author?: string\n}\n\nexport interface ScaffoldFile {\n /** Path relative to the template repo root. */\n path: string\n contents: string\n}\n\nfunction pascal(name: string): string {\n return name\n .split(/[-_/]/)\n .filter(Boolean)\n .map((s) => s.charAt(0).toUpperCase() + s.slice(1))\n .join('')\n}\n\nfunction rawUrl(repo: string | undefined, path: string): string {\n if (!repo) return path\n return `https://raw.githubusercontent.com/${repo}/main/${path}`\n}\n\nfunction pageSource(name: string, components: string[]): string {\n const comp = pascal(name)\n const importLine =\n components.length > 0\n ? `// Components installed via the template's registryDependencies:\\n// ${components.join(', ')}\\n`\n : ''\n return `${importLine}import './${name}.module.css'\n\n/**\n * ${comp} template page. Owned, copy-paste source — adapt freely.\n * Presentational only: no useState/useEffect (cascade house rules).\n */\nexport function ${comp}Page() {\n return (\n <main className=\"${name}-page\">\n <h1>${comp}</h1>\n {/* Compose the template's components here. */}\n </main>\n )\n}\n`\n}\n\nfunction cssSource(name: string): string {\n return `@layer components {\n .${name}-page {\n display: grid;\n gap: var(--cascivo-space-4, 1rem);\n padding: var(--cascivo-space-4, 1rem);\n }\n}\n`\n}\n\nfunction fixturesSource(name: string): string {\n return `/** Mock data for the ${name} template. Replace with your real source. */\nexport const ${name.replace(/-/g, '_')}Fixtures = {\n items: [],\n}\n`\n}\n\nfunction templateMetaSource(meta: TemplateMeta): string {\n return `import type { TemplateMeta } from '@cascivo/registry'\n\nexport const meta: TemplateMeta = ${JSON.stringify(meta, null, 2)}\n`\n}\n\n/**\n * Build the files + registry item for a new template. Pure — no I/O — so it can\n * be unit-tested. The caller writes the files and merges the item into\n * `cascade-registry.json`.\n */\nexport function buildTemplateScaffold(opts: TemplateInitOptions): {\n files: ScaffoldFile[]\n item: RegistryItem\n} {\n const { name, category, framework, components, repo, license, author } = opts\n const dir = `src/${name}`\n const pageTarget = `src/pages/${name}.tsx`\n const cssTarget = `src/pages/${name}.module.css`\n const fixturesTarget = `src/fixtures/${name}.ts`\n\n const files: ScaffoldFile[] = [\n { path: `${dir}/${name}.tsx`, contents: pageSource(name, components) },\n { path: `${dir}/${name}.module.css`, contents: cssSource(name) },\n { path: `${dir}/fixtures.ts`, contents: fixturesSource(name) },\n ]\n\n const meta: TemplateMeta = {\n intent: `${pascal(name)} template`,\n framework,\n category,\n screenshots: [\n { light: rawUrl(repo, `screenshots/${name}-light.png`), alt: `${name} preview (light)` },\n ],\n fileRoles: {\n [pageTarget]: 'page',\n [cssTarget]: 'asset',\n [fixturesTarget]: 'fixture',\n },\n pages: [{ name: pascal(name), target: pageTarget }],\n }\n\n const item: RegistryItem = {\n schemaVersion: 2,\n name,\n type: 'template',\n description: `${pascal(name)} template`,\n category,\n version: '0.1.0',\n license,\n ...(author ? { author } : {}),\n ...(repo ? { homepage: `https://github.com/${repo}` } : {}),\n files: [\n { url: rawUrl(repo, `${dir}/${name}.tsx`), target: pageTarget },\n { url: rawUrl(repo, `${dir}/${name}.module.css`), target: cssTarget },\n { url: rawUrl(repo, `${dir}/fixtures.ts`), target: fixturesTarget },\n ],\n dependencies: [],\n registryDependencies: components,\n tags: [category, 'template'],\n meta,\n }\n\n files.push({ path: `${dir}/${name}.template.ts`, contents: templateMetaSource(meta) })\n\n return { files, item }\n}\n\nfunction flag(args: string[], key: string): string | undefined {\n const eq = args.find((a) => a.startsWith(`--${key}=`))\n if (eq) return eq.slice(key.length + 3)\n const idx = args.indexOf(`--${key}`)\n const next = idx !== -1 ? args[idx + 1] : undefined\n return next && !next.startsWith('--') ? next : undefined\n}\n\nexport async function templateInit(args: string[], cwd: string = process.cwd()): Promise<void> {\n const name = args.find((a) => !a.startsWith('-'))\n if (!name) {\n console.error(\n 'Usage: cascivo template init <name> [--category dashboard] [--framework react-vite] [--components card,line-chart] [--repo owner/repo]',\n )\n process.exitCode = 1\n return\n }\n\n const componentsArg = flag(args, 'components')\n const opts: TemplateInitOptions = {\n name,\n category: flag(args, 'category') ?? 'misc',\n framework: (flag(args, 'framework') as TemplateMeta['framework']) ?? 'react-vite',\n components: componentsArg\n ? componentsArg\n .split(',')\n .map((c) => c.trim())\n .filter(Boolean)\n : [],\n license: flag(args, 'license') ?? 'MIT',\n ...(flag(args, 'repo') ? { repo: flag(args, 'repo')! } : {}),\n ...(flag(args, 'author') ? { author: flag(args, 'author')! } : {}),\n }\n\n const { files, item } = buildTemplateScaffold(opts)\n\n for (const file of files) {\n await writeFileSafe(join(cwd, file.path), file.contents)\n }\n\n // Merge into cascivo-registry.json (create if missing).\n const registryPath = resolve(cwd, 'cascivo-registry.json')\n let index: { schemaVersion: 2; name: string; items: RegistryItem[] }\n if (existsSync(registryPath)) {\n index = JSON.parse(await readFile(registryPath, 'utf8')) as typeof index\n index.items = (index.items ?? []).filter((i) => i.name !== item.name)\n index.items.push(item)\n } else {\n index = { schemaVersion: 2, name: opts.repo ?? name, items: [item] }\n }\n await writeFileSafe(registryPath, `${JSON.stringify(index, null, 2)}\\n`)\n\n console.log(`Scaffolded template \"${name}\" (${files.length} files + cascivo-registry.json).`)\n console.log('Next: fill in the page, capture screenshots, then `cascivo registry build`.')\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cascivo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "CLI — npx cascivo init / add / list / update",
|
|
6
6
|
"keywords": [
|
|
@@ -42,12 +42,13 @@
|
|
|
42
42
|
"provenance": true
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@cascivo/registry": "^0.1.
|
|
45
|
+
"@cascivo/registry": "^0.1.9"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/node": "^25",
|
|
49
49
|
"typescript": "^5",
|
|
50
|
-
"vite-plus": "^0.2.1"
|
|
50
|
+
"vite-plus": "^0.2.1",
|
|
51
|
+
"@cascivo/theme-kit": "0.0.0"
|
|
51
52
|
},
|
|
52
53
|
"scripts": {
|
|
53
54
|
"build": "vp pack && node -e \"const fs=require('fs');const f='dist/index.mjs';const c=fs.readFileSync(f,'utf8');if(!c.startsWith('#!/'))fs.writeFileSync(f,'#!/usr/bin/env node\\n'+c)\" && chmod +x dist/index.mjs",
|
package/readme.body.md
CHANGED
|
@@ -13,9 +13,18 @@ cascivo view owner/repo/dashboard # preview an item (or template) before instal
|
|
|
13
13
|
cascivo list # list available components
|
|
14
14
|
cascivo update # pull newer versions of copied components
|
|
15
15
|
cascivo audit --ai # flag hard-coded values, invented props, missing wiring
|
|
16
|
-
cascivo
|
|
16
|
+
cascivo search <query> # search components across registries
|
|
17
|
+
cascivo theme add <name> # install a first-party theme (12 available)
|
|
18
|
+
cascivo eject <component> # eject tokens into a scoped local override file
|
|
19
|
+
cascivo generate <config.json> # generate TSX from a ViewConfig JSON file
|
|
20
|
+
cascivo doctor [--ci] [--drift] # check components for rule violations / registry drift
|
|
21
|
+
cascivo tokens import <file> # import external design tokens as overrides
|
|
22
|
+
cascivo registry build # build a registry from your own components
|
|
23
|
+
cascivo template init <name> # scaffold a new template
|
|
17
24
|
```
|
|
18
25
|
|
|
26
|
+
Every command supports `--help` for its flags and examples.
|
|
27
|
+
|
|
19
28
|
A **template** is a registry item (`type: "template"`) that bundles a working page with the components it composes (in `registryDependencies`) and its own page/fixture files (each with a `target`). `cascivo add` installs the component closure into your components directory and writes the template's files to their targets; `create --template` does the same into a freshly scaffolded app.
|
|
20
29
|
|
|
21
30
|
## How it works
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"config-CsnCR5eh.mjs","names":[],"sources":["../src/utils/config.ts"],"sourcesContent":["import { existsSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { pathToFileURL } from 'node:url'\n\nexport type ThemeName = 'light' | 'dark' | 'warm'\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: 'https://cascivo.com/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/** Detect the package manager in use from lock files, defaulting to npm. */\nexport function detectPackageManager(cwd: string = process.cwd()): PackageManager {\n if (existsSync(join(cwd, 'pnpm-lock.yaml'))) return 'pnpm'\n if (existsSync(join(cwd, 'yarn.lock'))) return 'yarn'\n if (existsSync(join(cwd, 'bun.lockb'))) return 'bun'\n return 'npm'\n}\n\n/** The install subcommand each package manager uses to add dependencies. */\nexport function installCommand(pm: PackageManager, packages: string[]): [string, string[]] {\n const verb = pm === 'npm' ? 'install' : 'add'\n return [pm, [verb, ...packages]]\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAuBA,MAAa,iBAAgC;CAI3C,UAAU;CACV,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,qBAAqB,MAAc,QAAQ,IAAI,GAAmB;CAChF,IAAI,WAAW,KAAK,KAAK,gBAAgB,CAAC,GAAG,OAAO;CACpD,IAAI,WAAW,KAAK,KAAK,WAAW,CAAC,GAAG,OAAO;CAC/C,IAAI,WAAW,KAAK,KAAK,WAAW,CAAC,GAAG,OAAO;CAC/C,OAAO;AACT;;AAGA,SAAgB,eAAe,IAAoB,UAAwC;CAEzF,OAAO,CAAC,IAAI,CADC,OAAO,QAAQ,YAAY,OACrB,GAAG,QAAQ,CAAC;AACjC"}
|
package/dist/drift-D7JFzpP_.mjs
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"drift-D7JFzpP_.mjs","names":[],"sources":["../src/commands/drift.ts"],"sourcesContent":["export async function runDoctorDrift(_args: string[]): Promise<void> {\n console.log('cascade doctor --drift: not yet implemented')\n}\n"],"mappings":";AAAA,eAAsB,eAAe,OAAgC;CACnE,QAAQ,IAAI,6CAA6C;AAC3D"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"http-CLkdTpxD.mjs","names":[],"sources":["../src/utils/http.ts"],"sourcesContent":["import { createHash } from 'node:crypto'\nimport { existsSync } from 'node:fs'\nimport { mkdir, readFile, writeFile } from 'node:fs/promises'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\n\nconst CACHE_DIR = join(homedir(), '.cascade', 'cache')\nconst MAX_SIZE = 10 * 1024 * 1024 // 10 MB\nconst TIMEOUT_MS = 15_000\nconst MAX_RETRIES = 4\n\nexport type FetchFn = (url: string, opts?: RequestInit) => Promise<Response>\n\nlet _fetchFn: FetchFn = globalThis.fetch as FetchFn\n\nexport function setFetch(fn: FetchFn): void {\n _fetchFn = fn\n}\n\nexport function resetFetch(): void {\n _fetchFn = globalThis.fetch as FetchFn\n}\n\nfunction cacheKey(url: string): string {\n return createHash('sha256').update(url).digest('hex')\n}\n\nasync function getCached(url: string): Promise<unknown | null> {\n try {\n const file = join(CACHE_DIR, cacheKey(url))\n const data = await readFile(file, 'utf8')\n return JSON.parse(data) as unknown\n } catch {\n return null\n }\n}\n\nasync function setCached(url: string, data: unknown): Promise<void> {\n try {\n await mkdir(CACHE_DIR, { recursive: true })\n await writeFile(join(CACHE_DIR, cacheKey(url)), JSON.stringify(data))\n } catch {\n // cache write failure is non-fatal\n }\n}\n\nexport async function fetchJson(\n url: string,\n opts: { headers?: Record<string, string>; params?: Record<string, string>; cache?: boolean } = {},\n): Promise<unknown> {\n const fullUrl = opts.params ? `${url}?${new URLSearchParams(opts.params).toString()}` : url\n\n if (opts.cache !== false) {\n const cached = await getCached(fullUrl)\n if (cached !== null) return cached\n }\n\n let lastError: unknown\n for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {\n if (attempt > 0) {\n await new Promise((r) => setTimeout(r, 1000 * 2 ** (attempt - 1)))\n }\n try {\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), TIMEOUT_MS)\n let res: Response\n try {\n res = await _fetchFn(fullUrl, {\n headers: { Accept: 'application/json', ...opts.headers },\n signal: controller.signal,\n })\n } finally {\n clearTimeout(timer)\n }\n\n if (res.status === 401 || res.status === 403 || res.status === 429) {\n let msg = `HTTP ${res.status}`\n try {\n const body = (await res.json()) as Record<string, unknown>\n if (typeof body['message'] === 'string') msg += `: ${body['message']}`\n } catch {\n // ignore parse failure\n }\n throw new Error(msg)\n }\n\n if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${fullUrl}`)\n\n const len = Number(res.headers.get('content-length') ?? 0)\n if (len > MAX_SIZE) throw new Error(`Response too large (${len} bytes) from ${fullUrl}`)\n\n const text = await res.text()\n if (text.length > MAX_SIZE) throw new Error(`Response too large from ${fullUrl}`)\n\n const json = JSON.parse(text) as unknown\n\n if (opts.cache !== false) await setCached(fullUrl, json)\n return json\n } catch (e) {\n lastError = e\n // Don't retry auth/logic errors\n if (e instanceof Error && /HTTP 4\\d\\d/.test(e.message)) break\n }\n }\n\n const msg = lastError instanceof Error ? lastError.message : String(lastError)\n throw new Error(\n `Network error fetching ${fullUrl}: ${msg}\\n(Check your internet connection or registry URL)`,\n )\n}\n\nexport { existsSync }\n"],"mappings":";;;;;AAMA,MAAM,YAAY,KAAK,QAAQ,GAAG,YAAY,OAAO;AACrD,MAAM,WAAW,KAAK,OAAO;AAC7B,MAAM,aAAa;AACnB,MAAM,cAAc;AAIpB,IAAI,WAAoB,WAAW;AAUnC,SAAS,SAAS,KAAqB;CACrC,OAAO,WAAW,QAAQ,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,KAAK;AACtD;AAEA,eAAe,UAAU,KAAsC;CAC7D,IAAI;EAEF,MAAM,OAAO,MAAM,SADN,KAAK,WAAW,SAAS,GAAG,CACV,GAAG,MAAM;EACxC,OAAO,KAAK,MAAM,IAAI;CACxB,QAAQ;EACN,OAAO;CACT;AACF;AAEA,eAAe,UAAU,KAAa,MAA8B;CAClE,IAAI;EACF,MAAM,MAAM,WAAW,EAAE,WAAW,KAAK,CAAC;EAC1C,MAAM,UAAU,KAAK,WAAW,SAAS,GAAG,CAAC,GAAG,KAAK,UAAU,IAAI,CAAC;CACtE,QAAQ,CAER;AACF;AAEA,eAAsB,UACpB,KACA,OAA+F,CAAC,GAC9E;CAClB,MAAM,UAAU,KAAK,SAAS,GAAG,IAAI,GAAG,IAAI,gBAAgB,KAAK,MAAM,CAAC,CAAC,SAAS,MAAM;CAExF,IAAI,KAAK,UAAU,OAAO;EACxB,MAAM,SAAS,MAAM,UAAU,OAAO;EACtC,IAAI,WAAW,MAAM,OAAO;CAC9B;CAEA,IAAI;CACJ,KAAK,IAAI,UAAU,GAAG,UAAU,aAAa,WAAW;EACtD,IAAI,UAAU,GACZ,MAAM,IAAI,SAAS,MAAM,WAAW,GAAG,MAAO,MAAM,UAAU,EAAE,CAAC;EAEnE,IAAI;GACF,MAAM,aAAa,IAAI,gBAAgB;GACvC,MAAM,QAAQ,iBAAiB,WAAW,MAAM,GAAG,UAAU;GAC7D,IAAI;GACJ,IAAI;IACF,MAAM,MAAM,SAAS,SAAS;KAC5B,SAAS;MAAE,QAAQ;MAAoB,GAAG,KAAK;KAAQ;KACvD,QAAQ,WAAW;IACrB,CAAC;GACH,UAAU;IACR,aAAa,KAAK;GACpB;GAEA,IAAI,IAAI,WAAW,OAAO,IAAI,WAAW,OAAO,IAAI,WAAW,KAAK;IAClE,IAAI,MAAM,QAAQ,IAAI;IACtB,IAAI;KACF,MAAM,OAAQ,MAAM,IAAI,KAAK;KAC7B,IAAI,OAAO,KAAK,eAAe,UAAU,OAAO,KAAK,KAAK;IAC5D,QAAQ,CAER;IACA,MAAM,IAAI,MAAM,GAAG;GACrB;GAEA,IAAI,CAAC,IAAI,IAAI,MAAM,IAAI,MAAM,QAAQ,IAAI,OAAO,YAAY,SAAS;GAErE,MAAM,MAAM,OAAO,IAAI,QAAQ,IAAI,gBAAgB,KAAK,CAAC;GACzD,IAAI,MAAM,UAAU,MAAM,IAAI,MAAM,uBAAuB,IAAI,eAAe,SAAS;GAEvF,MAAM,OAAO,MAAM,IAAI,KAAK;GAC5B,IAAI,KAAK,SAAS,UAAU,MAAM,IAAI,MAAM,2BAA2B,SAAS;GAEhF,MAAM,OAAO,KAAK,MAAM,IAAI;GAE5B,IAAI,KAAK,UAAU,OAAO,MAAM,UAAU,SAAS,IAAI;GACvD,OAAO;EACT,SAAS,GAAG;GACV,YAAY;GAEZ,IAAI,aAAa,SAAS,aAAa,KAAK,EAAE,OAAO,GAAG;EAC1D;CACF;CAEA,MAAM,MAAM,qBAAqB,QAAQ,UAAU,UAAU,OAAO,SAAS;CAC7E,MAAM,IAAI,MACR,0BAA0B,QAAQ,IAAI,IAAI,mDAC5C;AACF"}
|