@shortwind/cli 0.1.0-beta.2 → 0.1.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{bench-NKKDz3ld.js → bench-CUJJk5AC.js} +182 -14
- package/dist/bench-CUJJk5AC.js.map +1 -0
- package/dist/bin.js +22 -3
- package/dist/bin.js.map +1 -1
- package/dist/catalog.generated-B_ds7MPV.js +83 -0
- package/dist/catalog.generated-B_ds7MPV.js.map +1 -0
- package/dist/index.d.ts +20 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/package.json +4 -3
- package/dist/bench-NKKDz3ld.js.map +0 -1
|
@@ -1,10 +1,10 @@
|
|
|
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
6
|
import { buildRegistry, isReservedRecipeName, 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";
|
|
@@ -116,6 +116,56 @@ function createRegistrySource(origin) {
|
|
|
116
116
|
if (origin.startsWith("http://") || origin.startsWith("https://")) return httpSource(origin);
|
|
117
117
|
return fileSource(origin);
|
|
118
118
|
}
|
|
119
|
+
async function resolveSource(origin) {
|
|
120
|
+
if (origin && origin !== "bundled:@shortwind/catalog") return createRegistrySource(origin);
|
|
121
|
+
return defaultCatalogSource();
|
|
122
|
+
}
|
|
123
|
+
const CATALOG_PACKAGE = "@shortwind/catalog";
|
|
124
|
+
const NPM_TIMEOUT_MS = 3e3;
|
|
125
|
+
async function defaultCatalogSource() {
|
|
126
|
+
try {
|
|
127
|
+
const cdn = httpSource(`https://cdn.jsdelivr.net/npm/${CATALOG_PACKAGE}@${await resolveCatalogVersion()}/dist/registry`);
|
|
128
|
+
await cdn.loadPresets();
|
|
129
|
+
return cdn;
|
|
130
|
+
} catch {
|
|
131
|
+
return bundledSource();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async function resolveCatalogVersion() {
|
|
135
|
+
const res = await fetch(`https://registry.npmjs.org/${CATALOG_PACKAGE}`, {
|
|
136
|
+
signal: AbortSignal.timeout(NPM_TIMEOUT_MS),
|
|
137
|
+
headers: { accept: "application/vnd.npm.install-v1+json" }
|
|
138
|
+
});
|
|
139
|
+
if (!res.ok) throw new Error(`npm: ${res.status}`);
|
|
140
|
+
const tags = (await res.json())["dist-tags"] ?? {};
|
|
141
|
+
const version = tags["latest"] ?? tags["beta"];
|
|
142
|
+
if (!version) throw new Error("no published catalog version");
|
|
143
|
+
return version;
|
|
144
|
+
}
|
|
145
|
+
const BUNDLED_ORIGIN = "bundled:@shortwind/catalog";
|
|
146
|
+
function bundledSource() {
|
|
147
|
+
let cache = null;
|
|
148
|
+
const load = () => {
|
|
149
|
+
cache ??= import("./catalog.generated-B_ds7MPV.js");
|
|
150
|
+
return cache;
|
|
151
|
+
};
|
|
152
|
+
return {
|
|
153
|
+
origin: BUNDLED_ORIGIN,
|
|
154
|
+
async loadPresets() {
|
|
155
|
+
return (await load()).CATALOG_PRESETS;
|
|
156
|
+
},
|
|
157
|
+
async loadFamily(family) {
|
|
158
|
+
assertValidFamilyName(family);
|
|
159
|
+
const { CATALOG_RECIPES } = await load();
|
|
160
|
+
const css = CATALOG_RECIPES[family];
|
|
161
|
+
if (css === void 0) throw new Error(`unknown family: ${family}`);
|
|
162
|
+
return css;
|
|
163
|
+
},
|
|
164
|
+
async listAllFamilies() {
|
|
165
|
+
return [...(await load()).CATALOG_FAMILIES];
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
119
169
|
function fileSource(origin) {
|
|
120
170
|
const root = origin.startsWith("file://") ? fileURLToPath(origin) : origin;
|
|
121
171
|
return {
|
|
@@ -423,17 +473,54 @@ function addImport(source, line) {
|
|
|
423
473
|
return source.slice(0, lastEnd) + `\n${line}` + source.slice(lastEnd);
|
|
424
474
|
}
|
|
425
475
|
//#endregion
|
|
476
|
+
//#region src/agents-file.ts
|
|
477
|
+
const MARKER = "skills/shortwind/SKILL.md";
|
|
478
|
+
function line(skillRel) {
|
|
479
|
+
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}\`.`;
|
|
480
|
+
}
|
|
481
|
+
const CANDIDATES = ["AGENTS.md", "CLAUDE.md"];
|
|
482
|
+
async function wireAgentsInstructions(cwd, skillPath) {
|
|
483
|
+
const pointer = line(path.relative(cwd, skillPath).split(path.sep).join("/"));
|
|
484
|
+
let touched = null;
|
|
485
|
+
for (const name of CANDIDATES) {
|
|
486
|
+
const file = path.join(cwd, name);
|
|
487
|
+
if (!existsSync(file)) continue;
|
|
488
|
+
const current = await readFile(file, "utf8");
|
|
489
|
+
if (current.includes(MARKER)) {
|
|
490
|
+
touched ??= {
|
|
491
|
+
path: file,
|
|
492
|
+
action: "skipped"
|
|
493
|
+
};
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
await writeFile(file, current + (current.endsWith("\n\n") ? "" : current.endsWith("\n") ? "\n" : "\n\n") + pointer + "\n");
|
|
497
|
+
return {
|
|
498
|
+
path: file,
|
|
499
|
+
action: "appended"
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
if (touched) return touched;
|
|
503
|
+
const target = path.join(cwd, "AGENTS.md");
|
|
504
|
+
await writeFile(target, `# AGENTS.md\n\n${pointer}\n`);
|
|
505
|
+
return {
|
|
506
|
+
path: target,
|
|
507
|
+
action: "created"
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
//#endregion
|
|
426
511
|
//#region src/init.ts
|
|
427
|
-
const DEFAULT_REGISTRY =
|
|
512
|
+
const DEFAULT_REGISTRY = BUNDLED_ORIGIN;
|
|
428
513
|
async function init(options) {
|
|
429
514
|
const cwd = path.resolve(options.cwd);
|
|
430
|
-
const registry = options.registry ??
|
|
431
|
-
const source =
|
|
515
|
+
const registry = options.registry ?? DEFAULT_REGISTRY;
|
|
516
|
+
const source = await resolveSource(registry);
|
|
432
517
|
const shape = detectProject(cwd);
|
|
433
518
|
const families = await resolveFamilies(options.preset, source);
|
|
434
519
|
const pkgs = pickPackages(shape.bundler);
|
|
520
|
+
const version = cliVersion();
|
|
521
|
+
const specs = version ? pkgs.map((p) => `${p}@${version}`) : pkgs;
|
|
435
522
|
const installer = options.installPackages ?? defaultInstall;
|
|
436
|
-
if (
|
|
523
|
+
if (specs.length > 0) await installer(shape.packageManager, specs, cwd);
|
|
437
524
|
const recipesDir = path.join(cwd, "recipes");
|
|
438
525
|
const { installed, skipped } = await copyRecipes(source, families, recipesDir);
|
|
439
526
|
await updateLockfile(recipesDir, registry, installed);
|
|
@@ -450,6 +537,7 @@ async function init(options) {
|
|
|
450
537
|
await writeSkillMd(skillPath, recipesDir, families);
|
|
451
538
|
const theme = await scaffoldTheme(cwd);
|
|
452
539
|
const bundlerConfig = await wireBundler(cwd, shape.bundler);
|
|
540
|
+
const agentsFile = await wireAgentsInstructions(cwd, skillPath);
|
|
453
541
|
return {
|
|
454
542
|
packageManager: shape.packageManager,
|
|
455
543
|
preset: options.preset,
|
|
@@ -466,13 +554,23 @@ async function init(options) {
|
|
|
466
554
|
themeAction: theme.action,
|
|
467
555
|
bundlerConfigPath: bundlerConfig.configPath,
|
|
468
556
|
bundlerConfigAction: bundlerConfig.action,
|
|
469
|
-
...bundlerConfig.snippet ? { bundlerConfigSnippet: bundlerConfig.snippet } : {}
|
|
557
|
+
...bundlerConfig.snippet ? { bundlerConfigSnippet: bundlerConfig.snippet } : {},
|
|
558
|
+
agentsFilePath: agentsFile.path,
|
|
559
|
+
agentsFileAction: agentsFile.action
|
|
470
560
|
};
|
|
471
561
|
}
|
|
472
562
|
async function resolveFamilies(preset, source) {
|
|
473
563
|
if (preset === "none") return [];
|
|
474
564
|
return resolvePresetFamilies(preset, await source.loadPresets(), await source.listAllFamilies());
|
|
475
565
|
}
|
|
566
|
+
function cliVersion() {
|
|
567
|
+
try {
|
|
568
|
+
const pkgUrl = new URL("../package.json", import.meta.url);
|
|
569
|
+
return JSON.parse(readFileSync(fileURLToPath(pkgUrl), "utf8")).version ?? null;
|
|
570
|
+
} catch {
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
476
574
|
function pickPackages(bundler) {
|
|
477
575
|
const base = ["@shortwind/tailwind"];
|
|
478
576
|
switch (bundler) {
|
|
@@ -698,7 +796,7 @@ async function add(options) {
|
|
|
698
796
|
const cwd = path.resolve(options.cwd);
|
|
699
797
|
const config = await readConfig(cwd);
|
|
700
798
|
const registry = options.registry ?? config.registry;
|
|
701
|
-
const source =
|
|
799
|
+
const source = await resolveSource(registry);
|
|
702
800
|
const recipesDir = path.join(cwd, config.recipesDir);
|
|
703
801
|
await mkdir(recipesDir, { recursive: true });
|
|
704
802
|
const lock = await readLockfile(recipesDir);
|
|
@@ -854,11 +952,55 @@ async function newFamily(options) {
|
|
|
854
952
|
};
|
|
855
953
|
}
|
|
856
954
|
//#endregion
|
|
955
|
+
//#region src/commands/reseal.ts
|
|
956
|
+
async function reseal(options) {
|
|
957
|
+
const cwd = path.resolve(options.cwd);
|
|
958
|
+
const config = await readConfig(cwd);
|
|
959
|
+
const recipesDir = path.join(cwd, config.recipesDir);
|
|
960
|
+
const families = options.families && options.families.length > 0 ? options.families : installedFamilies(recipesDir);
|
|
961
|
+
const lock = await readLockfile(recipesDir);
|
|
962
|
+
const resealed = [];
|
|
963
|
+
const unchanged = [];
|
|
964
|
+
const notFound = [];
|
|
965
|
+
const noHeader = [];
|
|
966
|
+
for (const family of families) {
|
|
967
|
+
const file = path.join(recipesDir, `${family}.css`);
|
|
968
|
+
if (!existsSync(file)) {
|
|
969
|
+
notFound.push(family);
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
const source = readFileSync(file, "utf8");
|
|
973
|
+
const header = extractHeader(source);
|
|
974
|
+
if (!header) {
|
|
975
|
+
noHeader.push(family);
|
|
976
|
+
continue;
|
|
977
|
+
}
|
|
978
|
+
const sha = computeBodySha(source);
|
|
979
|
+
if (sha === header.sha && lock.families[family]?.sha === sha) {
|
|
980
|
+
unchanged.push(family);
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
983
|
+
await writeFile(file, rewriteHeaderSha(source, sha));
|
|
984
|
+
lock.families[family] = {
|
|
985
|
+
version: header.version,
|
|
986
|
+
sha
|
|
987
|
+
};
|
|
988
|
+
resealed.push(family);
|
|
989
|
+
}
|
|
990
|
+
await writeLockfile(recipesDir, lock);
|
|
991
|
+
return {
|
|
992
|
+
resealed,
|
|
993
|
+
unchanged,
|
|
994
|
+
notFound,
|
|
995
|
+
noHeader
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
//#endregion
|
|
857
999
|
//#region src/commands/preset.ts
|
|
858
1000
|
async function preset(options) {
|
|
859
1001
|
const cwd = path.resolve(options.cwd);
|
|
860
1002
|
const config = await readConfig(cwd);
|
|
861
|
-
const source =
|
|
1003
|
+
const source = await resolveSource(options.registry ?? config.registry);
|
|
862
1004
|
if (options.name === "none") throw new Error("Use `shortwind remove` to uninstall families; preset 'none' is for `init` only.");
|
|
863
1005
|
const presets = await source.loadPresets();
|
|
864
1006
|
const all = await source.listAllFamilies();
|
|
@@ -887,7 +1029,7 @@ async function ls(options) {
|
|
|
887
1029
|
});
|
|
888
1030
|
let available = [];
|
|
889
1031
|
if (!options.installedOnly) {
|
|
890
|
-
const source =
|
|
1032
|
+
const source = await resolveSource(options.registry ?? config.registry);
|
|
891
1033
|
try {
|
|
892
1034
|
available = await source.listAllFamilies();
|
|
893
1035
|
} catch {
|
|
@@ -1061,7 +1203,7 @@ async function upgrade(options) {
|
|
|
1061
1203
|
const cwd = path.resolve(options.cwd);
|
|
1062
1204
|
const config = await readConfig(cwd);
|
|
1063
1205
|
const registry = options.registry ?? config.registry;
|
|
1064
|
-
const source = options.source ??
|
|
1206
|
+
const source = options.source ?? await resolveSource(registry);
|
|
1065
1207
|
const recipesDir = path.join(cwd, config.recipesDir);
|
|
1066
1208
|
const installed = installedFamilies(recipesDir);
|
|
1067
1209
|
const targets = options.families && options.families.length > 0 ? options.families : installed;
|
|
@@ -1766,6 +1908,7 @@ function checkDynamicClass(file, dynamicTokens) {
|
|
|
1766
1908
|
const CLASS_ATTR_STR_RE = /\b(?:class|className)\s*=\s*(["'])([^"']*)\1/g;
|
|
1767
1909
|
const CLASS_ATTR_BRACE_RE = /\b(?:class|className)\s*=\s*\{/g;
|
|
1768
1910
|
const STRING_LITERAL_RE = /(["'`])((?:\\.|(?!\1)[^\\])*)\1/g;
|
|
1911
|
+
const CALL_EXPANDER_RE = /\b(?:cva|tv)\s*\(/g;
|
|
1769
1912
|
function extractClassUsages(source) {
|
|
1770
1913
|
const usages = [];
|
|
1771
1914
|
for (const m of source.matchAll(CLASS_ATTR_STR_RE)) {
|
|
@@ -1804,6 +1947,28 @@ function extractClassUsages(source) {
|
|
|
1804
1947
|
});
|
|
1805
1948
|
}
|
|
1806
1949
|
}
|
|
1950
|
+
for (const m of source.matchAll(CALL_EXPANDER_RE)) {
|
|
1951
|
+
const openParen = (m.index ?? 0) + m[0].length - 1;
|
|
1952
|
+
const close = findMatchingDelimiter(source, openParen, "(", ")");
|
|
1953
|
+
if (close === -1) continue;
|
|
1954
|
+
const inner = source.slice(openParen + 1, close);
|
|
1955
|
+
for (const sm of inner.matchAll(STRING_LITERAL_RE)) {
|
|
1956
|
+
const value = sm[2] ?? "";
|
|
1957
|
+
if (value.length === 0) continue;
|
|
1958
|
+
const literalStart = openParen + 1 + (sm.index ?? 0);
|
|
1959
|
+
const valueStart = literalStart + 1;
|
|
1960
|
+
const { tokens, dynamicTokens } = tokenizeClassString(source, value, valueStart);
|
|
1961
|
+
if (!tokens.some((t) => t.value.startsWith("@")) && dynamicTokens.length === 0) continue;
|
|
1962
|
+
usages.push({
|
|
1963
|
+
fileOffset: literalStart,
|
|
1964
|
+
valueStart,
|
|
1965
|
+
raw: value,
|
|
1966
|
+
tokens,
|
|
1967
|
+
dynamicTokens,
|
|
1968
|
+
fixable: false
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1807
1972
|
return usages;
|
|
1808
1973
|
}
|
|
1809
1974
|
function tokenizeClassString(source, value, valueStart) {
|
|
@@ -1838,6 +2003,9 @@ function tokenizeClassString(source, value, valueStart) {
|
|
|
1838
2003
|
};
|
|
1839
2004
|
}
|
|
1840
2005
|
function findMatchingBrace(source, openIdx) {
|
|
2006
|
+
return findMatchingDelimiter(source, openIdx, "{", "}");
|
|
2007
|
+
}
|
|
2008
|
+
function findMatchingDelimiter(source, openIdx, open, close) {
|
|
1841
2009
|
let depth = 1;
|
|
1842
2010
|
let i = openIdx + 1;
|
|
1843
2011
|
while (i < source.length && depth > 0) {
|
|
@@ -1883,8 +2051,8 @@ function findMatchingBrace(source, openIdx) {
|
|
|
1883
2051
|
}
|
|
1884
2052
|
continue;
|
|
1885
2053
|
}
|
|
1886
|
-
if (ch ===
|
|
1887
|
-
else if (ch ===
|
|
2054
|
+
if (ch === open) depth++;
|
|
2055
|
+
else if (ch === close) depth--;
|
|
1888
2056
|
i++;
|
|
1889
2057
|
}
|
|
1890
2058
|
return depth === 0 ? i - 1 : -1;
|
|
@@ -2108,6 +2276,6 @@ function formatBenchTable(result) {
|
|
|
2108
2276
|
return lines.join("\n");
|
|
2109
2277
|
}
|
|
2110
2278
|
//#endregion
|
|
2111
|
-
export {
|
|
2279
|
+
export { buildHeaderLine as A, cliVersion as C, createRegistrySource as D, writeLockfile as E, sealRecipeFile as F, extractHeader as M, normalizeBody as N, resolvePresetFamilies as O, rewriteHeaderSha 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
2280
|
|
|
2113
|
-
//# sourceMappingURL=bench-
|
|
2281
|
+
//# sourceMappingURL=bench-CUJJk5AC.js.map
|