cascivo 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,1438 @@
1
+ #!/usr/bin/env node
2
+ import { argv, stdin, stdout } from "node:process";
3
+ import { pathToFileURL } from "node:url";
4
+ import { spawnSync } from "node:child_process";
5
+ import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
6
+ import { basename, dirname, join, resolve } from "node:path";
7
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
8
+ import { createHash } from "node:crypto";
9
+ import { homedir } from "node:os";
10
+ import { createInterface } from "node:readline/promises";
11
+ import { buildRegistry, parseLegacyRegistry, validateIndex } from "@cascivo/registry";
12
+ //#region src/utils/config.ts
13
+ const DEFAULT_CONFIG = {
14
+ registry: "https://raw.githubusercontent.com/urbanisierung/cascivo/main/registry.json",
15
+ outputDir: "src/components/ui",
16
+ theme: "light"
17
+ };
18
+ const CONFIG_FILES = [
19
+ "cascivo.config.ts",
20
+ "cascivo.config.js",
21
+ "cascivo.config.mjs"
22
+ ];
23
+ /** Apply defaults over a (possibly partial) user config object. */
24
+ function resolveConfig(partial) {
25
+ return {
26
+ ...DEFAULT_CONFIG,
27
+ ...partial
28
+ };
29
+ }
30
+ /** Let env vars override config — used by the MCP server to pass `outputDir`. */
31
+ function applyEnvOverrides(config, env = process.env) {
32
+ return {
33
+ ...config,
34
+ ...env.CASCIVO_REGISTRY ? { registry: env.CASCIVO_REGISTRY } : {},
35
+ ...env.CASCIVO_OUTPUT_DIR ? { outputDir: env.CASCIVO_OUTPUT_DIR } : {}
36
+ };
37
+ }
38
+ /**
39
+ * Locate and load `cascivo.config.{ts,js,mjs}` from `cwd`. Falls back to the
40
+ * default config when no file is found or the file cannot be loaded. Env vars
41
+ * (`CASCIVO_REGISTRY`, `CASCIVO_OUTPUT_DIR`) take precedence.
42
+ */
43
+ async function loadConfig(cwd = process.cwd()) {
44
+ for (const file of CONFIG_FILES) {
45
+ const path = join(cwd, file);
46
+ if (!existsSync(path)) continue;
47
+ try {
48
+ const mod = await import(pathToFileURL(path).href);
49
+ return applyEnvOverrides(resolveConfig(mod.default ?? mod));
50
+ } catch {
51
+ return applyEnvOverrides(resolveConfig(null));
52
+ }
53
+ }
54
+ return applyEnvOverrides(resolveConfig(null));
55
+ }
56
+ /** Detect the package manager in use from lock files, defaulting to npm. */
57
+ function detectPackageManager(cwd = process.cwd()) {
58
+ if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
59
+ if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
60
+ if (existsSync(join(cwd, "bun.lockb"))) return "bun";
61
+ return "npm";
62
+ }
63
+ /** The install subcommand each package manager uses to add dependencies. */
64
+ function installCommand(pm, packages) {
65
+ return [pm, [pm === "npm" ? "install" : "add", ...packages]];
66
+ }
67
+ //#endregion
68
+ //#region src/utils/exec.ts
69
+ /**
70
+ * Install npm packages using the detected package manager, inheriting stdio so
71
+ * the user sees install progress. Returns false if the install failed.
72
+ */
73
+ function installPackages(packages, cwd = process.cwd()) {
74
+ if (packages.length === 0) return true;
75
+ const [cmd, args] = installCommand(detectPackageManager(cwd), packages);
76
+ console.log(`Installing ${packages.join(", ")} with ${cmd}…`);
77
+ if (spawnSync(cmd, args, {
78
+ cwd,
79
+ stdio: "inherit",
80
+ shell: process.platform === "win32"
81
+ }).status !== 0) {
82
+ console.error(`Failed to install: ${packages.join(", ")}`);
83
+ return false;
84
+ }
85
+ return true;
86
+ }
87
+ //#endregion
88
+ //#region src/utils/fs.ts
89
+ /** Resolve where a component file should be written. */
90
+ function resolveOutputPath(outputDir, component, file, cwd = process.cwd()) {
91
+ return join(resolve(cwd, outputDir), component, file);
92
+ }
93
+ /** Write a file, creating parent directories as needed. */
94
+ async function writeFileSafe(path, content) {
95
+ await mkdir(dirname(path), { recursive: true });
96
+ await writeFile(path, content, "utf8");
97
+ }
98
+ /** Read a file, returning null if it does not exist. */
99
+ async function readFileSafe(path) {
100
+ try {
101
+ return await readFile(path, "utf8");
102
+ } catch {
103
+ return null;
104
+ }
105
+ }
106
+ //#endregion
107
+ //#region src/utils/registry.ts
108
+ function asStringArray(value) {
109
+ return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
110
+ }
111
+ /** Validate and normalize raw JSON into a typed Registry. Throws on bad shape. */
112
+ function parseRegistry(raw) {
113
+ if (typeof raw !== "object" || raw === null) throw new Error("Invalid registry: expected an object");
114
+ const obj = raw;
115
+ if (!Array.isArray(obj.components)) throw new Error("Invalid registry: \"components\" must be an array");
116
+ const components = obj.components.map((entry, i) => {
117
+ if (typeof entry !== "object" || entry === null) throw new Error(`Invalid registry: component at index ${i} is not an object`);
118
+ const c = entry;
119
+ if (typeof c.name !== "string") throw new Error(`Invalid registry: component at index ${i} is missing "name"`);
120
+ const rawType = c.type;
121
+ const type = rawType === "component" || rawType === "layout" || rawType === "block" || rawType === "chart" ? rawType : void 0;
122
+ const rawMeta = c.meta;
123
+ const metaName = typeof rawMeta === "object" && rawMeta !== null && typeof rawMeta.name === "string" ? rawMeta.name : c.name;
124
+ const result = {
125
+ name: c.name,
126
+ description: typeof c.description === "string" ? c.description : "",
127
+ category: typeof c.category === "string" ? c.category : "",
128
+ version: typeof c.version === "string" ? c.version : "0.0.0",
129
+ files: asStringArray(c.files),
130
+ dependencies: asStringArray(c.dependencies),
131
+ tags: asStringArray(c.tags),
132
+ meta: { name: metaName }
133
+ };
134
+ if (type !== void 0) result.type = type;
135
+ if (typeof c.install === "string") result.install = c.install;
136
+ return result;
137
+ });
138
+ const blocks = Array.isArray(obj.blocks) ? obj.blocks.flatMap((entry) => {
139
+ if (typeof entry !== "object" || entry === null) return [];
140
+ const b = entry;
141
+ if (typeof b.name !== "string") return [];
142
+ const rawScreenshot = typeof b.screenshot === "object" && b.screenshot !== null ? b.screenshot : {};
143
+ return [{
144
+ name: b.name,
145
+ type: "block",
146
+ displayName: typeof b.displayName === "string" ? b.displayName : b.name,
147
+ description: typeof b.description === "string" ? b.description : "",
148
+ category: typeof b.category === "string" ? b.category : "",
149
+ version: typeof b.version === "string" ? b.version : "0.0.0",
150
+ files: asStringArray(b.files),
151
+ dependencies: asStringArray(b.dependencies),
152
+ tags: asStringArray(b.tags),
153
+ screenshot: {
154
+ light: typeof rawScreenshot.light === "string" ? rawScreenshot.light : "",
155
+ dark: typeof rawScreenshot.dark === "string" ? rawScreenshot.dark : ""
156
+ }
157
+ }];
158
+ }) : [];
159
+ return {
160
+ version: typeof obj.version === "string" ? obj.version : "0.0.0",
161
+ generatedAt: typeof obj.generatedAt === "string" ? obj.generatedAt : "",
162
+ components,
163
+ blocks
164
+ };
165
+ }
166
+ /** Fetch and parse the registry from a URL. */
167
+ async function fetchRegistry(url) {
168
+ const res = await fetch(url);
169
+ if (!res.ok) throw new Error(`Failed to fetch registry from ${url}: ${res.status} ${res.statusText}`);
170
+ return parseRegistry(await res.json());
171
+ }
172
+ /**
173
+ * Find a component by name (case-insensitive, full name or unambiguous suffix).
174
+ *
175
+ * Examples:
176
+ * "layout/app-shell" → exact match on full name
177
+ * "app-shell" → suffix match if exactly one entry ends with "/app-shell"
178
+ * "button" → exact match (no slash, no suffix ambiguity)
179
+ */
180
+ function findComponent(registry, name) {
181
+ const target = name.toLowerCase();
182
+ if (target.startsWith("block/")) {
183
+ const blockName = target.slice(6);
184
+ const block = (registry.blocks ?? []).find((b) => b.name.toLowerCase() === blockName);
185
+ if (!block) throw new Error(`Block "${blockName}" not found in registry.`);
186
+ return block;
187
+ }
188
+ const exact = registry.components.find((c) => c.name.toLowerCase() === target);
189
+ if (exact) return exact;
190
+ const suffix = `/${target}`;
191
+ const matches = registry.components.filter((c) => c.name.toLowerCase().endsWith(suffix));
192
+ if (matches.length === 1) return matches[0];
193
+ }
194
+ /** Extract the file name from a registry file URL. */
195
+ function fileName(url) {
196
+ const stripped = url.split(/[?#]/)[0] ?? url;
197
+ return stripped.split("/").pop() || stripped;
198
+ }
199
+ //#endregion
200
+ //#region src/utils/lock.ts
201
+ function sha256(content) {
202
+ return `sha256-${createHash("sha256").update(content).digest("hex")}`;
203
+ }
204
+ const LOCK_FILENAME = "cascade.lock";
205
+ async function readLock(cwd = process.cwd()) {
206
+ const path = join(cwd, LOCK_FILENAME);
207
+ if (!existsSync(path)) return null;
208
+ try {
209
+ const raw = JSON.parse(await readFile(path, "utf8"));
210
+ if (typeof raw !== "object" || raw === null) return null;
211
+ return raw;
212
+ } catch {
213
+ return null;
214
+ }
215
+ }
216
+ async function writeLock(lock, cwd = process.cwd()) {
217
+ await writeFile(join(cwd, LOCK_FILENAME), `${JSON.stringify(lock, null, 2)}\n`, "utf8");
218
+ }
219
+ function createLock() {
220
+ return {
221
+ lockVersion: 1,
222
+ items: {}
223
+ };
224
+ }
225
+ function updateLockEntry(lock, name, entry) {
226
+ return {
227
+ ...lock,
228
+ items: {
229
+ ...lock.items,
230
+ [name]: entry
231
+ }
232
+ };
233
+ }
234
+ //#endregion
235
+ //#region src/utils/http.ts
236
+ const CACHE_DIR = join(homedir(), ".cascade", "cache");
237
+ const MAX_SIZE = 10 * 1024 * 1024;
238
+ const TIMEOUT_MS = 15e3;
239
+ const MAX_RETRIES = 4;
240
+ let _fetchFn = globalThis.fetch;
241
+ function cacheKey(url) {
242
+ return createHash("sha256").update(url).digest("hex");
243
+ }
244
+ async function getCached(url) {
245
+ try {
246
+ const data = await readFile(join(CACHE_DIR, cacheKey(url)), "utf8");
247
+ return JSON.parse(data);
248
+ } catch {
249
+ return null;
250
+ }
251
+ }
252
+ async function setCached(url, data) {
253
+ try {
254
+ await mkdir(CACHE_DIR, { recursive: true });
255
+ await writeFile(join(CACHE_DIR, cacheKey(url)), JSON.stringify(data));
256
+ } catch {}
257
+ }
258
+ async function fetchJson(url, opts = {}) {
259
+ const fullUrl = opts.params ? `${url}?${new URLSearchParams(opts.params).toString()}` : url;
260
+ if (opts.cache !== false) {
261
+ const cached = await getCached(fullUrl);
262
+ if (cached !== null) return cached;
263
+ }
264
+ let lastError;
265
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
266
+ if (attempt > 0) await new Promise((r) => setTimeout(r, 1e3 * 2 ** (attempt - 1)));
267
+ try {
268
+ const controller = new AbortController();
269
+ const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
270
+ let res;
271
+ try {
272
+ res = await _fetchFn(fullUrl, {
273
+ headers: {
274
+ Accept: "application/json",
275
+ ...opts.headers
276
+ },
277
+ signal: controller.signal
278
+ });
279
+ } finally {
280
+ clearTimeout(timer);
281
+ }
282
+ if (res.status === 401 || res.status === 403 || res.status === 429) {
283
+ let msg = `HTTP ${res.status}`;
284
+ try {
285
+ const body = await res.json();
286
+ if (typeof body["message"] === "string") msg += `: ${body["message"]}`;
287
+ } catch {}
288
+ throw new Error(msg);
289
+ }
290
+ if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${fullUrl}`);
291
+ const len = Number(res.headers.get("content-length") ?? 0);
292
+ if (len > MAX_SIZE) throw new Error(`Response too large (${len} bytes) from ${fullUrl}`);
293
+ const text = await res.text();
294
+ if (text.length > MAX_SIZE) throw new Error(`Response too large from ${fullUrl}`);
295
+ const json = JSON.parse(text);
296
+ if (opts.cache !== false) await setCached(fullUrl, json);
297
+ return json;
298
+ } catch (e) {
299
+ lastError = e;
300
+ if (e instanceof Error && /HTTP 4\d\d/.test(e.message)) break;
301
+ }
302
+ }
303
+ const msg = lastError instanceof Error ? lastError.message : String(lastError);
304
+ throw new Error(`Network error fetching ${fullUrl}: ${msg}\n(Check your internet connection or registry URL)`);
305
+ }
306
+ //#endregion
307
+ //#region src/utils/resolve.ts
308
+ const GITHUB_RE = /^([^/]+)\/([^/]+)\/([^#]+)(?:#(.+))?$/;
309
+ function expandEnv(str, env = process.env) {
310
+ return str.replace(/\$\{([^}]+)\}/g, (_, key) => {
311
+ const val = env[key];
312
+ if (val === void 0) throw new Error(`Environment variable ${key} is not set`);
313
+ return val;
314
+ });
315
+ }
316
+ function parseAddress(spec, env = process.env) {
317
+ if (spec.startsWith("http://") || spec.startsWith("https://") || spec.startsWith("./") || spec.startsWith("/")) return {
318
+ kind: "url",
319
+ name: spec,
320
+ raw: spec
321
+ };
322
+ if (spec.startsWith("@")) {
323
+ const slash = spec.indexOf("/", 1);
324
+ if (slash === -1) throw new Error(`Invalid namespace address: ${spec} (expected @ns/name)`);
325
+ return {
326
+ kind: "namespace",
327
+ namespace: spec.slice(0, slash),
328
+ name: spec.slice(slash + 1),
329
+ raw: spec
330
+ };
331
+ }
332
+ const specWithoutRef = spec.includes("#") ? spec.slice(0, spec.indexOf("#")) : spec;
333
+ const githubMatch = GITHUB_RE.exec(spec);
334
+ if (githubMatch && !specWithoutRef.includes(".") && specWithoutRef.split("/").length >= 3) {
335
+ const [, owner, repo, item, ref] = githubMatch;
336
+ return {
337
+ kind: "github",
338
+ name: `${owner}/${repo}/${item}`,
339
+ ref: ref ?? "main",
340
+ raw: spec
341
+ };
342
+ }
343
+ return {
344
+ kind: "bare",
345
+ name: spec,
346
+ raw: spec
347
+ };
348
+ }
349
+ function resolveNamespaceUrl(ns, name, nsCfg, env) {
350
+ const template = typeof nsCfg === "string" ? nsCfg : nsCfg.url;
351
+ if (!template.includes("{name}")) throw new Error(`Registry namespace "${ns}" URL template must contain {name}: ${template}`);
352
+ return expandEnv(template.replace("{name}", name), env);
353
+ }
354
+ async function resolveItemUrl(addr, config, env = process.env, resolveNamespaceHook) {
355
+ switch (addr.kind) {
356
+ case "url": return { url: addr.name };
357
+ case "bare": return { url: `${config.registry.replace(/\/registry\.json$/, "").replace(/\/r$/, "")}/r/${addr.name}.json` };
358
+ case "github": {
359
+ const parts = addr.name.split("/");
360
+ const owner = parts[0];
361
+ const repo = parts[1];
362
+ const item = parts.slice(2).join("/");
363
+ return { url: `https://raw.githubusercontent.com/${owner}/${repo}/${addr.ref ?? "main"}/r/${item}.json` };
364
+ }
365
+ case "namespace": {
366
+ const ns = addr.namespace;
367
+ const name = addr.name;
368
+ if (config.registries?.[ns]) {
369
+ const nsCfg = config.registries[ns];
370
+ const url = resolveNamespaceUrl(ns, name, nsCfg, env);
371
+ const headers = typeof nsCfg !== "string" ? nsCfg.headers : void 0;
372
+ const params = typeof nsCfg !== "string" ? nsCfg.params : void 0;
373
+ const expandedHeaders = headers ? Object.fromEntries(Object.entries(headers).map(([k, v]) => [k, expandEnv(v, env)])) : void 0;
374
+ const expandedParams = params ? Object.fromEntries(Object.entries(params).map(([k, v]) => [k, expandEnv(v, env)])) : void 0;
375
+ const result = { url };
376
+ if (expandedHeaders) result.headers = expandedHeaders;
377
+ if (expandedParams) result.params = expandedParams;
378
+ return result;
379
+ }
380
+ if (resolveNamespaceHook) {
381
+ const hookResult = await resolveNamespaceHook(ns);
382
+ if (hookResult) return { url: resolveNamespaceUrl(ns, name, hookResult, env) };
383
+ }
384
+ if (ns === "@cascade") return { url: `https://cascivo.com/r/${name}.json` };
385
+ throw new Error(`Unknown namespace "${ns}". Add it to registries in cascivo.config.ts or check the directory at cascivo.com`);
386
+ }
387
+ }
388
+ }
389
+ async function resolveClosure(specs, config, env = process.env, resolveNamespaceHook) {
390
+ const seen = /* @__PURE__ */ new Set();
391
+ const queue = [...specs];
392
+ const plan = [];
393
+ while (queue.length > 0) {
394
+ const spec = queue.shift();
395
+ const addr = parseAddress(spec, env);
396
+ const identity = addr.raw;
397
+ if (seen.has(identity)) continue;
398
+ seen.add(identity);
399
+ const { url, headers, params } = await resolveItemUrl(addr, config, env, resolveNamespaceHook);
400
+ const fetchOpts = {};
401
+ if (headers) fetchOpts.headers = headers;
402
+ if (params) fetchOpts.params = params;
403
+ const item = await fetchJson(url, fetchOpts);
404
+ const entry = {
405
+ spec,
406
+ addr,
407
+ itemUrl: url,
408
+ item,
409
+ registryBase: url.replace(/\/[^/]+\.json$/, "")
410
+ };
411
+ if (headers) entry.headers = headers;
412
+ if (params) entry.params = params;
413
+ plan.push(entry);
414
+ for (const dep of item.registryDependencies ?? []) queue.push(dep);
415
+ }
416
+ return { items: plan };
417
+ }
418
+ //#endregion
419
+ //#region src/utils/directory.ts
420
+ const DIRECTORY_URL = "https://cascivo.com/r/registries.json";
421
+ async function resolveFromDirectory(ns, fetchFn) {
422
+ try {
423
+ let data;
424
+ if (fetchFn) {
425
+ const res = await fetchFn(DIRECTORY_URL, { headers: { Accept: "application/json" } });
426
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
427
+ data = await res.json();
428
+ } else data = await fetchJson(DIRECTORY_URL);
429
+ const entry = data.registries?.find((r) => r.namespace === ns);
430
+ if (!entry) return null;
431
+ return entry.registryUrl;
432
+ } catch (e) {
433
+ const msg = e instanceof Error ? e.message : String(e);
434
+ console.warn(`Could not reach cascivo.com directory: ${msg}`);
435
+ return null;
436
+ }
437
+ }
438
+ //#endregion
439
+ //#region src/commands/add.ts
440
+ /** Dependencies always present in a cascade project — never auto-installed. */
441
+ const ASSUMED_DEPS = new Set(["react", "react-dom"]);
442
+ async function fetchText$1(url) {
443
+ const res = await fetch(url);
444
+ if (!res.ok) throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`);
445
+ return res.text();
446
+ }
447
+ function isMultiRegistrySpec(name) {
448
+ return name.startsWith("@") || name.startsWith("http://") || name.startsWith("https://") || name.startsWith("./") || name.startsWith("/") || name.split("/").length >= 3;
449
+ }
450
+ async function add(names, config, opts = {}) {
451
+ if (names.length === 0) {
452
+ console.error("Usage: cascivo add <component...>");
453
+ return;
454
+ }
455
+ const multiSpecs = names.filter(isMultiRegistrySpec);
456
+ const bareSpecs = names.filter((n) => !isMultiRegistrySpec(n));
457
+ let lock = await readLock() ?? createLock();
458
+ if (multiSpecs.length > 0) {
459
+ const plan = await resolveClosure(multiSpecs, config, process.env, resolveFromDirectory);
460
+ if (opts.dryRun) {
461
+ console.log("Dry run — would install:");
462
+ for (const item of plan.items) {
463
+ console.log(` ${item.spec} (${item.item.name}@${item.item.version}) from ${item.itemUrl}`);
464
+ for (const f of item.item.files) console.log(` ${f.url}`);
465
+ }
466
+ return;
467
+ }
468
+ const isThirdParty = (itemUrl) => !itemUrl.includes("cascivo.com");
469
+ for (const { item, itemUrl, registryBase } of plan.items) {
470
+ if (isThirdParty(itemUrl)) console.log(`\nReview before use — files from non-directory registry: ${registryBase}\n` + (item.files ?? []).map((f) => ` ${f.url}`).join("\n"));
471
+ await installItem(item, config, lock, registryBase);
472
+ lock = updateLockEntry(lock, item.name, {
473
+ registry: registryBase,
474
+ version: item.version,
475
+ installedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
476
+ files: {}
477
+ });
478
+ }
479
+ await writeLock(lock);
480
+ return;
481
+ }
482
+ const registry = await fetchRegistry(config.registry);
483
+ const missingDeps = /* @__PURE__ */ new Set();
484
+ for (const name of bareSpecs) {
485
+ const entry = findComponent(registry, name);
486
+ if (!entry) {
487
+ console.error(`Component "${name}" not found in registry. Run "cascivo list".`);
488
+ continue;
489
+ }
490
+ if (entry.type === "chart") {
491
+ const pkg = entry.install ?? "@cascivo/charts";
492
+ console.log(`Chart "${entry.name}" is distributed as an npm package.`);
493
+ console.log(`Install it with: npm install ${pkg}`);
494
+ console.log(`Then import: import { ${entry.meta.name} } from '${pkg}'`);
495
+ continue;
496
+ }
497
+ const outputName = entry.name.includes("/") ? entry.name.split("/").pop() : entry.name;
498
+ const fileHashes = {};
499
+ for (const url of entry.files) {
500
+ const content = await fetchText$1(url);
501
+ const dest = resolveOutputPath(config.outputDir, outputName, fileName(url));
502
+ await writeFileSafe(dest, content);
503
+ fileHashes[dest] = sha256(content);
504
+ }
505
+ for (const dep of entry.dependencies) if (!ASSUMED_DEPS.has(dep)) missingDeps.add(dep);
506
+ const registryBase = config.registry.replace(/\/registry\.json$/, "").replace(/\/r$/, "");
507
+ lock = updateLockEntry(lock, entry.name, {
508
+ registry: registryBase,
509
+ version: entry.version,
510
+ installedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
511
+ files: fileHashes
512
+ });
513
+ console.log(`Added ${entry.name} to ${config.outputDir}/${outputName}/`);
514
+ }
515
+ if (missingDeps.size > 0) installPackages([...missingDeps]);
516
+ await writeLock(lock);
517
+ }
518
+ async function installItem(item, config, _lock, _registryBase) {
519
+ const outputName = item.name.includes("/") ? item.name.split("/").pop() : item.name;
520
+ const missingDeps = /* @__PURE__ */ new Set();
521
+ for (const file of item.files ?? []) try {
522
+ const content = await fetchText$1(file.url);
523
+ await writeFileSafe(resolveOutputPath(config.outputDir, outputName, fileName(file.url)), content);
524
+ } catch {
525
+ console.warn(` Could not fetch ${file.url}`);
526
+ }
527
+ for (const dep of item.dependencies ?? []) if (!ASSUMED_DEPS.has(dep)) missingDeps.add(dep);
528
+ if (missingDeps.size > 0) installPackages([...missingDeps]);
529
+ console.log(`Added ${item.name} to ${config.outputDir}/${outputName}/`);
530
+ }
531
+ //#endregion
532
+ //#region src/commands/doctor.ts
533
+ const BANNED_HOOKS = [
534
+ "useState",
535
+ "useEffect",
536
+ "useLayoutEffect",
537
+ "useContext",
538
+ "useReducer"
539
+ ];
540
+ async function runDoctor(root) {
541
+ const violations = [];
542
+ const componentsDir = join(root, "packages", "components", "src");
543
+ if (!existsSync(componentsDir)) return {
544
+ violations: [],
545
+ passed: true
546
+ };
547
+ const components = readdirSync(componentsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
548
+ const reactIndex = join(root, "packages", "react", "src", "index.ts");
549
+ const indexContent = existsSync(reactIndex) ? readFileSync(reactIndex, "utf8") : "";
550
+ for (const name of components) {
551
+ const tsxPath = join(componentsDir, name, `${name}.tsx`);
552
+ if (!existsSync(tsxPath)) continue;
553
+ const content = readFileSync(tsxPath, "utf8");
554
+ for (const hook of BANNED_HOOKS) if (new RegExp(`\\b${hook}\\b`).test(content)) violations.push({
555
+ file: tsxPath,
556
+ rule: "no-react-hooks",
557
+ detail: `Banned hook '${hook}' in ${name}.tsx`
558
+ });
559
+ if (/aria-label="[A-Z]/.test(content)) violations.push({
560
+ file: tsxPath,
561
+ rule: "no-hardcoded-strings",
562
+ detail: `Hardcoded aria-label in ${name}.tsx — use t(builtin.*)`
563
+ });
564
+ if (!new RegExp(`from.*components/src/${name}`).test(indexContent)) violations.push({
565
+ file: tsxPath,
566
+ rule: "missing-react-export",
567
+ detail: `${name} not exported from @cascivo/react`
568
+ });
569
+ }
570
+ return {
571
+ violations,
572
+ passed: violations.length === 0
573
+ };
574
+ }
575
+ //#endregion
576
+ //#region src/commands/generate.ts
577
+ function isTranslationRef(v) {
578
+ return typeof v === "object" && v !== null && "$t" in v;
579
+ }
580
+ function serializeProp(value) {
581
+ if (typeof value === "string") return `"${value.replace(/"/g, "\\\"")}"`;
582
+ if (typeof value === "number" || typeof value === "boolean") return `{${value}}`;
583
+ if (value === null) return `{null}`;
584
+ if (isTranslationRef(value)) return `{"${value.$t}"} {/* i18n: ${value.$t} */}`;
585
+ return `{${JSON.stringify(value)}}`;
586
+ }
587
+ function renderChildren(children, indent, componentPropNames, knownComponents) {
588
+ if (children === void 0) return "";
589
+ if (typeof children === "string") return children;
590
+ if (isTranslationRef(children)) return `{/* i18n: ${children.$t} */}`;
591
+ if (Array.isArray(children)) return children.map((child) => renderNode(child, indent + " ", componentPropNames, knownComponents)).join("\n");
592
+ return "";
593
+ }
594
+ function renderNode(node, indent, _parentPropNames, knownComponents) {
595
+ const compName = node.component;
596
+ knownComponents.add(compName);
597
+ const propParts = [];
598
+ if (node.props) for (const [k, v] of Object.entries(node.props)) propParts.push(`${k}=${serializeProp(v)}`);
599
+ if (node.bind) for (const [k, ref] of Object.entries(node.bind)) {
600
+ const path = ref.replace(/^\$data\./, "").replace(/\./g, ".");
601
+ propParts.push(`${k}={data.${path}}`);
602
+ }
603
+ if (node.events) for (const [k, ref] of Object.entries(node.events)) {
604
+ const actionName = ref.replace(/^\$actions\./, "");
605
+ propParts.push(`${k}={actions.${actionName}}`);
606
+ }
607
+ const propsStr = propParts.length > 0 ? " " + propParts.join(" ") : "";
608
+ const childrenStr = node.children !== void 0 ? renderChildren(node.children, indent, [], knownComponents) : void 0;
609
+ if (childrenStr !== void 0 && childrenStr !== "") return `${indent}<${compName}${propsStr}>\n${indent} ${childrenStr}\n${indent}</${compName}>`;
610
+ return `${indent}<${compName}${propsStr} />`;
611
+ }
612
+ function collectBindAndEvents(nodes) {
613
+ const boundProps = [];
614
+ const actionProps = [];
615
+ function walk(node) {
616
+ if (node.bind) for (const ref of Object.values(node.bind)) {
617
+ const path = ref.replace(/^\$data\./, "");
618
+ if (!boundProps.includes(path)) boundProps.push(path);
619
+ }
620
+ if (node.events) for (const ref of Object.values(node.events)) {
621
+ const name = ref.replace(/^\$actions\./, "");
622
+ if (!actionProps.includes(name)) actionProps.push(name);
623
+ }
624
+ if (Array.isArray(node.children)) node.children.forEach(walk);
625
+ }
626
+ nodes.forEach(walk);
627
+ return {
628
+ boundProps,
629
+ actionProps
630
+ };
631
+ }
632
+ function generateTsx(config, _propMetas, componentsPath) {
633
+ const knownComponents = /* @__PURE__ */ new Set();
634
+ const { boundProps, actionProps } = collectBindAndEvents(Object.values(config.view.regions).flat());
635
+ const regionBlocks = Object.entries(config.view.regions).map(([regionName, nodes]) => {
636
+ return ` <div className="region-${regionName}">\n${nodes.map((n) => renderNode(n, " ", [], knownComponents)).join("\n")}\n </div>`;
637
+ }).join("\n");
638
+ const importLines = [...knownComponents].sort().map((c) => `import { ${c} } from '${componentsPath}/${c.toLowerCase()}/${c.toLowerCase()}'`).join("\n");
639
+ const hasData = boundProps.length > 0;
640
+ const hasActions = actionProps.length > 0;
641
+ const params = [hasData ? `data: {\n${boundProps.map((p) => ` ${p.replace(/\./g, "_")}: unknown // TODO: type your data`).join("\n")}\n }` : "", hasActions ? `actions: {\n${actionProps.map((a) => ` ${a}: (...args: unknown[]) => unknown`).join("\n")}\n }` : ""].filter(Boolean).join(",\n ");
642
+ return `import React from 'react'
643
+ ${importLines}
644
+
645
+ interface PageProps ${params ? `{\n ${params}\n}` : "{}"}
646
+
647
+ export function GeneratedPage({ ${hasData ? "data, " : ""}${hasActions ? "actions" : ""} }: PageProps) {
648
+ return (
649
+ <div className="cascade-view">
650
+ ${regionBlocks}
651
+ </div>
652
+ )
653
+ }
654
+ `;
655
+ }
656
+ async function generate(args, config) {
657
+ const outArg = args.find((_, i) => args[i - 1] === "--out");
658
+ const inputArg = args.find((a) => !a.startsWith("--"));
659
+ const componentsDirArg = args.find((_, i) => args[i - 1] === "--components-dir");
660
+ if (!inputArg) {
661
+ console.error("Usage: cascivo generate <config.json> [--out output.tsx] [--components-dir ./src/components/ui]");
662
+ process.exitCode = 1;
663
+ return;
664
+ }
665
+ const { readFileSync } = await import("node:fs");
666
+ const configJson = readFileSync(inputArg, "utf-8");
667
+ const viewConfig = JSON.parse(configJson);
668
+ await fetchRegistry(config.registry);
669
+ const tsx = generateTsx(viewConfig, /* @__PURE__ */ new Map(), componentsDirArg ?? config.outputDir ?? "./src/components/ui");
670
+ const outPath = outArg ?? join(dirname(inputArg), `${basename(inputArg, ".json")}.tsx`);
671
+ writeFileSync(outPath, tsx, "utf-8");
672
+ console.log(`Generated ${outPath}`);
673
+ try {
674
+ const { spawnSync } = await import("node:child_process");
675
+ if (spawnSync("pnpm", [
676
+ "exec",
677
+ "vp",
678
+ "fmt",
679
+ outPath
680
+ ], {
681
+ encoding: "utf8",
682
+ stdio: "inherit"
683
+ }).status !== 0) {
684
+ if (spawnSync("npx", [
685
+ "--yes",
686
+ "prettier",
687
+ "--write",
688
+ outPath
689
+ ], {
690
+ encoding: "utf8",
691
+ stdio: "inherit"
692
+ }).status !== 0) console.log("(Note: formatter not found — output may need manual formatting)");
693
+ }
694
+ } catch {}
695
+ }
696
+ //#endregion
697
+ //#region src/commands/init.ts
698
+ const THEMES$1 = [
699
+ "light",
700
+ "dark",
701
+ "warm"
702
+ ];
703
+ async function promptTheme() {
704
+ const rl = createInterface({
705
+ input: stdin,
706
+ output: stdout
707
+ });
708
+ try {
709
+ const answer = (await rl.question("Theme? (light/dark/warm) [light]: ")).trim().toLowerCase();
710
+ return THEMES$1.includes(answer) ? answer : "light";
711
+ } finally {
712
+ rl.close();
713
+ }
714
+ }
715
+ function configFileContents(theme) {
716
+ return `import type { CascadeConfig } from 'cascivo'
717
+
718
+ const config: CascadeConfig = {
719
+ registry: '${DEFAULT_CONFIG.registry}',
720
+ outputDir: '${DEFAULT_CONFIG.outputDir}',
721
+ theme: '${theme}',
722
+ }
723
+
724
+ export default config
725
+ `;
726
+ }
727
+ async function init(cwd = process.cwd()) {
728
+ installPackages(["@cascivo/core", "@cascivo/tokens"], cwd);
729
+ const theme = await promptTheme();
730
+ await writeFileSafe(join(cwd, "cascivo.config.ts"), configFileContents(theme));
731
+ console.log(`\nCreated cascivo.config.ts (theme: ${theme})`);
732
+ console.log("Import the theme in your root CSS or entry file:");
733
+ console.log(` import '@cascivo/themes/${theme}.css'`);
734
+ console.log(`Then set the theme on your root element:`);
735
+ console.log(` <html data-theme="${theme}">`);
736
+ console.log("\nAdd components with: cascivo add <name>");
737
+ }
738
+ //#endregion
739
+ //#region src/commands/list.ts
740
+ const TYPE_LABELS = {
741
+ component: "Components",
742
+ layout: "Layouts",
743
+ block: "Blocks",
744
+ section: "Sections"
745
+ };
746
+ /** Render a group of entries as an aligned text table (no section header). */
747
+ function formatGroup(entries) {
748
+ const rows = entries.map((c) => [
749
+ c.name,
750
+ c.category,
751
+ c.description
752
+ ]);
753
+ const headers = [
754
+ "Name",
755
+ "Category",
756
+ "Description"
757
+ ];
758
+ const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)));
759
+ const line = (cells) => cells.map((cell, i) => cell.padEnd(widths[i] ?? cell.length)).join(" ").trimEnd();
760
+ const sep = widths.map((w) => "-".repeat(w)).join(" ");
761
+ return [
762
+ line(headers),
763
+ sep,
764
+ ...rows.map(line)
765
+ ].join("\n");
766
+ }
767
+ /** Render the component list as an aligned text table, grouped by type. */
768
+ function formatList(components) {
769
+ if (components.length === 0) return "No components found.";
770
+ if (!components.some((c) => c.type !== void 0)) return formatGroup(components);
771
+ const groups = /* @__PURE__ */ new Map();
772
+ for (const c of components) {
773
+ const key = c.type ?? "component";
774
+ if (!groups.has(key)) groups.set(key, []);
775
+ groups.get(key).push(c);
776
+ }
777
+ const sections = [];
778
+ for (const [key, entries] of groups) {
779
+ const label = TYPE_LABELS[key] ?? key;
780
+ sections.push(`${label}\n${formatGroup(entries)}`);
781
+ }
782
+ return sections.join("\n\n");
783
+ }
784
+ async function list(config, options = {}) {
785
+ let components = (await fetchRegistry(config.registry)).components;
786
+ if (options.installed) components = components.filter((c) => {
787
+ const outputName = c.name.includes("/") ? c.name.split("/").pop() : c.name;
788
+ return existsSync(join(process.cwd(), config.outputDir, outputName));
789
+ });
790
+ console.log(formatList(components));
791
+ }
792
+ //#endregion
793
+ //#region src/commands/registry.ts
794
+ async function registryBuild(args) {
795
+ let inFile = "cascade-registry.json";
796
+ let outDir = "public/r";
797
+ for (let i = 0; i < args.length; i++) if (args[i] === "--in" && args[i + 1]) {
798
+ inFile = args[i + 1];
799
+ i++;
800
+ } else if (args[i] === "--out" && args[i + 1]) {
801
+ outDir = args[i + 1];
802
+ i++;
803
+ }
804
+ const inPath = resolve(inFile);
805
+ if (!existsSync(inPath)) {
806
+ console.error(`cascade registry build: file not found: ${inPath}`);
807
+ process.exitCode = 1;
808
+ return;
809
+ }
810
+ const raw = JSON.parse(await readFile(inPath, "utf8"));
811
+ dirname(inPath);
812
+ let index = (() => {
813
+ if (typeof raw === "object" && raw !== null && raw["schemaVersion"] === 2) return raw;
814
+ return parseLegacyRegistry(raw);
815
+ })();
816
+ const result = validateIndex(index);
817
+ if (result.warnings.length > 0) for (const w of result.warnings) console.warn(`warn: ${w}`);
818
+ if (!result.ok) {
819
+ for (const e of result.errors) console.error(`error: ${e}`);
820
+ process.exitCode = 1;
821
+ return;
822
+ }
823
+ const outPath = resolve(outDir);
824
+ await buildRegistry(index, outPath);
825
+ console.log(`cascade registry build: wrote static output to ${outPath}`);
826
+ }
827
+ //#endregion
828
+ //#region src/commands/search.ts
829
+ function matchesQuery(item, query) {
830
+ const q = query.toLowerCase();
831
+ return item.name.toLowerCase().includes(q) || item.description.toLowerCase().includes(q) || item.tags.some((t) => t.toLowerCase().includes(q));
832
+ }
833
+ async function search(args, config) {
834
+ const nsFlag = args.indexOf("--registry");
835
+ const registryFilter = nsFlag !== -1 ? args[nsFlag + 1] : void 0;
836
+ const query = args.filter((a, i) => a !== "--registry" && args[i - 1] !== "--registry").join(" ").trim();
837
+ if (!query) {
838
+ console.error("Usage: cascade search <query> [--registry @ns]");
839
+ return;
840
+ }
841
+ const results = [];
842
+ if (!registryFilter) {
843
+ const registry = await fetchRegistry(config.registry);
844
+ for (const c of registry.components) if (matchesQuery(c, query)) results.push({
845
+ name: c.name,
846
+ version: c.version,
847
+ description: c.description
848
+ });
849
+ }
850
+ const registries = config.registries ?? {};
851
+ for (const [ns, nsCfg] of Object.entries(registries)) {
852
+ if (registryFilter && ns !== registryFilter) continue;
853
+ try {
854
+ const raw = await fetchJson((typeof nsCfg === "string" ? nsCfg : nsCfg.url).replace(/{name}[^$]*$/, "").replace(/\/$/, "") + "/registry.json");
855
+ for (const item of raw.items ?? []) if (matchesQuery(item, query)) results.push({
856
+ name: `${ns}/${item.name}`,
857
+ version: item.version,
858
+ description: item.description,
859
+ registry: ns
860
+ });
861
+ } catch {
862
+ console.warn(` [warn] Could not search ${ns} registry`);
863
+ }
864
+ }
865
+ if (results.length === 0) {
866
+ console.log("No results found.");
867
+ return;
868
+ }
869
+ for (const r of results) {
870
+ const reg = r.registry ? ` (${r.registry})` : "";
871
+ console.log(`${r.name}@${r.version}${reg} — ${r.description}`);
872
+ }
873
+ }
874
+ //#endregion
875
+ //#region src/commands/theme.ts
876
+ const THEMES = [
877
+ "light",
878
+ "dark",
879
+ "warm",
880
+ "flat",
881
+ "minimal",
882
+ "midnight",
883
+ "pastel",
884
+ "brutalist",
885
+ "corporate",
886
+ "terminal"
887
+ ];
888
+ async function theme(args) {
889
+ const [sub, name] = args;
890
+ if (sub !== "add" || !name) {
891
+ console.error(`Usage: cascade theme add <${THEMES.join("|")}>`);
892
+ return;
893
+ }
894
+ if (!THEMES.includes(name)) {
895
+ console.error(`Unknown theme "${name}". Available: ${THEMES.join(", ")}`);
896
+ return;
897
+ }
898
+ installPackages(["@cascivo/themes"]);
899
+ console.log(`\nImport the theme in your root CSS or entry file:`);
900
+ console.log(` import '@cascivo/themes/${name}.css'`);
901
+ console.log(`Then set it on a container:`);
902
+ console.log(` <div data-theme="${name}">…</div>`);
903
+ }
904
+ //#endregion
905
+ //#region src/utils/diff.ts
906
+ /**
907
+ * Minimal line-based diff (LCS) rendered in a unified-ish format. Zero deps —
908
+ * good enough to preview what `cascade update` will change.
909
+ */
910
+ function diffLines(oldStr, newStr) {
911
+ const a = oldStr.split("\n");
912
+ const b = newStr.split("\n");
913
+ const n = a.length;
914
+ const m = b.length;
915
+ const lcs = Array.from({ length: n + 1 }, () => Array.from({ length: m + 1 }, () => 0));
916
+ for (let i = n - 1; i >= 0; i--) for (let j = m - 1; j >= 0; j--) lcs[i][j] = a[i] === b[j] ? lcs[i + 1][j + 1] + 1 : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
917
+ const out = [];
918
+ let i = 0;
919
+ let j = 0;
920
+ while (i < n && j < m) if (a[i] === b[j]) {
921
+ out.push(` ${a[i]}`);
922
+ i++;
923
+ j++;
924
+ } else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
925
+ out.push(`- ${a[i]}`);
926
+ i++;
927
+ } else {
928
+ out.push(`+ ${b[j]}`);
929
+ j++;
930
+ }
931
+ while (i < n) out.push(`- ${a[i++]}`);
932
+ while (j < m) out.push(`+ ${b[j++]}`);
933
+ return out;
934
+ }
935
+ /** True when the two strings differ. */
936
+ function hasChanges(oldStr, newStr) {
937
+ return oldStr !== newStr;
938
+ }
939
+ //#endregion
940
+ //#region src/utils/merge.ts
941
+ function lcs(a, b) {
942
+ const m = a.length;
943
+ const n = b.length;
944
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
945
+ for (let i = 1; i <= m; i++) for (let j = 1; j <= n; j++) dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] + 1 : Math.max(dp[i - 1][j], dp[i][j - 1]);
946
+ return dp;
947
+ }
948
+ function diff(base, changed) {
949
+ const dp = lcs(base, changed);
950
+ const chunks = [];
951
+ let i = base.length;
952
+ let j = changed.length;
953
+ const leftOnly = [];
954
+ const rightOnly = [];
955
+ const common = [];
956
+ function flush() {
957
+ if (leftOnly.length > 0 || rightOnly.length > 0) {
958
+ if (common.length > 0) {
959
+ chunks.unshift({
960
+ type: "common",
961
+ lines: [...common]
962
+ });
963
+ common.length = 0;
964
+ }
965
+ chunks.unshift({
966
+ type: leftOnly.length > 0 ? "left" : "right",
967
+ lines: leftOnly.length > 0 ? [...leftOnly] : [...rightOnly]
968
+ });
969
+ leftOnly.length = 0;
970
+ rightOnly.length = 0;
971
+ } else if (common.length > 0) {
972
+ chunks.unshift({
973
+ type: "common",
974
+ lines: [...common]
975
+ });
976
+ common.length = 0;
977
+ }
978
+ }
979
+ while (i > 0 || j > 0) if (i > 0 && j > 0 && base[i - 1] === changed[j - 1]) {
980
+ if (leftOnly.length > 0 || rightOnly.length > 0) flush();
981
+ common.unshift(base[i - 1]);
982
+ i--;
983
+ j--;
984
+ } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
985
+ flush();
986
+ rightOnly.unshift(changed[j - 1]);
987
+ j--;
988
+ } else {
989
+ flush();
990
+ leftOnly.unshift(base[i - 1]);
991
+ i--;
992
+ }
993
+ flush();
994
+ return chunks;
995
+ }
996
+ function merge(base, local, upstream) {
997
+ const baseLines = base.split("\n");
998
+ const localLines = local.split("\n");
999
+ const upstreamLines = upstream.split("\n");
1000
+ const localChunks = diff(baseLines, localLines);
1001
+ diff(baseLines, upstreamLines);
1002
+ const localMap = /* @__PURE__ */ new Map();
1003
+ let li = 0;
1004
+ for (const c of localChunks) if (c.type === "left") localMap.set(`del:${li}`, c.lines);
1005
+ else if (c.type === "right") {
1006
+ localMap.set(`ins:${li}`, c.lines);
1007
+ li += c.lines.length;
1008
+ } else li += c.lines.length;
1009
+ const result = [];
1010
+ let conflicts = 0;
1011
+ baseLines.length;
1012
+ localLines.length;
1013
+ upstreamLines.length;
1014
+ if (local === base) return {
1015
+ text: upstream,
1016
+ conflicts: 0
1017
+ };
1018
+ if (upstream === base) return {
1019
+ text: local,
1020
+ conflicts: 0
1021
+ };
1022
+ const localDiffs = diff(baseLines, localLines);
1023
+ const upstreamDiffs = diff(baseLines, upstreamLines);
1024
+ let basePos = 0;
1025
+ let lPos = 0;
1026
+ let uPos = 0;
1027
+ const localOut = [];
1028
+ const upstreamOut = [];
1029
+ for (const c of localDiffs) if (c.type === "common") {
1030
+ lPos += c.lines.length;
1031
+ basePos += c.lines.length;
1032
+ } else if (c.type === "right") {
1033
+ localOut.push({
1034
+ baseStart: basePos,
1035
+ baseEnd: basePos,
1036
+ lines: c.lines
1037
+ });
1038
+ lPos += c.lines.length;
1039
+ } else {
1040
+ localOut.push({
1041
+ baseStart: basePos,
1042
+ baseEnd: basePos + c.lines.length,
1043
+ lines: []
1044
+ });
1045
+ basePos += c.lines.length;
1046
+ }
1047
+ basePos = 0;
1048
+ for (const c of upstreamDiffs) if (c.type === "common") {
1049
+ uPos += c.lines.length;
1050
+ basePos += c.lines.length;
1051
+ } else if (c.type === "right") {
1052
+ upstreamOut.push({
1053
+ baseStart: basePos,
1054
+ baseEnd: basePos,
1055
+ lines: c.lines
1056
+ });
1057
+ uPos += c.lines.length;
1058
+ } else {
1059
+ upstreamOut.push({
1060
+ baseStart: basePos,
1061
+ baseEnd: basePos + c.lines.length,
1062
+ lines: []
1063
+ });
1064
+ basePos += c.lines.length;
1065
+ }
1066
+ const localByBase = /* @__PURE__ */ new Map();
1067
+ for (const e of localOut) localByBase.set(e.baseStart, {
1068
+ end: e.baseEnd,
1069
+ lines: e.lines
1070
+ });
1071
+ const upstreamByBase = /* @__PURE__ */ new Map();
1072
+ for (const e of upstreamOut) upstreamByBase.set(e.baseStart, {
1073
+ end: e.baseEnd,
1074
+ lines: e.lines
1075
+ });
1076
+ let b = 0;
1077
+ while (b <= baseLines.length) {
1078
+ const lEdit = localByBase.get(b);
1079
+ const uEdit = upstreamByBase.get(b);
1080
+ if (lEdit && uEdit) {
1081
+ const lEnd = Math.max(lEdit.end, b);
1082
+ const uEnd = Math.max(uEdit.end, b);
1083
+ const lLines = lEdit.lines;
1084
+ const uLines = uEdit.lines;
1085
+ if (JSON.stringify(lLines) === JSON.stringify(uLines)) result.push(...lLines);
1086
+ else {
1087
+ result.push("<<<<<<< local");
1088
+ result.push(...lLines);
1089
+ result.push("=======");
1090
+ result.push(...uLines);
1091
+ result.push(">>>>>>> upstream");
1092
+ conflicts++;
1093
+ }
1094
+ b = Math.max(lEnd, uEnd, b + 1);
1095
+ } else if (lEdit) {
1096
+ result.push(...lEdit.lines);
1097
+ b = Math.max(lEdit.end, b + 1);
1098
+ } else if (uEdit) {
1099
+ result.push(...uEdit.lines);
1100
+ b = Math.max(uEdit.end, b + 1);
1101
+ } else {
1102
+ if (b < baseLines.length) result.push(baseLines[b]);
1103
+ b++;
1104
+ }
1105
+ }
1106
+ return {
1107
+ text: result.join("\n"),
1108
+ conflicts
1109
+ };
1110
+ }
1111
+ //#endregion
1112
+ //#region src/commands/update.ts
1113
+ async function confirm(question) {
1114
+ const rl = createInterface({
1115
+ input: stdin,
1116
+ output: stdout
1117
+ });
1118
+ try {
1119
+ const answer = (await rl.question(`${question} (y/N): `)).trim().toLowerCase();
1120
+ return answer === "y" || answer === "yes";
1121
+ } finally {
1122
+ rl.close();
1123
+ }
1124
+ }
1125
+ async function fetchText(url) {
1126
+ const res = await fetch(url);
1127
+ if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${url}`);
1128
+ return res.text();
1129
+ }
1130
+ async function update(name, config, opts = {}) {
1131
+ if (opts.check) return updateCheck(config);
1132
+ if (!name) {
1133
+ console.error("Usage: cascivo update <component>");
1134
+ return;
1135
+ }
1136
+ const lockEntry = (await readLock())?.items[name];
1137
+ const entry = findComponent(await fetchRegistry(config.registry), name);
1138
+ if (!entry) {
1139
+ console.error(`Component "${name}" not found in registry. Run "cascivo list".`);
1140
+ return;
1141
+ }
1142
+ if (lockEntry) {
1143
+ const lockedVersion = lockEntry.version;
1144
+ const baseItemUrl = `${lockEntry.registry}/r/${name}@${lockedVersion}.json`;
1145
+ let baseItem = null;
1146
+ try {
1147
+ baseItem = await fetchJson(baseItemUrl);
1148
+ } catch {
1149
+ console.warn(`Warning: could not fetch base version ${lockedVersion} from ${baseItemUrl}. Falling back to two-way diff.`);
1150
+ }
1151
+ if (baseItem) await threeWayUpdate(name, entry, baseItem, config, opts);
1152
+ else await twoWayUpdate(name, entry, config, opts);
1153
+ } else {
1154
+ console.warn(`Warning: "${name}" has no lock entry. Falling back to two-way diff update (pre-v11 install).`);
1155
+ await twoWayUpdate(name, entry, config, opts);
1156
+ }
1157
+ }
1158
+ async function threeWayUpdate(name, upstreamEntry, baseItem, config, opts) {
1159
+ if (!upstreamEntry) return;
1160
+ const outputName = name.includes("/") ? name.split("/").pop() : name;
1161
+ const summaries = [];
1162
+ const pending = [];
1163
+ for (const url of upstreamEntry.files) {
1164
+ const fname = fileName(url);
1165
+ const baseFile = baseItem.files.find((f) => fileName(f.url) === fname);
1166
+ const dest = resolveOutputPath(config.outputDir, outputName, fname);
1167
+ const upstream = await fetchText(url);
1168
+ const local = existsSync(dest) ? await readFile(dest, "utf8") : "";
1169
+ const base = baseFile ? await fetchText(baseFile.url) : "";
1170
+ if (base === "" && local === "") {
1171
+ summaries.push({
1172
+ file: fname,
1173
+ result: "clean"
1174
+ });
1175
+ pending.push({
1176
+ dest,
1177
+ content: upstream,
1178
+ conflicts: 0
1179
+ });
1180
+ continue;
1181
+ }
1182
+ const { text, conflicts } = merge(base, local, upstream);
1183
+ if (conflicts > 0) summaries.push({
1184
+ file: fname,
1185
+ result: "conflict"
1186
+ });
1187
+ else if (text === local) {
1188
+ summaries.push({
1189
+ file: fname,
1190
+ result: "unchanged"
1191
+ });
1192
+ continue;
1193
+ } else summaries.push({
1194
+ file: fname,
1195
+ result: "clean"
1196
+ });
1197
+ pending.push({
1198
+ dest,
1199
+ content: text,
1200
+ conflicts
1201
+ });
1202
+ }
1203
+ if (pending.length === 0) {
1204
+ console.log(`${name} is already up to date.`);
1205
+ return;
1206
+ }
1207
+ for (const s of summaries) {
1208
+ const icon = s.result === "conflict" ? "⚠" : s.result === "clean" ? "✓" : "·";
1209
+ console.log(` ${icon} ${s.file} (${s.result})`);
1210
+ }
1211
+ const conflicted = pending.filter((p) => p.conflicts > 0);
1212
+ if ((opts.yes ? conflicted.length === 0 : await confirm(`\nApply ${pending.length} change(s) to ${name}?`)) || opts.yes) {
1213
+ const lock = await readLock();
1214
+ let updatedLock = lock;
1215
+ for (const { dest, content, conflicts } of pending) {
1216
+ if (opts.yes && conflicts > 0) {
1217
+ console.log(` skipped (conflicts): ${dest}`);
1218
+ continue;
1219
+ }
1220
+ await writeFileSafe(dest, content);
1221
+ }
1222
+ if (lock && updatedLock) {
1223
+ const hasConflicts = conflicted.length > 0;
1224
+ updatedLock = updateLockEntry(updatedLock, name, {
1225
+ ...lock.items[name],
1226
+ version: upstreamEntry.version,
1227
+ ...hasConflicts ? { conflicted: true } : {}
1228
+ });
1229
+ await writeLock(updatedLock);
1230
+ }
1231
+ console.log(`Updated ${name}.`);
1232
+ if (conflicted.length > 0) console.log(`${conflicted.length} file(s) have conflict markers — resolve them manually.`);
1233
+ } else console.log("Aborted. No files changed.");
1234
+ }
1235
+ async function twoWayUpdate(name, entry, config, opts) {
1236
+ if (!entry) return;
1237
+ const pending = [];
1238
+ for (const url of entry.files) {
1239
+ const res = await fetch(url);
1240
+ if (!res.ok) {
1241
+ console.error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`);
1242
+ return;
1243
+ }
1244
+ const latest = await res.text();
1245
+ const dest = resolveOutputPath(config.outputDir, entry.name, fileName(url));
1246
+ const current = await readFileSafe(dest);
1247
+ if (current === null) {
1248
+ console.log(`New file: ${dest}`);
1249
+ pending.push({
1250
+ path: dest,
1251
+ content: latest
1252
+ });
1253
+ } else if (hasChanges(current, latest)) {
1254
+ console.log(`\n--- ${dest}`);
1255
+ console.log(diffLines(current, latest).join("\n"));
1256
+ pending.push({
1257
+ path: dest,
1258
+ content: latest
1259
+ });
1260
+ }
1261
+ }
1262
+ if (pending.length === 0) {
1263
+ console.log(`${name} is already up to date.`);
1264
+ return;
1265
+ }
1266
+ if (opts.yes || await confirm(`\nApply ${pending.length} change(s) to ${name}?`)) {
1267
+ for (const { path, content } of pending) await writeFileSafe(path, content);
1268
+ console.log(`Updated ${name}.`);
1269
+ } else console.log("Aborted. No files changed.");
1270
+ }
1271
+ async function updateCheck(config) {
1272
+ const lock = await readLock();
1273
+ if (!lock || Object.keys(lock.items).length === 0) {
1274
+ console.log("No installed components found in cascivo.lock.");
1275
+ return;
1276
+ }
1277
+ const registry = await fetchRegistry(config.registry);
1278
+ let outdated = 0;
1279
+ for (const [name, entry] of Object.entries(lock.items)) {
1280
+ const current = findComponent(registry, name);
1281
+ if (!current) continue;
1282
+ if (current.version !== entry.version) {
1283
+ console.log(`${name}: ${entry.version} → ${current.version}`);
1284
+ outdated++;
1285
+ }
1286
+ }
1287
+ if (outdated > 0) {
1288
+ console.log(`${outdated} component(s) outdated.`);
1289
+ process.exitCode = 1;
1290
+ } else console.log("All components up to date.");
1291
+ }
1292
+ //#endregion
1293
+ //#region src/commands/view.ts
1294
+ async function view(args, config) {
1295
+ const spec = args[0];
1296
+ if (!spec) {
1297
+ console.error("Usage: cascade view <component-spec>");
1298
+ return;
1299
+ }
1300
+ const { url, headers, params } = await resolveItemUrl(parseAddress(spec), config);
1301
+ const fetchOpts = {};
1302
+ if (headers) fetchOpts.headers = headers;
1303
+ if (params) fetchOpts.params = params;
1304
+ const item = await fetchJson(url, fetchOpts);
1305
+ console.log(`\n${item.name} v${item.version}`);
1306
+ console.log(`Type: ${item.type}${item.category ? ` / ${item.category}` : ""}`);
1307
+ console.log(`${item.description}`);
1308
+ if (item.author) console.log(`Author: ${item.author}`);
1309
+ if (item.license) console.log(`License: ${item.license}`);
1310
+ if (item.homepage) console.log(`Homepage: ${item.homepage}`);
1311
+ if (item.changelog?.length) {
1312
+ console.log("\nChangelog:");
1313
+ for (const c of item.changelog.slice(0, 5)) console.log(` ${c.version}: ${c.note}`);
1314
+ }
1315
+ if (item.files?.length) {
1316
+ console.log("\nFiles:");
1317
+ for (const f of item.files) {
1318
+ const target = f.target ? ` → ${f.target}` : "";
1319
+ console.log(` ${f.url}${target}`);
1320
+ }
1321
+ }
1322
+ if (item.dependencies?.length) console.log(`\nDependencies: ${item.dependencies.join(", ")}`);
1323
+ if (item.registryDependencies?.length) console.log(`Registry dependencies: ${item.registryDependencies.join(", ")}`);
1324
+ }
1325
+ //#endregion
1326
+ //#region src/index.ts
1327
+ const VERSION = "0.0.0";
1328
+ const HELP = `cascivo ${VERSION} — the CSS-native, signal-driven, AI-first design system
1329
+
1330
+ Usage: cascivo <command> [options]
1331
+
1332
+ Commands:
1333
+ init Set up cascivo in the current project
1334
+ add <component...> Add one or more components to your project
1335
+ list [--installed] List available components
1336
+ update [component] Update installed components (--check: list outdated)
1337
+ search <query> Search components across registries
1338
+ view <spec> View a component before installing
1339
+ theme add <name> Install a theme (light | dark | warm)
1340
+ generate <config.json> Generate TSX from a ViewConfig JSON file
1341
+ doctor [--ci] Check components for rule violations
1342
+ audit --ai <paths...> Audit AI-generated code against the cascivo contract
1343
+ registry build Build a static registry from a cascivo-registry.json file
1344
+
1345
+ Run "cascivo <command> --help" for details.`;
1346
+ async function run(args) {
1347
+ const [command, ...rest] = args;
1348
+ switch (command) {
1349
+ case "init":
1350
+ await init();
1351
+ break;
1352
+ case "add":
1353
+ await add(rest, await loadConfig(), {
1354
+ dryRun: rest.includes("--dry-run"),
1355
+ yes: rest.includes("--yes")
1356
+ });
1357
+ break;
1358
+ case "list":
1359
+ await list(await loadConfig(), { installed: rest.includes("--installed") });
1360
+ break;
1361
+ case "update":
1362
+ await update(rest.filter((r) => !r.startsWith("--"))[0], await loadConfig(), {
1363
+ check: rest.includes("--check"),
1364
+ yes: rest.includes("--yes")
1365
+ });
1366
+ break;
1367
+ case "search":
1368
+ await search(rest, await loadConfig());
1369
+ break;
1370
+ case "view":
1371
+ await view(rest, await loadConfig());
1372
+ break;
1373
+ case "theme":
1374
+ await theme(rest);
1375
+ break;
1376
+ case "generate":
1377
+ await generate(rest, await loadConfig());
1378
+ break;
1379
+ case "registry":
1380
+ if (rest[0] === "build") await registryBuild(rest.slice(1));
1381
+ else {
1382
+ console.error(`Unknown registry subcommand: ${rest[0]}`);
1383
+ process.exitCode = 1;
1384
+ }
1385
+ break;
1386
+ case "doctor": {
1387
+ const ci = rest.includes("--ci");
1388
+ if (rest.includes("--drift")) {
1389
+ const { runDoctorDrift } = await import("./drift-D7JFzpP_.mjs");
1390
+ await runDoctorDrift(rest);
1391
+ } else {
1392
+ const result = await runDoctor(process.cwd());
1393
+ if (result.passed) console.log("No violations found.");
1394
+ else {
1395
+ for (const v of result.violations) console.error(`[${v.rule}] ${v.detail}\n ${v.file}`);
1396
+ if (ci) process.exitCode = 1;
1397
+ }
1398
+ }
1399
+ break;
1400
+ }
1401
+ case "audit": {
1402
+ const { audit } = await import("./audit-AHr4MDMK.mjs");
1403
+ await audit(rest, await loadConfig());
1404
+ break;
1405
+ }
1406
+ case "tokens":
1407
+ if (rest[0] === "import") {
1408
+ const { tokensImport } = await import("./tokens-CO1xRiYC.mjs");
1409
+ await tokensImport(rest.slice(1));
1410
+ } else {
1411
+ console.error(`Unknown tokens subcommand: ${rest[0]}`);
1412
+ process.exitCode = 1;
1413
+ }
1414
+ break;
1415
+ case void 0:
1416
+ case "--help":
1417
+ case "-h":
1418
+ case "help":
1419
+ console.log(HELP);
1420
+ break;
1421
+ case "--version":
1422
+ case "-v":
1423
+ console.log(VERSION);
1424
+ break;
1425
+ default:
1426
+ console.error(`Unknown command: ${command}\n`);
1427
+ console.log(HELP);
1428
+ process.exitCode = 1;
1429
+ }
1430
+ }
1431
+ if (argv[1] !== void 0 && import.meta.url === pathToFileURL(argv[1]).href) run(argv.slice(2)).catch((error) => {
1432
+ console.error(error instanceof Error ? error.message : String(error));
1433
+ process.exitCode = 1;
1434
+ });
1435
+ //#endregion
1436
+ export { VERSION, run };
1437
+
1438
+ //# sourceMappingURL=index.mjs.map