cascivo 0.1.6 → 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 +23 -8
- package/dist/{audit-AHr4MDMK.mjs → audit-InQqdY1s.mjs} +8 -8
- package/dist/{audit-AHr4MDMK.mjs.map → audit-InQqdY1s.mjs.map} +1 -1
- package/dist/config-BsyJbMvD.mjs +106 -0
- 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-D_mC1lsq.mjs → eject-Br8V7I3m.mjs} +3 -3
- package/dist/{eject-D_mC1lsq.mjs.map → eject-Br8V7I3m.mjs.map} +1 -1
- package/dist/fs-m7ZvuBBm.mjs +24 -0
- package/dist/fs-m7ZvuBBm.mjs.map +1 -0
- 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 +1102 -318
- 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-C46fRXo-.mjs +179 -0
- package/dist/template-init-C46fRXo-.mjs.map +1 -0
- package/package.json +4 -3
- package/readme.body.md +23 -8
- package/dist/drift-D7JFzpP_.mjs +0 -8
- package/dist/drift-D7JFzpP_.mjs.map +0 -1
- package/dist/http-CLkdTpxD.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,70 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as
|
|
2
|
+
import { a as detectPackageManager, c as __exportAll, n as DEFAULT_CONFIG$1, o as installCommand, r as THEMES, s as loadConfig, t as CASCIVO_HOST } from "./config-BsyJbMvD.mjs";
|
|
3
|
+
import { n as resolveOutputPath, r as writeFileSafe, t as readFileSafe } from "./fs-m7ZvuBBm.mjs";
|
|
4
|
+
import { r as fetchTextRetry, t as fetchJson } from "./http-CJZ5W0Fa.mjs";
|
|
5
|
+
import { a as updateLockEntry, c as fileName, i as sha256, l as findComponent, n as createLock, o as writeLock, r as readLock, s as fetchRegistry, t as checkPeerVersions } from "./peer-versions-B2bCgko-.mjs";
|
|
6
|
+
import { createRequire } from "node:module";
|
|
3
7
|
import { argv, stdin, stdout } from "node:process";
|
|
4
8
|
import { pathToFileURL } from "node:url";
|
|
5
|
-
import { spawnSync } from "node:child_process";
|
|
6
9
|
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
7
10
|
import { basename, dirname, join, resolve } from "node:path";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
12
|
+
import { readFile } from "node:fs/promises";
|
|
13
|
+
import { asTemplateMeta, buildRegistry, isTemplateItem, parseLegacyRegistry, validateIndex, validateTemplate } from "@cascivo/registry";
|
|
10
14
|
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://cascivo.com/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
15
|
//#region src/utils/exec.ts
|
|
69
16
|
/**
|
|
70
17
|
* Install npm packages using the detected package manager, inheriting stdio so
|
|
@@ -85,153 +32,6 @@ function installPackages(packages, cwd = process.cwd()) {
|
|
|
85
32
|
return true;
|
|
86
33
|
}
|
|
87
34
|
//#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
35
|
//#region src/utils/resolve.ts
|
|
236
36
|
const GITHUB_RE = /^([^/]+)\/([^/]+)\/([^#]+)(?:#(.+))?$/;
|
|
237
37
|
function expandEnv(str, env = process.env) {
|
|
@@ -309,7 +109,7 @@ async function resolveItemUrl(addr, config, env = process.env, resolveNamespaceH
|
|
|
309
109
|
const hookResult = await resolveNamespaceHook(ns);
|
|
310
110
|
if (hookResult) return { url: resolveNamespaceUrl(ns, name, hookResult, env) };
|
|
311
111
|
}
|
|
312
|
-
if (ns === "@cascade") return { url:
|
|
112
|
+
if (ns === "@cascivo" || ns === "@cascade") return { url: `${CASCIVO_HOST}/r/${name}.json` };
|
|
313
113
|
throw new Error(`Unknown namespace "${ns}". Add it to registries in cascivo.config.ts or check the directory at cascivo.com`);
|
|
314
114
|
}
|
|
315
115
|
}
|
|
@@ -345,7 +145,7 @@ async function resolveClosure(specs, config, env = process.env, resolveNamespace
|
|
|
345
145
|
}
|
|
346
146
|
//#endregion
|
|
347
147
|
//#region src/utils/directory.ts
|
|
348
|
-
const DIRECTORY_URL =
|
|
148
|
+
const DIRECTORY_URL = `${CASCIVO_HOST}/r/registries.json`;
|
|
349
149
|
async function resolveFromDirectory(ns, fetchFn) {
|
|
350
150
|
try {
|
|
351
151
|
let data;
|
|
@@ -365,12 +165,95 @@ async function resolveFromDirectory(ns, fetchFn) {
|
|
|
365
165
|
}
|
|
366
166
|
//#endregion
|
|
367
167
|
//#region src/commands/add.ts
|
|
168
|
+
var add_exports = /* @__PURE__ */ __exportAll({
|
|
169
|
+
LAYOUT_ALIASES: () => LAYOUT_ALIASES,
|
|
170
|
+
add: () => add,
|
|
171
|
+
isThirdParty: () => isThirdParty,
|
|
172
|
+
resolveBareClosure: () => resolveBareClosure,
|
|
173
|
+
resolveTemplateTarget: () => resolveTemplateTarget
|
|
174
|
+
});
|
|
368
175
|
/** Dependencies always present in a cascade project — never auto-installed. */
|
|
369
176
|
const ASSUMED_DEPS = new Set(["react", "react-dom"]);
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
177
|
+
/**
|
|
178
|
+
* Copied components read `--cascivo-*` custom properties; without
|
|
179
|
+
* @cascivo/tokens or @cascivo/themes imported somewhere, they render unstyled.
|
|
180
|
+
* Print the wiring once after a successful install if neither is a dependency.
|
|
181
|
+
*/
|
|
182
|
+
function hintThemesIfMissing(cwd) {
|
|
183
|
+
let deps = {};
|
|
184
|
+
try {
|
|
185
|
+
const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
|
|
186
|
+
deps = {
|
|
187
|
+
...pkg.dependencies,
|
|
188
|
+
...pkg.devDependencies
|
|
189
|
+
};
|
|
190
|
+
} catch {}
|
|
191
|
+
if (deps["@cascivo/themes"] !== void 0 || deps["@cascivo/tokens"] !== void 0) return;
|
|
192
|
+
console.log("\nStyling: cascivo components read --cascivo-* tokens, which this project does not import yet.");
|
|
193
|
+
console.log(" npm install @cascivo/themes");
|
|
194
|
+
console.log(" import '@cascivo/themes/all' // once, in your entry file");
|
|
195
|
+
console.log(" <html data-theme=\"light\"> // or any container");
|
|
196
|
+
}
|
|
197
|
+
/** Resolve where a template's own file is written — relative to the project root. */
|
|
198
|
+
function resolveTemplateTarget(cwd, target) {
|
|
199
|
+
return resolve(cwd, target);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Names from other libraries → the cascivo layout primitive that fills the same
|
|
203
|
+
* role, so `cascivo add flex` installs `layout/stack`. Bare names that already
|
|
204
|
+
* resolve (e.g. `stack` → `layout/stack` via suffix match) need no alias.
|
|
205
|
+
*/
|
|
206
|
+
const LAYOUT_ALIASES = {
|
|
207
|
+
flex: "stack",
|
|
208
|
+
box: "stack",
|
|
209
|
+
hstack: "stack",
|
|
210
|
+
vstack: "stack",
|
|
211
|
+
gap: "spacer"
|
|
212
|
+
};
|
|
213
|
+
/**
|
|
214
|
+
* Resolve the transitive closure of bare-name registry entries, following each
|
|
215
|
+
* entry's `registryDependencies`. De-duped by resolved registry name and
|
|
216
|
+
* cycle-safe (an entry is processed at most once). Returns resolved entries in
|
|
217
|
+
* install order plus the names that could not be found. Pure — no I/O.
|
|
218
|
+
*/
|
|
219
|
+
function resolveBareClosure(registry, bareSpecs) {
|
|
220
|
+
const queue = bareSpecs.map((name) => ({
|
|
221
|
+
name,
|
|
222
|
+
requested: true
|
|
223
|
+
}));
|
|
224
|
+
const installed = /* @__PURE__ */ new Set();
|
|
225
|
+
const resolved = [];
|
|
226
|
+
const missing = [];
|
|
227
|
+
while (queue.length > 0) {
|
|
228
|
+
const { name, requested } = queue.shift();
|
|
229
|
+
const entry = findComponent(registry, LAYOUT_ALIASES[name.toLowerCase()] ?? name);
|
|
230
|
+
if (!entry) {
|
|
231
|
+
missing.push(name);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (installed.has(entry.name)) continue;
|
|
235
|
+
installed.add(entry.name);
|
|
236
|
+
resolved.push({
|
|
237
|
+
entry,
|
|
238
|
+
requested
|
|
239
|
+
});
|
|
240
|
+
for (const dep of entry.registryDependencies ?? []) queue.push({
|
|
241
|
+
name: dep,
|
|
242
|
+
requested: false
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
resolved,
|
|
247
|
+
missing
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* First-party items are hosted on cascivo.com or the cascivo/cascivo GitHub
|
|
252
|
+
* repo (raw file URLs). Anything else gets a review-before-use notice.
|
|
253
|
+
*/
|
|
254
|
+
const FIRST_PARTY_URL = /^https:\/\/(cascivo\.com|raw\.githubusercontent\.com\/cascivo\/cascivo)\//;
|
|
255
|
+
function isThirdParty(itemUrl) {
|
|
256
|
+
return !FIRST_PARTY_URL.test(itemUrl);
|
|
374
257
|
}
|
|
375
258
|
function isMultiRegistrySpec(name) {
|
|
376
259
|
return name.startsWith("@") || name.startsWith("http://") || name.startsWith("https://") || name.startsWith("./") || name.startsWith("/") || name.split("/").length >= 3;
|
|
@@ -380,81 +263,539 @@ async function add(names, config, opts = {}) {
|
|
|
380
263
|
console.error("Usage: cascivo add <component...>");
|
|
381
264
|
return;
|
|
382
265
|
}
|
|
266
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
383
267
|
const multiSpecs = names.filter(isMultiRegistrySpec);
|
|
384
|
-
|
|
385
|
-
let
|
|
268
|
+
let bareSpecs = names.filter((n) => !isMultiRegistrySpec(n));
|
|
269
|
+
let registry = null;
|
|
270
|
+
if (bareSpecs.length > 0) {
|
|
271
|
+
registry = await fetchRegistry(config.registry);
|
|
272
|
+
const templateNames = new Set(registry.templates.map((t) => t.toLowerCase()));
|
|
273
|
+
const templateSpecs = bareSpecs.filter((n) => templateNames.has(n.toLowerCase()));
|
|
274
|
+
if (templateSpecs.length > 0) {
|
|
275
|
+
bareSpecs = bareSpecs.filter((n) => !templateNames.has(n.toLowerCase()));
|
|
276
|
+
multiSpecs.push(...templateSpecs.map((n) => `@cascivo/${n.toLowerCase()}`));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
let lock = await readLock(cwd) ?? createLock();
|
|
386
280
|
if (multiSpecs.length > 0) {
|
|
387
281
|
const plan = await resolveClosure(multiSpecs, config, process.env, resolveFromDirectory);
|
|
388
282
|
if (opts.dryRun) {
|
|
389
283
|
console.log("Dry run — would install:");
|
|
390
284
|
for (const item of plan.items) {
|
|
391
|
-
|
|
392
|
-
|
|
285
|
+
const kind = isTemplateItem(item.item) ? " [template]" : "";
|
|
286
|
+
console.log(` ${item.spec} (${item.item.name}@${item.item.version})${kind} from ${item.itemUrl}`);
|
|
287
|
+
for (const f of item.item.files) {
|
|
288
|
+
const arrow = isTemplateItem(item.item) && f.target ? ` → ${f.target}` : "";
|
|
289
|
+
console.log(` ${f.url}${arrow}`);
|
|
290
|
+
}
|
|
291
|
+
if (item.item.registryDependencies?.length) console.log(` components: ${item.item.registryDependencies.join(", ")}`);
|
|
393
292
|
}
|
|
394
293
|
return;
|
|
395
294
|
}
|
|
396
|
-
const isThirdParty = (itemUrl) => !itemUrl.includes("cascivo.com");
|
|
397
295
|
for (const { item, itemUrl, registryBase } of plan.items) {
|
|
398
296
|
if (isThirdParty(itemUrl)) console.log(`\nReview before use — files from non-directory registry: ${registryBase}\n` + (item.files ?? []).map((f) => ` ${f.url}`).join("\n"));
|
|
399
|
-
|
|
297
|
+
let fileHashes;
|
|
298
|
+
try {
|
|
299
|
+
fileHashes = await installItem(item, config, cwd);
|
|
300
|
+
} catch (e) {
|
|
301
|
+
console.error(`Failed to install ${item.name}: ${e instanceof Error ? e.message : e}`);
|
|
302
|
+
process.exitCode = 1;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
400
305
|
lock = updateLockEntry(lock, item.name, {
|
|
401
306
|
registry: registryBase,
|
|
402
307
|
version: item.version,
|
|
403
308
|
installedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
404
|
-
files:
|
|
309
|
+
files: fileHashes
|
|
405
310
|
});
|
|
406
311
|
}
|
|
407
|
-
await writeLock(lock);
|
|
408
|
-
|
|
312
|
+
await writeLock(lock, cwd);
|
|
313
|
+
if (bareSpecs.length === 0) {
|
|
314
|
+
hintThemesIfMissing(cwd);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
409
317
|
}
|
|
410
|
-
|
|
318
|
+
if (bareSpecs.length === 0) return;
|
|
319
|
+
registry ??= await fetchRegistry(config.registry);
|
|
411
320
|
const missingDeps = /* @__PURE__ */ new Set();
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
321
|
+
const registryBase = config.registry.replace(/\/registry\.json$/, "").replace(/\/r$/, "");
|
|
322
|
+
for (const spec of bareSpecs) {
|
|
323
|
+
const alias = LAYOUT_ALIASES[spec.toLowerCase()];
|
|
324
|
+
if (!alias) continue;
|
|
325
|
+
const target = findComponent(registry, alias);
|
|
326
|
+
if (target) console.log(`"${spec}" is not a cascivo component — installing ${target.name} (its ${spec} primitive).`);
|
|
327
|
+
}
|
|
328
|
+
const { resolved, missing } = resolveBareClosure(registry, bareSpecs);
|
|
329
|
+
for (const name of missing) console.error(`Component "${name}" not found in registry. Run "cascivo list".`);
|
|
330
|
+
const peerFloors = {};
|
|
331
|
+
for (const { entry } of resolved) for (const [pkg, floor] of Object.entries(entry.peerVersions ?? {})) peerFloors[pkg] = floor;
|
|
332
|
+
for (const { entry, requested } of resolved) {
|
|
333
|
+
if (entry.install) {
|
|
334
|
+
const pkg = entry.install;
|
|
335
|
+
console.log(`"${entry.name}" is distributed as the npm package ${pkg}.`);
|
|
421
336
|
console.log(`Install it with: npm install ${pkg}`);
|
|
422
337
|
console.log(`Then import: import { ${entry.meta.name} } from '${pkg}'`);
|
|
338
|
+
if (entry.styles) console.log(`And import its stylesheet once: import '${entry.styles}'`);
|
|
423
339
|
continue;
|
|
424
340
|
}
|
|
425
341
|
const outputName = entry.name.includes("/") ? entry.name.split("/").pop() : entry.name;
|
|
426
342
|
const fileHashes = {};
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
343
|
+
let fetched;
|
|
344
|
+
try {
|
|
345
|
+
fetched = await Promise.all(entry.files.map(async (url) => ({
|
|
346
|
+
url,
|
|
347
|
+
content: await fetchTextRetry(url)
|
|
348
|
+
})));
|
|
349
|
+
} catch (e) {
|
|
350
|
+
console.error(`Failed to install ${entry.name}: ${e instanceof Error ? e.message : e}`);
|
|
351
|
+
process.exitCode = 1;
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
for (const { url, content } of fetched) {
|
|
355
|
+
const dest = resolveOutputPath(config.outputDir, outputName, fileName(url), cwd);
|
|
430
356
|
await writeFileSafe(dest, content);
|
|
431
357
|
fileHashes[dest] = sha256(content);
|
|
432
358
|
}
|
|
433
359
|
for (const dep of entry.dependencies) if (!ASSUMED_DEPS.has(dep)) missingDeps.add(dep);
|
|
434
|
-
const registryBase = config.registry.replace(/\/registry\.json$/, "").replace(/\/r$/, "");
|
|
435
360
|
lock = updateLockEntry(lock, entry.name, {
|
|
436
361
|
registry: registryBase,
|
|
437
362
|
version: entry.version,
|
|
438
363
|
installedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
439
364
|
files: fileHashes
|
|
440
365
|
});
|
|
441
|
-
|
|
366
|
+
const suffix = requested ? "" : " (dependency)";
|
|
367
|
+
console.log(`Added ${entry.name} to ${config.outputDir}/${outputName}/${suffix}`);
|
|
442
368
|
}
|
|
443
369
|
if (missingDeps.size > 0) installPackages([...missingDeps]);
|
|
444
|
-
|
|
370
|
+
if (Object.keys(peerFloors).length > 0) {
|
|
371
|
+
const violations = await checkPeerVersions(cwd, peerFloors);
|
|
372
|
+
for (const v of violations) {
|
|
373
|
+
console.error(`\nWarning: ${v.pkg} ${v.installed ? `${v.installed} is` : "is not"} installed, but the copied component source needs ${v.pkg} ${v.required}. Run: npm install ${v.pkg}@latest`);
|
|
374
|
+
process.exitCode = 1;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
await writeLock(lock, cwd);
|
|
378
|
+
hintThemesIfMissing(cwd);
|
|
445
379
|
}
|
|
446
|
-
|
|
447
|
-
|
|
380
|
+
/**
|
|
381
|
+
* Install a single resolved item, returning a map of written path → content
|
|
382
|
+
* hash for the lockfile. A template writes its own files to their `target`
|
|
383
|
+
* paths (relative to the project root); every other item writes its files under
|
|
384
|
+
* the components output directory. A template's components are installed
|
|
385
|
+
* separately as their own plan entries (via `registryDependencies`).
|
|
386
|
+
*/
|
|
387
|
+
async function installItem(item, config, cwd) {
|
|
388
|
+
const fileHashes = {};
|
|
448
389
|
const missingDeps = /* @__PURE__ */ new Set();
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
390
|
+
const template = isTemplateItem(item);
|
|
391
|
+
const outputName = item.name.includes("/") ? item.name.split("/").pop() : item.name;
|
|
392
|
+
const fetched = await Promise.all((item.files ?? []).map(async (file) => ({
|
|
393
|
+
file,
|
|
394
|
+
content: await fetchTextRetry(file.url)
|
|
395
|
+
})));
|
|
396
|
+
for (const { file, content } of fetched) {
|
|
397
|
+
const dest = template && file.target ? resolveTemplateTarget(cwd, file.target) : resolveOutputPath(config.outputDir, outputName, fileName(file.url), cwd);
|
|
398
|
+
await writeFileSafe(dest, content);
|
|
399
|
+
fileHashes[dest] = sha256(content);
|
|
454
400
|
}
|
|
455
401
|
for (const dep of item.dependencies ?? []) if (!ASSUMED_DEPS.has(dep)) missingDeps.add(dep);
|
|
456
402
|
if (missingDeps.size > 0) installPackages([...missingDeps]);
|
|
457
|
-
|
|
403
|
+
if (template) {
|
|
404
|
+
const pages = item.files?.filter((f) => f.target).map((f) => f.target) ?? [];
|
|
405
|
+
console.log(`Installed template ${item.name} (${pages.length} files)`);
|
|
406
|
+
} else console.log(`Added ${item.name} to ${config.outputDir}/${outputName}/`);
|
|
407
|
+
return fileHashes;
|
|
408
|
+
}
|
|
409
|
+
//#endregion
|
|
410
|
+
//#region src/commands/create.ts
|
|
411
|
+
/** Version specifier used for every `@cascivo/*` dependency in generated apps. */
|
|
412
|
+
const CASCIVO_DEP = "latest";
|
|
413
|
+
/** Slug suitable for a union-member string literal: lower-kebab, alnum only. */
|
|
414
|
+
function slug(label) {
|
|
415
|
+
return label.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "section";
|
|
416
|
+
}
|
|
417
|
+
/** PascalCase identifier derived from a free-text label. */
|
|
418
|
+
function pascalCase(label) {
|
|
419
|
+
const safe = label.trim().split(/[^a-zA-Z0-9]+/).filter(Boolean).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("") || "Section";
|
|
420
|
+
return /^[0-9]/.test(safe) ? `Section${safe}` : safe;
|
|
421
|
+
}
|
|
422
|
+
/** Normalize the project name into a valid npm package name. */
|
|
423
|
+
function packageName(name) {
|
|
424
|
+
return name.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "cascivo-app";
|
|
425
|
+
}
|
|
426
|
+
/** Turn raw section labels into unique keyed/component-named sections. */
|
|
427
|
+
function resolveSections(labels) {
|
|
428
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
429
|
+
const seenComponents = /* @__PURE__ */ new Set();
|
|
430
|
+
const sections = [];
|
|
431
|
+
for (const label of labels) {
|
|
432
|
+
const trimmed = label.trim();
|
|
433
|
+
if (!trimmed) continue;
|
|
434
|
+
let key = slug(trimmed);
|
|
435
|
+
let component = pascalCase(trimmed);
|
|
436
|
+
let n = 2;
|
|
437
|
+
while (seenKeys.has(key) || seenComponents.has(component)) {
|
|
438
|
+
key = `${slug(trimmed)}-${n}`;
|
|
439
|
+
component = `${pascalCase(trimmed)}${n}`;
|
|
440
|
+
n++;
|
|
441
|
+
}
|
|
442
|
+
seenKeys.add(key);
|
|
443
|
+
seenComponents.add(component);
|
|
444
|
+
sections.push({
|
|
445
|
+
key,
|
|
446
|
+
label: trimmed,
|
|
447
|
+
component
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
return sections.length > 0 ? sections : [{
|
|
451
|
+
key: "home",
|
|
452
|
+
label: "Home",
|
|
453
|
+
component: "Home"
|
|
454
|
+
}];
|
|
455
|
+
}
|
|
456
|
+
function packageJson(opts) {
|
|
457
|
+
const pkg = {
|
|
458
|
+
name: packageName(opts.name),
|
|
459
|
+
private: true,
|
|
460
|
+
version: "0.0.0",
|
|
461
|
+
type: "module",
|
|
462
|
+
scripts: {
|
|
463
|
+
dev: "vite",
|
|
464
|
+
build: "tsc && vite build",
|
|
465
|
+
preview: "vite preview"
|
|
466
|
+
},
|
|
467
|
+
dependencies: {
|
|
468
|
+
"@cascivo/core": CASCIVO_DEP,
|
|
469
|
+
"@cascivo/react": CASCIVO_DEP,
|
|
470
|
+
"@cascivo/themes": CASCIVO_DEP,
|
|
471
|
+
"@cascivo/tokens": CASCIVO_DEP,
|
|
472
|
+
react: "^19.0.0",
|
|
473
|
+
"react-dom": "^19.0.0"
|
|
474
|
+
},
|
|
475
|
+
devDependencies: {
|
|
476
|
+
"@types/react": "^19.0.0",
|
|
477
|
+
"@types/react-dom": "^19.0.0",
|
|
478
|
+
"@vitejs/plugin-react": "^5.0.0",
|
|
479
|
+
typescript: "^5.7.0",
|
|
480
|
+
vite: "^7.0.0"
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
return JSON.stringify(pkg, null, 2) + "\n";
|
|
484
|
+
}
|
|
485
|
+
function tsconfig() {
|
|
486
|
+
return JSON.stringify({
|
|
487
|
+
compilerOptions: {
|
|
488
|
+
target: "ES2022",
|
|
489
|
+
useDefineForClassFields: true,
|
|
490
|
+
lib: [
|
|
491
|
+
"ES2022",
|
|
492
|
+
"DOM",
|
|
493
|
+
"DOM.Iterable"
|
|
494
|
+
],
|
|
495
|
+
module: "ESNext",
|
|
496
|
+
skipLibCheck: true,
|
|
497
|
+
moduleResolution: "bundler",
|
|
498
|
+
allowImportingTsExtensions: true,
|
|
499
|
+
resolveJsonModule: true,
|
|
500
|
+
isolatedModules: true,
|
|
501
|
+
moduleDetection: "force",
|
|
502
|
+
noEmit: true,
|
|
503
|
+
jsx: "react-jsx",
|
|
504
|
+
strict: true,
|
|
505
|
+
noUnusedLocals: true,
|
|
506
|
+
noUnusedParameters: true,
|
|
507
|
+
noFallthroughCasesInSwitch: true
|
|
508
|
+
},
|
|
509
|
+
include: ["src"]
|
|
510
|
+
}, null, 2) + "\n";
|
|
511
|
+
}
|
|
512
|
+
function viteConfig() {
|
|
513
|
+
return `import react from '@vitejs/plugin-react'
|
|
514
|
+
import { defineConfig } from 'vite'
|
|
515
|
+
|
|
516
|
+
export default defineConfig({
|
|
517
|
+
plugins: [react()],
|
|
518
|
+
})
|
|
519
|
+
`;
|
|
520
|
+
}
|
|
521
|
+
function indexHtml(opts) {
|
|
522
|
+
return `<!doctype html>
|
|
523
|
+
<html lang="en" data-theme="${opts.theme}">
|
|
524
|
+
<head>
|
|
525
|
+
<meta charset="UTF-8" />
|
|
526
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
527
|
+
<title>${opts.name}</title>
|
|
528
|
+
<style>
|
|
529
|
+
@layer cascivo.reset, cascivo.tokens, cascivo.component, cascivo.theme;
|
|
530
|
+
@layer cascivo.reset {
|
|
531
|
+
*,
|
|
532
|
+
*::before,
|
|
533
|
+
*::after {
|
|
534
|
+
box-sizing: border-box;
|
|
535
|
+
margin: 0;
|
|
536
|
+
padding: 0;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
html,
|
|
540
|
+
body,
|
|
541
|
+
#root {
|
|
542
|
+
height: 100%;
|
|
543
|
+
}
|
|
544
|
+
</style>
|
|
545
|
+
</head>
|
|
546
|
+
<body>
|
|
547
|
+
<div id="root"></div>
|
|
548
|
+
<script type="module" src="/src/main.tsx"><\/script>
|
|
549
|
+
</body>
|
|
550
|
+
</html>
|
|
551
|
+
`;
|
|
552
|
+
}
|
|
553
|
+
function mainTsx() {
|
|
554
|
+
return `import React from 'react'
|
|
555
|
+
import ReactDOM from 'react-dom/client'
|
|
556
|
+
import App from './App'
|
|
557
|
+
|
|
558
|
+
const root = document.getElementById('root')
|
|
559
|
+
if (root) {
|
|
560
|
+
ReactDOM.createRoot(root).render(
|
|
561
|
+
<React.StrictMode>
|
|
562
|
+
<App />
|
|
563
|
+
</React.StrictMode>,
|
|
564
|
+
)
|
|
565
|
+
}
|
|
566
|
+
`;
|
|
567
|
+
}
|
|
568
|
+
function viteEnv() {
|
|
569
|
+
return `/// <reference types="vite/client" />\n`;
|
|
570
|
+
}
|
|
571
|
+
function appTsx(opts, sections) {
|
|
572
|
+
const sectionImports = sections.map((s) => `import { ${s.component} } from './sections/${s.component}'`).join("\n");
|
|
573
|
+
const unionType = sections.map((s) => `'${s.key}'`).join(" | ");
|
|
574
|
+
const navItems = sections.map((s) => ` {
|
|
575
|
+
label: '${s.label.replace(/'/g, "\\'")}',
|
|
576
|
+
active: section.value === '${s.key}',
|
|
577
|
+
onClick: (e) => {
|
|
578
|
+
e.preventDefault()
|
|
579
|
+
section.value = '${s.key}'
|
|
580
|
+
},
|
|
581
|
+
},`).join("\n");
|
|
582
|
+
const renderedSections = sections.map((s) => ` {section.value === '${s.key}' && <${s.component} />}`).join("\n");
|
|
583
|
+
return `'use client'
|
|
584
|
+
import { signal, useSignals } from '@cascivo/core'
|
|
585
|
+
import { AppShell, ShellHeader, SideNav, type SideNavItem } from '@cascivo/react'
|
|
586
|
+
${sectionImports}
|
|
587
|
+
|
|
588
|
+
import '@cascivo/tokens'
|
|
589
|
+
import '@cascivo/themes/${opts.theme}.css'
|
|
590
|
+
import '@cascivo/react/styles.css'
|
|
591
|
+
|
|
592
|
+
type Section = ${unionType}
|
|
593
|
+
|
|
594
|
+
const section = signal<Section>('${sections[0].key}')
|
|
595
|
+
|
|
596
|
+
export default function App() {
|
|
597
|
+
useSignals()
|
|
598
|
+
|
|
599
|
+
const navItems: SideNavItem[] = [
|
|
600
|
+
${navItems}
|
|
601
|
+
]
|
|
602
|
+
|
|
603
|
+
return (
|
|
604
|
+
<AppShell
|
|
605
|
+
header={<ShellHeader brand={{ name: '${opts.name.replace(/'/g, "\\'")}' }} />}
|
|
606
|
+
nav={<SideNav items={navItems} />}
|
|
607
|
+
>
|
|
608
|
+
${renderedSections}
|
|
609
|
+
</AppShell>
|
|
610
|
+
)
|
|
611
|
+
}
|
|
612
|
+
`;
|
|
613
|
+
}
|
|
614
|
+
function sectionTsx(section) {
|
|
615
|
+
return `import { Card, CardContent, CardHeader, CardTitle, Heading, Text } from '@cascivo/react'
|
|
616
|
+
|
|
617
|
+
export function ${section.component}() {
|
|
618
|
+
return (
|
|
619
|
+
<div
|
|
620
|
+
style={{
|
|
621
|
+
display: 'grid',
|
|
622
|
+
gap: 'var(--cascivo-space-6)',
|
|
623
|
+
padding: 'var(--cascivo-space-6)',
|
|
624
|
+
maxWidth: '64rem',
|
|
625
|
+
}}
|
|
626
|
+
>
|
|
627
|
+
<div style={{ display: 'grid', gap: 'var(--cascivo-space-2)' }}>
|
|
628
|
+
<Heading level={1}>${section.label}</Heading>
|
|
629
|
+
<Text muted>
|
|
630
|
+
Edit <code>src/sections/${section.component}.tsx</code> to build out this page.
|
|
631
|
+
</Text>
|
|
632
|
+
</div>
|
|
633
|
+
|
|
634
|
+
<Card>
|
|
635
|
+
<CardHeader>
|
|
636
|
+
<CardTitle>Get started</CardTitle>
|
|
637
|
+
</CardHeader>
|
|
638
|
+
<CardContent>
|
|
639
|
+
<Text>
|
|
640
|
+
This page is wired into the app shell. Add components with{' '}
|
|
641
|
+
<code>npx cascivo add <component></code>.
|
|
642
|
+
</Text>
|
|
643
|
+
</CardContent>
|
|
644
|
+
</Card>
|
|
645
|
+
</div>
|
|
646
|
+
)
|
|
647
|
+
}
|
|
648
|
+
`;
|
|
649
|
+
}
|
|
650
|
+
function cascivoConfig(opts) {
|
|
651
|
+
return `import type { CascadeConfig } from 'cascivo'
|
|
652
|
+
|
|
653
|
+
const config: CascadeConfig = {
|
|
654
|
+
registry: '${CASCIVO_HOST}/registry.json',
|
|
655
|
+
outputDir: 'src/components/ui',
|
|
656
|
+
theme: '${opts.theme}',
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
export default config
|
|
660
|
+
`;
|
|
661
|
+
}
|
|
662
|
+
function gitignore() {
|
|
663
|
+
return `node_modules
|
|
664
|
+
dist
|
|
665
|
+
*.local
|
|
666
|
+
.DS_Store
|
|
667
|
+
`;
|
|
668
|
+
}
|
|
669
|
+
function readme(opts) {
|
|
670
|
+
return `# ${opts.name}
|
|
671
|
+
|
|
672
|
+
A [cascivo](https://cascivo.com) app — Vite + React + TypeScript, pre-wired with
|
|
673
|
+
the cascivo app shell, side navigation, and the \`${opts.theme}\` theme.
|
|
674
|
+
|
|
675
|
+
## Develop
|
|
676
|
+
|
|
677
|
+
\`\`\`sh
|
|
678
|
+
npm install
|
|
679
|
+
npm run dev
|
|
680
|
+
\`\`\`
|
|
681
|
+
|
|
682
|
+
## Structure
|
|
683
|
+
|
|
684
|
+
- \`src/App.tsx\` — app shell, navigation, and section routing
|
|
685
|
+
- \`src/sections/\` — one component per nav item
|
|
686
|
+
|
|
687
|
+
Add more components with \`npx cascivo add <component>\`.
|
|
688
|
+
`;
|
|
689
|
+
}
|
|
690
|
+
/** Build the full set of files for a new cascivo app. Pure — no filesystem I/O. */
|
|
691
|
+
function buildScaffold(opts) {
|
|
692
|
+
const sections = resolveSections(opts.sections);
|
|
693
|
+
return [
|
|
694
|
+
{
|
|
695
|
+
path: "package.json",
|
|
696
|
+
contents: packageJson(opts)
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
path: "tsconfig.json",
|
|
700
|
+
contents: tsconfig()
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
path: "vite.config.ts",
|
|
704
|
+
contents: viteConfig()
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
path: "index.html",
|
|
708
|
+
contents: indexHtml(opts)
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
path: "cascivo.config.ts",
|
|
712
|
+
contents: cascivoConfig(opts)
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
path: ".gitignore",
|
|
716
|
+
contents: gitignore()
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
path: "README.md",
|
|
720
|
+
contents: readme(opts)
|
|
721
|
+
},
|
|
722
|
+
{
|
|
723
|
+
path: "src/main.tsx",
|
|
724
|
+
contents: mainTsx()
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
path: "src/vite-env.d.ts",
|
|
728
|
+
contents: viteEnv()
|
|
729
|
+
},
|
|
730
|
+
{
|
|
731
|
+
path: "src/App.tsx",
|
|
732
|
+
contents: appTsx(opts, sections)
|
|
733
|
+
},
|
|
734
|
+
...sections.map((s) => ({
|
|
735
|
+
path: `src/sections/${s.component}.tsx`,
|
|
736
|
+
contents: sectionTsx(s)
|
|
737
|
+
}))
|
|
738
|
+
];
|
|
739
|
+
}
|
|
740
|
+
function flagValue$1(args, key) {
|
|
741
|
+
const eq = args.find((a) => a.startsWith(`--${key}=`));
|
|
742
|
+
if (eq) return eq.slice(key.length + 3);
|
|
743
|
+
const idx = args.indexOf(`--${key}`);
|
|
744
|
+
const next = idx !== -1 ? args[idx + 1] : void 0;
|
|
745
|
+
return next && !next.startsWith("--") ? next : void 0;
|
|
746
|
+
}
|
|
747
|
+
const DEFAULT_SECTIONS = [
|
|
748
|
+
"Dashboard",
|
|
749
|
+
"Reports",
|
|
750
|
+
"Settings"
|
|
751
|
+
];
|
|
752
|
+
async function create(args, cwd = process.cwd()) {
|
|
753
|
+
const yes = args.includes("--yes") || args.includes("-y");
|
|
754
|
+
const nameArg = args.find((a) => !a.startsWith("-"));
|
|
755
|
+
const themeArg = flagValue$1(args, "theme");
|
|
756
|
+
const sectionsArg = flagValue$1(args, "sections");
|
|
757
|
+
const rl = !yes && stdin.isTTY ? createInterface({
|
|
758
|
+
input: stdin,
|
|
759
|
+
output: stdout
|
|
760
|
+
}) : null;
|
|
761
|
+
try {
|
|
762
|
+
let name = nameArg;
|
|
763
|
+
if (!name && rl) name = (await rl.question("Project name? [my-cascivo-app]: ")).trim();
|
|
764
|
+
name = name || "my-cascivo-app";
|
|
765
|
+
let theme = (themeArg ?? "").toLowerCase();
|
|
766
|
+
if (!THEMES.includes(theme) && rl) theme = (await rl.question(`Theme? (${THEMES.join("/")}) [light]: `)).trim().toLowerCase();
|
|
767
|
+
const resolvedTheme = THEMES.includes(theme) ? theme : "light";
|
|
768
|
+
let sectionsInput = sectionsArg;
|
|
769
|
+
if (!sectionsInput && rl) sectionsInput = (await rl.question(`Nav sections? (comma-separated) [${DEFAULT_SECTIONS.join(", ")}]: `)).trim();
|
|
770
|
+
const sections = (sectionsInput ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
771
|
+
const opts = {
|
|
772
|
+
name,
|
|
773
|
+
theme: resolvedTheme,
|
|
774
|
+
sections: sections.length > 0 ? sections : DEFAULT_SECTIONS
|
|
775
|
+
};
|
|
776
|
+
const targetDir = join(cwd, name);
|
|
777
|
+
if (existsSync(targetDir) && readdirSync(targetDir).length > 0) {
|
|
778
|
+
console.error(`Target directory "${name}" already exists and is not empty.`);
|
|
779
|
+
process.exitCode = 1;
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
const files = buildScaffold(opts);
|
|
783
|
+
for (const file of files) await writeFileSafe(join(targetDir, file.path), file.contents);
|
|
784
|
+
console.log(`\nCreated ${name} with the ${resolvedTheme} theme (${files.length} files).`);
|
|
785
|
+
const templateSpec = flagValue$1(args, "template");
|
|
786
|
+
if (templateSpec) {
|
|
787
|
+
const { add } = await Promise.resolve().then(() => add_exports);
|
|
788
|
+
const { loadConfig } = await import("./config-BsyJbMvD.mjs").then((n) => n.i);
|
|
789
|
+
console.log(`\nInstalling template "${templateSpec}"…`);
|
|
790
|
+
await add([templateSpec], await loadConfig(), { cwd: targetDir });
|
|
791
|
+
}
|
|
792
|
+
console.log("\nNext steps:");
|
|
793
|
+
console.log(` cd ${name}`);
|
|
794
|
+
console.log(" npm install");
|
|
795
|
+
console.log(" npm run dev");
|
|
796
|
+
} finally {
|
|
797
|
+
rl?.close();
|
|
798
|
+
}
|
|
458
799
|
}
|
|
459
800
|
//#endregion
|
|
460
801
|
//#region src/commands/doctor.ts
|
|
@@ -465,6 +806,47 @@ const BANNED_HOOKS = [
|
|
|
465
806
|
"useContext",
|
|
466
807
|
"useReducer"
|
|
467
808
|
];
|
|
809
|
+
/**
|
|
810
|
+
* Blank out comments and string/template-literal contents so identifier scans
|
|
811
|
+
* only match real code. A mention of `useEffect` in a comment must not trip
|
|
812
|
+
* the no-react-hooks rule. Single pass — a `//` inside a string (URLs) is not
|
|
813
|
+
* treated as a comment.
|
|
814
|
+
*/
|
|
815
|
+
function stripCommentsAndStrings(source) {
|
|
816
|
+
let out = "";
|
|
817
|
+
let i = 0;
|
|
818
|
+
const n = source.length;
|
|
819
|
+
while (i < n) {
|
|
820
|
+
const c = source[i];
|
|
821
|
+
const next = source[i + 1];
|
|
822
|
+
if (c === "/" && next === "/") while (i < n && source[i] !== "\n") i++;
|
|
823
|
+
else if (c === "/" && next === "*") {
|
|
824
|
+
i += 2;
|
|
825
|
+
while (i < n && !(source[i] === "*" && source[i + 1] === "/")) i++;
|
|
826
|
+
i += 2;
|
|
827
|
+
} else if (c === "'" || c === "\"") {
|
|
828
|
+
i++;
|
|
829
|
+
while (i < n && source[i] !== c) {
|
|
830
|
+
if (source[i] === "\\") i++;
|
|
831
|
+
i++;
|
|
832
|
+
}
|
|
833
|
+
i++;
|
|
834
|
+
out += c + c;
|
|
835
|
+
} else if (c === "`") {
|
|
836
|
+
i++;
|
|
837
|
+
while (i < n && source[i] !== "`") {
|
|
838
|
+
if (source[i] === "\\") i++;
|
|
839
|
+
i++;
|
|
840
|
+
}
|
|
841
|
+
i++;
|
|
842
|
+
out += "``";
|
|
843
|
+
} else {
|
|
844
|
+
out += c;
|
|
845
|
+
i++;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return out;
|
|
849
|
+
}
|
|
468
850
|
async function runDoctor(root) {
|
|
469
851
|
const violations = [];
|
|
470
852
|
const componentsDir = join(root, "packages", "components", "src");
|
|
@@ -479,7 +861,8 @@ async function runDoctor(root) {
|
|
|
479
861
|
const tsxPath = join(componentsDir, name, `${name}.tsx`);
|
|
480
862
|
if (!existsSync(tsxPath)) continue;
|
|
481
863
|
const content = readFileSync(tsxPath, "utf8");
|
|
482
|
-
|
|
864
|
+
const code = stripCommentsAndStrings(content);
|
|
865
|
+
for (const hook of BANNED_HOOKS) if (new RegExp(`\\b${hook}\\b`).test(code)) violations.push({
|
|
483
866
|
file: tsxPath,
|
|
484
867
|
rule: "no-react-hooks",
|
|
485
868
|
detail: `Banned hook '${hook}' in ${name}.tsx`
|
|
@@ -623,19 +1006,21 @@ async function generate(args, config) {
|
|
|
623
1006
|
}
|
|
624
1007
|
//#endregion
|
|
625
1008
|
//#region src/commands/init.ts
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
];
|
|
1009
|
+
function flagValue(args, key) {
|
|
1010
|
+
const eq = args.find((a) => a.startsWith(`--${key}=`));
|
|
1011
|
+
if (eq) return eq.slice(key.length + 3);
|
|
1012
|
+
const idx = args.indexOf(`--${key}`);
|
|
1013
|
+
const next = idx !== -1 ? args[idx + 1] : void 0;
|
|
1014
|
+
return next && !next.startsWith("--") ? next : void 0;
|
|
1015
|
+
}
|
|
631
1016
|
async function promptTheme() {
|
|
632
1017
|
const rl = createInterface({
|
|
633
1018
|
input: stdin,
|
|
634
1019
|
output: stdout
|
|
635
1020
|
});
|
|
636
1021
|
try {
|
|
637
|
-
const answer = (await rl.question(
|
|
638
|
-
return THEMES
|
|
1022
|
+
const answer = (await rl.question(`Theme? (${THEMES.join("/")}) [light]: `)).trim().toLowerCase();
|
|
1023
|
+
return THEMES.includes(answer) ? answer : "light";
|
|
639
1024
|
} finally {
|
|
640
1025
|
rl.close();
|
|
641
1026
|
}
|
|
@@ -644,17 +1029,25 @@ function configFileContents(theme) {
|
|
|
644
1029
|
return `import type { CascadeConfig } from 'cascivo'
|
|
645
1030
|
|
|
646
1031
|
const config: CascadeConfig = {
|
|
647
|
-
registry: '${DEFAULT_CONFIG.registry}',
|
|
648
|
-
outputDir: '${DEFAULT_CONFIG.outputDir}',
|
|
1032
|
+
registry: '${DEFAULT_CONFIG$1.registry}',
|
|
1033
|
+
outputDir: '${DEFAULT_CONFIG$1.outputDir}',
|
|
649
1034
|
theme: '${theme}',
|
|
650
1035
|
}
|
|
651
1036
|
|
|
652
1037
|
export default config
|
|
653
1038
|
`;
|
|
654
1039
|
}
|
|
655
|
-
async function init(cwd = process.cwd()) {
|
|
1040
|
+
async function init(args = [], cwd = process.cwd()) {
|
|
1041
|
+
const yes = args.includes("--yes") || args.includes("-y");
|
|
1042
|
+
const themeArg = flagValue(args, "theme")?.toLowerCase();
|
|
1043
|
+
if (themeArg !== void 0 && !THEMES.includes(themeArg)) {
|
|
1044
|
+
console.error(`Unknown theme "${themeArg}". Available: ${THEMES.join(", ")}`);
|
|
1045
|
+
process.exitCode = 1;
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
656
1048
|
installPackages(["@cascivo/core", "@cascivo/tokens"], cwd);
|
|
657
|
-
const
|
|
1049
|
+
const interactive = themeArg === void 0 && !yes && stdin.isTTY;
|
|
1050
|
+
const theme = themeArg ?? (interactive ? await promptTheme() : "light");
|
|
658
1051
|
await writeFileSafe(join(cwd, "cascivo.config.ts"), configFileContents(theme));
|
|
659
1052
|
console.log(`\nCreated cascivo.config.ts (theme: ${theme})`);
|
|
660
1053
|
console.log("Import the theme in your root CSS or entry file:");
|
|
@@ -667,9 +1060,12 @@ async function init(cwd = process.cwd()) {
|
|
|
667
1060
|
//#region src/commands/list.ts
|
|
668
1061
|
const TYPE_LABELS = {
|
|
669
1062
|
component: "Components",
|
|
670
|
-
layout: "Layouts",
|
|
671
|
-
block: "Blocks",
|
|
672
|
-
section: "Sections"
|
|
1063
|
+
layout: "Layouts (copy-paste)",
|
|
1064
|
+
block: "Blocks (copy-paste)",
|
|
1065
|
+
section: "Sections (copy-paste)",
|
|
1066
|
+
chart: "Charts (npm: @cascivo/charts)",
|
|
1067
|
+
flow: "Flow (npm: @cascivo/flow)",
|
|
1068
|
+
editor: "Editor (npm: @cascivo/editor)"
|
|
673
1069
|
};
|
|
674
1070
|
/** Render a group of entries as an aligned text table (no section header). */
|
|
675
1071
|
function formatGroup(entries) {
|
|
@@ -716,9 +1112,30 @@ async function list(config, options = {}) {
|
|
|
716
1112
|
return existsSync(join(process.cwd(), config.outputDir, outputName));
|
|
717
1113
|
});
|
|
718
1114
|
console.log(formatList(components));
|
|
1115
|
+
console.log("\nTip: install any entry by its bare name — `cascivo add stack` resolves to `layout/stack`. Layout primitives (Stack, Grid, AutoGrid, Columns, Spacer, Center) replace inline-style layout; see docs/cookbooks/layout-and-spacing.md.");
|
|
719
1116
|
}
|
|
720
1117
|
//#endregion
|
|
721
1118
|
//#region src/commands/registry.ts
|
|
1119
|
+
/**
|
|
1120
|
+
* Validate every template item in an index: each must pass `validateTemplate`,
|
|
1121
|
+
* and any bare `registryDependencies` not present in the same index produce a
|
|
1122
|
+
* warning (they are expected to resolve from another registry at install time).
|
|
1123
|
+
* Returns `{ errors, warnings }`.
|
|
1124
|
+
*/
|
|
1125
|
+
function validateTemplates(index) {
|
|
1126
|
+
const errors = [];
|
|
1127
|
+
const warnings = [];
|
|
1128
|
+
const localNames = new Set(index.items.map((i) => i.name));
|
|
1129
|
+
for (const item of index.items) {
|
|
1130
|
+
if (!isTemplateItem(item)) continue;
|
|
1131
|
+
for (const e of validateTemplate(item)) errors.push(`template "${item.name}": ${e}`);
|
|
1132
|
+
for (const dep of item.registryDependencies ?? []) if (!(dep.includes("/") || dep.startsWith("@") || dep.startsWith("http")) && !localNames.has(dep)) warnings.push(`template "${item.name}": component "${dep}" is not in this index — it must resolve from another registry at install time`);
|
|
1133
|
+
}
|
|
1134
|
+
return {
|
|
1135
|
+
errors,
|
|
1136
|
+
warnings
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
722
1139
|
async function registryBuild(args) {
|
|
723
1140
|
let inFile = "cascade-registry.json";
|
|
724
1141
|
let outDir = "public/r";
|
|
@@ -748,6 +1165,13 @@ async function registryBuild(args) {
|
|
|
748
1165
|
process.exitCode = 1;
|
|
749
1166
|
return;
|
|
750
1167
|
}
|
|
1168
|
+
const templateResult = validateTemplates(index);
|
|
1169
|
+
for (const w of templateResult.warnings) console.warn(`warn: ${w}`);
|
|
1170
|
+
if (templateResult.errors.length > 0) {
|
|
1171
|
+
for (const e of templateResult.errors) console.error(`error: ${e}`);
|
|
1172
|
+
process.exitCode = 1;
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
751
1175
|
const outPath = resolve(outDir);
|
|
752
1176
|
await buildRegistry(index, outPath);
|
|
753
1177
|
console.log(`cascade registry build: wrote static output to ${outPath}`);
|
|
@@ -763,7 +1187,7 @@ async function search(args, config) {
|
|
|
763
1187
|
const registryFilter = nsFlag !== -1 ? args[nsFlag + 1] : void 0;
|
|
764
1188
|
const query = args.filter((a, i) => a !== "--registry" && args[i - 1] !== "--registry").join(" ").trim();
|
|
765
1189
|
if (!query) {
|
|
766
|
-
console.error("Usage:
|
|
1190
|
+
console.error("Usage: cascivo search <query> [--registry @ns]");
|
|
767
1191
|
return;
|
|
768
1192
|
}
|
|
769
1193
|
const results = [];
|
|
@@ -800,25 +1224,224 @@ async function search(args, config) {
|
|
|
800
1224
|
}
|
|
801
1225
|
}
|
|
802
1226
|
//#endregion
|
|
1227
|
+
//#region ../theme-kit/src/config.ts
|
|
1228
|
+
const DEFAULT_CONFIG = {
|
|
1229
|
+
baseMode: "light",
|
|
1230
|
+
accentHue: 250,
|
|
1231
|
+
accentChroma: .2,
|
|
1232
|
+
radiusBase: .375,
|
|
1233
|
+
fontFamily: "system",
|
|
1234
|
+
presetId: "light"
|
|
1235
|
+
};
|
|
1236
|
+
//#endregion
|
|
1237
|
+
//#region ../theme-kit/src/hash.ts
|
|
1238
|
+
/** Decode a hash produced by {@link configToHash}. Returns null on malformed
|
|
1239
|
+
* input; unknown/missing fields fall back to {@link DEFAULT_CONFIG}. */
|
|
1240
|
+
function hashToConfig(hash) {
|
|
1241
|
+
if (!hash) return null;
|
|
1242
|
+
try {
|
|
1243
|
+
const base64 = hash.replace(/-/g, "+").replace(/_/g, "/");
|
|
1244
|
+
const json = atob(base64);
|
|
1245
|
+
const parsed = JSON.parse(json);
|
|
1246
|
+
return {
|
|
1247
|
+
baseMode: parsed.baseMode === "dark" ? "dark" : "light",
|
|
1248
|
+
accentHue: typeof parsed.accentHue === "number" ? parsed.accentHue : DEFAULT_CONFIG.accentHue,
|
|
1249
|
+
accentChroma: typeof parsed.accentChroma === "number" ? parsed.accentChroma : DEFAULT_CONFIG.accentChroma,
|
|
1250
|
+
radiusBase: typeof parsed.radiusBase === "number" ? parsed.radiusBase : DEFAULT_CONFIG.radiusBase,
|
|
1251
|
+
fontFamily: parsed.fontFamily ?? DEFAULT_CONFIG.fontFamily,
|
|
1252
|
+
presetId: parsed.presetId ?? null
|
|
1253
|
+
};
|
|
1254
|
+
} catch {
|
|
1255
|
+
return null;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
//#endregion
|
|
1259
|
+
//#region ../theme-kit/src/css.ts
|
|
1260
|
+
const FONT_STACKS = {
|
|
1261
|
+
geometric: "'Futura', 'Century Gothic', 'Trebuchet MS', ui-sans-serif, sans-serif",
|
|
1262
|
+
humanist: "'Gill Sans', 'Trebuchet MS', 'Calibri', ui-sans-serif, sans-serif",
|
|
1263
|
+
transitional: "'Optima', 'Candara', 'Palatino Linotype', ui-sans-serif, sans-serif",
|
|
1264
|
+
mono: "ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace"
|
|
1265
|
+
};
|
|
1266
|
+
const BASE_LIGHT = `
|
|
1267
|
+
color-scheme: light;
|
|
1268
|
+
--cascivo-color-background: oklch(1 0 0);
|
|
1269
|
+
--cascivo-color-surface: oklch(0.985 0.002 264);
|
|
1270
|
+
--cascivo-color-surface-2: oklch(0.967 0.003 264);
|
|
1271
|
+
--cascivo-color-bg: var(--cascivo-color-background);
|
|
1272
|
+
--cascivo-color-bg-subtle: var(--cascivo-color-surface);
|
|
1273
|
+
--cascivo-color-surface-raised: var(--cascivo-color-surface);
|
|
1274
|
+
--cascivo-color-surface-overlay: var(--cascivo-color-background);
|
|
1275
|
+
--cascivo-color-border: oklch(0.928 0.006 264);
|
|
1276
|
+
--cascivo-color-border-strong: oklch(0.872 0.008 264);
|
|
1277
|
+
--cascivo-border-subtle: oklch(0.928 0.006 264);
|
|
1278
|
+
--cascivo-border-default: oklch(0.872 0.008 264);
|
|
1279
|
+
--cascivo-border-strong: oklch(0.707 0.015 264);
|
|
1280
|
+
--cascivo-color-foreground: oklch(0.145 0.005 264);
|
|
1281
|
+
--cascivo-color-foreground-muted: oklch(0.554 0.018 264);
|
|
1282
|
+
--cascivo-color-text: var(--cascivo-color-foreground);
|
|
1283
|
+
--cascivo-color-text-subtle: oklch(0.446 0.018 264);
|
|
1284
|
+
--cascivo-color-text-muted: oklch(0.707 0.015 264);
|
|
1285
|
+
--cascivo-color-text-on-accent: oklch(1 0 0);
|
|
1286
|
+
--cascivo-color-text-on-destructive: oklch(1 0 0);
|
|
1287
|
+
--cascivo-color-primary: oklch(0.205 0 0);
|
|
1288
|
+
--cascivo-color-primary-fg: oklch(0.985 0 0);
|
|
1289
|
+
--cascivo-color-primary-hover: oklch(0.27 0 0);
|
|
1290
|
+
--cascivo-color-primary-active: oklch(0.32 0 0);
|
|
1291
|
+
--cascivo-color-active-bg: oklch(0.145 0.005 264 / 6%);
|
|
1292
|
+
--cascivo-color-destructive: oklch(0.54 0.188 22);
|
|
1293
|
+
--cascivo-color-destructive-hover: oklch(0.448 0.17 22);
|
|
1294
|
+
--cascivo-color-destructive-subtle: oklch(0.971 0.013 22);
|
|
1295
|
+
--cascivo-color-error: oklch(0.628 0.188 22);
|
|
1296
|
+
--cascivo-color-warning: oklch(0.768 0.145 75);
|
|
1297
|
+
--cascivo-color-success: oklch(0.648 0.15 145);
|
|
1298
|
+
--cascivo-color-success-subtle: oklch(0.982 0.018 145);
|
|
1299
|
+
--cascivo-color-warning-subtle: oklch(0.98 0.02 75);
|
|
1300
|
+
--cascivo-color-info: oklch(0.546 0.224 250);
|
|
1301
|
+
--cascivo-color-info-subtle: oklch(0.97 0.025 250);
|
|
1302
|
+
--cascivo-color-success-foreground: oklch(0.45 0.14 145);
|
|
1303
|
+
--cascivo-color-warning-foreground: oklch(0.5 0.14 75);
|
|
1304
|
+
--cascivo-shadow-xs: 0 1px 2px oklch(0 0 0 / 0.05);
|
|
1305
|
+
--cascivo-shadow-sm: 0 1px 3px oklch(0 0 0 / 0.07), 0 1px 2px oklch(0 0 0 / 0.04);
|
|
1306
|
+
--cascivo-shadow-md: 0 2px 8px oklch(0 0 0 / 0.07), 0 1px 2px oklch(0 0 0 / 0.04);
|
|
1307
|
+
--cascivo-shadow-overlay: 0 4px 32px oklch(0 0 0 / 0.16), 0 0 0 1px oklch(0 0 0 / 0.04);
|
|
1308
|
+
--cascivo-shadow-lg: var(--cascivo-shadow-overlay);
|
|
1309
|
+
--cascivo-ring-width: 2px;
|
|
1310
|
+
--cascivo-ring-offset: 0px;
|
|
1311
|
+
--cascivo-ring-color: color-mix(in oklch, var(--cascivo-color-accent) 55%, transparent);
|
|
1312
|
+
--cascivo-focus-ring: 0 0 0 var(--cascivo-ring-width) var(--cascivo-ring-color);`;
|
|
1313
|
+
const BASE_DARK = `
|
|
1314
|
+
color-scheme: dark;
|
|
1315
|
+
--cascivo-color-background: oklch(0.145 0.005 250);
|
|
1316
|
+
--cascivo-color-surface: oklch(0.185 0.007 250);
|
|
1317
|
+
--cascivo-color-surface-2: oklch(0.22 0.008 250);
|
|
1318
|
+
--cascivo-color-bg: var(--cascivo-color-background);
|
|
1319
|
+
--cascivo-color-bg-subtle: var(--cascivo-color-surface);
|
|
1320
|
+
--cascivo-color-surface-raised: var(--cascivo-color-surface-2);
|
|
1321
|
+
--cascivo-color-surface-overlay: var(--cascivo-color-surface);
|
|
1322
|
+
--cascivo-color-border: oklch(1 0 0 / 10%);
|
|
1323
|
+
--cascivo-color-border-strong: oklch(1 0 0 / 16%);
|
|
1324
|
+
--cascivo-border-subtle: oklch(1 0 0 / 0.06);
|
|
1325
|
+
--cascivo-border-default: oklch(1 0 0 / 0.1);
|
|
1326
|
+
--cascivo-border-strong: oklch(1 0 0 / 0.2);
|
|
1327
|
+
--cascivo-color-foreground: oklch(0.985 0.002 264);
|
|
1328
|
+
--cascivo-color-foreground-muted: oklch(0.707 0.015 264);
|
|
1329
|
+
--cascivo-color-text: var(--cascivo-color-foreground);
|
|
1330
|
+
--cascivo-color-text-subtle: oklch(0.707 0.015 264);
|
|
1331
|
+
--cascivo-color-text-muted: oklch(0.554 0.018 264);
|
|
1332
|
+
--cascivo-color-text-on-accent: oklch(0.145 0.005 250);
|
|
1333
|
+
--cascivo-color-text-on-destructive: oklch(1 0 0);
|
|
1334
|
+
--cascivo-color-primary: oklch(0.922 0 0);
|
|
1335
|
+
--cascivo-color-primary-fg: oklch(0.205 0 0);
|
|
1336
|
+
--cascivo-color-primary-hover: oklch(0.86 0 0);
|
|
1337
|
+
--cascivo-color-primary-active: oklch(0.8 0 0);
|
|
1338
|
+
--cascivo-color-active-bg: oklch(1 0 0 / 8%);
|
|
1339
|
+
--cascivo-color-destructive: oklch(0.72 0.16 22);
|
|
1340
|
+
--cascivo-color-destructive-hover: oklch(0.628 0.188 22);
|
|
1341
|
+
--cascivo-color-destructive-subtle: oklch(0.628 0.188 22 / 0.1);
|
|
1342
|
+
--cascivo-color-error: oklch(0.72 0.16 22);
|
|
1343
|
+
--cascivo-color-warning: oklch(0.82 0.13 75);
|
|
1344
|
+
--cascivo-color-success: oklch(0.72 0.13 145);
|
|
1345
|
+
--cascivo-color-success-subtle: oklch(0.72 0.13 145 / 0.1);
|
|
1346
|
+
--cascivo-color-warning-subtle: oklch(0.82 0.13 75 / 0.1);
|
|
1347
|
+
--cascivo-color-info: oklch(0.65 0.2 250);
|
|
1348
|
+
--cascivo-color-info-subtle: oklch(0.65 0.2 250 / 0.1);
|
|
1349
|
+
--cascivo-color-success-foreground: oklch(0.72 0.13 145);
|
|
1350
|
+
--cascivo-color-warning-foreground: oklch(0.82 0.13 75);
|
|
1351
|
+
--cascivo-shadow-xs: none;
|
|
1352
|
+
--cascivo-shadow-sm: 0 1px 3px oklch(0 0 0 / 0.4);
|
|
1353
|
+
--cascivo-shadow-md: 0 2px 8px oklch(0 0 0 / 0.45);
|
|
1354
|
+
--cascivo-shadow-overlay: 0 4px 32px oklch(0 0 0 / 0.6), 0 0 0 1px oklch(1 0 0 / 0.06);
|
|
1355
|
+
--cascivo-shadow-lg: var(--cascivo-shadow-overlay);
|
|
1356
|
+
--cascivo-ring-width: 2px;
|
|
1357
|
+
--cascivo-ring-offset: 0px;
|
|
1358
|
+
--cascivo-ring-color: color-mix(in oklch, var(--cascivo-color-accent) 65%, transparent);
|
|
1359
|
+
--cascivo-focus-ring: 0 0 0 var(--cascivo-ring-width) var(--cascivo-ring-color);`;
|
|
1360
|
+
function configToCSS(config, options = {}) {
|
|
1361
|
+
const { previewMode = false, themeName = "create-custom" } = options;
|
|
1362
|
+
const light = config.baseMode === "light";
|
|
1363
|
+
const h = config.accentHue;
|
|
1364
|
+
const c = config.accentChroma;
|
|
1365
|
+
const acc = (l, chroma = c) => `oklch(${l} ${chroma.toFixed(3)} ${h})`;
|
|
1366
|
+
const lines = [
|
|
1367
|
+
`/* Add to your project CSS after importing the base theme: */`,
|
|
1368
|
+
`/* @import '@cascivo/themes/${config.baseMode}.css'; */`,
|
|
1369
|
+
`/* @import './theme.css'; */`,
|
|
1370
|
+
``,
|
|
1371
|
+
`@layer cascivo.theme {`,
|
|
1372
|
+
` [data-theme="${themeName}"] {`
|
|
1373
|
+
];
|
|
1374
|
+
if (previewMode) {
|
|
1375
|
+
lines.push(light ? BASE_LIGHT : BASE_DARK);
|
|
1376
|
+
lines.push(``);
|
|
1377
|
+
}
|
|
1378
|
+
lines.push(` /* ── Accent ─────────────────────────────────── */`, ` --cascivo-color-accent: ${acc(light ? .52 : .65)};`, ` --cascivo-color-accent-foreground: ${light ? "oklch(1 0 0)" : `oklch(0.145 0.005 ${h})`};`, ` --cascivo-color-accent-hover: ${acc(light ? .45 : .707)};`, ` --cascivo-color-accent-active: ${acc(light ? .373 : .808, Math.min(c, .155))};`, ` --cascivo-color-accent-subtle: ${acc(light ? .97 : .623, .025)};`, ` --cascivo-color-accent-muted: ${acc(light ? .932 : .623, light ? .055 : c * .2)};`, ``, ` /* ── Radius ─────────────────────────────────── */`, ` --cascivo-radius-base: ${config.radiusBase}rem;`, ` --cascivo-radius-control: var(--cascivo-radius-base);`, ` --cascivo-radius-surface: calc(var(--cascivo-radius-base) * 1.66);`, ` --cascivo-radius-indicator: calc(var(--cascivo-radius-base) / 2);`, ` --cascivo-radius-full: 9999px;`, ` --cascivo-radius-component: var(--cascivo-radius-base);`, ` --cascivo-radius-button: var(--cascivo-radius-base);`, ` --cascivo-radius-input: var(--cascivo-radius-base);`, ` --cascivo-radius-card: calc(var(--cascivo-radius-base) * 1.66);`, ` --cascivo-radius-badge: var(--cascivo-radius-full);`, ` --cascivo-radius-modal: calc(var(--cascivo-radius-base) * 2);`);
|
|
1379
|
+
if (config.fontFamily !== "system") {
|
|
1380
|
+
lines.push(``);
|
|
1381
|
+
lines.push(` /* ── Font ───────────────────────────────────── */`);
|
|
1382
|
+
lines.push(` --cascivo-font-sans: ${FONT_STACKS[config.fontFamily] ?? FONT_STACKS["humanist"]};`);
|
|
1383
|
+
}
|
|
1384
|
+
lines.push(` }`);
|
|
1385
|
+
lines.push(`}`);
|
|
1386
|
+
return lines.join("\n");
|
|
1387
|
+
}
|
|
1388
|
+
//#endregion
|
|
803
1389
|
//#region src/commands/theme.ts
|
|
804
|
-
const
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
"
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
1390
|
+
const THEME_NAME_RE = /^[a-z][a-z0-9-]*$/;
|
|
1391
|
+
/** Read a `--key value` or `--key=value` flag from an argv slice. */
|
|
1392
|
+
function flag(args, key) {
|
|
1393
|
+
const eq = args.find((a) => a.startsWith(`--${key}=`));
|
|
1394
|
+
if (eq) return eq.slice(key.length + 3);
|
|
1395
|
+
const idx = args.indexOf(`--${key}`);
|
|
1396
|
+
const next = idx !== -1 ? args[idx + 1] : void 0;
|
|
1397
|
+
return next && !next.startsWith("--") ? next : void 0;
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* `cascivo theme create <name> --from <hash>` — turn a /create theme-builder
|
|
1401
|
+
* config (the `#hash` in the builder URL, or its "Copy CLI command" output) into
|
|
1402
|
+
* an owned `<name>.theme.css` in the project. Uses the same generator the builder
|
|
1403
|
+
* previews with, so the file matches the preview exactly.
|
|
1404
|
+
*/
|
|
1405
|
+
async function themeCreate(args, cwd) {
|
|
1406
|
+
const name = args.find((a) => !a.startsWith("--"));
|
|
1407
|
+
const hash = flag(args, "from");
|
|
1408
|
+
const dryRun = args.includes("--dry-run");
|
|
1409
|
+
if (!name || !hash) {
|
|
1410
|
+
console.error("Usage: cascivo theme create <name> --from <hash> [--out <path>]");
|
|
1411
|
+
console.error("Get <hash> from the theme builder at https://cascivo.com/create (Copy CLI command).");
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
if (!THEME_NAME_RE.test(name)) {
|
|
1415
|
+
console.error(`Invalid theme name "${name}". Use lowercase letters, digits, and dashes.`);
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
const config = hashToConfig(hash);
|
|
1419
|
+
if (!config) {
|
|
1420
|
+
console.error("Could not decode --from. Copy the command again from the theme builder.");
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
const css = configToCSS(config, { themeName: name });
|
|
1424
|
+
if (dryRun) {
|
|
1425
|
+
console.log(css);
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
await writeFileSafe(resolve(cwd, flag(args, "out") ?? `${name}.theme.css`), css);
|
|
1429
|
+
console.log(`\n✓ Wrote ${name}.theme.css`);
|
|
1430
|
+
console.log(`\nImport the base theme and your overlay in your entry CSS:`);
|
|
1431
|
+
console.log(` @import '@cascivo/themes/${config.baseMode}.css';`);
|
|
1432
|
+
console.log(` @import './${name}.theme.css';`);
|
|
1433
|
+
console.log(`Then set it on a container:`);
|
|
1434
|
+
console.log(` <div data-theme="${name}">…</div>`);
|
|
1435
|
+
}
|
|
818
1436
|
async function theme(args) {
|
|
819
1437
|
const [sub, name] = args;
|
|
1438
|
+
if (sub === "create") {
|
|
1439
|
+
await themeCreate(args.slice(1), process.cwd());
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
820
1442
|
if (sub !== "add" || !name) {
|
|
821
|
-
console.error(`Usage:
|
|
1443
|
+
console.error(`Usage: cascivo theme add <${THEMES.join("|")}>`);
|
|
1444
|
+
console.error(` cascivo theme create <name> --from <hash>`);
|
|
822
1445
|
return;
|
|
823
1446
|
}
|
|
824
1447
|
if (!THEMES.includes(name)) {
|
|
@@ -1051,11 +1674,6 @@ async function confirm(question) {
|
|
|
1051
1674
|
rl.close();
|
|
1052
1675
|
}
|
|
1053
1676
|
}
|
|
1054
|
-
async function fetchText(url) {
|
|
1055
|
-
const res = await fetch(url);
|
|
1056
|
-
if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${url}`);
|
|
1057
|
-
return res.text();
|
|
1058
|
-
}
|
|
1059
1677
|
async function update(name, config, opts = {}) {
|
|
1060
1678
|
if (opts.check) return updateCheck(config);
|
|
1061
1679
|
if (!name) {
|
|
@@ -1093,9 +1711,9 @@ async function threeWayUpdate(name, upstreamEntry, baseItem, config, opts) {
|
|
|
1093
1711
|
const fname = fileName(url);
|
|
1094
1712
|
const baseFile = baseItem.files.find((f) => fileName(f.url) === fname);
|
|
1095
1713
|
const dest = resolveOutputPath(config.outputDir, outputName, fname);
|
|
1096
|
-
const upstream = await
|
|
1714
|
+
const upstream = await fetchTextRetry(url);
|
|
1097
1715
|
const local = existsSync(dest) ? await readFile(dest, "utf8") : "";
|
|
1098
|
-
const base = baseFile ? await
|
|
1716
|
+
const base = baseFile ? await fetchTextRetry(baseFile.url) : "";
|
|
1099
1717
|
if (base === "" && local === "") {
|
|
1100
1718
|
summaries.push({
|
|
1101
1719
|
file: fname,
|
|
@@ -1165,12 +1783,13 @@ async function twoWayUpdate(name, entry, config, opts) {
|
|
|
1165
1783
|
if (!entry) return;
|
|
1166
1784
|
const pending = [];
|
|
1167
1785
|
for (const url of entry.files) {
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1786
|
+
let latest;
|
|
1787
|
+
try {
|
|
1788
|
+
latest = await fetchTextRetry(url);
|
|
1789
|
+
} catch (e) {
|
|
1790
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
1171
1791
|
return;
|
|
1172
1792
|
}
|
|
1173
|
-
const latest = await res.text();
|
|
1174
1793
|
const dest = resolveOutputPath(config.outputDir, entry.name, fileName(url));
|
|
1175
1794
|
const current = await readFileSafe(dest);
|
|
1176
1795
|
if (current === null) {
|
|
@@ -1208,7 +1827,14 @@ async function updateCheck(config) {
|
|
|
1208
1827
|
for (const [name, entry] of Object.entries(lock.items)) {
|
|
1209
1828
|
const current = findComponent(registry, name);
|
|
1210
1829
|
if (!current) continue;
|
|
1211
|
-
if (current.
|
|
1830
|
+
if (current.fileHashes && Object.keys(current.fileHashes).length > 0) {
|
|
1831
|
+
const lockByBase = new Map(Object.entries(entry.files).map(([dest, hash]) => [dest.split("/").pop(), hash]));
|
|
1832
|
+
const changed = Object.entries(current.fileHashes).filter(([file, hash]) => lockByBase.get(file) !== hash);
|
|
1833
|
+
if (changed.length > 0) {
|
|
1834
|
+
console.log(`${name}: ${changed.length} file(s) changed upstream (${entry.version} → ${current.version})`);
|
|
1835
|
+
outdated++;
|
|
1836
|
+
}
|
|
1837
|
+
} else if (current.version !== entry.version) {
|
|
1212
1838
|
console.log(`${name}: ${entry.version} → ${current.version}`);
|
|
1213
1839
|
outdated++;
|
|
1214
1840
|
}
|
|
@@ -1220,10 +1846,48 @@ async function updateCheck(config) {
|
|
|
1220
1846
|
}
|
|
1221
1847
|
//#endregion
|
|
1222
1848
|
//#region src/commands/view.ts
|
|
1849
|
+
/** Render an item's details as text. Pure, so it can be unit-tested. */
|
|
1850
|
+
function formatItem(item) {
|
|
1851
|
+
const lines = [];
|
|
1852
|
+
lines.push(`\n${item.name} v${item.version}`);
|
|
1853
|
+
lines.push(`Type: ${item.type}${item.category ? ` / ${item.category}` : ""}`);
|
|
1854
|
+
lines.push(item.description);
|
|
1855
|
+
if (item.author) lines.push(`Author: ${item.author}`);
|
|
1856
|
+
if (item.license) lines.push(`License: ${item.license}`);
|
|
1857
|
+
if (item.homepage) lines.push(`Homepage: ${item.homepage}`);
|
|
1858
|
+
if (item.install) {
|
|
1859
|
+
lines.push(`Distribution: npm package — pnpm add ${item.install}`);
|
|
1860
|
+
if (item.styles) lines.push(`Stylesheet: import '${item.styles}' (required)`);
|
|
1861
|
+
} else if (!isTemplateItem(item)) lines.push(`Distribution: copy-paste — npx cascivo add ${item.name}`);
|
|
1862
|
+
if (isTemplateItem(item)) try {
|
|
1863
|
+
const meta = asTemplateMeta(item.meta);
|
|
1864
|
+
lines.push(`\nTemplate: ${meta.intent}`);
|
|
1865
|
+
lines.push(`Framework: ${meta.framework}`);
|
|
1866
|
+
if (meta.demoUrl) lines.push(`Demo: ${meta.demoUrl}`);
|
|
1867
|
+
if (meta.pages?.length) {
|
|
1868
|
+
lines.push("Pages:");
|
|
1869
|
+
for (const p of meta.pages) lines.push(` ${p.name} → ${p.target}`);
|
|
1870
|
+
}
|
|
1871
|
+
} catch {}
|
|
1872
|
+
if (item.changelog?.length) {
|
|
1873
|
+
lines.push("\nChangelog:");
|
|
1874
|
+
for (const c of item.changelog.slice(0, 5)) lines.push(` ${c.version}: ${c.note}`);
|
|
1875
|
+
}
|
|
1876
|
+
if (item.files?.length) {
|
|
1877
|
+
lines.push("\nFiles:");
|
|
1878
|
+
for (const f of item.files) lines.push(` ${f.url}${f.target ? ` → ${f.target}` : ""}`);
|
|
1879
|
+
}
|
|
1880
|
+
if (item.dependencies?.length) lines.push(`\nDependencies: ${item.dependencies.join(", ")}`);
|
|
1881
|
+
if (item.registryDependencies?.length) {
|
|
1882
|
+
const label = isTemplateItem(item) ? "Components" : "Registry dependencies";
|
|
1883
|
+
lines.push(`${label}: ${item.registryDependencies.join(", ")}`);
|
|
1884
|
+
}
|
|
1885
|
+
return lines.join("\n");
|
|
1886
|
+
}
|
|
1223
1887
|
async function view(args, config) {
|
|
1224
1888
|
const spec = args[0];
|
|
1225
1889
|
if (!spec) {
|
|
1226
|
-
console.error("Usage:
|
|
1890
|
+
console.error("Usage: cascivo view <component-spec>");
|
|
1227
1891
|
return;
|
|
1228
1892
|
}
|
|
1229
1893
|
const { url, headers, params } = await resolveItemUrl(parseAddress(spec), config);
|
|
@@ -1231,53 +1895,164 @@ async function view(args, config) {
|
|
|
1231
1895
|
if (headers) fetchOpts.headers = headers;
|
|
1232
1896
|
if (params) fetchOpts.params = params;
|
|
1233
1897
|
const item = await fetchJson(url, fetchOpts);
|
|
1234
|
-
console.log(
|
|
1235
|
-
console.log(`Type: ${item.type}${item.category ? ` / ${item.category}` : ""}`);
|
|
1236
|
-
console.log(`${item.description}`);
|
|
1237
|
-
if (item.author) console.log(`Author: ${item.author}`);
|
|
1238
|
-
if (item.license) console.log(`License: ${item.license}`);
|
|
1239
|
-
if (item.homepage) console.log(`Homepage: ${item.homepage}`);
|
|
1240
|
-
if (item.changelog?.length) {
|
|
1241
|
-
console.log("\nChangelog:");
|
|
1242
|
-
for (const c of item.changelog.slice(0, 5)) console.log(` ${c.version}: ${c.note}`);
|
|
1243
|
-
}
|
|
1244
|
-
if (item.files?.length) {
|
|
1245
|
-
console.log("\nFiles:");
|
|
1246
|
-
for (const f of item.files) {
|
|
1247
|
-
const target = f.target ? ` → ${f.target}` : "";
|
|
1248
|
-
console.log(` ${f.url}${target}`);
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
if (item.dependencies?.length) console.log(`\nDependencies: ${item.dependencies.join(", ")}`);
|
|
1252
|
-
if (item.registryDependencies?.length) console.log(`Registry dependencies: ${item.registryDependencies.join(", ")}`);
|
|
1898
|
+
console.log(formatItem(item));
|
|
1253
1899
|
}
|
|
1254
1900
|
//#endregion
|
|
1255
1901
|
//#region src/index.ts
|
|
1256
|
-
const VERSION = "
|
|
1902
|
+
const VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
1257
1903
|
const HELP = `cascivo ${VERSION} — the CSS-native, signal-driven, AI-first design system
|
|
1258
1904
|
|
|
1259
1905
|
Usage: cascivo <command> [options]
|
|
1260
1906
|
|
|
1261
1907
|
Commands:
|
|
1908
|
+
create [name] Scaffold a new ready-to-run app (shell + nav + theme)
|
|
1909
|
+
(--template <spec>: start from a marketplace template)
|
|
1262
1910
|
init Set up cascivo in the current project
|
|
1263
|
-
add <component...> Add
|
|
1911
|
+
add <component...> Add components or a template to your project
|
|
1264
1912
|
list [--installed] List available components
|
|
1265
1913
|
update [component] Update installed components (--check: list outdated)
|
|
1266
1914
|
search <query> Search components across registries
|
|
1267
1915
|
view <spec> View a component before installing
|
|
1268
|
-
theme add
|
|
1916
|
+
theme <add|create> Install a first-party theme, or build a custom one from /create
|
|
1269
1917
|
eject <component> Eject specific tokens into a scoped local override file
|
|
1270
1918
|
generate <config.json> Generate TSX from a ViewConfig JSON file
|
|
1271
1919
|
doctor [--ci] Check components for rule violations
|
|
1272
1920
|
audit --ai <paths...> Audit AI-generated code against the cascivo contract
|
|
1273
1921
|
registry build Build a static registry from a cascivo-registry.json file
|
|
1922
|
+
template init <name> Scaffold a new template (source + manifest + registry entry)
|
|
1923
|
+
tokens import <file> Import external design tokens as cascivo overrides
|
|
1274
1924
|
|
|
1275
1925
|
Run "cascivo <command> --help" for details.`;
|
|
1926
|
+
const THEME_LIST = THEMES.join(" | ");
|
|
1927
|
+
const COMMAND_HELP = {
|
|
1928
|
+
create: `Usage: cascivo create [name] [options]
|
|
1929
|
+
|
|
1930
|
+
Scaffold a new ready-to-run app — Vite + React + TypeScript, pre-wired with the
|
|
1931
|
+
cascivo app shell, side navigation, header, and a theme.
|
|
1932
|
+
|
|
1933
|
+
Options:
|
|
1934
|
+
--template <spec> Start from a marketplace template (@ns/name or owner/repo/name)
|
|
1935
|
+
--theme <name> Theme to install (${THEME_LIST})
|
|
1936
|
+
--sections "<a, b>" Comma-separated nav section labels (one component each)
|
|
1937
|
+
--yes, -y Accept defaults, never prompt`,
|
|
1938
|
+
init: `Usage: cascivo init [options]
|
|
1939
|
+
|
|
1940
|
+
Set up cascivo in the current project: installs @cascivo/core + @cascivo/tokens
|
|
1941
|
+
and writes cascivo.config.ts.
|
|
1942
|
+
|
|
1943
|
+
Options:
|
|
1944
|
+
--theme <name> Theme to configure (${THEME_LIST})
|
|
1945
|
+
--yes, -y Accept defaults, never prompt (implied when stdin is not a TTY)`,
|
|
1946
|
+
add: `Usage: cascivo add <component...> [options]
|
|
1947
|
+
|
|
1948
|
+
Copy component source (TSX + CSS module) from the registry into your project,
|
|
1949
|
+
resolving component dependencies. Also installs templates (@ns/name) and
|
|
1950
|
+
third-party components (owner/repo/name).
|
|
1951
|
+
|
|
1952
|
+
Options:
|
|
1953
|
+
--dry-run Show what would be written without writing
|
|
1954
|
+
--yes, -y Skip confirmation prompts`,
|
|
1955
|
+
list: `Usage: cascivo list [options]
|
|
1956
|
+
|
|
1957
|
+
List components available in the configured registry.
|
|
1958
|
+
|
|
1959
|
+
Options:
|
|
1960
|
+
--installed Only list components already installed in this project`,
|
|
1961
|
+
update: `Usage: cascivo update [component] [options]
|
|
1962
|
+
|
|
1963
|
+
Update installed components to the current registry version. Without a
|
|
1964
|
+
component argument, updates everything in the lockfile.
|
|
1965
|
+
|
|
1966
|
+
Options:
|
|
1967
|
+
--check List outdated components without writing
|
|
1968
|
+
--yes, -y Skip confirmation prompts`,
|
|
1969
|
+
search: `Usage: cascivo search <query> [options]
|
|
1970
|
+
|
|
1971
|
+
Search components across the configured registries.
|
|
1972
|
+
|
|
1973
|
+
Options:
|
|
1974
|
+
--registry <@ns> Restrict the search to one registry namespace`,
|
|
1975
|
+
view: `Usage: cascivo view <spec>
|
|
1976
|
+
|
|
1977
|
+
Preview a component or template (files, dependencies, description) before
|
|
1978
|
+
installing. <spec> is a bare name, @ns/name, or owner/repo/name.`,
|
|
1979
|
+
theme: `Usage: cascivo theme add <name>
|
|
1980
|
+
cascivo theme create <name> --from <hash> [--out <path>]
|
|
1981
|
+
|
|
1982
|
+
add Install @cascivo/themes and print the import + data-theme wiring.
|
|
1983
|
+
create Turn a theme-builder config into an owned <name>.theme.css. Get <hash>
|
|
1984
|
+
from https://cascivo.com/create ("Copy CLI command"). --dry-run prints
|
|
1985
|
+
the CSS without writing.
|
|
1986
|
+
|
|
1987
|
+
Themes: ${THEME_LIST}`,
|
|
1988
|
+
eject: `Usage: cascivo eject <component> [options]
|
|
1989
|
+
|
|
1990
|
+
Eject specific tokens into a scoped local override file so you can restyle a
|
|
1991
|
+
component without forking it.
|
|
1992
|
+
|
|
1993
|
+
Options:
|
|
1994
|
+
--tokens <a,b> Comma-separated token names to eject (default: all)
|
|
1995
|
+
--scope <sel> CSS selector the overrides are scoped to
|
|
1996
|
+
--out <file> Output file path
|
|
1997
|
+
--dry-run Print the override file without writing`,
|
|
1998
|
+
generate: `Usage: cascivo generate <config.json> [options]
|
|
1999
|
+
|
|
2000
|
+
Generate TSX from a ViewConfig JSON file (see the MCP scaffold_view tool).
|
|
2001
|
+
|
|
2002
|
+
Options:
|
|
2003
|
+
--out <file> Output file (default: stdout)
|
|
2004
|
+
--components-dir <dir> Components import base (default: ./src/components/ui)`,
|
|
2005
|
+
doctor: `Usage: cascivo doctor [options]
|
|
2006
|
+
|
|
2007
|
+
Check components in this repo for cascivo rule violations (banned React hooks,
|
|
2008
|
+
hardcoded strings, missing @cascivo/react exports).
|
|
2009
|
+
|
|
2010
|
+
Options:
|
|
2011
|
+
--ci Exit non-zero when violations are found
|
|
2012
|
+
--drift Compare installed components against the registry (copy-paste drift)`,
|
|
2013
|
+
audit: `Usage: cascivo audit --ai <paths...> [options]
|
|
2014
|
+
|
|
2015
|
+
Audit AI-generated code against the cascivo contract: hard-coded values that
|
|
2016
|
+
should be tokens, invented props, missing required props, raw strings where
|
|
2017
|
+
i18n is expected.
|
|
2018
|
+
|
|
2019
|
+
Options:
|
|
2020
|
+
--fix Rewrite unambiguous CSS literals to their token equivalents
|
|
2021
|
+
--json Machine-readable output
|
|
2022
|
+
--level <name> Minimum finding level to report (error | warn; default error)`,
|
|
2023
|
+
registry: `Usage: cascivo registry build [dir]
|
|
2024
|
+
|
|
2025
|
+
Build a static registry (registry.json + file payloads) from a
|
|
2026
|
+
cascivo-registry.json manifest, ready to host on any static file server.`,
|
|
2027
|
+
template: `Usage: cascivo template init <name> [options]
|
|
2028
|
+
|
|
2029
|
+
Scaffold a new template: source, manifest, and registry entry.
|
|
2030
|
+
|
|
2031
|
+
Options:
|
|
2032
|
+
--category <name> Template category (e.g. dashboard)
|
|
2033
|
+
--framework <name> Target framework (default react-vite)
|
|
2034
|
+
--components <a,b> Registry components the template composes
|
|
2035
|
+
--repo <owner/repo> Repository the template will be hosted in`,
|
|
2036
|
+
tokens: `Usage: cascivo tokens import <file>
|
|
2037
|
+
|
|
2038
|
+
Import external design tokens (W3C design-tokens JSON) as cascivo token
|
|
2039
|
+
overrides.`
|
|
2040
|
+
};
|
|
1276
2041
|
async function run(args) {
|
|
1277
2042
|
const [command, ...rest] = args;
|
|
2043
|
+
if (command !== void 0 && (rest.includes("--help") || rest.includes("-h"))) {
|
|
2044
|
+
const commandHelp = COMMAND_HELP[command];
|
|
2045
|
+
if (commandHelp !== void 0) {
|
|
2046
|
+
console.log(commandHelp);
|
|
2047
|
+
return;
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
1278
2050
|
switch (command) {
|
|
2051
|
+
case "create":
|
|
2052
|
+
await create(rest);
|
|
2053
|
+
break;
|
|
1279
2054
|
case "init":
|
|
1280
|
-
await init();
|
|
2055
|
+
await init(rest);
|
|
1281
2056
|
break;
|
|
1282
2057
|
case "add":
|
|
1283
2058
|
await add(rest, await loadConfig(), {
|
|
@@ -1304,7 +2079,7 @@ async function run(args) {
|
|
|
1304
2079
|
await theme(rest);
|
|
1305
2080
|
break;
|
|
1306
2081
|
case "eject": {
|
|
1307
|
-
const { eject } = await import("./eject-
|
|
2082
|
+
const { eject } = await import("./eject-Br8V7I3m.mjs");
|
|
1308
2083
|
const flag = (key) => {
|
|
1309
2084
|
const eq = rest.find((r) => r.startsWith(`--${key}=`));
|
|
1310
2085
|
if (eq) return eq.slice(key.length + 3);
|
|
@@ -1331,11 +2106,20 @@ async function run(args) {
|
|
|
1331
2106
|
process.exitCode = 1;
|
|
1332
2107
|
}
|
|
1333
2108
|
break;
|
|
2109
|
+
case "template":
|
|
2110
|
+
if (rest[0] === "init") {
|
|
2111
|
+
const { templateInit } = await import("./template-init-C46fRXo-.mjs");
|
|
2112
|
+
await templateInit(rest.slice(1));
|
|
2113
|
+
} else {
|
|
2114
|
+
console.error(`Unknown template subcommand: ${rest[0]}`);
|
|
2115
|
+
process.exitCode = 1;
|
|
2116
|
+
}
|
|
2117
|
+
break;
|
|
1334
2118
|
case "doctor": {
|
|
1335
2119
|
const ci = rest.includes("--ci");
|
|
1336
2120
|
if (rest.includes("--drift")) {
|
|
1337
|
-
const { runDoctorDrift } = await import("./drift-
|
|
1338
|
-
await runDoctorDrift(
|
|
2121
|
+
const { runDoctorDrift } = await import("./drift-Ciw5qeEt.mjs");
|
|
2122
|
+
await runDoctorDrift(await loadConfig());
|
|
1339
2123
|
} else {
|
|
1340
2124
|
const result = await runDoctor(process.cwd());
|
|
1341
2125
|
if (result.passed) console.log("No violations found.");
|
|
@@ -1347,7 +2131,7 @@ async function run(args) {
|
|
|
1347
2131
|
break;
|
|
1348
2132
|
}
|
|
1349
2133
|
case "audit": {
|
|
1350
|
-
const { audit } = await import("./audit-
|
|
2134
|
+
const { audit } = await import("./audit-InQqdY1s.mjs");
|
|
1351
2135
|
await audit(rest, await loadConfig());
|
|
1352
2136
|
break;
|
|
1353
2137
|
}
|