@shortwind/cli 0.1.0-beta.1 → 0.1.0-beta.11
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/README.md +1 -1
- package/dist/{bench-NKKDz3ld.js → bench-oy9aPOvX.js} +358 -84
- package/dist/bench-oy9aPOvX.js.map +1 -0
- package/dist/bin.js +50 -7
- package/dist/bin.js.map +1 -1
- package/dist/catalog.generated-DRttebfK.js +83 -0
- package/dist/catalog.generated-DRttebfK.js.map +1 -0
- package/dist/index.d.ts +29 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/package.json +13 -3
- package/dist/bench-NKKDz3ld.js.map +0 -1
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
2
|
import { mkdir, open, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { applyEdits, modify, parse } from "jsonc-parser";
|
|
5
|
-
import { buildRegistry, isReservedRecipeName, parseRecipeFile, renderSkillMarkdown } from "@shortwind/core";
|
|
6
|
+
import { PLACEHOLDER_SHA, RECIPE_SHA_HEX_LENGTH, buildRegistry, isReservedRecipeName, normalizeRecipeBody, parseRecipeFile, renderSkillMarkdown } from "@shortwind/core";
|
|
6
7
|
import { createHash } from "node:crypto";
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { glob } from "tinyglobby";
|
|
9
9
|
import chokidar from "chokidar";
|
|
10
10
|
import { Tiktoken } from "js-tiktoken/lite";
|
|
11
11
|
import cl100k_base from "js-tiktoken/ranks/cl100k_base";
|
|
12
|
-
import { loadRegistryFromDir, transformContent } from "@shortwind/tailwind";
|
|
12
|
+
import { loadRegistryFromDir, modeForFile, transformContent } from "@shortwind/tailwind";
|
|
13
13
|
//#region src/fingerprint.ts
|
|
14
14
|
const HEADER_PATTERN = /^\/\*\s*shortwind:\s+(\S+)@(\S+)\s+sha:([^\s*]+)(?:\s+—\s+DO NOT EDIT THIS LINE)?\s*\*\/\s*$/;
|
|
15
15
|
function extractHeader(source) {
|
|
@@ -26,12 +26,19 @@ function bodyAfterHeader(source) {
|
|
|
26
26
|
const eol = source.indexOf("\n");
|
|
27
27
|
return eol === -1 ? "" : source.slice(eol + 1);
|
|
28
28
|
}
|
|
29
|
-
function normalizeBody(body) {
|
|
30
|
-
return body.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n").map((line) => line.replace(/[\t ]+$/, "")).join("\n");
|
|
31
|
-
}
|
|
32
29
|
function computeBodySha(source) {
|
|
33
|
-
const normalized =
|
|
34
|
-
return createHash("sha256").update(normalized).digest("hex").slice(0,
|
|
30
|
+
const normalized = normalizeRecipeBody(bodyAfterHeader(source));
|
|
31
|
+
return createHash("sha256").update(normalized).digest("hex").slice(0, RECIPE_SHA_HEX_LENGTH);
|
|
32
|
+
}
|
|
33
|
+
function isLegacyFingerprint(sha) {
|
|
34
|
+
return sha !== PLACEHOLDER_SHA && sha.length < RECIPE_SHA_HEX_LENGTH && /^[0-9a-f]+$/.test(sha);
|
|
35
|
+
}
|
|
36
|
+
function verifyFetchedFamily(source, family) {
|
|
37
|
+
const header = extractHeader(source);
|
|
38
|
+
if (!header || header.sha === PLACEHOLDER_SHA) return;
|
|
39
|
+
if (header.family !== family) throw new Error(`integrity check failed for "${family}": registry returned a recipe sealed as "${header.family}" — wrong family or a tampered/corrupted response`);
|
|
40
|
+
const actual = computeBodySha(source);
|
|
41
|
+
if (header.sha !== actual) throw new Error(`integrity check failed for "${family}": header sha ${header.sha} does not match content sha ${actual} — the registry response was tampered with or corrupted in transit`);
|
|
35
42
|
}
|
|
36
43
|
function buildHeaderLine(family, version, sha) {
|
|
37
44
|
return `/* shortwind: ${family}@${version} sha:${sha} — DO NOT EDIT THIS LINE */`;
|
|
@@ -51,13 +58,20 @@ function sealRecipeFile(source, family, version) {
|
|
|
51
58
|
}
|
|
52
59
|
//#endregion
|
|
53
60
|
//#region src/detect.ts
|
|
61
|
+
function parsePackageJson(pkgPath) {
|
|
62
|
+
let parsed;
|
|
63
|
+
try {
|
|
64
|
+
parsed = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
65
|
+
} catch (err) {
|
|
66
|
+
throw new Error(`${pkgPath}: invalid JSON — ${err.message}`);
|
|
67
|
+
}
|
|
68
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
69
|
+
return parsed;
|
|
70
|
+
}
|
|
54
71
|
function detectProject(cwd) {
|
|
55
72
|
const pkgPath = path.join(cwd, "package.json");
|
|
56
73
|
const hasPackageJson = existsSync(pkgPath);
|
|
57
|
-
const pkg = hasPackageJson ?
|
|
58
|
-
dependencies: {},
|
|
59
|
-
devDependencies: {}
|
|
60
|
-
};
|
|
74
|
+
const pkg = hasPackageJson ? parsePackageJson(pkgPath) : {};
|
|
61
75
|
const deps = {
|
|
62
76
|
...pkg.dependencies ?? {},
|
|
63
77
|
...pkg.devDependencies ?? {}
|
|
@@ -113,9 +127,64 @@ function assertValidFamilyName(family) {
|
|
|
113
127
|
if (!FAMILY_RE.test(family)) throw new Error(`invalid family name: ${JSON.stringify(family)} (must match ${FAMILY_RE})`);
|
|
114
128
|
}
|
|
115
129
|
function createRegistrySource(origin) {
|
|
116
|
-
if (origin.startsWith("http://")
|
|
130
|
+
if (origin.startsWith("http://")) {
|
|
131
|
+
console.warn(`[shortwind] registry origin ${origin} uses plaintext http:// — recipe content is unauthenticated and tamperable in transit; prefer https://`);
|
|
132
|
+
return httpSource(origin);
|
|
133
|
+
}
|
|
134
|
+
if (origin.startsWith("https://")) return httpSource(origin);
|
|
117
135
|
return fileSource(origin);
|
|
118
136
|
}
|
|
137
|
+
async function resolveSource(origin) {
|
|
138
|
+
if (origin && origin !== "bundled:@shortwind/catalog") return createRegistrySource(origin);
|
|
139
|
+
return defaultCatalogSource();
|
|
140
|
+
}
|
|
141
|
+
const CATALOG_PACKAGE = "@shortwind/catalog";
|
|
142
|
+
const NPM_TIMEOUT_MS = 3e3;
|
|
143
|
+
const FETCH_TIMEOUT_MS = 1e4;
|
|
144
|
+
async function defaultCatalogSource() {
|
|
145
|
+
try {
|
|
146
|
+
const cdn = httpSource(`https://cdn.jsdelivr.net/npm/${CATALOG_PACKAGE}@${await resolveCatalogVersion()}/dist/registry`);
|
|
147
|
+
await cdn.loadPresets();
|
|
148
|
+
return cdn;
|
|
149
|
+
} catch {
|
|
150
|
+
return bundledSource();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async function resolveCatalogVersion() {
|
|
154
|
+
const res = await fetch(`https://registry.npmjs.org/${CATALOG_PACKAGE}`, {
|
|
155
|
+
signal: AbortSignal.timeout(NPM_TIMEOUT_MS),
|
|
156
|
+
headers: { accept: "application/vnd.npm.install-v1+json" }
|
|
157
|
+
});
|
|
158
|
+
if (!res.ok) throw new Error(`npm: ${res.status}`);
|
|
159
|
+
const tags = (await res.json())["dist-tags"] ?? {};
|
|
160
|
+
const version = tags["latest"] ?? tags["beta"];
|
|
161
|
+
if (!version) throw new Error("no published catalog version");
|
|
162
|
+
return version;
|
|
163
|
+
}
|
|
164
|
+
const BUNDLED_ORIGIN = "bundled:@shortwind/catalog";
|
|
165
|
+
function bundledSource() {
|
|
166
|
+
let cache = null;
|
|
167
|
+
const load = () => {
|
|
168
|
+
cache ??= import("./catalog.generated-DRttebfK.js");
|
|
169
|
+
return cache;
|
|
170
|
+
};
|
|
171
|
+
return {
|
|
172
|
+
origin: BUNDLED_ORIGIN,
|
|
173
|
+
async loadPresets() {
|
|
174
|
+
return (await load()).CATALOG_PRESETS;
|
|
175
|
+
},
|
|
176
|
+
async loadFamily(family) {
|
|
177
|
+
assertValidFamilyName(family);
|
|
178
|
+
const { CATALOG_RECIPES } = await load();
|
|
179
|
+
const css = CATALOG_RECIPES[family];
|
|
180
|
+
if (css === void 0) throw new Error(`unknown family: ${family}`);
|
|
181
|
+
return css;
|
|
182
|
+
},
|
|
183
|
+
async listAllFamilies() {
|
|
184
|
+
return [...(await load()).CATALOG_FAMILIES];
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
119
188
|
function fileSource(origin) {
|
|
120
189
|
const root = origin.startsWith("file://") ? fileURLToPath(origin) : origin;
|
|
121
190
|
return {
|
|
@@ -136,21 +205,26 @@ function fileSource(origin) {
|
|
|
136
205
|
}
|
|
137
206
|
function httpSource(origin) {
|
|
138
207
|
const base = origin.replace(/\/+$/, "");
|
|
208
|
+
const get = (url) => fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
209
|
+
let presetsCache = null;
|
|
139
210
|
return {
|
|
140
211
|
origin,
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
212
|
+
loadPresets() {
|
|
213
|
+
presetsCache ??= (async () => {
|
|
214
|
+
const res = await get(`${base}/presets.json`);
|
|
215
|
+
if (!res.ok) throw new Error(`presets.json: ${res.status} ${res.statusText}`);
|
|
216
|
+
return await res.json();
|
|
217
|
+
})();
|
|
218
|
+
return presetsCache;
|
|
145
219
|
},
|
|
146
220
|
async loadFamily(family) {
|
|
147
221
|
assertValidFamilyName(family);
|
|
148
|
-
const res = await
|
|
222
|
+
const res = await get(`${base}/recipes/${family}.css`);
|
|
149
223
|
if (!res.ok) throw new Error(`${family}.css: ${res.status} ${res.statusText}`);
|
|
150
224
|
return res.text();
|
|
151
225
|
},
|
|
152
226
|
async listAllFamilies() {
|
|
153
|
-
const res = await
|
|
227
|
+
const res = await get(`${base}/index.json`);
|
|
154
228
|
if (!res.ok) throw new Error(`index.json: ${res.status} ${res.statusText}`);
|
|
155
229
|
return (await res.json()).families.filter((name) => FAMILY_RE.test(name));
|
|
156
230
|
}
|
|
@@ -341,6 +415,21 @@ async function scaffoldTheme(cwd) {
|
|
|
341
415
|
action: "created"
|
|
342
416
|
};
|
|
343
417
|
}
|
|
418
|
+
const THEME_COLOR_TOKENS = new Set([...THEME_BLOCK.matchAll(/--color-([\w-]+)\s*:/g)].map((m) => m[1] ?? ""));
|
|
419
|
+
const COLOR_UTILITY_RE = /^(?:bg|text|border|ring|outline|fill|stroke|divide|accent|caret|decoration|shadow|from|via|to|placeholder)-(.+)$/;
|
|
420
|
+
function referencedThemeTokens(flattened) {
|
|
421
|
+
const out = /* @__PURE__ */ new Set();
|
|
422
|
+
for (const utilities of Object.values(flattened)) for (const raw of utilities) {
|
|
423
|
+
const m = (raw.split(":").pop() ?? raw).match(COLOR_UTILITY_RE);
|
|
424
|
+
if (!m) continue;
|
|
425
|
+
const name = (m[1] ?? "").replace(/\/.*$/, "");
|
|
426
|
+
if (THEME_COLOR_TOKENS.has(name)) out.add(name);
|
|
427
|
+
}
|
|
428
|
+
return [...out].sort();
|
|
429
|
+
}
|
|
430
|
+
function findMissingThemeTokens(css, flattened) {
|
|
431
|
+
return referencedThemeTokens(flattened).filter((name) => !new RegExp(`--(?:color-)?${name}\\s*:`).test(css));
|
|
432
|
+
}
|
|
344
433
|
function isTailwindV4(cwd) {
|
|
345
434
|
try {
|
|
346
435
|
const pkg = JSON.parse(readFileSync(path.join(cwd, "package.json"), "utf8"));
|
|
@@ -371,7 +460,7 @@ async function wireBundler(cwd, bundler) {
|
|
|
371
460
|
if (bundler === "next") return {
|
|
372
461
|
configPath: null,
|
|
373
462
|
action: "manual",
|
|
374
|
-
snippet: `import { withShortwind } from "@shortwind/next";\n// wrap your Next config
|
|
463
|
+
snippet: `import { withShortwind } from "@shortwind/next";\n// withShortwind is curried — wrap your Next config:\n// export default withShortwind()(nextConfig);`,
|
|
375
464
|
reason: "Next config wiring is manual"
|
|
376
465
|
};
|
|
377
466
|
if (bundler === "astro") return {
|
|
@@ -423,17 +512,61 @@ function addImport(source, line) {
|
|
|
423
512
|
return source.slice(0, lastEnd) + `\n${line}` + source.slice(lastEnd);
|
|
424
513
|
}
|
|
425
514
|
//#endregion
|
|
515
|
+
//#region src/agents-file.ts
|
|
516
|
+
const MARKER = "skills/shortwind/SKILL.md";
|
|
517
|
+
function line(skillRel) {
|
|
518
|
+
return `For UI, prefer Shortwind \`@recipe\` class names (e.g. \`@card\`, \`@btn-primary\`, \`@row\`) over raw Tailwind where a recipe fits — full catalog in \`${skillRel}\`.`;
|
|
519
|
+
}
|
|
520
|
+
const CANDIDATES = ["AGENTS.md", "CLAUDE.md"];
|
|
521
|
+
async function wireAgentsInstructions(cwd, skillPath) {
|
|
522
|
+
const pointer = line(path.relative(cwd, skillPath).split(path.sep).join("/"));
|
|
523
|
+
let touched = null;
|
|
524
|
+
for (const name of CANDIDATES) {
|
|
525
|
+
const file = path.join(cwd, name);
|
|
526
|
+
if (!existsSync(file)) continue;
|
|
527
|
+
const current = await readFile(file, "utf8");
|
|
528
|
+
if (current.includes(MARKER)) {
|
|
529
|
+
touched ??= {
|
|
530
|
+
path: file,
|
|
531
|
+
action: "skipped"
|
|
532
|
+
};
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
await writeFile(file, current + (current.endsWith("\n\n") ? "" : current.endsWith("\n") ? "\n" : "\n\n") + pointer + "\n");
|
|
536
|
+
return {
|
|
537
|
+
path: file,
|
|
538
|
+
action: "appended"
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
if (touched) return touched;
|
|
542
|
+
const target = path.join(cwd, "AGENTS.md");
|
|
543
|
+
await writeFile(target, `# AGENTS.md\n\n${pointer}\n`);
|
|
544
|
+
return {
|
|
545
|
+
path: target,
|
|
546
|
+
action: "created"
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
//#endregion
|
|
426
550
|
//#region src/init.ts
|
|
427
|
-
const DEFAULT_REGISTRY =
|
|
551
|
+
const DEFAULT_REGISTRY = BUNDLED_ORIGIN;
|
|
428
552
|
async function init(options) {
|
|
429
553
|
const cwd = path.resolve(options.cwd);
|
|
430
|
-
const registry = options.registry ??
|
|
431
|
-
const source =
|
|
554
|
+
const registry = options.registry ?? DEFAULT_REGISTRY;
|
|
555
|
+
const source = await resolveSource(registry);
|
|
432
556
|
const shape = detectProject(cwd);
|
|
433
557
|
const families = await resolveFamilies(options.preset, source);
|
|
434
558
|
const pkgs = pickPackages(shape.bundler);
|
|
559
|
+
const version = cliVersion();
|
|
560
|
+
const specs = version ? pkgs.map((p) => `${p}@${version}`) : pkgs;
|
|
435
561
|
const installer = options.installPackages ?? defaultInstall;
|
|
436
|
-
|
|
562
|
+
let installOk = true;
|
|
563
|
+
let installError = null;
|
|
564
|
+
if (specs.length > 0) try {
|
|
565
|
+
await installer(shape.packageManager, specs, cwd);
|
|
566
|
+
} catch (err) {
|
|
567
|
+
installOk = false;
|
|
568
|
+
installError = err instanceof Error ? err.message : String(err);
|
|
569
|
+
}
|
|
437
570
|
const recipesDir = path.join(cwd, "recipes");
|
|
438
571
|
const { installed, skipped } = await copyRecipes(source, families, recipesDir);
|
|
439
572
|
await updateLockfile(recipesDir, registry, installed);
|
|
@@ -447,9 +580,12 @@ async function init(options) {
|
|
|
447
580
|
const huskyPath = path.join(cwd, ".husky", "pre-commit");
|
|
448
581
|
await installHuskyHook(huskyPath);
|
|
449
582
|
const skillPath = path.join(cwd, "skills", "shortwind", "SKILL.md");
|
|
450
|
-
await writeSkillMd(skillPath, recipesDir, families);
|
|
583
|
+
const skillRegistry = await writeSkillMd(skillPath, recipesDir, families);
|
|
451
584
|
const theme = await scaffoldTheme(cwd);
|
|
585
|
+
let missingThemeTokens = [];
|
|
586
|
+
if (theme.action === "skipped" && theme.themePath && skillRegistry) missingThemeTokens = findMissingThemeTokens(await readFile(theme.themePath, "utf8"), skillRegistry.flattened);
|
|
452
587
|
const bundlerConfig = await wireBundler(cwd, shape.bundler);
|
|
588
|
+
const agentsFile = await wireAgentsInstructions(cwd, skillPath);
|
|
453
589
|
return {
|
|
454
590
|
packageManager: shape.packageManager,
|
|
455
591
|
preset: options.preset,
|
|
@@ -464,15 +600,28 @@ async function init(options) {
|
|
|
464
600
|
skillPath,
|
|
465
601
|
themePath: theme.themePath,
|
|
466
602
|
themeAction: theme.action,
|
|
603
|
+
missingThemeTokens,
|
|
467
604
|
bundlerConfigPath: bundlerConfig.configPath,
|
|
468
605
|
bundlerConfigAction: bundlerConfig.action,
|
|
469
|
-
...bundlerConfig.snippet ? { bundlerConfigSnippet: bundlerConfig.snippet } : {}
|
|
606
|
+
...bundlerConfig.snippet ? { bundlerConfigSnippet: bundlerConfig.snippet } : {},
|
|
607
|
+
agentsFilePath: agentsFile.path,
|
|
608
|
+
agentsFileAction: agentsFile.action,
|
|
609
|
+
installOk,
|
|
610
|
+
installError
|
|
470
611
|
};
|
|
471
612
|
}
|
|
472
613
|
async function resolveFamilies(preset, source) {
|
|
473
614
|
if (preset === "none") return [];
|
|
474
615
|
return resolvePresetFamilies(preset, await source.loadPresets(), await source.listAllFamilies());
|
|
475
616
|
}
|
|
617
|
+
function cliVersion() {
|
|
618
|
+
try {
|
|
619
|
+
const pkgUrl = new URL("../package.json", import.meta.url);
|
|
620
|
+
return JSON.parse(readFileSync(fileURLToPath(pkgUrl), "utf8")).version ?? null;
|
|
621
|
+
} catch {
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
476
625
|
function pickPackages(bundler) {
|
|
477
626
|
const base = ["@shortwind/tailwind"];
|
|
478
627
|
switch (bundler) {
|
|
@@ -544,6 +693,7 @@ async function copyRecipes(source, families, recipesDir) {
|
|
|
544
693
|
continue;
|
|
545
694
|
}
|
|
546
695
|
const body = await source.loadFamily(family);
|
|
696
|
+
verifyFetchedFamily(body, family);
|
|
547
697
|
await writeFile(target, rewriteHeaderSha(body, computeBodySha(body)));
|
|
548
698
|
installed.push(family);
|
|
549
699
|
}
|
|
@@ -562,8 +712,14 @@ async function writeConfig(configPath, next) {
|
|
|
562
712
|
await writeFile(configPath, JSON.stringify(desired, null, 2) + "\n");
|
|
563
713
|
return;
|
|
564
714
|
}
|
|
715
|
+
let current;
|
|
716
|
+
try {
|
|
717
|
+
current = JSON.parse(await readFile(configPath, "utf8"));
|
|
718
|
+
} catch (err) {
|
|
719
|
+
throw new Error(`${configPath}: invalid JSON — ${err.message}`);
|
|
720
|
+
}
|
|
565
721
|
const merged = {
|
|
566
|
-
...
|
|
722
|
+
...current !== null && typeof current === "object" && !Array.isArray(current) ? current : {},
|
|
567
723
|
...desired
|
|
568
724
|
};
|
|
569
725
|
await writeFile(configPath, JSON.stringify(merged, null, 2) + "\n");
|
|
@@ -595,9 +751,9 @@ async function installHuskyHook(huskyPath) {
|
|
|
595
751
|
await writeFile(huskyPath, current.endsWith("\n") ? current + HUSKY_LINE + "\n" : current + "\nnpx shortwind build\n", { mode: 493 });
|
|
596
752
|
}
|
|
597
753
|
async function writeSkillMd(skillPath, recipesDir, families) {
|
|
598
|
-
await mkdir(path.dirname(skillPath), { recursive: true });
|
|
599
754
|
const allRecipes = [];
|
|
600
755
|
const guidance = {};
|
|
756
|
+
const problems = [];
|
|
601
757
|
for (const family of families) {
|
|
602
758
|
const filePath = path.join(recipesDir, `${family}.css`);
|
|
603
759
|
if (!existsSync(filePath)) continue;
|
|
@@ -605,23 +761,34 @@ async function writeSkillMd(skillPath, recipesDir, families) {
|
|
|
605
761
|
if (parsed.ok) {
|
|
606
762
|
allRecipes.push(...parsed.value.recipes);
|
|
607
763
|
if (parsed.value.guidance) guidance[family] = parsed.value.guidance;
|
|
608
|
-
}
|
|
764
|
+
} else problems.push(`${family}.css: ${parsed.errors.map((e) => e.message).join("; ")}`);
|
|
609
765
|
}
|
|
610
|
-
let registry = {
|
|
611
|
-
families: {},
|
|
612
|
-
flattened: {}
|
|
613
|
-
};
|
|
614
766
|
const resolved = buildRegistry(allRecipes, { guidance });
|
|
615
|
-
if (
|
|
616
|
-
|
|
767
|
+
if (problems.length > 0 || !resolved.ok) {
|
|
768
|
+
const all = resolved.ok ? problems : [...problems, ...resolved.errors.map((e) => e.message)];
|
|
769
|
+
console.warn(`[shortwind] SKILL.md not generated — recipe errors:\n ${all.join("\n ")}`);
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
await mkdir(path.dirname(skillPath), { recursive: true });
|
|
773
|
+
await writeFile(skillPath, renderSkillMarkdown(resolved.value, { order: families }));
|
|
774
|
+
return resolved.value;
|
|
617
775
|
}
|
|
618
776
|
//#endregion
|
|
619
777
|
//#region src/project.ts
|
|
620
778
|
const DEFAULT_CONFIG = {
|
|
621
|
-
registry:
|
|
779
|
+
registry: BUNDLED_ORIGIN,
|
|
622
780
|
recipesDir: "recipes",
|
|
623
781
|
outputPath: "skills/shortwind/SKILL.md"
|
|
624
782
|
};
|
|
783
|
+
function assertConfigString(value, field, configPath) {
|
|
784
|
+
if (typeof value !== "string") throw new Error(`${configPath}: "${field}" must be a string`);
|
|
785
|
+
return value;
|
|
786
|
+
}
|
|
787
|
+
function assertWithinCwd(cwd, value, field, configPath) {
|
|
788
|
+
const rel = path.relative(cwd, path.resolve(cwd, value));
|
|
789
|
+
if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) throw new Error(`${configPath}: "${field}" (${JSON.stringify(value)}) must be a path inside the project directory`);
|
|
790
|
+
return value;
|
|
791
|
+
}
|
|
625
792
|
async function readConfig(cwd) {
|
|
626
793
|
const configPath = path.join(cwd, "shortwind.config.json");
|
|
627
794
|
if (!existsSync(configPath)) return DEFAULT_CONFIG;
|
|
@@ -632,10 +799,16 @@ async function readConfig(cwd) {
|
|
|
632
799
|
} catch (err) {
|
|
633
800
|
throw new Error(`${configPath}: invalid JSON — ${err.message}`);
|
|
634
801
|
}
|
|
635
|
-
|
|
802
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`${configPath}: expected a JSON object`);
|
|
803
|
+
const merged = {
|
|
636
804
|
...DEFAULT_CONFIG,
|
|
637
805
|
...parsed
|
|
638
806
|
};
|
|
807
|
+
return {
|
|
808
|
+
registry: assertConfigString(merged.registry, "registry", configPath),
|
|
809
|
+
recipesDir: assertWithinCwd(cwd, assertConfigString(merged.recipesDir, "recipesDir", configPath), "recipesDir", configPath),
|
|
810
|
+
outputPath: assertWithinCwd(cwd, assertConfigString(merged.outputPath, "outputPath", configPath), "outputPath", configPath)
|
|
811
|
+
};
|
|
639
812
|
}
|
|
640
813
|
function installedFamilies(recipesDir) {
|
|
641
814
|
if (!existsSync(recipesDir)) return [];
|
|
@@ -655,24 +828,25 @@ async function regenerateSkillMd(cwd, config) {
|
|
|
655
828
|
const recipesDir = path.join(cwd, config.recipesDir);
|
|
656
829
|
const families = installedFamilies(recipesDir);
|
|
657
830
|
const skillPath = path.join(cwd, config.outputPath);
|
|
658
|
-
const { mkdir } = await import("node:fs/promises");
|
|
659
|
-
await mkdir(path.dirname(skillPath), { recursive: true });
|
|
660
831
|
const allRecipes = [];
|
|
661
832
|
const guidance = {};
|
|
833
|
+
const problems = [];
|
|
662
834
|
for (const family of families) {
|
|
663
835
|
const parsed = parseRecipeFile(readFileSync(path.join(recipesDir, `${family}.css`), "utf8"), `${family}.css`);
|
|
664
836
|
if (parsed.ok) {
|
|
665
837
|
allRecipes.push(...parsed.value.recipes);
|
|
666
838
|
if (parsed.value.guidance) guidance[family] = parsed.value.guidance;
|
|
667
|
-
}
|
|
839
|
+
} else problems.push(`${family}.css: ${parsed.errors.map((e) => e.message).join("; ")}`);
|
|
668
840
|
}
|
|
669
|
-
let registry = {
|
|
670
|
-
families: {},
|
|
671
|
-
flattened: {}
|
|
672
|
-
};
|
|
673
841
|
const resolved = buildRegistry(allRecipes, { guidance });
|
|
674
|
-
if (
|
|
675
|
-
|
|
842
|
+
if (problems.length > 0 || !resolved.ok) {
|
|
843
|
+
const all = resolved.ok ? problems : [...problems, ...resolved.errors.map((e) => e.message)];
|
|
844
|
+
console.warn(`[shortwind] SKILL.md not regenerated — fix these recipe errors first:\n ${all.join("\n ")}\n ${path.relative(cwd, skillPath)} left unchanged.`);
|
|
845
|
+
return skillPath;
|
|
846
|
+
}
|
|
847
|
+
const { mkdir } = await import("node:fs/promises");
|
|
848
|
+
await mkdir(path.dirname(skillPath), { recursive: true });
|
|
849
|
+
await writeFile(skillPath, renderSkillMarkdown(resolved.value, { order: families }));
|
|
676
850
|
return skillPath;
|
|
677
851
|
}
|
|
678
852
|
/**
|
|
@@ -698,7 +872,7 @@ async function add(options) {
|
|
|
698
872
|
const cwd = path.resolve(options.cwd);
|
|
699
873
|
const config = await readConfig(cwd);
|
|
700
874
|
const registry = options.registry ?? config.registry;
|
|
701
|
-
const source =
|
|
875
|
+
const source = await resolveSource(registry);
|
|
702
876
|
const recipesDir = path.join(cwd, config.recipesDir);
|
|
703
877
|
await mkdir(recipesDir, { recursive: true });
|
|
704
878
|
const lock = await readLockfile(recipesDir);
|
|
@@ -706,6 +880,7 @@ async function add(options) {
|
|
|
706
880
|
const requested = options.all ? await source.listAllFamilies() : options.families;
|
|
707
881
|
if (options.all && options.as) throw new Error("--as cannot be combined with --all");
|
|
708
882
|
if (options.as && requested.length !== 1) throw new Error("--as requires exactly one family argument");
|
|
883
|
+
if (options.as !== void 0) assertValidFamilyName(options.as);
|
|
709
884
|
const added = [];
|
|
710
885
|
const skipped = [];
|
|
711
886
|
const overwritten = [];
|
|
@@ -720,6 +895,7 @@ async function add(options) {
|
|
|
720
895
|
continue;
|
|
721
896
|
}
|
|
722
897
|
const sourceCss = await source.loadFamily(family);
|
|
898
|
+
verifyFetchedFamily(sourceCss, family);
|
|
723
899
|
const renamed = options.as ? renameFamilyInSource(sourceCss, family, options.as) : sourceCss;
|
|
724
900
|
const sha = computeBodySha(renamed);
|
|
725
901
|
const finalCss = rewriteHeaderSha(renamed, sha);
|
|
@@ -854,11 +1030,55 @@ async function newFamily(options) {
|
|
|
854
1030
|
};
|
|
855
1031
|
}
|
|
856
1032
|
//#endregion
|
|
1033
|
+
//#region src/commands/reseal.ts
|
|
1034
|
+
async function reseal(options) {
|
|
1035
|
+
const cwd = path.resolve(options.cwd);
|
|
1036
|
+
const config = await readConfig(cwd);
|
|
1037
|
+
const recipesDir = path.join(cwd, config.recipesDir);
|
|
1038
|
+
const families = options.families && options.families.length > 0 ? options.families : installedFamilies(recipesDir);
|
|
1039
|
+
const lock = await readLockfile(recipesDir);
|
|
1040
|
+
const resealed = [];
|
|
1041
|
+
const unchanged = [];
|
|
1042
|
+
const notFound = [];
|
|
1043
|
+
const noHeader = [];
|
|
1044
|
+
for (const family of families) {
|
|
1045
|
+
const file = path.join(recipesDir, `${family}.css`);
|
|
1046
|
+
if (!existsSync(file)) {
|
|
1047
|
+
notFound.push(family);
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
const source = readFileSync(file, "utf8");
|
|
1051
|
+
const header = extractHeader(source);
|
|
1052
|
+
if (!header) {
|
|
1053
|
+
noHeader.push(family);
|
|
1054
|
+
continue;
|
|
1055
|
+
}
|
|
1056
|
+
const sha = computeBodySha(source);
|
|
1057
|
+
if (sha === header.sha && lock.families[family]?.sha === sha) {
|
|
1058
|
+
unchanged.push(family);
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
1061
|
+
await writeFile(file, rewriteHeaderSha(source, sha));
|
|
1062
|
+
lock.families[family] = {
|
|
1063
|
+
version: header.version,
|
|
1064
|
+
sha
|
|
1065
|
+
};
|
|
1066
|
+
resealed.push(family);
|
|
1067
|
+
}
|
|
1068
|
+
await writeLockfile(recipesDir, lock);
|
|
1069
|
+
return {
|
|
1070
|
+
resealed,
|
|
1071
|
+
unchanged,
|
|
1072
|
+
notFound,
|
|
1073
|
+
noHeader
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
//#endregion
|
|
857
1077
|
//#region src/commands/preset.ts
|
|
858
1078
|
async function preset(options) {
|
|
859
1079
|
const cwd = path.resolve(options.cwd);
|
|
860
1080
|
const config = await readConfig(cwd);
|
|
861
|
-
const source =
|
|
1081
|
+
const source = await resolveSource(options.registry ?? config.registry);
|
|
862
1082
|
if (options.name === "none") throw new Error("Use `shortwind remove` to uninstall families; preset 'none' is for `init` only.");
|
|
863
1083
|
const presets = await source.loadPresets();
|
|
864
1084
|
const all = await source.listAllFamilies();
|
|
@@ -887,7 +1107,7 @@ async function ls(options) {
|
|
|
887
1107
|
});
|
|
888
1108
|
let available = [];
|
|
889
1109
|
if (!options.installedOnly) {
|
|
890
|
-
const source =
|
|
1110
|
+
const source = await resolveSource(options.registry ?? config.registry);
|
|
891
1111
|
try {
|
|
892
1112
|
available = await source.listAllFamilies();
|
|
893
1113
|
} catch {
|
|
@@ -1021,6 +1241,12 @@ async function dev(options) {
|
|
|
1021
1241
|
timer = setTimeout(() => void runBuild(false), debounceMs);
|
|
1022
1242
|
};
|
|
1023
1243
|
watcher.on("add", schedule).on("change", schedule).on("unlink", schedule);
|
|
1244
|
+
watcher.on("error", (err) => {
|
|
1245
|
+
status({
|
|
1246
|
+
kind: "error",
|
|
1247
|
+
message: err instanceof Error ? err.message : String(err)
|
|
1248
|
+
});
|
|
1249
|
+
});
|
|
1024
1250
|
let stopped = false;
|
|
1025
1251
|
const stop = async () => {
|
|
1026
1252
|
if (stopped) return;
|
|
@@ -1061,7 +1287,7 @@ async function upgrade(options) {
|
|
|
1061
1287
|
const cwd = path.resolve(options.cwd);
|
|
1062
1288
|
const config = await readConfig(cwd);
|
|
1063
1289
|
const registry = options.registry ?? config.registry;
|
|
1064
|
-
const source = options.source ??
|
|
1290
|
+
const source = options.source ?? await resolveSource(registry);
|
|
1065
1291
|
const recipesDir = path.join(cwd, config.recipesDir);
|
|
1066
1292
|
const installed = installedFamilies(recipesDir);
|
|
1067
1293
|
const targets = options.families && options.families.length > 0 ? options.families : installed;
|
|
@@ -1090,6 +1316,7 @@ async function upgrade(options) {
|
|
|
1090
1316
|
let incomingBody;
|
|
1091
1317
|
try {
|
|
1092
1318
|
incomingBody = await source.loadFamily(family);
|
|
1319
|
+
verifyFetchedFamily(incomingBody, family);
|
|
1093
1320
|
} catch (err) {
|
|
1094
1321
|
errors.push({
|
|
1095
1322
|
family,
|
|
@@ -1111,7 +1338,9 @@ async function upgrade(options) {
|
|
|
1111
1338
|
const recordedSha = localHeader?.sha ?? "";
|
|
1112
1339
|
const actualSha = computeBodySha(localBody);
|
|
1113
1340
|
const lockedVersion = lock.families[family]?.version ?? localHeader?.version ?? "";
|
|
1114
|
-
const
|
|
1341
|
+
const recordedIsLegacy = isLegacyFingerprint(recordedSha);
|
|
1342
|
+
if (recordedIsLegacy) console.warn(`[shortwind] ${family}.css uses an older fingerprint format — run \`shortwind reseal\` to upgrade its seal (the recipe body is unchanged).`);
|
|
1343
|
+
const isTouched = recordedSha !== "" && recordedSha !== actualSha && !recordedIsLegacy;
|
|
1115
1344
|
if ((isTouched ? "touched" : lockedVersion === incomingVersion ? "unchanged" : "pristine") === "unchanged" && !isTouched) {
|
|
1116
1345
|
outcomes.push({
|
|
1117
1346
|
family,
|
|
@@ -1208,8 +1437,9 @@ async function upgrade(options) {
|
|
|
1208
1437
|
skillPath
|
|
1209
1438
|
};
|
|
1210
1439
|
}
|
|
1440
|
+
let atomicWriteSeq = 0;
|
|
1211
1441
|
async function atomicWrite(filePath, body) {
|
|
1212
|
-
const tmp = filePath
|
|
1442
|
+
const tmp = `${filePath}.${process.pid}.${atomicWriteSeq++}.tmp`;
|
|
1213
1443
|
const fh = await open(tmp, "w");
|
|
1214
1444
|
try {
|
|
1215
1445
|
await fh.writeFile(body);
|
|
@@ -1243,13 +1473,24 @@ async function verify(options) {
|
|
|
1243
1473
|
continue;
|
|
1244
1474
|
}
|
|
1245
1475
|
const actual = computeBodySha(source);
|
|
1246
|
-
if (header.sha !== actual)
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1476
|
+
if (header.sha !== actual) {
|
|
1477
|
+
if (isLegacyFingerprint(header.sha)) {
|
|
1478
|
+
issues.push({
|
|
1479
|
+
family,
|
|
1480
|
+
kind: "legacy-fingerprint",
|
|
1481
|
+
file: filePath,
|
|
1482
|
+
recorded: header.sha
|
|
1483
|
+
});
|
|
1484
|
+
continue;
|
|
1485
|
+
}
|
|
1486
|
+
issues.push({
|
|
1487
|
+
family,
|
|
1488
|
+
kind: "header-tampered",
|
|
1489
|
+
file: filePath,
|
|
1490
|
+
recorded: header.sha,
|
|
1491
|
+
actual
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1253
1494
|
const locked = lock.families[family];
|
|
1254
1495
|
if (!locked) issues.push({
|
|
1255
1496
|
family,
|
|
@@ -1479,7 +1720,7 @@ async function lint(options) {
|
|
|
1479
1720
|
for (const token of u.tokens) {
|
|
1480
1721
|
if (!token.value.startsWith("@")) continue;
|
|
1481
1722
|
const name = token.value.slice(1);
|
|
1482
|
-
if (registry.flattened
|
|
1723
|
+
if (Object.hasOwn(registry.flattened, name)) usedRecipes.add(name);
|
|
1483
1724
|
else if (enabledRules.has("recipe/unknown")) findings.push({
|
|
1484
1725
|
rule: "recipe/unknown",
|
|
1485
1726
|
severity: "error",
|
|
@@ -1669,7 +1910,7 @@ function checkUsageSuffixOrder(file, tokens, registry) {
|
|
|
1669
1910
|
for (const token of tokens) {
|
|
1670
1911
|
if (!token.value.startsWith("@")) continue;
|
|
1671
1912
|
const name = token.value.slice(1);
|
|
1672
|
-
if (!registry.flattened
|
|
1913
|
+
if (!Object.hasOwn(registry.flattened, name)) continue;
|
|
1673
1914
|
const meta = recipeMeta(name, familyForRecipe(registry, name));
|
|
1674
1915
|
if (!meta.badOrder) continue;
|
|
1675
1916
|
findings.push({
|
|
@@ -1688,7 +1929,7 @@ function checkConflictingIntent(file, tokens, registry) {
|
|
|
1688
1929
|
for (const token of tokens) {
|
|
1689
1930
|
if (!token.value.startsWith("@")) continue;
|
|
1690
1931
|
const name = token.value.slice(1);
|
|
1691
|
-
if (!registry.flattened
|
|
1932
|
+
if (!Object.hasOwn(registry.flattened, name)) continue;
|
|
1692
1933
|
const meta = recipeMeta(name, familyForRecipe(registry, name));
|
|
1693
1934
|
if (!meta.intent) continue;
|
|
1694
1935
|
const familyIntents = byFamily.get(meta.family) ?? /* @__PURE__ */ new Map();
|
|
@@ -1722,7 +1963,7 @@ function checkSiblingOverlap(file, tokens, registry) {
|
|
|
1722
1963
|
for (const token of tokens) {
|
|
1723
1964
|
if (!token.value.startsWith("@")) continue;
|
|
1724
1965
|
const name = token.value.slice(1);
|
|
1725
|
-
if (!registry.flattened
|
|
1966
|
+
if (!Object.hasOwn(registry.flattened, name)) continue;
|
|
1726
1967
|
const family = familyForRecipe(registry, name) ?? name.split("-")[0] ?? name;
|
|
1727
1968
|
const arr = byFamily.get(family) ?? [];
|
|
1728
1969
|
arr.push({
|
|
@@ -1766,6 +2007,7 @@ function checkDynamicClass(file, dynamicTokens) {
|
|
|
1766
2007
|
const CLASS_ATTR_STR_RE = /\b(?:class|className)\s*=\s*(["'])([^"']*)\1/g;
|
|
1767
2008
|
const CLASS_ATTR_BRACE_RE = /\b(?:class|className)\s*=\s*\{/g;
|
|
1768
2009
|
const STRING_LITERAL_RE = /(["'`])((?:\\.|(?!\1)[^\\])*)\1/g;
|
|
2010
|
+
const CALL_EXPANDER_RE = /\b(?:cva|tv)\s*\(/g;
|
|
1769
2011
|
function extractClassUsages(source) {
|
|
1770
2012
|
const usages = [];
|
|
1771
2013
|
for (const m of source.matchAll(CLASS_ATTR_STR_RE)) {
|
|
@@ -1804,6 +2046,28 @@ function extractClassUsages(source) {
|
|
|
1804
2046
|
});
|
|
1805
2047
|
}
|
|
1806
2048
|
}
|
|
2049
|
+
for (const m of source.matchAll(CALL_EXPANDER_RE)) {
|
|
2050
|
+
const openParen = (m.index ?? 0) + m[0].length - 1;
|
|
2051
|
+
const close = findMatchingDelimiter(source, openParen, "(", ")");
|
|
2052
|
+
if (close === -1) continue;
|
|
2053
|
+
const inner = source.slice(openParen + 1, close);
|
|
2054
|
+
for (const sm of inner.matchAll(STRING_LITERAL_RE)) {
|
|
2055
|
+
const value = sm[2] ?? "";
|
|
2056
|
+
if (value.length === 0) continue;
|
|
2057
|
+
const literalStart = openParen + 1 + (sm.index ?? 0);
|
|
2058
|
+
const valueStart = literalStart + 1;
|
|
2059
|
+
const { tokens, dynamicTokens } = tokenizeClassString(source, value, valueStart);
|
|
2060
|
+
if (!tokens.some((t) => t.value.startsWith("@")) && dynamicTokens.length === 0) continue;
|
|
2061
|
+
usages.push({
|
|
2062
|
+
fileOffset: literalStart,
|
|
2063
|
+
valueStart,
|
|
2064
|
+
raw: value,
|
|
2065
|
+
tokens,
|
|
2066
|
+
dynamicTokens,
|
|
2067
|
+
fixable: false
|
|
2068
|
+
});
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
1807
2071
|
return usages;
|
|
1808
2072
|
}
|
|
1809
2073
|
function tokenizeClassString(source, value, valueStart) {
|
|
@@ -1838,6 +2102,23 @@ function tokenizeClassString(source, value, valueStart) {
|
|
|
1838
2102
|
};
|
|
1839
2103
|
}
|
|
1840
2104
|
function findMatchingBrace(source, openIdx) {
|
|
2105
|
+
return findMatchingDelimiter(source, openIdx, "{", "}");
|
|
2106
|
+
}
|
|
2107
|
+
function spliceRedundantTokens(raw, isRedundant) {
|
|
2108
|
+
const pieces = raw.split(/(\s+)/);
|
|
2109
|
+
const drop = new Array(pieces.length).fill(false);
|
|
2110
|
+
for (let i = 0; i < pieces.length; i++) {
|
|
2111
|
+
const piece = pieces[i] ?? "";
|
|
2112
|
+
if (piece.length === 0 || /^\s+$/.test(piece)) continue;
|
|
2113
|
+
if (piece.startsWith("@") || piece.includes("${")) continue;
|
|
2114
|
+
if (!isRedundant(piece)) continue;
|
|
2115
|
+
drop[i] = true;
|
|
2116
|
+
if (i > 0 && /^\s+$/.test(pieces[i - 1] ?? "")) drop[i - 1] = true;
|
|
2117
|
+
else if (/^\s+$/.test(pieces[i + 1] ?? "")) drop[i + 1] = true;
|
|
2118
|
+
}
|
|
2119
|
+
return pieces.filter((_, i) => !drop[i]).join("");
|
|
2120
|
+
}
|
|
2121
|
+
function findMatchingDelimiter(source, openIdx, open, close) {
|
|
1841
2122
|
let depth = 1;
|
|
1842
2123
|
let i = openIdx + 1;
|
|
1843
2124
|
while (i < source.length && depth > 0) {
|
|
@@ -1883,8 +2164,8 @@ function findMatchingBrace(source, openIdx) {
|
|
|
1883
2164
|
}
|
|
1884
2165
|
continue;
|
|
1885
2166
|
}
|
|
1886
|
-
if (ch ===
|
|
1887
|
-
else if (ch ===
|
|
2167
|
+
if (ch === open) depth++;
|
|
2168
|
+
else if (ch === close) depth--;
|
|
1888
2169
|
i++;
|
|
1889
2170
|
}
|
|
1890
2171
|
return depth === 0 ? i - 1 : -1;
|
|
@@ -1916,25 +2197,18 @@ function checkRedundantUtility(file, source, registry, applyFix) {
|
|
|
1916
2197
|
for (const t of exp) expansions.add(t);
|
|
1917
2198
|
}
|
|
1918
2199
|
if (expansions.size === 0) continue;
|
|
1919
|
-
const
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
column: tok.column,
|
|
1928
|
-
message: `${tok.value} is already included by a recipe on this element`
|
|
1929
|
-
});
|
|
1930
|
-
continue;
|
|
1931
|
-
}
|
|
1932
|
-
kept.push(tok.value);
|
|
1933
|
-
}
|
|
2200
|
+
for (const tok of usage.tokens) if (!tok.value.startsWith("@") && expansions.has(tok.value)) findings.push({
|
|
2201
|
+
rule: "recipe/no-redundant-utility",
|
|
2202
|
+
severity: "info",
|
|
2203
|
+
file,
|
|
2204
|
+
line: tok.line,
|
|
2205
|
+
column: tok.column,
|
|
2206
|
+
message: `${tok.value} is already included by a recipe on this element`
|
|
2207
|
+
});
|
|
1934
2208
|
if (fixed !== null && usage.fixable) {
|
|
1935
2209
|
if (usage.valueStart < cursor) continue;
|
|
1936
2210
|
fixed += source.slice(cursor, usage.valueStart);
|
|
1937
|
-
fixed +=
|
|
2211
|
+
fixed += spliceRedundantTokens(usage.raw, (t) => expansions.has(t));
|
|
1938
2212
|
cursor = usage.valueStart + usage.raw.length;
|
|
1939
2213
|
}
|
|
1940
2214
|
}
|
|
@@ -2001,7 +2275,7 @@ async function bench(options) {
|
|
|
2001
2275
|
expandedLlmTokens: 0
|
|
2002
2276
|
};
|
|
2003
2277
|
for (const { filename, content } of filesToBench) {
|
|
2004
|
-
const expanded = transformContent(content, registry, { mode: filename
|
|
2278
|
+
const expanded = transformContent(content, registry, { mode: modeForFile(filename) });
|
|
2005
2279
|
const compactUsages = extractClassUsages(content);
|
|
2006
2280
|
const expandedUsages = extractClassUsages(expanded);
|
|
2007
2281
|
const fileResult = {
|
|
@@ -2108,6 +2382,6 @@ function formatBenchTable(result) {
|
|
|
2108
2382
|
return lines.join("\n");
|
|
2109
2383
|
}
|
|
2110
2384
|
//#endregion
|
|
2111
|
-
export {
|
|
2385
|
+
export { buildHeaderLine as A, cliVersion as C, createRegistrySource as D, writeLockfile as E, verifyFetchedFamily as F, extractHeader as M, rewriteHeaderSha as N, resolvePresetFamilies as O, sealRecipeFile as P, DEFAULT_REGISTRY as S, readLockfile as T, NewFamilyError as _, formatFindingsText as a, add as b, UpgradeError as c, BuildError as d, build as f, reseal as g, preset as h, extractClassUsages as i, computeBodySha as j, detectProject as k, upgrade as l, ls as m, formatBenchTable as n, lint as o, formatLsText as p, ALL_RULES as r, verify as s, bench as t, dev as u, newFamily as v, init as w, renameFamilyInSource as x, remove as y };
|
|
2112
2386
|
|
|
2113
|
-
//# sourceMappingURL=bench-
|
|
2387
|
+
//# sourceMappingURL=bench-oy9aPOvX.js.map
|