elcrm 1.1.36 → 1.1.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,8 +2,8 @@
2
2
  // @bun
3
3
 
4
4
  // src/index.ts
5
- import { readFileSync as readFileSync29 } from "fs";
6
- import { dirname as dirname4, join as join26 } from "path";
5
+ import { readFileSync as readFileSync30 } from "fs";
6
+ import { dirname as dirname5, join as join27 } from "path";
7
7
  import { fileURLToPath as fileURLToPath2 } from "url";
8
8
 
9
9
  // src/parseArgs.ts
@@ -40,7 +40,8 @@ function parseArgs(argv = process.argv.slice(2)) {
40
40
  "web",
41
41
  "access",
42
42
  "tag",
43
- "otp"
43
+ "otp",
44
+ "pkgs"
44
45
  ]);
45
46
  if (valueOpts.has(body)) {
46
47
  options[body] = next;
@@ -1247,11 +1248,6 @@ function isColorToken(name, value) {
1247
1248
  return true;
1248
1249
  return false;
1249
1250
  }
1250
- function isButtonShadowNoise(name, value) {
1251
- if (!/^--button-.+-shadow(-(hover|active))?$/.test(name))
1252
- return false;
1253
- return /^none$/i.test(value.trim());
1254
- }
1255
1251
  function listCssFiles(dir, acc = []) {
1256
1252
  if (!existsSync5(dir))
1257
1253
  return acc;
@@ -1308,10 +1304,10 @@ function inspectPkgCss(pkgName, cwd) {
1308
1304
  return null;
1309
1305
  const pkg = readPkgJson(root);
1310
1306
  const exp = pkg?.exports;
1311
- const abs = (rel2) => rel2 ? join5(root, rel2.replace(/^\.\//, "")) : undefined;
1307
+ const abs = (rel) => rel ? join5(root, rel.replace(/^\.\//, "")) : undefined;
1312
1308
  const fileIfExists = (key) => {
1313
- const rel2 = exportPath(exp, key);
1314
- const a = abs(rel2);
1309
+ const rel = exportPath(exp, key);
1310
+ const a = abs(rel);
1315
1311
  return a && existsSync5(a) ? a : undefined;
1316
1312
  };
1317
1313
  return {
@@ -1334,10 +1330,10 @@ function resolveElcrmCssImport(spec, cwd) {
1334
1330
  if (!root)
1335
1331
  return null;
1336
1332
  const pkg = readPkgJson(root);
1337
- const rel2 = exportPath(pkg?.exports, `./${sub}`);
1338
- if (!rel2)
1333
+ const rel = exportPath(pkg?.exports, `./${sub}`);
1334
+ if (!rel)
1339
1335
  return null;
1340
- const abs = join5(root, rel2.replace(/^\.\//, ""));
1336
+ const abs = join5(root, rel.replace(/^\.\//, ""));
1341
1337
  return existsSync5(abs) ? abs : null;
1342
1338
  }
1343
1339
  function pruneBrokenElcrmImports(css, cwd) {
@@ -1359,16 +1355,17 @@ function addDef(acc, seen, pkg, bucket, name, value) {
1359
1355
  seen.add(key);
1360
1356
  acc.push({ name, value, bucket, pkg });
1361
1357
  }
1362
- function collectPkgTokens(pkgName, cwd) {
1363
- const root = resolvePkgRoot(pkgName, cwd);
1364
- if (!root)
1365
- return [];
1366
- const files = [
1358
+ function pkgTokenCssFiles(root) {
1359
+ return [
1367
1360
  ...listCssFiles(join5(root, "dist")),
1361
+ ...listCssFiles(join5(root, "src/lib")),
1368
1362
  ...listCssFiles(join5(root, "src/lib/styles")),
1369
1363
  ...listCssFiles(join5(root, "src/style")),
1370
1364
  join5(root, "test/main.css")
1371
1365
  ].filter((f) => existsSync5(f));
1366
+ }
1367
+ function collectTokensFromPkgRoot(root, pkg) {
1368
+ const files = pkgTokenCssFiles(root);
1372
1369
  const acc = [];
1373
1370
  const seen = new Set;
1374
1371
  for (const file of files) {
@@ -1379,14 +1376,14 @@ function collectPkgTokens(pkgName, cwd) {
1379
1376
  const color = isColorToken(name, value);
1380
1377
  if (kind === "geometry") {
1381
1378
  if (color) {
1382
- addDef(acc, seen, pkgName, "light", name, value);
1383
- addDef(acc, seen, pkgName, "dark", name, value);
1379
+ addDef(acc, seen, pkg, "light", name, value);
1380
+ addDef(acc, seen, pkg, "dark", name, value);
1384
1381
  } else {
1385
- addDef(acc, seen, pkgName, "geometry", name, value);
1382
+ addDef(acc, seen, pkg, "geometry", name, value);
1386
1383
  }
1387
1384
  continue;
1388
1385
  }
1389
- addDef(acc, seen, pkgName, color ? kind : "geometry", name, value);
1386
+ addDef(acc, seen, pkg, color ? kind : "geometry", name, value);
1390
1387
  }
1391
1388
  continue;
1392
1389
  }
@@ -1398,19 +1395,25 @@ function collectPkgTokens(pkgName, cwd) {
1398
1395
  const color = isColorToken(name, value);
1399
1396
  if (bucket === "geometry") {
1400
1397
  if (color) {
1401
- addDef(acc, seen, pkgName, "light", name, value);
1402
- addDef(acc, seen, pkgName, "dark", name, value);
1398
+ addDef(acc, seen, pkg, "light", name, value);
1399
+ addDef(acc, seen, pkg, "dark", name, value);
1403
1400
  } else {
1404
- addDef(acc, seen, pkgName, "geometry", name, value);
1401
+ addDef(acc, seen, pkg, "geometry", name, value);
1405
1402
  }
1406
1403
  continue;
1407
1404
  }
1408
- addDef(acc, seen, pkgName, color ? bucket : "geometry", name, value);
1405
+ addDef(acc, seen, pkg, color ? bucket : "geometry", name, value);
1409
1406
  }
1410
1407
  });
1411
1408
  }
1412
1409
  return acc;
1413
1410
  }
1411
+ function collectPkgTokens(pkgName, cwd) {
1412
+ const root = resolvePkgRoot(pkgName, cwd);
1413
+ if (!root)
1414
+ return [];
1415
+ return collectTokensFromPkgRoot(root, pkgName);
1416
+ }
1414
1417
  function existingTokenNames(css) {
1415
1418
  return new Set(parseCustomProps(css).keys());
1416
1419
  }
@@ -1544,7 +1547,7 @@ function syncElcrmCss(options) {
1544
1547
  continue;
1545
1548
  let css = readFileSync7(file, "utf8");
1546
1549
  const have = existingTokenNames(css);
1547
- const missing = allDefs.filter((d) => d.bucket === bucket && !have.has(d.name) && !d.name.startsWith("--search-") && !TOKEN_DEPRECATED.has(d.name) && !isButtonShadowNoise(d.name, d.value) && !(bucket === "geometry" && pkgsWithTokens.has(d.pkg)));
1550
+ const missing = allDefs.filter((d) => d.bucket === bucket && !have.has(d.name) && !d.name.startsWith("--search-") && !TOKEN_DEPRECATED.has(d.name) && !(bucket === "geometry" && pkgsWithTokens.has(d.pkg)));
1548
1551
  const uniq = [];
1549
1552
  const seen = new Set;
1550
1553
  for (const d of missing) {
@@ -1592,31 +1595,60 @@ function syncElcrmCss(options) {
1592
1595
  }
1593
1596
  return { styleDir, added, imports, removedImports, skippedPkgs };
1594
1597
  }
1595
- function stripButtonShadowNoiseFromTheme(styleDir, dry = false) {
1596
- const file = join5(styleDir, "theme.css");
1597
- if (!existsSync5(file))
1598
- return [];
1599
- const css = readFileSync7(file, "utf8");
1600
- const lines = css.split(/\r?\n/);
1601
- const removed = [];
1602
- const kept = lines.filter((line) => {
1603
- const m = line.match(/^\s*(--button-[a-z0-9-]+shadow[a-z0-9-]*)\s*:\s*(.+);/i);
1604
- if (!m)
1605
- return true;
1606
- if (!isButtonShadowNoise(m[1], m[2]))
1607
- return true;
1608
- removed.push(m[1]);
1609
- return false;
1610
- });
1611
- if (!removed.length)
1612
- return [];
1613
- const next = kept.join(`
1614
- `).replace(/\n{3,}/g, `
1615
-
1616
- `);
1617
- if (!dry)
1618
- writeFileSync3(file, next, "utf8");
1619
- return removed;
1598
+ function checkElcrmTokenCoverage(options) {
1599
+ const styleDir = findStyleDir(options.cwd);
1600
+ const defined = new Set;
1601
+ if (styleDir) {
1602
+ for (const name of ["theme.css", "theme-light.css", "theme-dark.css"]) {
1603
+ const file = join5(styleDir, name);
1604
+ if (!existsSync5(file))
1605
+ continue;
1606
+ for (const k of parseCustomProps(readFileSync7(file, "utf8")).keys()) {
1607
+ defined.add(k);
1608
+ }
1609
+ }
1610
+ }
1611
+ const gaps = [];
1612
+ let checked = 0;
1613
+ for (const pkgName of options.pkgNames) {
1614
+ const exp = inspectPkgCss(pkgName, options.cwd);
1615
+ if (!exp)
1616
+ continue;
1617
+ for (const d of collectPkgTokens(pkgName, options.cwd)) {
1618
+ defined.add(d.name);
1619
+ }
1620
+ const styleFiles = [
1621
+ exp.style,
1622
+ exp.tokens,
1623
+ exp.light,
1624
+ exp.dark,
1625
+ ...listCssFiles(join5(exp.root, "dist")).filter((f) => /index\.css$|style\.css$|\.module\.css$/i.test(f)),
1626
+ ...listCssFiles(join5(exp.root, "src/lib")).filter((f) => /\.css$/i.test(f))
1627
+ ].filter((f) => Boolean(f && existsSync5(f)));
1628
+ const used = new Set;
1629
+ for (const file of styleFiles) {
1630
+ const css = readFileSync7(file, "utf8");
1631
+ for (const k of parseCustomProps(css).keys())
1632
+ defined.add(k);
1633
+ for (const m of css.matchAll(/var\(\s*(--[a-zA-Z0-9-]+)/g)) {
1634
+ used.add(m[1]);
1635
+ }
1636
+ }
1637
+ checked += used.size;
1638
+ for (const token of [...used].sort()) {
1639
+ if (TOKEN_DEPRECATED.has(token))
1640
+ continue;
1641
+ if (defined.has(token))
1642
+ continue;
1643
+ if (/^--tooltip-arrow-/.test(token))
1644
+ continue;
1645
+ if (token === "--alpha-color" || token === "--preview-color" || token === "--swatch") {
1646
+ continue;
1647
+ }
1648
+ gaps.push({ pkg: pkgName, token });
1649
+ }
1650
+ }
1651
+ return { styleDir, gaps, checked };
1620
1652
  }
1621
1653
 
1622
1654
  // src/lib/migrate/socket-server.ts
@@ -1730,7 +1762,7 @@ async function migrateSocketServer(cwd, dry) {
1730
1762
  pkg.dependencies["@elcrm/server"] = ver;
1731
1763
  }
1732
1764
  }
1733
- if (!dry && changes.some((c2) => c2.file === "package.json")) {
1765
+ if (!dry && changes.some((c) => c.file === "package.json")) {
1734
1766
  writePackageJson(pkg, cwd);
1735
1767
  }
1736
1768
  }
@@ -2172,25 +2204,25 @@ function sliceAttrValue(attrs, eqIndex) {
2172
2204
  let brace = 0;
2173
2205
  let quote = null;
2174
2206
  while (i < attrs.length) {
2175
- const c2 = attrs[i];
2207
+ const c = attrs[i];
2176
2208
  if (quote) {
2177
- if (c2 === "\\" && quote === '"') {
2209
+ if (c === "\\" && quote === '"') {
2178
2210
  i += 2;
2179
2211
  continue;
2180
2212
  }
2181
- if (c2 === quote)
2213
+ if (c === quote)
2182
2214
  quote = null;
2183
2215
  i++;
2184
2216
  continue;
2185
2217
  }
2186
- if (c2 === '"' || c2 === "'") {
2187
- quote = c2;
2218
+ if (c === '"' || c === "'") {
2219
+ quote = c;
2188
2220
  i++;
2189
2221
  continue;
2190
2222
  }
2191
- if (c2 === "{")
2223
+ if (c === "{")
2192
2224
  brace++;
2193
- else if (c2 === "}") {
2225
+ else if (c === "}") {
2194
2226
  brace--;
2195
2227
  if (brace === 0) {
2196
2228
  i++;
@@ -2449,8 +2481,8 @@ async function migrateFrontAll(cwd, dry) {
2449
2481
  const all = [];
2450
2482
  for (const name of FRONT_MIGRATE_ORDER) {
2451
2483
  const part = await FRONT_MIGRATIONS[name](cwd, dry);
2452
- for (const c2 of part) {
2453
- all.push({ file: c2.file, detail: `[${name}] ${c2.detail}` });
2484
+ for (const c of part) {
2485
+ all.push({ file: c.file, detail: `[${name}] ${c.detail}` });
2454
2486
  }
2455
2487
  }
2456
2488
  return all;
@@ -2564,12 +2596,12 @@ import { resolve } from "path";
2564
2596
  import { existsSync as existsSync8, readdirSync as readdirSync5, readFileSync as readFileSync16, statSync as statSync5 } from "fs";
2565
2597
  import { join as join8, relative as relative2 } from "path";
2566
2598
  var SKIP = new Set(["node_modules", "dist", ".git", "coverage", ".bun"]);
2567
- function isThemeTokenFile(rel2) {
2568
- const n = rel2.replace(/\\/g, "/");
2599
+ function isThemeTokenFile(rel) {
2600
+ const n = rel.replace(/\\/g, "/");
2569
2601
  return /(?:^|\/)(?:theme(?:-light|-dark)?|elcrm)\.css$/.test(n);
2570
2602
  }
2571
- function isGeneratedIcons(rel2) {
2572
- return /icons\.generated\.(tsx?|jsx?)$/.test(rel2.replace(/\\/g, "/"));
2603
+ function isGeneratedIcons(rel) {
2604
+ return /icons\.generated\.(tsx?|jsx?)$/.test(rel.replace(/\\/g, "/"));
2573
2605
  }
2574
2606
  var HARD_HEX_RE = /#[0-9a-fA-F]{3,8}\b/;
2575
2607
  var HARD_RGB_RE = /\b(?:rgb|rgba|hsl|hsla)\s*\(/i;
@@ -2712,14 +2744,14 @@ function collectFrontIssues(cwd, pkg) {
2712
2744
  }
2713
2745
  for (const file of files) {
2714
2746
  const text = readFileSync16(file, "utf8");
2715
- const rel2 = relative2(cwd, file);
2716
- const skipHardColor = isLib || isThemeTokenFile(rel2) || isGeneratedIcons(rel2);
2747
+ const rel = relative2(cwd, file);
2748
+ const skipHardColor = isLib || isThemeTokenFile(rel) || isGeneratedIcons(rel);
2717
2749
  if (DEPRECATED_SOCKET_IMPORT_RE.test(text)) {
2718
2750
  issues.push({
2719
2751
  level: "error",
2720
2752
  code: "deprecated-import",
2721
2753
  message: "\u0438\u043C\u043F\u043E\u0440\u0442 @elcrm/socket/server \u0438\u043B\u0438 socket-server",
2722
- file: rel2,
2754
+ file: rel,
2723
2755
  fixable: true
2724
2756
  });
2725
2757
  }
@@ -2731,13 +2763,13 @@ function collectFrontIssues(cwd, pkg) {
2731
2763
  level: "error",
2732
2764
  code: "themes-css",
2733
2765
  message: "import \u2026/themes.css \u0443\u0441\u0442\u0430\u0440\u0435\u043B \u2014 \u0442\u043E\u043A\u0435\u043D\u044B \u0437\u0430\u0434\u0430\u0451\u0442 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435 (elcrm-theme / theme.css)",
2734
- file: rel2,
2766
+ file: rel,
2735
2767
  line
2736
2768
  });
2737
2769
  }
2738
2770
  if (LEGACY_PASSKEY_IMPORT_RE.test(text) && LEGACY_PASSKEY_SYMBOL_RE.test(text)) {
2739
- const lines2 = text.split(/\r?\n/);
2740
- lines2.forEach((line, i) => {
2771
+ const lines = text.split(/\r?\n/);
2772
+ lines.forEach((line, i) => {
2741
2773
  if (/\bcreatePasskey\b/.test(line) || /\bauthenticateWithPasskey\b/.test(line)) {
2742
2774
  if (pkg.name === "@elcrm/passkey")
2743
2775
  return;
@@ -2745,7 +2777,7 @@ function collectFrontIssues(cwd, pkg) {
2745
2777
  level: "error",
2746
2778
  code: "legacy-passkey-api",
2747
2779
  message: "createPasskey / authenticateWithPasskey \u043D\u0435 \u044D\u043A\u0441\u043F\u043E\u0440\u0442\u0438\u0440\u0443\u044E\u0442\u0441\u044F \u2014 createPasskeyClient",
2748
- file: rel2,
2780
+ file: rel,
2749
2781
  line: i + 1
2750
2782
  });
2751
2783
  }
@@ -2764,19 +2796,19 @@ function collectFrontIssues(cwd, pkg) {
2764
2796
  level: "error",
2765
2797
  code: "native-html-input",
2766
2798
  message: "\u043D\u0430\u0442\u0438\u0432\u043D\u044B\u0439 <input|textarea|select> \u2014 @elcrm/form (elcrm migrate native-html)",
2767
- file: rel2,
2799
+ file: rel,
2768
2800
  line: i + 1,
2769
2801
  fixable: true
2770
2802
  });
2771
2803
  }
2772
2804
  }
2773
- if (!/(^|\/)pages\/Auth\.tsx$/.test(rel2.replace(/\\/g, "/")) && /(?:^|\s)native(?:\s|$|=|\/|>)/.test(line) && !/\bnavigator\b|\bnativeEvent\b|\bcreateNative/.test(line) && !/['"`]native['"`]/.test(line) && !(/\binstallMode\b/.test(line) && /\bnative\b/.test(line))) {
2805
+ if (!/(^|\/)pages\/Auth\.tsx$/.test(rel.replace(/\\/g, "/")) && /(?:^|\s)native(?:\s|$|=|\/|>)/.test(line) && !/\bnavigator\b|\bnativeEvent\b|\bcreateNative/.test(line) && !/['"`]native['"`]/.test(line) && !(/\binstallMode\b/.test(line) && /\bnative\b/.test(line))) {
2774
2806
  if (pkg.name !== "@elcrm/form") {
2775
2807
  issues.push({
2776
2808
  level: "warn",
2777
2809
  code: "form-native-prop",
2778
2810
  message: "prop native \u0443 \u043F\u043E\u043B\u044F \u0444\u043E\u0440\u043C\u044B \u2014 \u0443\u0431\u0435\u0440\u0438\u0442\u0435 (elcrm migrate form-native)",
2779
- file: rel2,
2811
+ file: rel,
2780
2812
  line: i + 1,
2781
2813
  fixable: true
2782
2814
  });
@@ -2787,7 +2819,7 @@ function collectFrontIssues(cwd, pkg) {
2787
2819
  level: "warn",
2788
2820
  code: "size-legacy",
2789
2821
  message: 'size="sm"|"md" \u0443\u0441\u0442\u0430\u0440\u0435\u043B\u043E \u2014 "s"|"m" (elcrm migrate size-sml)',
2790
- file: rel2,
2822
+ file: rel,
2791
2823
  line: i + 1,
2792
2824
  fixable: true
2793
2825
  });
@@ -2797,7 +2829,7 @@ function collectFrontIssues(cwd, pkg) {
2797
2829
  level: "warn",
2798
2830
  code: "modal-add",
2799
2831
  message: "Modal.Add \u2014 Modal.Create + Open (elcrm migrate modal-create)",
2800
- file: rel2,
2832
+ file: rel,
2801
2833
  line: i + 1,
2802
2834
  fixable: true
2803
2835
  });
@@ -2807,7 +2839,7 @@ function collectFrontIssues(cwd, pkg) {
2807
2839
  level: "error",
2808
2840
  code: "field-border-shorthand",
2809
2841
  message: "field-border hover/focus/disabled \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u0446\u0432\u0435\u0442 (elcrm migrate field-border)",
2810
- file: rel2,
2842
+ file: rel,
2811
2843
  line: i + 1,
2812
2844
  fixable: true
2813
2845
  });
@@ -2817,7 +2849,7 @@ function collectFrontIssues(cwd, pkg) {
2817
2849
  level: "warn",
2818
2850
  code: "form-token-legacy",
2819
2851
  message: "legacy padding/shadow-\u0442\u043E\u043A\u0435\u043D \u2192 --field-padding* / --popup-shadow (elcrm migrate form-tokens)",
2820
- file: rel2,
2852
+ file: rel,
2821
2853
  line: i + 1,
2822
2854
  fixable: true
2823
2855
  });
@@ -2827,7 +2859,7 @@ function collectFrontIssues(cwd, pkg) {
2827
2859
  level: "warn",
2828
2860
  code: "components-token-legacy",
2829
2861
  message: "legacy \u0442\u043E\u043A\u0435\u043D @elcrm/components \u2192 --shell-* (elcrm migrate components)",
2830
- file: rel2,
2862
+ file: rel,
2831
2863
  line: i + 1,
2832
2864
  fixable: true
2833
2865
  });
@@ -2837,7 +2869,7 @@ function collectFrontIssues(cwd, pkg) {
2837
2869
  level: "error",
2838
2870
  code: "css-orphan-mix",
2839
2871
  message: "\u043E\u0441\u0438\u0440\u043E\u0442\u0435\u0432\u0448\u0438\u0439 \u0445\u0432\u043E\u0441\u0442 color-mix (in srgb\u2026) \u2014 elcrm migrate css-sanitize",
2840
- file: rel2,
2872
+ file: rel,
2841
2873
  line: i + 1,
2842
2874
  fixable: true
2843
2875
  });
@@ -2850,7 +2882,7 @@ function collectFrontIssues(cwd, pkg) {
2850
2882
  level: "warn",
2851
2883
  code: "components-stack",
2852
2884
  message: "Block/Row/Column \u0443\u0441\u0442\u0430\u0440\u0435\u043B\u0438 \u2014 Stack (elcrm migrate components)",
2853
- file: rel2,
2885
+ file: rel,
2854
2886
  line: i + 1,
2855
2887
  fixable: true
2856
2888
  });
@@ -2861,7 +2893,7 @@ function collectFrontIssues(cwd, pkg) {
2861
2893
  level: "warn",
2862
2894
  code: "components-value",
2863
2895
  message: "activeKey/defaultActiveKey \u2192 value/defaultValue (elcrm migrate components)",
2864
- file: rel2,
2896
+ file: rel,
2865
2897
  line: i + 1,
2866
2898
  fixable: true
2867
2899
  });
@@ -2873,7 +2905,7 @@ function collectFrontIssues(cwd, pkg) {
2873
2905
  level: "warn",
2874
2906
  code: "hardcoded-color",
2875
2907
  message: why,
2876
- file: rel2,
2908
+ file: rel,
2877
2909
  line: i + 1
2878
2910
  });
2879
2911
  }
@@ -3263,7 +3295,7 @@ function walkFiles2(root, dir, out) {
3263
3295
  function scanCodeFile(root, file, text) {
3264
3296
  const findings = [];
3265
3297
  const lines = text.split(/\r?\n/);
3266
- const rel2 = relative3(root, file) || file;
3298
+ const rel = relative3(root, file) || file;
3267
3299
  for (const rule of CODE_RULES) {
3268
3300
  lines.forEach((line, i) => {
3269
3301
  rule.test.lastIndex = 0;
@@ -3274,7 +3306,7 @@ function scanCodeFile(root, file, text) {
3274
3306
  findings.push({
3275
3307
  ruleId: rule.id,
3276
3308
  severity: rule.severity,
3277
- file: rel2,
3309
+ file: rel,
3278
3310
  line: i + 1,
3279
3311
  match: (m?.[0] ?? line).trim().slice(0, 120),
3280
3312
  message: rule.message,
@@ -3287,7 +3319,7 @@ function scanCodeFile(root, file, text) {
3287
3319
  }
3288
3320
  function scanPackageJson(root, file, text) {
3289
3321
  const findings = [];
3290
- const rel2 = relative3(root, file) || file;
3322
+ const rel = relative3(root, file) || file;
3291
3323
  let pkg;
3292
3324
  try {
3293
3325
  pkg = JSON.parse(text);
@@ -3300,7 +3332,7 @@ function scanPackageJson(root, file, text) {
3300
3332
  findings.push({
3301
3333
  ruleId: `pkg-${name}`,
3302
3334
  severity: "error",
3303
- file: rel2,
3335
+ file: rel,
3304
3336
  line: 1,
3305
3337
  match: `"${name}": "${deps[name]}"`,
3306
3338
  message: `\u0417\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u044C ${name} \u2192 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439\u0442\u0435 @elcrm/server (+ subpath)`,
@@ -3313,7 +3345,7 @@ function scanPackageJson(root, file, text) {
3313
3345
  findings.push({
3314
3346
  ruleId: "pkg-need-server",
3315
3347
  severity: "error",
3316
- file: rel2,
3348
+ file: rel,
3317
3349
  line: 1,
3318
3350
  match: "missing @elcrm/server",
3319
3351
  message: "\u041D\u0443\u0436\u043D\u0430 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u044C @elcrm/server",
@@ -3765,8 +3797,8 @@ async function cmdMigrate(parsed) {
3765
3797
  if (hasFlag(parsed, "list") || parsed.positionals[0] === "list") {
3766
3798
  console.log("\u0414\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0435 \u043C\u0438\u0433\u0440\u0430\u0446\u0438\u0438:");
3767
3799
  console.log(" front | ui \u043F\u0430\u043A\u0435\u0442 \u0444\u0440\u043E\u043D\u0442\u043E\u0432\u044B\u0445 \u043C\u0438\u0433\u0440\u0430\u0446\u0438\u0439:");
3768
- for (const name2 of FRONT_MIGRATE_ORDER) {
3769
- console.log(` - ${name2}`);
3800
+ for (const name of FRONT_MIGRATE_ORDER) {
3801
+ console.log(` - ${name}`);
3770
3802
  }
3771
3803
  console.log(" socket-server (\u0442\u0430\u043A\u0436\u0435 \u0432 front)");
3772
3804
  return;
@@ -3792,8 +3824,8 @@ async function cmdMigrate(parsed) {
3792
3824
  console.log("\u0418\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0439 \u043D\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044F.");
3793
3825
  return;
3794
3826
  }
3795
- for (const c2 of changes) {
3796
- console.log(` ${c2.file}: ${c2.detail}`);
3827
+ for (const c of changes) {
3828
+ console.log(` ${c.file}: ${c.detail}`);
3797
3829
  }
3798
3830
  console.log(dry ? `
3799
3831
  --dry: ${changes.length} \u043F\u043E\u0442\u0435\u043D\u0446\u0438\u0430\u043B\u044C\u043D\u044B\u0445 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0439.` : `
@@ -4165,8 +4197,8 @@ function emitKitManifest(cwd) {
4165
4197
  throw new Error(`elcrm kit emit \u2014 \u043F\u0430\u043A\u0435\u0442 ${pkg.name} \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044F (\u043D\u0443\u0436\u0435\u043D components|form)`);
4166
4198
  }
4167
4199
  const distDir = join15(cwd, "dist");
4168
- const components = scanDistComponents(distDir, pkg.name).map((c2) => ({
4169
- ...c2,
4200
+ const components = scanDistComponents(distDir, pkg.name).map((c) => ({
4201
+ ...c,
4170
4202
  importFrom: pkg.name
4171
4203
  }));
4172
4204
  return {
@@ -4332,6 +4364,7 @@ var ALL_ELCRM_DOCS = [
4332
4364
  "TOOLTIP.elCRM.md",
4333
4365
  "ICONS.elCRM.md",
4334
4366
  "FORM.elCRM.md",
4367
+ "SEARCH.elCRM.md",
4335
4368
  "COMPONENTS.elCRM.md",
4336
4369
  "SERVER.elCRM.md"
4337
4370
  ];
@@ -4356,6 +4389,9 @@ import {
4356
4389
  writeFileSync as writeFileSync9
4357
4390
  } from "fs";
4358
4391
  import { dirname as dirname3, join as join18 } from "path";
4392
+ function bundledDir(...parts) {
4393
+ return join18(packageRoot(), "templates", ...parts);
4394
+ }
4359
4395
  function writeScaffoldFile(dest, body, opts) {
4360
4396
  if (opts.dry)
4361
4397
  return "dry";
@@ -4715,16 +4751,24 @@ async function cmdCss(parsed) {
4715
4751
  return;
4716
4752
  }
4717
4753
  const dry = hasFlag(parsed, "dry");
4718
- console.log(`${dry ? "[dry] " : ""}\u041F\u0430\u043A\u0435\u0442\u044B: ${names.join(", ")}`);
4754
+ const checkOnly = hasFlag(parsed, "check");
4755
+ console.log(`${dry || checkOnly ? "[check/dry] " : ""}\u041F\u0430\u043A\u0435\u0442\u044B: ${names.join(", ")}`);
4719
4756
  try {
4757
+ if (checkOnly) {
4758
+ const cov = checkElcrmTokenCoverage({ cwd, pkgNames: names });
4759
+ console.log(`\u041A\u0430\u0442\u0430\u043B\u043E\u0433 \u0441\u0442\u0438\u043B\u0435\u0439: ${cov.styleDir ?? "(\u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D)"} \xB7 \u043F\u0440\u043E\u0432\u0435\u0440\u0435\u043D\u043E var(): ${cov.checked}`);
4760
+ if (!cov.gaps.length) {
4761
+ console.log(c.green("\u041F\u043E\u043A\u0440\u044B\u0442\u0438\u0435 \u0442\u043E\u043A\u0435\u043D\u043E\u0432 OK."));
4762
+ return;
4763
+ }
4764
+ console.error(`${c.red("\u041F\u0440\u043E\u0431\u0435\u043B\u044B:")} ${cov.gaps.length} (\u043D\u0435\u0442 \u0432 theme*/\u043F\u0430\u043A\u0435\u0442\u0435 light|dark|tokens)`);
4765
+ for (const g of cov.gaps) {
4766
+ console.error(` ${c.dim(g.pkg)} ${g.token}`);
4767
+ }
4768
+ process.exit(1);
4769
+ }
4720
4770
  const result = syncElcrmCss({ cwd, pkgNames: names, dry });
4721
4771
  console.log(`\u041A\u0430\u0442\u0430\u043B\u043E\u0433 \u0441\u0442\u0438\u043B\u0435\u0439: ${result.styleDir}`);
4722
- const stripped = stripButtonShadowNoiseFromTheme(result.styleDir, dry);
4723
- if (stripped.length) {
4724
- console.log(` ${dry ? "[dry] " : ""}${c.yellow("\u2212")} theme.css: \u0443\u0431\u0440\u0430\u043D\u043E ${stripped.length} button-shadow (none)`);
4725
- for (const t of stripped)
4726
- console.log(` ${c.dim(t)}`);
4727
- }
4728
4772
  let total = 0;
4729
4773
  for (const [file, tokens] of Object.entries(result.added)) {
4730
4774
  if (!tokens.length) {
@@ -4763,6 +4807,17 @@ css-sanitize: \u043F\u043E\u0447\u0438\u043D\u0435\u043D\u043E ${cleaned.length}
4763
4807
  for (const ch of cleaned)
4764
4808
  console.log(` ${ch.file}: ${ch.detail}`);
4765
4809
  }
4810
+ const cov = checkElcrmTokenCoverage({ cwd, pkgNames: names });
4811
+ if (cov.gaps.length) {
4812
+ console.log(`
4813
+ ${c.yellow("\u26A0")} \u043F\u043E\u043A\u0440\u044B\u0442\u0438\u0435: ${cov.gaps.length} \u043F\u0440\u043E\u0431\u0435\u043B(\u043E\u0432) \u2014 \u0441\u043C. elcrm css --check`);
4814
+ for (const g of cov.gaps.slice(0, 12)) {
4815
+ console.log(` ${c.dim(g.pkg)} ${g.token}`);
4816
+ }
4817
+ if (cov.gaps.length > 12) {
4818
+ console.log(` ${c.dim(`\u2026 \u0435\u0449\u0451 ${cov.gaps.length - 12}`)}`);
4819
+ }
4820
+ }
4766
4821
  } catch (e) {
4767
4822
  console.error(e instanceof Error ? e.message : e);
4768
4823
  process.exit(1);
@@ -4803,7 +4858,7 @@ function scan(cwd) {
4803
4858
  const files = projectCodeRoots(cwd).flatMap((r) => walkCodeFiles(r));
4804
4859
  for (const file of files) {
4805
4860
  const text = readFileSync27(file, "utf8");
4806
- const rel2 = relative4(cwd, file);
4861
+ const rel = relative4(cwd, file);
4807
4862
  const lines = text.split(/\r?\n/);
4808
4863
  lines.forEach((line, i) => {
4809
4864
  const n = i + 1;
@@ -4814,7 +4869,7 @@ function scan(cwd) {
4814
4869
  level: "warn",
4815
4870
  code: "size-legacy",
4816
4871
  message: 'size="sm"|"md" \u2192 "s"|"m"',
4817
- file: rel2,
4872
+ file: rel,
4818
4873
  line: n,
4819
4874
  fix: "elcrm migrate size-sml"
4820
4875
  });
@@ -4824,7 +4879,7 @@ function scan(cwd) {
4824
4879
  level: "warn",
4825
4880
  code: "modal-add",
4826
4881
  message: "Modal.Add \u2014 \u043F\u0440\u0435\u0434\u043F\u043E\u0447\u0442\u0438\u0442\u0435\u043B\u0435\u043D Modal.Create + Open",
4827
- file: rel2,
4882
+ file: rel,
4828
4883
  line: n,
4829
4884
  fix: "elcrm migrate modal-create"
4830
4885
  });
@@ -4834,7 +4889,7 @@ function scan(cwd) {
4834
4889
  level: "error",
4835
4890
  code: "native-html-input",
4836
4891
  message: "\u043D\u0430\u0442\u0438\u0432\u043D\u044B\u0439 <input|textarea|select> \u2192 @elcrm/form",
4837
- file: rel2,
4892
+ file: rel,
4838
4893
  line: n,
4839
4894
  fix: "elcrm migrate native-html"
4840
4895
  });
@@ -4844,7 +4899,7 @@ function scan(cwd) {
4844
4899
  level: "warn",
4845
4900
  code: "form-native-prop",
4846
4901
  message: "prop native \u0443 \u043F\u043E\u043B\u044F \u0444\u043E\u0440\u043C\u044B \u2014 \u0443\u0431\u0435\u0440\u0438\u0442\u0435",
4847
- file: rel2,
4902
+ file: rel,
4848
4903
  line: n,
4849
4904
  fix: "elcrm migrate form-native"
4850
4905
  });
@@ -4854,7 +4909,7 @@ function scan(cwd) {
4854
4909
  level: "info",
4855
4910
  code: "modal-date-key",
4856
4911
  message: "key={Date.now()} \u0443 \u043C\u043E\u0434\u0430\u043B\u043A\u0438 \u2014 Create \u0437\u0430\u0434\u0430\u0451\u0442 \u0441\u0442\u0430\u0431\u0438\u043B\u044C\u043D\u044B\u0439 name",
4857
- file: rel2,
4912
+ file: rel,
4858
4913
  line: n
4859
4914
  });
4860
4915
  }
@@ -4863,17 +4918,17 @@ function scan(cwd) {
4863
4918
  level: "error",
4864
4919
  code: "field-border-shorthand",
4865
4920
  message: "hover/focus border \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u0446\u0432\u0435\u0442, \u043D\u0435 shorthand",
4866
- file: rel2,
4921
+ file: rel,
4867
4922
  line: n,
4868
4923
  fix: "elcrm migrate field-border"
4869
4924
  });
4870
4925
  }
4871
- if (/!important/.test(line) && /drag|file-upload|DragDrop/i.test(rel2 + line)) {
4926
+ if (/!important/.test(line) && /drag|file-upload|DragDrop/i.test(rel + line)) {
4872
4927
  out.push({
4873
4928
  level: "info",
4874
4929
  code: "dragdrop-important",
4875
4930
  message: '!important \u0443 DragDrop \u2014 layout="fill"|"overlay" \u0432 @elcrm/form',
4876
- file: rel2,
4931
+ file: rel,
4877
4932
  line: n
4878
4933
  });
4879
4934
  }
@@ -4882,7 +4937,7 @@ function scan(cwd) {
4882
4937
  level: "warn",
4883
4938
  code: "react-global",
4884
4939
  message: "globalThis.React \u0445\u0430\u043A \u2014 migrate legacy-hacks / \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u0435 modal",
4885
- file: rel2,
4940
+ file: rel,
4886
4941
  line: n,
4887
4942
  fix: "elcrm migrate legacy-hacks"
4888
4943
  });
@@ -4892,7 +4947,7 @@ function scan(cwd) {
4892
4947
  level: "error",
4893
4948
  code: "form-alias",
4894
4949
  message: "\u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0438\u0439 \u0430\u043B\u0438\u0430\u0441 @elcrm/form (ProgressField/OtpField/\u2026) \u2192 \u043A\u0430\u043D\u043E\u043D",
4895
- file: rel2,
4950
+ file: rel,
4896
4951
  line: n,
4897
4952
  fix: "elcrm migrate form-aliases"
4898
4953
  });
@@ -5170,34 +5225,339 @@ async function cmdKit(parsed) {
5170
5225
  process.exit(1);
5171
5226
  }
5172
5227
 
5228
+ // src/commands/hub.ts
5229
+ import { resolve as resolve14 } from "path";
5230
+
5231
+ // src/lib/docsHub.ts
5232
+ import {
5233
+ existsSync as existsSync20,
5234
+ mkdirSync as mkdirSync6,
5235
+ readdirSync as readdirSync12,
5236
+ readFileSync as readFileSync28,
5237
+ writeFileSync as writeFileSync12
5238
+ } from "fs";
5239
+ import { dirname as dirname4, join as join25, resolve as resolve13 } from "path";
5240
+ var HUB_START = "<!-- elcrm-hub-tokens:start -->";
5241
+ var HUB_END = "<!-- elcrm-hub-tokens:end -->";
5242
+ var PKG_TO_DOC = {
5243
+ "@elcrm/alert": "ALERT.elCRM.md",
5244
+ "@elcrm/api": "API.elCRM.md",
5245
+ "@elcrm/button": "BUTTON.elCRM.md",
5246
+ "@elcrm/components": "COMPONENTS.elCRM.md",
5247
+ "@elcrm/form": "FORM.elCRM.md",
5248
+ "@elcrm/icons": "ICONS.elCRM.md",
5249
+ "@elcrm/modal": "MODAL.elCRM.md",
5250
+ "@elcrm/notice": "NOTICE.elCRM.md",
5251
+ "@elcrm/notifications": "NOTIFICATIONS.elCRM.md",
5252
+ "@elcrm/passkey": "PASSKEY.elCRM.md",
5253
+ "@elcrm/router": "ROUTER.elCRM.md",
5254
+ "@elcrm/search": "SEARCH.elCRM.md",
5255
+ "@elcrm/server": "SERVER.elCRM.md",
5256
+ "@elcrm/socket": "SOCKET.elCRM.md",
5257
+ "@elcrm/tooltip": "TOOLTIP.elCRM.md"
5258
+ };
5259
+ function findPkgsRoot(from, override) {
5260
+ if (override?.trim()) {
5261
+ const p = resolve13(override.trim());
5262
+ return existsSync20(join25(p, "elCRM.cli", "package.json")) ? p : null;
5263
+ }
5264
+ const env = process.env.ELCRM_PKGS_DIR?.trim();
5265
+ if (env && existsSync20(join25(env, "elCRM.cli", "package.json"))) {
5266
+ return resolve13(env);
5267
+ }
5268
+ let dir = resolve13(from);
5269
+ for (let i = 0;i < 8; i++) {
5270
+ if (existsSync20(join25(dir, "elCRM.cli", "package.json")))
5271
+ return dir;
5272
+ if (existsSync20(join25(dir, "package.json"))) {
5273
+ try {
5274
+ const n = JSON.parse(readFileSync28(join25(dir, "package.json"), "utf8")).name;
5275
+ if (n === "elcrm" && existsSync20(join25(dir, "..", "elCRM.notice"))) {
5276
+ return resolve13(join25(dir, ".."));
5277
+ }
5278
+ } catch {}
5279
+ }
5280
+ const parent = join25(dir, "..");
5281
+ if (parent === dir)
5282
+ break;
5283
+ dir = parent;
5284
+ }
5285
+ return null;
5286
+ }
5287
+ function listHubPackages(pkgsRoot) {
5288
+ const out = [];
5289
+ for (const name of readdirSync12(pkgsRoot)) {
5290
+ if (!name.startsWith("elCRM."))
5291
+ continue;
5292
+ const dir = join25(pkgsRoot, name);
5293
+ const pkgFile = join25(dir, "package.json");
5294
+ if (!existsSync20(pkgFile))
5295
+ continue;
5296
+ let pkg;
5297
+ try {
5298
+ pkg = JSON.parse(readFileSync28(pkgFile, "utf8"));
5299
+ } catch {
5300
+ continue;
5301
+ }
5302
+ if (!pkg.name?.startsWith("@elcrm/"))
5303
+ continue;
5304
+ const doc = PKG_TO_DOC[pkg.name] ?? null;
5305
+ out.push({
5306
+ name: pkg.name,
5307
+ version: pkg.version ?? "0.0.0",
5308
+ dir,
5309
+ doc,
5310
+ tokens: collectTokensFromPkgRoot(dir, pkg.name)
5311
+ });
5312
+ }
5313
+ return out.sort((a, b) => a.name.localeCompare(b.name));
5314
+ }
5315
+ function formatHubTokenBlock(pkg) {
5316
+ const rows = [...pkg.tokens].sort((a, b) => a.bucket === b.bucket ? a.name.localeCompare(b.name) : a.bucket.localeCompare(b.bucket)).map((t) => `| \`${t.name}\` | ${t.bucket} | \`${t.value.replace(/\|/g, "\\|")}\` |`);
5317
+ const table = rows.length ? `| \u0422\u043E\u043A\u0435\u043D | \u0421\u043B\u043E\u0439 | \u0417\u043D\u0430\u0447\u0435\u043D\u0438\u0435 |
5318
+ | --- | --- | --- |
5319
+ ${rows.join(`
5320
+ `)}` : "_\u0412 \u043F\u0430\u043A\u0435\u0442\u0435 \u043D\u0435\u0442 CSS-\u0442\u043E\u043A\u0435\u043D\u043E\u0432 (\u0438\u043B\u0438 \u0442\u043E\u043B\u044C\u043A\u043E runtime var)._";
5321
+ return `${HUB_START}
5322
+ \u0410\u0432\u0442\u043E\u0442\u0430\u0431\u043B\u0438\u0446\u0430: \`elcrm hub collect\` \xB7 **${pkg.name}@${pkg.version}**. \u041D\u0435 \u043F\u0440\u0430\u0432\u0438\u0442\u044C \u0440\u0443\u043A\u0430\u043C\u0438.
5323
+
5324
+ ${table}
5325
+
5326
+ ${HUB_END}
5327
+ `;
5328
+ }
5329
+ function injectHubTokens(markdown, pkg) {
5330
+ const block = formatHubTokenBlock(pkg).trimEnd() + `
5331
+ `;
5332
+ let next = markdown.replace(/<!--\s*\u041F\u0430\u043A\u0435\u0442:\s*(@elcrm\/[a-z0-9-]+)(?:@[^.\s]+)?\s*\.?\s*-->/i, `<!-- \u041F\u0430\u043A\u0435\u0442: ${pkg.name}@${pkg.version}. -->`);
5333
+ if (next.includes(HUB_START) && next.includes(HUB_END)) {
5334
+ return next.replace(new RegExp(`${HUB_START}[\\s\\S]*?${HUB_END}\\n?`), block);
5335
+ }
5336
+ return `${next.trimEnd()}
5337
+
5338
+ ${block}`;
5339
+ }
5340
+ function collectIntoCliTemplates(pkgs, cliRoot, dry = false) {
5341
+ const written = [];
5342
+ for (const pkg of pkgs) {
5343
+ if (!pkg.doc)
5344
+ continue;
5345
+ const file = join25(cliRoot, "templates", "elcrm-docs", pkg.doc);
5346
+ if (!existsSync20(file))
5347
+ continue;
5348
+ const prev = readFileSync28(file, "utf8");
5349
+ const next = injectHubTokens(prev, pkg);
5350
+ if (next !== prev && !dry)
5351
+ writeFileSync12(file, next, "utf8");
5352
+ written.push({ file: pkg.doc, tokens: pkg.tokens.length });
5353
+ }
5354
+ const snap = {
5355
+ generatedAt: new Date().toISOString(),
5356
+ packages: pkgs.map((p) => ({
5357
+ name: p.name,
5358
+ version: p.version,
5359
+ tokens: p.tokens.map((t) => ({
5360
+ name: t.name,
5361
+ value: t.value,
5362
+ bucket: t.bucket
5363
+ }))
5364
+ }))
5365
+ };
5366
+ const snapFile = join25(cliRoot, "templates", "elcrm-docs", "generated", "tokens.json");
5367
+ if (!dry) {
5368
+ mkdirSync6(dirname4(snapFile), { recursive: true });
5369
+ writeFileSync12(snapFile, `${JSON.stringify(snap, null, 2)}
5370
+ `, "utf8");
5371
+ }
5372
+ written.push({ file: "generated/tokens.json", tokens: pkgs.length });
5373
+ return written;
5374
+ }
5375
+ function checkHubDocs(pkgs, cliRoot) {
5376
+ const gaps = [];
5377
+ for (const pkg of pkgs) {
5378
+ if (!pkg.doc || !pkg.tokens.length)
5379
+ continue;
5380
+ const file = join25(cliRoot, "templates", "elcrm-docs", pkg.doc);
5381
+ if (!existsSync20(file)) {
5382
+ for (const t of pkg.tokens)
5383
+ gaps.push({ pkg: pkg.name, token: t.name });
5384
+ continue;
5385
+ }
5386
+ const md = readFileSync28(file, "utf8");
5387
+ const names = new Set(pkg.tokens.map((t) => t.name));
5388
+ for (const name of names) {
5389
+ if (!md.includes(name))
5390
+ gaps.push({ pkg: pkg.name, token: name });
5391
+ }
5392
+ }
5393
+ return gaps;
5394
+ }
5395
+ function pushHubToElui(opts) {
5396
+ const dry = opts.dry ?? false;
5397
+ const apiDir = findEluiApiDir(opts.cwd, opts.apiDir);
5398
+ const webDir = findEluiWebDir(opts.cwd, opts.webDir);
5399
+ const docsDir = bundledDir("elcrm-docs");
5400
+ const out = {};
5401
+ if (!dry) {
5402
+ saveKitPathsConfig(opts.cwd, {
5403
+ ...apiDir ? { apiDir } : {},
5404
+ ...webDir ? { webDir } : {}
5405
+ }, true);
5406
+ }
5407
+ if (apiDir) {
5408
+ const dest = join25(apiDir, "src/kit/package-tokens.json");
5409
+ const src = join25(docsDir, "generated", "tokens.json");
5410
+ if (existsSync20(src) && !dry) {
5411
+ writeFileSync12(dest, readFileSync28(src, "utf8"), "utf8");
5412
+ }
5413
+ out.api = dest;
5414
+ }
5415
+ if (webDir) {
5416
+ const destDir = join25(webDir, "docs", "elcrm");
5417
+ if (!dry)
5418
+ mkdirSync6(destDir, { recursive: true });
5419
+ for (const name of readdirSync12(docsDir)) {
5420
+ if (!name.endsWith(".md") && name !== "generated")
5421
+ continue;
5422
+ const from = join25(docsDir, name);
5423
+ if (name === "generated") {
5424
+ const gdest = join25(destDir, "generated");
5425
+ if (!dry)
5426
+ mkdirSync6(gdest, { recursive: true });
5427
+ for (const g of readdirSync12(from)) {
5428
+ if (!dry) {
5429
+ writeFileSync12(join25(gdest, g), readFileSync28(join25(from, g), "utf8"));
5430
+ }
5431
+ }
5432
+ continue;
5433
+ }
5434
+ if (!dry) {
5435
+ writeFileSync12(join25(destDir, name), readFileSync28(from, "utf8"), "utf8");
5436
+ }
5437
+ }
5438
+ out.web = destDir;
5439
+ }
5440
+ return out;
5441
+ }
5442
+ function cliPackageRoot(from) {
5443
+ const pkgs = findPkgsRoot(from);
5444
+ if (!pkgs)
5445
+ return null;
5446
+ const cli = join25(pkgs, "elCRM.cli");
5447
+ return existsSync20(join25(cli, "package.json")) ? cli : null;
5448
+ }
5449
+
5450
+ // src/commands/hub.ts
5451
+ function showHelp() {
5452
+ console.log(`elcrm hub \u2014 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u0430\u0446\u0438\u044F: \u043F\u0430\u043A\u0435\u0442\u044B \u2192 CLI \u2192 elUI
5453
+
5454
+ \u041D\u0435 \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u044B\u0439 \u0441\u0435\u0440\u0432\u0435\u0440. \u0425\u0430\u0431 = \u044D\u0442\u043E\u0442 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 (elCRM.cli/templates).
5455
+
5456
+ collect [--pkgs=\u2026] [--dry]
5457
+ \u041F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u044C tokens/light/dark \u0441\u043E\u0441\u0435\u0434\u043D\u0438\u0445 @elcrm/* \u0438 \u0432\u0441\u0442\u0430\u0432\u0438\u0442\u044C
5458
+ \u0430\u0432\u0442\u043E\u0442\u0430\u0431\u043B\u0438\u0446\u044B \u0432 templates/elcrm-docs/*.elCRM.md
5459
+
5460
+ check [--pkgs=\u2026]
5461
+ \u041A\u0430\u0436\u0434\u044B\u0439 \u0442\u043E\u043A\u0435\u043D \u043F\u0430\u043A\u0435\u0442\u0430 \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u0432 \u0448\u043F\u0430\u0440\u0433\u0430\u043B\u043A\u0435 CLI
5462
+
5463
+ push [--api=\u2026] [--web=\u2026] [--dry]
5464
+ \u0421\u043A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0448\u043F\u0430\u0440\u0433\u0430\u043B\u043A\u0438 \u0432 elUI.web/docs/elcrm
5465
+ \u0438 tokens.json \u0432 elUI.api/src/kit/package-tokens.json
5466
+
5467
+ (\u0431\u0435\u0437 \u043F\u043E\u0434\u043A\u043E\u043C\u0430\u043D\u0434\u044B) collect \u2192 check \u2192 push
5468
+
5469
+ \u041E\u0434\u0438\u043D \u0440\u0430\u0437:
5470
+ elcrm kit config --api=/path/to/elUI.api --web=/path/to/elUI.web --global
5471
+
5472
+ \u041F\u043E\u0441\u043B\u0435 \u043F\u0440\u0430\u0432\u043A\u0438 \u043F\u0430\u043A\u0435\u0442\u0430:
5473
+ cd elCRM.cli && bun run hub
5474
+ # API/\u043C\u043E\u0436\u043D\u043E-\u043D\u0435\u043B\u044C\u0437\u044F \u2014 \u0440\u0443\u043A\u0430\u043C\u0438 \u0432 templates/elcrm-docs/<\u041F\u0410\u041A\u0415\u0422>.elCRM.md
5475
+ `);
5476
+ }
5477
+ async function cmdHub(parsed) {
5478
+ const sub = parsed.positionals[0] || "all";
5479
+ if (sub === "help" || hasFlag(parsed, "help", "h")) {
5480
+ showHelp();
5481
+ return;
5482
+ }
5483
+ const cwd = resolve14(parsed.options.dir || process.cwd());
5484
+ const dry = hasFlag(parsed, "dry");
5485
+ const pkgsRoot = findPkgsRoot(cwd, parsed.options.pkgs);
5486
+ const cliRoot = cliPackageRoot(cwd) ?? packageRoot();
5487
+ if (!pkgsRoot) {
5488
+ console.error("\u041D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u043A\u0430\u0442\u0430\u043B\u043E\u0433 \u043F\u0430\u043A\u0435\u0442\u043E\u0432 (elCRM.cli \u0440\u044F\u0434\u043E\u043C \u0441 elCRM.notice). \u0417\u0430\u0434\u0430\u0439\u0442\u0435 --pkgs=\u2026 \u0438\u043B\u0438 ELCRM_PKGS_DIR.");
5489
+ process.exit(1);
5490
+ }
5491
+ const pkgs = listHubPackages(pkgsRoot);
5492
+ console.log(`${dry ? "[dry] " : ""}hub: ${pkgs.length} \u043F\u0430\u043A\u0435\u0442\u043E\u0432 \u0432 ${pkgsRoot}`);
5493
+ const runCollect = sub === "all" || sub === "collect";
5494
+ const runCheck = sub === "all" || sub === "check";
5495
+ const runPush = sub === "all" || sub === "push";
5496
+ if (runCollect) {
5497
+ const files = collectIntoCliTemplates(pkgs, cliRoot, dry);
5498
+ for (const f of files) {
5499
+ console.log(` ${c.green("+")} ${f.file} (${f.tokens})`);
5500
+ }
5501
+ }
5502
+ if (runCheck) {
5503
+ const gaps = checkHubDocs(pkgs, cliRoot);
5504
+ if (gaps.length) {
5505
+ console.error(`${c.red("hub check:")} ${gaps.length} \u0442\u043E\u043A\u0435\u043D(\u043E\u0432) \u043D\u0435\u0442 \u0432 \u0448\u043F\u0430\u0440\u0433\u0430\u043B\u043A\u0435`);
5506
+ for (const g of gaps.slice(0, 40)) {
5507
+ console.error(` ${c.dim(g.pkg)} ${g.token}`);
5508
+ }
5509
+ if (gaps.length > 40)
5510
+ console.error(` \u2026 \u0435\u0449\u0451 ${gaps.length - 40}`);
5511
+ process.exit(1);
5512
+ }
5513
+ console.log(c.green("hub check: OK"));
5514
+ }
5515
+ if (runPush) {
5516
+ const dest = pushHubToElui({
5517
+ cliRoot,
5518
+ cwd,
5519
+ apiDir: parsed.options.api,
5520
+ webDir: parsed.options.web,
5521
+ dry
5522
+ });
5523
+ if (dest.api)
5524
+ console.log(` ${c.green("\u2192")} elUI.api ${dest.api}`);
5525
+ if (dest.web)
5526
+ console.log(` ${c.green("\u2192")} elUI.web ${dest.web}`);
5527
+ if (!dest.api && !dest.web) {
5528
+ console.warn(c.yellow("elUI \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D. elcrm kit config --api=\u2026 --web=\u2026 --global"));
5529
+ }
5530
+ }
5531
+ }
5532
+
5173
5533
  // src/commands/publish.ts
5174
- import { copyFileSync, existsSync as existsSync20, readFileSync as readFileSync28, unlinkSync, writeFileSync as writeFileSync12 } from "fs";
5175
- import { join as join25, resolve as resolve13 } from "path";
5534
+ import { copyFileSync, existsSync as existsSync21, readFileSync as readFileSync29, unlinkSync, writeFileSync as writeFileSync13 } from "fs";
5535
+ import { join as join26, resolve as resolve15 } from "path";
5176
5536
  import { spawnSync as spawnSync3 } from "child_process";
5177
5537
  var PUBLIC_NAME = "package.public.json";
5178
5538
  var PKG_NAME = "package.json";
5179
5539
  var BACKUP_NAME = ".package.json.elcrm-publish.bak";
5180
5540
  async function cmdPublish(parsed) {
5181
- const cwd = resolve13(process.cwd(), parsed.options.dir || ".");
5541
+ const cwd = resolve15(process.cwd(), parsed.options.dir || ".");
5182
5542
  const dry = hasFlag(parsed, "dry");
5183
5543
  const noCheck = hasFlag(parsed, "no-check", "noCheck");
5184
- const pkgPath = join25(cwd, PKG_NAME);
5185
- const publicPath = join25(cwd, PUBLIC_NAME);
5186
- const backupPath = join25(cwd, BACKUP_NAME);
5187
- if (!existsSync20(pkgPath)) {
5544
+ const pkgPath = join26(cwd, PKG_NAME);
5545
+ const publicPath = join26(cwd, PUBLIC_NAME);
5546
+ const backupPath = join26(cwd, BACKUP_NAME);
5547
+ if (!existsSync21(pkgPath)) {
5188
5548
  throw new Error(`\u041D\u0435\u0442 ${PKG_NAME} \u0432 ${cwd}`);
5189
5549
  }
5190
5550
  const local = readPackageJson(cwd);
5191
5551
  if (!local?.name || !local.version) {
5192
5552
  throw new Error(`${PKG_NAME}: \u043D\u0443\u0436\u043D\u044B \u043F\u043E\u043B\u044F name \u0438 version`);
5193
5553
  }
5194
- if (!existsSync20(publicPath)) {
5554
+ if (!existsSync21(publicPath)) {
5195
5555
  await publishPlain(cwd, local, parsed, dry, noCheck);
5196
5556
  return;
5197
5557
  }
5198
5558
  let pub;
5199
5559
  try {
5200
- pub = JSON.parse(readFileSync28(publicPath, "utf8"));
5560
+ pub = JSON.parse(readFileSync29(publicPath, "utf8"));
5201
5561
  } catch {
5202
5562
  throw new Error(`${PUBLIC_NAME}: \u043D\u0435\u0432\u0430\u043B\u0438\u0434\u043D\u044B\u0439 JSON`);
5203
5563
  }
@@ -5218,10 +5578,10 @@ async function cmdPublish(parsed) {
5218
5578
  console.log("[publish] --dry: \u043F\u043E\u0434\u043C\u0435\u043D\u0430 \u0438 publish \u043D\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u043B\u0438\u0441\u044C");
5219
5579
  return;
5220
5580
  }
5221
- const originalBytes = readFileSync28(pkgPath);
5581
+ const originalBytes = readFileSync29(pkgPath);
5222
5582
  copyFileSync(pkgPath, backupPath);
5223
- const indent = detectIndent2(readFileSync28(publicPath, "utf8"));
5224
- writeFileSync12(pkgPath, JSON.stringify(pub, null, indent) + `
5583
+ const indent = detectIndent2(readFileSync29(publicPath, "utf8"));
5584
+ writeFileSync13(pkgPath, JSON.stringify(pub, null, indent) + `
5225
5585
  `, "utf8");
5226
5586
  let publishStatus = 1;
5227
5587
  try {
@@ -5235,8 +5595,8 @@ async function cmdPublish(parsed) {
5235
5595
  console.error(r.error.message);
5236
5596
  }
5237
5597
  } finally {
5238
- writeFileSync12(pkgPath, originalBytes);
5239
- if (existsSync20(backupPath)) {
5598
+ writeFileSync13(pkgPath, originalBytes);
5599
+ if (existsSync21(backupPath)) {
5240
5600
  try {
5241
5601
  unlinkSync(backupPath);
5242
5602
  } catch {}
@@ -5343,13 +5703,13 @@ function detectIndent2(jsonText) {
5343
5703
 
5344
5704
  // src/index.ts
5345
5705
  function cliVersion() {
5346
- const here = dirname4(fileURLToPath2(import.meta.url));
5706
+ const here = dirname5(fileURLToPath2(import.meta.url));
5347
5707
  for (const p of [
5348
- join26(here, "..", "package.json"),
5349
- join26(here, "package.json")
5708
+ join27(here, "..", "package.json"),
5709
+ join27(here, "package.json")
5350
5710
  ]) {
5351
5711
  try {
5352
- const v = JSON.parse(readFileSync29(p, "utf8")).version;
5712
+ const v = JSON.parse(readFileSync30(p, "utf8")).version;
5353
5713
  if (typeof v === "string" && v)
5354
5714
  return v;
5355
5715
  } catch {}
@@ -5359,7 +5719,7 @@ function cliVersion() {
5359
5719
  function showVersion() {
5360
5720
  console.log(`elcrm ${cliVersion()}`);
5361
5721
  }
5362
- function showHelp() {
5722
+ function showHelp2() {
5363
5723
  console.log(`elcrm \u2014 CLI \u0434\u043B\u044F \u044D\u043A\u043E\u0441\u0438\u0441\u0442\u0435\u043C\u044B @elcrm/*
5364
5724
 
5365
5725
  \u0415\u0436\u0435\u0434\u043D\u0435\u0432\u043D\u044B\u0439 \u0446\u0438\u043A\u043B:
@@ -5374,9 +5734,9 @@ function showHelp() {
5374
5734
  --fix = doctor --fix (migrate + css)
5375
5735
  --test = elcrm test
5376
5736
 
5377
- css [--dry] [--dir=\u2026]
5737
+ css [--dry] [--check] [--dir=\u2026]
5378
5738
  \u0422\u043E\u043A\u0435\u043D\u044B \u2192 theme*.css; elcrm.css: +\u0440\u0430\u0431\u043E\u0447\u0438\u0435 @import, \u2212\u0431\u0438\u0442\u044B\u0435 (\u043D\u0435\u0442 \u0444\u0430\u0439\u043B\u0430 \u0432 \u043F\u0430\u043A\u0435\u0442\u0435)
5379
- \u0417\u0430\u0442\u0435\u043C css-sanitize (\u043E\u0441\u0438\u0440\u043E\u0442\u0435\u0432\u0448\u0438\u0439 color-mix)
5739
+ \u0417\u0430\u0442\u0435\u043C css-sanitize. --check: \u043F\u043E\u043A\u0440\u044B\u0442\u0438\u0435 var(--*) \u0431\u0435\u0437 \u0437\u0430\u043F\u0438\u0441\u0438 (exit 1)
5380
5740
 
5381
5741
  docs [--dry] [--keep] [--dir=\u2026] [--lib]
5382
5742
  docs/*.elCRM.md (\u0448\u043F\u0430\u0440\u0433\u0430\u043B\u043A\u0438 \u043F\u0430\u043A\u0435\u0442\u043E\u0432)
@@ -5391,6 +5751,9 @@ function showHelp() {
5391
5751
  sync \u2014 manifest \u2192 elUI.api/src/kit/manifests + reseed
5392
5752
  pull \u2014 emit + sync (postbuild \u043F\u0430\u043A\u0435\u0442\u0430)
5393
5753
 
5754
+ hub [collect|check|push] [--pkgs=\u2026] [--api=\u2026] [--web=\u2026] [--dry]
5755
+ \u043F\u0430\u043A\u0435\u0442\u044B @elcrm/* \u2192 \u0448\u043F\u0430\u0440\u0433\u0430\u043B\u043A\u0438 CLI \u2192 elUI (\u0431\u0435\u0437 \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u043E\u0433\u043E \u0441\u0435\u0440\u0432\u0435\u0440\u0430)
5756
+
5394
5757
  migrate <name> [--dry] | migrate --list
5395
5758
  front|ui = \u043F\u0430\u043A\u0435\u0442 \u043C\u0438\u0433\u0440\u0430\u0446\u0438\u0439 (aliases, form-native, native-html, tokens, \u2026)
5396
5759
  \u043F\u043E \u043E\u0434\u043D\u043E\u0439: form-aliases, form-native, native-html, form-tokens, components,
@@ -5437,7 +5800,7 @@ async function main() {
5437
5800
  return;
5438
5801
  }
5439
5802
  if (hasFlag(parsed, "help", "h") || parsed.command === "help" || !parsed.command) {
5440
- showHelp();
5803
+ showHelp2();
5441
5804
  return;
5442
5805
  }
5443
5806
  if (hasFlag(parsed, "dev")) {
@@ -5483,6 +5846,9 @@ async function main() {
5483
5846
  case "kit":
5484
5847
  await cmdKit(parsed);
5485
5848
  break;
5849
+ case "hub":
5850
+ await cmdHub(parsed);
5851
+ break;
5486
5852
  case "publish":
5487
5853
  await cmdPublish(parsed);
5488
5854
  break;