@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.
@@ -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 = "https://shortwind.dev/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 ?? "https://shortwind.dev/registry";
431
- const source = createRegistrySource(registry);
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 (pkgs.length > 0) await installer(shape.packageManager, pkgs, cwd);
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 = createRegistrySource(registry);
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 = createRegistrySource(options.registry ?? config.registry);
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 = createRegistrySource(options.registry ?? config.registry);
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 ?? createRegistrySource(registry);
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 === "{") depth++;
1887
- else if (ch === "}") depth--;
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 { extractHeader as A, readLockfile as C, detectProject as D, resolvePresetFamilies as E, rewriteHeaderSha as M, sealRecipeFile as N, buildHeaderLine as O, init as S, createRegistrySource as T, newFamily as _, formatFindingsText as a, renameFamilyInSource as b, UpgradeError as c, BuildError as d, build as f, NewFamilyError as g, preset as h, extractClassUsages as i, normalizeBody as j, computeBodySha 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, remove as v, writeLockfile as w, DEFAULT_REGISTRY as x, add as y };
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-NKKDz3ld.js.map
2281
+ //# sourceMappingURL=bench-CUJJk5AC.js.map