elcrm 1.1.1 → 1.1.2

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.
Files changed (32) hide show
  1. package/README.md +1 -0
  2. package/dist/index.js +2477 -922
  3. package/dist/vite/discover-lib.js +4 -4
  4. package/dist/vite/plugin-css-scoped.js +2 -2
  5. package/package.json +2 -2
  6. package/templates/elcrm-cursor/AGENTS.md +13 -0
  7. package/templates/elcrm-cursor/rules/elcrm-lib.mdc +101 -0
  8. package/templates/elcrm-cursor/rules/elcrm-module.mdc +25 -0
  9. package/templates/elcrm-cursor/rules/elcrm-packages.mdc +43 -0
  10. package/templates/elcrm-cursor/rules/elcrm-rpc.mdc +23 -0
  11. package/templates/elcrm-cursor/rules/elcrm-server.mdc +30 -0
  12. package/templates/elcrm-cursor/rules/elcrm-theme.mdc +13 -0
  13. package/templates/elcrm-cursor/rules/elcrm-ui.mdc +41 -0
  14. package/templates/elcrm-cursor/rules/elcrm.mdc +43 -0
  15. package/templates/elcrm-docs/ALERT.elCRM.md +26 -0
  16. package/templates/elcrm-docs/API.elCRM.md +38 -0
  17. package/templates/elcrm-docs/BUTTON.elCRM.md +36 -0
  18. package/templates/elcrm-docs/CLI.elCRM.md +27 -0
  19. package/templates/elcrm-docs/COMPONENTS.elCRM.md +68 -0
  20. package/templates/elcrm-docs/FORM.elCRM.md +39 -0
  21. package/templates/elcrm-docs/ICONS.elCRM.md +40 -0
  22. package/templates/elcrm-docs/MODAL.elCRM.md +41 -0
  23. package/templates/elcrm-docs/NOTICE.elCRM.md +31 -0
  24. package/templates/elcrm-docs/NOTIFICATIONS.elCRM.md +39 -0
  25. package/templates/elcrm-docs/PASSKEY.elCRM.md +26 -0
  26. package/templates/elcrm-docs/ROUTER.elCRM.md +34 -0
  27. package/templates/elcrm-docs/SERVER.elCRM.md +53 -0
  28. package/templates/elcrm-docs/SOCKET.elCRM.md +34 -0
  29. package/templates/elcrm-docs/TOOLTIP.elCRM.md +25 -0
  30. package/templates/panel/web/src/style/theme-dark.css +39 -3
  31. package/templates/panel/web/src/style/theme-light.css +39 -3
  32. package/templates/panel/web/src/style/theme.css +51 -3
package/dist/index.js CHANGED
@@ -1,103 +1,22 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
-
4
- // src/index.ts
5
- import { readFileSync as readFileSync9 } from "fs";
6
- import { dirname as dirname2, join as join11 } from "path";
7
- import { fileURLToPath as fileURLToPath2 } from "url";
8
-
9
- // src/parseArgs.ts
10
- function parseArgs(argv = process.argv.slice(2)) {
11
- const options = {};
12
- const flags = [];
13
- const positionals = [];
14
- for (let i = 0;i < argv.length; i++) {
15
- const arg = argv[i];
16
- if (arg === "--") {
17
- positionals.push(...argv.slice(i + 1));
18
- break;
19
- }
20
- if (arg.startsWith("--")) {
21
- const body = arg.slice(2);
22
- if (body.includes("=")) {
23
- const eq = body.indexOf("=");
24
- const key = body.slice(0, eq);
25
- const value = body.slice(eq + 1);
26
- if (key)
27
- options[key] = value;
28
- continue;
29
- }
30
- const next = argv[i + 1];
31
- if (next && !next.startsWith("-")) {
32
- const valueOpts = new Set([
33
- "template",
34
- "url",
35
- "name",
36
- "dir",
37
- "folder",
38
- "out"
39
- ]);
40
- if (valueOpts.has(body)) {
41
- options[body] = next;
42
- i++;
43
- continue;
44
- }
45
- }
46
- if (body && !flags.includes(body))
47
- flags.push(body);
48
- continue;
49
- }
50
- if (arg.startsWith("-") && arg.length > 1) {
51
- const short = arg.slice(1);
52
- if (short === "t" && argv[i + 1] && !argv[i + 1].startsWith("-")) {
53
- options.template = argv[i + 1];
54
- i++;
55
- continue;
56
- }
57
- if (short && !flags.includes(short))
58
- flags.push(short);
59
- continue;
60
- }
61
- positionals.push(arg);
62
- }
63
- const [command = "", ...rest] = positionals;
64
- return {
65
- command,
66
- positionals: rest,
67
- options,
68
- flags
69
- };
70
- }
71
- function hasFlag(parsed, ...names) {
72
- return names.some((n) => parsed.flags.includes(n));
73
- }
74
-
75
- // src/lib/elcrmDeps.ts
76
- var SECTIONS = [
77
- "dependencies",
78
- "devDependencies",
79
- "peerDependencies"
80
- ];
81
- function listElcrmDeps(pkg) {
82
- const out = [];
83
- for (const section of SECTIONS) {
84
- const map = pkg[section];
85
- if (!map || typeof map !== "object")
86
- continue;
87
- for (const [name, version] of Object.entries(map)) {
88
- if (name.startsWith("@elcrm/") && typeof version === "string") {
89
- out.push({ name, version, section });
90
- }
91
- }
92
- }
93
- return out;
94
- }
95
- function uniqueElcrmNames(deps) {
96
- return [...new Set(deps.map((d) => d.name))].sort();
3
+ var __defProp = Object.defineProperty;
4
+ var __returnValue = (v) => v;
5
+ function __exportSetter(name, newValue) {
6
+ this[name] = __returnValue.bind(null, newValue);
97
7
  }
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, {
11
+ get: all[name],
12
+ enumerable: true,
13
+ configurable: true,
14
+ set: __exportSetter.bind(all, name)
15
+ });
16
+ };
17
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
98
18
 
99
19
  // src/lib/latest.ts
100
- var CACHE = new Map;
101
20
  async function fetchLatestVersion(name) {
102
21
  if (CACHE.has(name))
103
22
  return CACHE.get(name);
@@ -149,30 +68,10 @@ async function fetchCorePinnedVersions() {
149
68
  } catch {}
150
69
  return map;
151
70
  }
152
-
153
- // src/lib/install.ts
154
- import { existsSync } from "fs";
155
- import { join } from "path";
156
- import { spawnSync } from "child_process";
157
- function detectPackageManager(cwd = process.cwd()) {
158
- if (existsSync(join(cwd, "bun.lockb")) || existsSync(join(cwd, "bun.lock"))) {
159
- return "bun";
160
- }
161
- const bun = spawnSync("bun", ["--version"], { encoding: "utf8" });
162
- if (bun.status === 0)
163
- return "bun";
164
- return "npm";
165
- }
166
- function installDeps(cwd = process.cwd()) {
167
- const pm = detectPackageManager(cwd);
168
- console.log(`\u0423\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0435\u0439 (${pm} install)\u2026`);
169
- const r = spawnSync(pm, ["install"], {
170
- cwd,
171
- stdio: "inherit",
172
- env: process.env
173
- });
174
- return r.status === 0;
175
- }
71
+ var CACHE;
72
+ var init_latest = __esm(() => {
73
+ CACHE = new Map;
74
+ });
176
75
 
177
76
  // src/lib/pkg.ts
178
77
  import { existsSync as existsSync2, readFileSync, writeFileSync } from "fs";
@@ -194,318 +93,34 @@ function writePackageJson(pkg, cwd = process.cwd()) {
194
93
  writeFileSync(packageJsonPath(cwd), JSON.stringify(pkg, null, 2) + `
195
94
  `, "utf8");
196
95
  }
96
+ var init_pkg = () => {};
197
97
 
198
- // src/lib/ansi.ts
199
- var enabled = process.env.NO_COLOR == null && process.env.FORCE_COLOR !== "0" && Boolean(process.stdout.isTTY);
200
- function wrap(code, text) {
201
- if (!enabled)
202
- return text;
203
- return `\x1B[${code}m${text}\x1B[0m`;
204
- }
205
- var c = {
206
- gray: (t) => wrap("90", t),
207
- green: (t) => wrap("32", t),
208
- blue: (t) => wrap("34", t),
209
- yellow: (t) => wrap("33", t),
210
- dim: (t) => wrap("2", t)
211
- };
212
-
213
- // src/commands/update.ts
214
- async function cmdUpdate(parsed) {
215
- const cwd = process.cwd();
216
- const pkg = readPackageJson(cwd);
217
- if (!pkg) {
218
- console.error("package.json \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u0432 \u0442\u0435\u043A\u0443\u0449\u0435\u043C \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0435.");
219
- process.exit(1);
220
- }
221
- const deps = listElcrmDeps(pkg);
222
- if (deps.length === 0) {
223
- console.log("\u0417\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0435\u0439 @elcrm/* \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043E.");
224
- return;
225
- }
226
- const dry = hasFlag(parsed, "dry");
227
- const useCore = hasFlag(parsed, "core");
228
- const noInstall = hasFlag(parsed, "no-install");
229
- const names = uniqueElcrmNames(deps);
230
- console.log(`\u041D\u0430\u0439\u0434\u0435\u043D\u043E @elcrm/*: ${names.length}`);
231
- let versions;
232
- if (useCore) {
233
- console.log("\u0420\u0435\u0436\u0438\u043C --core: \u0432\u0435\u0440\u0441\u0438\u0438 \u0438\u0437 \u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D\u043D\u043E\u0433\u043E @elcrm/core\u2026");
234
- versions = await fetchCorePinnedVersions();
235
- const missing = names.filter((n) => !versions.has(n));
236
- if (missing.length) {
237
- const extra = await fetchLatestMap(missing);
238
- for (const [k, v] of extra)
239
- versions.set(k, v);
240
- }
241
- } else {
242
- console.log("\u0417\u0430\u043F\u0440\u043E\u0441 latest \u0441 npm registry\u2026");
243
- versions = await fetchLatestMap(names);
244
- }
245
- let changed = 0;
246
- const rows = [];
247
- for (const dep of deps) {
248
- const latest = versions.get(dep.name);
249
- if (!latest) {
250
- rows.push(` ${c.yellow("?")} ${dep.name}: ${c.gray(dep.version)} \u2192 ${c.yellow("(\u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043E \u043D\u0430 npm)")}`);
251
- continue;
252
- }
253
- const current = dep.version.replace(/^[\^~]/, "");
254
- if (current === latest) {
255
- rows.push(` ${c.gray("=")} ${dep.name}: ${c.gray(dep.version)}`);
256
- continue;
257
- }
258
- const prefix = dep.version.startsWith("^") ? "^" : dep.version.startsWith("~") ? "~" : "";
259
- const next = `${prefix}${latest}`;
260
- rows.push(` ${c.blue("\u2191")} ${dep.name}: ${c.gray(dep.version)} \u2192 ${c.green(next)}`);
261
- if (!dry) {
262
- const section = pkg[dep.section];
263
- section[dep.name] = next;
264
- changed++;
265
- } else {
266
- changed++;
267
- }
268
- }
269
- console.log(rows.join(`
270
- `));
271
- if (dry) {
272
- console.log(`
273
- --dry: \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0439 \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043D\u043E (\u043A \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u044E: ${changed}).`);
274
- return;
275
- }
276
- if (changed === 0) {
277
- console.log(`
278
- \u0412\u0441\u0435 @elcrm/* \u0443\u0436\u0435 \u043D\u0430 \u0430\u043A\u0442\u0443\u0430\u043B\u044C\u043D\u044B\u0445 \u0432\u0435\u0440\u0441\u0438\u044F\u0445.`);
279
- return;
280
- }
281
- writePackageJson(pkg, cwd);
282
- console.log(`
283
- package.json \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D (${changed}).`);
284
- if (!noInstall) {
285
- const ok = installDeps(cwd);
286
- if (!ok) {
287
- console.error("\u0423\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0435\u0439 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0430\u0441\u044C \u0441 \u043E\u0448\u0438\u0431\u043A\u043E\u0439.");
288
- process.exit(1);
289
- }
290
- } else {
291
- console.log("\u041F\u0440\u043E\u043F\u0443\u0441\u043A install (--no-install). \u0417\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u0435 bun/npm install \u0432\u0440\u0443\u0447\u043D\u0443\u044E.");
292
- }
293
- }
294
-
295
- // src/lib/copyTemplate.ts
98
+ // src/lib/migrate/socket-server.ts
296
99
  import {
297
- cpSync,
298
- existsSync as existsSync4,
299
- mkdirSync,
100
+ existsSync as existsSync3,
300
101
  readdirSync,
301
102
  readFileSync as readFileSync2,
302
103
  statSync,
303
104
  writeFileSync as writeFileSync2
304
105
  } from "fs";
305
- import { join as join4, resolve } from "path";
306
-
307
- // src/lib/paths.ts
308
- import { existsSync as existsSync3 } from "fs";
309
- import { dirname, join as join3 } from "path";
310
- import { fileURLToPath } from "url";
311
- function packageRoot() {
312
- const here = dirname(fileURLToPath(import.meta.url));
313
- const candidates = [here, join3(here, ".."), join3(here, "..", "..")];
314
- for (const dir of candidates) {
315
- if (existsSync3(join3(dir, "package.json")) && existsSync3(join3(dir, "templates"))) {
316
- return dir;
106
+ import { join as join3 } from "path";
107
+ function walkFiles(dir, out = []) {
108
+ if (!existsSync3(dir))
109
+ return out;
110
+ for (const name of readdirSync(dir)) {
111
+ if (SKIP_DIRS.has(name))
112
+ continue;
113
+ const full = join3(dir, name);
114
+ const st = statSync(full);
115
+ if (st.isDirectory())
116
+ walkFiles(full, out);
117
+ else {
118
+ const ext = name.includes(".") ? `.${name.split(".").pop()}` : "";
119
+ if (TEXT_EXT.has(ext))
120
+ out.push(full);
317
121
  }
318
122
  }
319
- for (const dir of candidates) {
320
- if (existsSync3(join3(dir, "package.json")))
321
- return dir;
322
- }
323
- return join3(here, "..");
324
- }
325
- function templatesDir() {
326
- return join3(packageRoot(), "templates");
327
- }
328
-
329
- // src/lib/copyTemplate.ts
330
- var SKIP = new Set(["node_modules", "dist", ".git"]);
331
- function folderToPackageName(projectPath) {
332
- const base = projectPath.replace(/\\/g, "/").split("/").filter(Boolean).pop() ?? "app";
333
- const slug = base.toLowerCase().replace(/[^a-z0-9-._~/]/g, "-").replace(/^[-_.]+|[-_.]+$/g, "");
334
- return slug || "app";
335
- }
336
- function isEmptyDir(dir) {
337
- if (!existsSync4(dir))
338
- return true;
339
- const entries = readdirSync(dir).filter((e) => e !== ".git" && e !== ".DS_Store");
340
- return entries.length === 0;
341
- }
342
- function writePkgName(pkgPath, name) {
343
- if (!existsSync4(pkgPath))
344
- return;
345
- const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
346
- pkg.name = name;
347
- pkg.version = pkg.version ?? "0.0.0";
348
- writeFileSync2(pkgPath, JSON.stringify(pkg, null, 2) + `
349
- `, "utf8");
350
- }
351
- function finalizePanelNames(targetDir, baseName) {
352
- writePkgName(join4(targetDir, "package.json"), baseName);
353
- writePkgName(join4(targetDir, "web", "package.json"), `${baseName}-web`);
354
- writePkgName(join4(targetDir, "api", "package.json"), `${baseName}-api`);
355
- }
356
- function copyTemplate(templateName, targetDir, packageName) {
357
- const src = join4(templatesDir(), templateName);
358
- if (!existsSync4(src) || !statSync(src).isDirectory()) {
359
- throw new Error(`\u0428\u0430\u0431\u043B\u043E\u043D \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D: ${templateName}
360
- \u041E\u0436\u0438\u0434\u0430\u043B\u0441\u044F \u043A\u0430\u0442\u0430\u043B\u043E\u0433: ${src}`);
361
- }
362
- mkdirSync(targetDir, { recursive: true });
363
- for (const entry of readdirSync(src)) {
364
- if (SKIP.has(entry))
365
- continue;
366
- const from = join4(src, entry);
367
- const to = join4(targetDir, entry);
368
- cpSync(from, to, { recursive: true });
369
- }
370
- const name = packageName ?? folderToPackageName(targetDir);
371
- if (templateName === "panel") {
372
- finalizePanelNames(targetDir, name);
373
- return;
374
- }
375
- writePkgName(join4(targetDir, "package.json"), name);
376
- }
377
- function listTemplates() {
378
- const dir = templatesDir();
379
- if (!existsSync4(dir))
380
- return [];
381
- return readdirSync(dir).filter((name) => {
382
- const p = join4(dir, name);
383
- return statSync(p).isDirectory() && !SKIP.has(name);
384
- });
385
- }
386
- function resolveTarget(pathOrName, cwd = process.cwd()) {
387
- return resolve(cwd, pathOrName);
388
- }
389
-
390
- // src/commands/create.ts
391
- function libPackageName(pathOrName) {
392
- let slug = folderToPackageName(pathOrName);
393
- slug = slug.replace(/^@elcrm\//, "").replace(/^elcrm[._-]/, "");
394
- return `@elcrm/${slug || "lib"}`;
395
- }
396
- function doneHint(template, cdPath) {
397
- const prefix = cdPath ? ` cd ${cdPath}
398
- ` : "";
399
- if (template === "panel") {
400
- console.log(`
401
- \u0413\u043E\u0442\u043E\u0432\u043E (panel: web + api).
402
-
403
- ${prefix} bun run dev
404
-
405
- Web \u2192 http://localhost:5173
406
- API \u2192 http://localhost:4545
407
- \u0412\u0445\u043E\u0434: admin / admin
408
- `);
409
- return;
410
- }
411
- console.log(`
412
- \u0413\u043E\u0442\u043E\u0432\u043E.
413
-
414
- ${prefix} bun run dev
415
- `);
416
- }
417
- async function cmdCreate(parsed) {
418
- const name = parsed.positionals[0];
419
- if (!name) {
420
- console.error("\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u0438\u043C\u044F \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0430: elcrm create <name>");
421
- console.error(`\u0428\u0430\u0431\u043B\u043E\u043D\u044B: ${listTemplates().join(", ") || "(\u043D\u0435\u0442)"}`);
422
- process.exit(1);
423
- }
424
- const template = parsed.options.template ?? "app";
425
- const target = resolveTarget(name);
426
- const noInstall = hasFlag(parsed, "no-install");
427
- if (!isEmptyDir(target)) {
428
- console.error(`\u041A\u0430\u0442\u0430\u043B\u043E\u0433 \u043D\u0435 \u043F\u0443\u0441\u0442: ${target}`);
429
- process.exit(1);
430
- }
431
- const pkgName = template === "lib" && !name.startsWith("@") ? libPackageName(name) : folderToPackageName(name);
432
- console.log(`\u0428\u0430\u0431\u043B\u043E\u043D: ${template}`);
433
- console.log(`\u041A\u0430\u0442\u0430\u043B\u043E\u0433: ${target}`);
434
- console.log(`package name: ${pkgName}`);
435
- copyTemplate(template, target, pkgName);
436
- console.log("\u0424\u0430\u0439\u043B\u044B \u0441\u043A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u043D\u044B.");
437
- if (!noInstall) {
438
- const ok = installDeps(target);
439
- if (!ok)
440
- process.exit(1);
441
- }
442
- doneHint(template, name);
443
- }
444
- async function cmdInit(parsed) {
445
- const template = parsed.options.template ?? "app";
446
- const cwd = process.cwd();
447
- const force = hasFlag(parsed, "force");
448
- const noInstall = hasFlag(parsed, "no-install");
449
- if (!force && !isEmptyDir(cwd)) {
450
- console.error("\u0422\u0435\u043A\u0443\u0449\u0438\u0439 \u043A\u0430\u0442\u0430\u043B\u043E\u0433 \u043D\u0435 \u043F\u0443\u0441\u0442. \u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439\u0442\u0435 --force \u0438\u043B\u0438 elcrm create <name>.");
451
- process.exit(1);
452
- }
453
- const pkgName = template === "lib" ? libPackageName(cwd) : folderToPackageName(cwd);
454
- console.log(`\u0428\u0430\u0431\u043B\u043E\u043D: ${template}`);
455
- console.log(`\u041A\u0430\u0442\u0430\u043B\u043E\u0433: ${cwd}`);
456
- copyTemplate(template, cwd, pkgName);
457
- console.log("\u041F\u0440\u043E\u0435\u043A\u0442 \u0438\u043D\u0438\u0446\u0438\u0430\u043B\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u043D.");
458
- if (!noInstall) {
459
- const ok = installDeps(cwd);
460
- if (!ok)
461
- process.exit(1);
462
- }
463
- doneHint(template);
464
- }
465
-
466
- // src/lib/migrate/socket-server.ts
467
- import {
468
- existsSync as existsSync5,
469
- readdirSync as readdirSync2,
470
- readFileSync as readFileSync3,
471
- statSync as statSync2,
472
- writeFileSync as writeFileSync3
473
- } from "fs";
474
- import { join as join5 } from "path";
475
- var SKIP_DIRS = new Set([
476
- "node_modules",
477
- "dist",
478
- ".git",
479
- "coverage",
480
- ".turbo"
481
- ]);
482
- var TEXT_EXT = new Set([
483
- ".ts",
484
- ".tsx",
485
- ".js",
486
- ".jsx",
487
- ".mjs",
488
- ".cjs",
489
- ".mts",
490
- ".cts"
491
- ]);
492
- function walkFiles(dir, out = []) {
493
- if (!existsSync5(dir))
494
- return out;
495
- for (const name of readdirSync2(dir)) {
496
- if (SKIP_DIRS.has(name))
497
- continue;
498
- const full = join5(dir, name);
499
- const st = statSync2(full);
500
- if (st.isDirectory())
501
- walkFiles(full, out);
502
- else {
503
- const ext = name.includes(".") ? `.${name.split(".").pop()}` : "";
504
- if (TEXT_EXT.has(ext))
505
- out.push(full);
506
- }
507
- }
508
- return out;
123
+ return out;
509
124
  }
510
125
  function rewriteImports(source) {
511
126
  let hits = 0;
@@ -578,10 +193,10 @@ async function migrateSocketServer(cwd, dry) {
578
193
  writePackageJson(pkg, cwd);
579
194
  }
580
195
  }
581
- const roots = ["src", "app", "server", "lib"].map((d) => join5(cwd, d));
196
+ const roots = ["src", "app", "server", "lib"].map((d) => join3(cwd, d));
582
197
  const files = roots.flatMap((r) => walkFiles(r));
583
198
  for (const file of files) {
584
- const raw = readFileSync3(file, "utf8");
199
+ const raw = readFileSync2(file, "utf8");
585
200
  const { next, hits } = rewriteImports(raw);
586
201
  if (hits > 0) {
587
202
  changes.push({
@@ -589,350 +204,1428 @@ async function migrateSocketServer(cwd, dry) {
589
204
  detail: `${hits} \u0437\u0430\u043C\u0435\u043D(\u044B) \u0438\u043C\u043F\u043E\u0440\u0442\u043E\u0432 \u2192 @elcrm/server`
590
205
  });
591
206
  if (!dry)
592
- writeFileSync3(file, next, "utf8");
207
+ writeFileSync2(file, next, "utf8");
593
208
  }
594
209
  }
595
210
  return changes;
596
211
  }
212
+ var SKIP_DIRS, TEXT_EXT;
213
+ var init_socket_server = __esm(() => {
214
+ init_latest();
215
+ init_pkg();
216
+ SKIP_DIRS = new Set([
217
+ "node_modules",
218
+ "dist",
219
+ ".git",
220
+ "coverage",
221
+ ".turbo"
222
+ ]);
223
+ TEXT_EXT = new Set([
224
+ ".ts",
225
+ ".tsx",
226
+ ".js",
227
+ ".jsx",
228
+ ".mjs",
229
+ ".cjs",
230
+ ".mts",
231
+ ".cts"
232
+ ]);
233
+ });
597
234
 
598
- // src/commands/migrate.ts
599
- var MIGRATIONS = {
600
- "socket-server": migrateSocketServer
601
- };
602
- async function cmdMigrate(parsed) {
603
- if (hasFlag(parsed, "list") || parsed.positionals[0] === "list") {
604
- console.log("\u0414\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0435 \u043C\u0438\u0433\u0440\u0430\u0446\u0438\u0438:");
605
- for (const name2 of Object.keys(MIGRATIONS)) {
606
- console.log(` ${name2}`);
607
- }
608
- return;
609
- }
610
- const name = parsed.positionals[0];
611
- if (!name) {
612
- console.error("\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u043C\u0438\u0433\u0440\u0430\u0446\u0438\u044E: elcrm migrate <name>");
613
- console.error(`\u0414\u043E\u0441\u0442\u0443\u043F\u043D\u043E: ${Object.keys(MIGRATIONS).join(", ")}`);
614
- console.error("\u0421\u043F\u0438\u0441\u043E\u043A: elcrm migrate --list");
615
- process.exit(1);
616
- }
617
- const fn = MIGRATIONS[name];
618
- if (!fn) {
619
- console.error(`\u041D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043C\u0438\u0433\u0440\u0430\u0446\u0438\u044F: ${name}`);
620
- console.error(`\u0414\u043E\u0441\u0442\u0443\u043F\u043D\u043E: ${Object.keys(MIGRATIONS).join(", ")}`);
621
- process.exit(1);
622
- }
623
- const dry = hasFlag(parsed, "dry");
624
- const cwd = process.cwd();
625
- console.log(`\u041C\u0438\u0433\u0440\u0430\u0446\u0438\u044F: ${name}${dry ? " (--dry)" : ""}`);
626
- const changes = await fn(cwd, dry);
627
- if (changes.length === 0) {
628
- console.log("\u0418\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0439 \u043D\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044F.");
629
- return;
630
- }
631
- for (const c2 of changes) {
632
- console.log(` ${c2.file}: ${c2.detail}`);
633
- }
634
- console.log(dry ? `
635
- --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.` : `
636
- \u041F\u0440\u0438\u043C\u0435\u043D\u0435\u043D\u043E: ${changes.length}.`);
637
- }
638
-
639
- // src/lib/test/front.ts
640
- import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync4, statSync as statSync3 } from "fs";
641
- import { join as join6, relative } from "path";
642
- var SKIP2 = new Set(["node_modules", "dist", ".git", "coverage", ".bun"]);
643
- var DEPRECATED_SOCKET_IMPORT_RE = /(?:from\s+|require\s*\(\s*)["']@elcrm\/(?:socket\/server|socket-server)["']/;
644
- var THEMES_CSS_IMPORT_RE = /(?:from\s+|import\s+)["']@elcrm\/[^"']+\/themes\.css["']|import\s+["']@elcrm\/[^"']+\/themes\.css["']/;
645
- var LEGACY_PASSKEY_IMPORT_RE = /(?:from\s+|require\s*\(\s*)["']@elcrm\/passkey["']/;
646
- var LEGACY_PASSKEY_SYMBOL_RE = /\b(?:createPasskey|authenticateWithPasskey)\b/;
647
- function walkCode(dir, out) {
648
- if (!existsSync6(dir))
649
- return;
650
- for (const name of readdirSync3(dir)) {
651
- if (SKIP2.has(name))
235
+ // src/lib/migrate/walk.ts
236
+ import {
237
+ existsSync as existsSync4,
238
+ readdirSync as readdirSync2,
239
+ readFileSync as readFileSync3,
240
+ statSync as statSync2,
241
+ writeFileSync as writeFileSync3
242
+ } from "fs";
243
+ import { join as join4, relative } from "path";
244
+ function walkCodeFiles(dir, out = []) {
245
+ if (!existsSync4(dir))
246
+ return out;
247
+ for (const name of readdirSync2(dir)) {
248
+ if (SKIP_DIRS2.has(name))
652
249
  continue;
653
- const full = join6(dir, name);
250
+ const full = join4(dir, name);
654
251
  let st;
655
252
  try {
656
- st = statSync3(full);
253
+ st = statSync2(full);
657
254
  } catch {
658
255
  continue;
659
256
  }
660
257
  if (st.isDirectory())
661
- walkCode(full, out);
662
- else if (/\.(tsx?|jsx?|mjs|cjs)$/.test(name))
258
+ walkCodeFiles(full, out);
259
+ else if (/\.(tsx?|jsx?|mjs|cjs|css)$/.test(name))
663
260
  out.push(full);
664
261
  }
262
+ return out;
665
263
  }
666
- function collectFrontIssues(cwd, pkg) {
667
- const issues = [];
668
- const isLib = Boolean(pkg.name?.startsWith("@elcrm/"));
669
- const scripts = pkg.scripts ?? {};
670
- if (pkg.type !== "module") {
671
- issues.push({
672
- level: "warn",
673
- code: "type-module",
674
- message: '\u041D\u0435\u0442 "type": "module"',
675
- fixable: true
676
- });
677
- }
678
- if (!pkg.engines?.node && !pkg.engines?.bun) {
679
- issues.push({
680
- level: "warn",
681
- code: "engines",
682
- message: "\u041D\u0435\u0442 engines.node (\u043E\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044F >=18) \u0438\u043B\u0438 engines.bun",
683
- fixable: true
264
+ function projectCodeRoots(cwd) {
265
+ return ["src", "app", "web/src", "lib", "modules"].map((d) => join4(cwd, d)).filter((p) => existsSync4(p));
266
+ }
267
+ function rel(cwd, file) {
268
+ return relative(cwd, file) || file;
269
+ }
270
+ function writeIfChanged(file, next, dry) {
271
+ const prev = readFileSync3(file, "utf8");
272
+ if (prev === next)
273
+ return false;
274
+ if (!dry)
275
+ writeFileSync3(file, next, "utf8");
276
+ return true;
277
+ }
278
+ var SKIP_DIRS2;
279
+ var init_walk = __esm(() => {
280
+ SKIP_DIRS2 = new Set([
281
+ "node_modules",
282
+ "dist",
283
+ ".git",
284
+ "coverage",
285
+ ".turbo",
286
+ ".next",
287
+ "build"
288
+ ]);
289
+ });
290
+
291
+ // src/lib/migrate/size-sml.ts
292
+ import { readFileSync as readFileSync4 } from "fs";
293
+ function rewrite(source) {
294
+ let next = source;
295
+ let hits = 0;
296
+ for (const [re, repl] of PATTERNS) {
297
+ next = next.replace(re, () => {
298
+ hits++;
299
+ return repl;
684
300
  });
685
301
  }
686
- if (isLib) {
687
- if (!scripts.build && !scripts["pack:check"]) {
688
- issues.push({
689
- level: "warn",
690
- code: "scripts",
691
- message: "\u0414\u043B\u044F \u0431\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0438 \u0436\u0435\u043B\u0430\u0442\u0435\u043B\u044C\u043D\u044B scripts.build \u0438/\u0438\u043B\u0438 pack:check"
692
- });
693
- }
694
- if (!scripts["pack:check"]) {
695
- issues.push({
696
- level: "info",
697
- code: "pack-check",
698
- message: "\u041D\u0435\u0442 scripts.pack:check",
699
- fixable: true
700
- });
701
- }
702
- if (existsSync6(join6(cwd, "src/lib")) && !scripts["check:css"]) {
703
- issues.push({
704
- level: "info",
705
- code: "check-css",
706
- message: "\u041D\u0435\u0442 scripts.check:css (css-scoped) \u2014 \u0440\u0435\u043A\u043E\u043C\u0435\u043D\u0434\u0443\u0435\u0442\u0441\u044F \u0434\u043B\u044F UI-lib"
707
- });
708
- }
709
- if (existsSync6(join6(cwd, "src/lib")) && !scripts["check:tokens"]) {
710
- issues.push({
711
- level: "info",
712
- code: "check-tokens",
713
- message: "\u041D\u0435\u0442 scripts.check:tokens \u2014 \u0440\u0435\u043A\u043E\u043C\u0435\u043D\u0434\u0443\u0435\u0442\u0441\u044F \u0434\u043B\u044F UI-lib \u0441 CSS Modules"
302
+ return { next, hits };
303
+ }
304
+ async function migrateSizeSml(cwd, dry) {
305
+ const changes = [];
306
+ const files = projectCodeRoots(cwd).flatMap((r) => walkCodeFiles(r));
307
+ for (const file of files) {
308
+ if (file.endsWith(".css"))
309
+ continue;
310
+ const raw = readFileSync4(file, "utf8");
311
+ const { next, hits } = rewrite(raw);
312
+ if (hits === 0)
313
+ continue;
314
+ if (writeIfChanged(file, next, dry) || dry) {
315
+ changes.push({
316
+ file: rel(cwd, file),
317
+ detail: `${hits}\xD7 size sm/md \u2192 s/m`
714
318
  });
715
319
  }
716
320
  }
717
- const deps = listElcrmDeps(pkg);
718
- if (deps.some((d) => d.name === "@elcrm/socket-server")) {
719
- issues.push({
720
- level: "error",
721
- code: "deprecated-socket-server",
722
- message: "\u0417\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u044C @elcrm/socket-server \u0443\u0441\u0442\u0430\u0440\u0435\u043B\u0430 \u2014 elcrm migrate socket-server",
723
- fixable: true
321
+ return changes;
322
+ }
323
+ var PATTERNS;
324
+ var init_size_sml = __esm(() => {
325
+ init_walk();
326
+ PATTERNS = [
327
+ [/size\s*=\s*"sm"/g, 'size="s"'],
328
+ [/size\s*=\s*'sm'/g, "size='s'"],
329
+ [/size\s*=\s*\{\s*"sm"\s*\}/g, 'size={"s"}'],
330
+ [/size\s*=\s*\{\s*'sm'\s*\}/g, "size={'s'}"],
331
+ [/size\s*=\s*"md"/g, 'size="m"'],
332
+ [/size\s*=\s*'md'/g, "size='m'"],
333
+ [/size\s*=\s*\{\s*"md"\s*\}/g, 'size={"m"}'],
334
+ [/size\s*=\s*\{\s*'md'\s*\}/g, "size={'m'}"]
335
+ ];
336
+ });
337
+
338
+ // src/lib/migrate/field-border.ts
339
+ import { readFileSync as readFileSync5 } from "fs";
340
+ function extractColor(value) {
341
+ const v = value.trim().replace(/;$/, "");
342
+ const m = v.match(/^(?:\d+(?:\.\d+)?(?:px|em|rem)?\s+)?(?:solid|dashed|dotted|double)\s+(.+)$/i);
343
+ if (m)
344
+ return m[1].trim();
345
+ if (/^(#|rgb|hsl|var\(|color-mix|transparent|currentColor)/i.test(v)) {
346
+ return null;
347
+ }
348
+ return null;
349
+ }
350
+ function rewriteCss(source) {
351
+ let hits = 0;
352
+ let next = source;
353
+ for (const name of VARS) {
354
+ const re = new RegExp(`(${name.replace(/-/g, "\\-")}\\s*:\\s*)([^;\\n}]+)`, "g");
355
+ next = next.replace(re, (full, prefix, rawVal) => {
356
+ const color = extractColor(rawVal);
357
+ if (!color)
358
+ return full;
359
+ hits++;
360
+ return `${prefix}${color}`;
724
361
  });
725
362
  }
726
- for (const name of ["@elcrm/db", "@elcrm/jwt", "@elcrm/cron", "@elcrm/tb"]) {
727
- if (deps.some((d) => d.name === name)) {
728
- issues.push({
729
- level: "error",
730
- code: `pkg-${name}`,
731
- message: `${name} \u2014 \u0441\u0435\u0440\u0432\u0435\u0440\u043D\u044B\u0439 \u043F\u0430\u043A\u0435\u0442; \u043D\u0430 \u0444\u0440\u043E\u043D\u0442\u0435 \u043D\u0435 \u043D\u0443\u0436\u0435\u043D (server: @elcrm/server/\u2026)`
363
+ return { next, hits };
364
+ }
365
+ async function migrateFieldBorder(cwd, dry) {
366
+ const changes = [];
367
+ const files = projectCodeRoots(cwd).flatMap((r) => walkCodeFiles(r)).filter((f) => f.endsWith(".css"));
368
+ for (const file of files) {
369
+ const raw = readFileSync5(file, "utf8");
370
+ const { next, hits } = rewriteCss(raw);
371
+ if (hits === 0)
372
+ continue;
373
+ if (writeIfChanged(file, next, dry) || dry) {
374
+ changes.push({
375
+ file: rel(cwd, file),
376
+ detail: `${hits}\xD7 border hover/focus \u2192 \u0442\u043E\u043B\u044C\u043A\u043E \u0446\u0432\u0435\u0442`
732
377
  });
733
378
  }
734
379
  }
735
- const umbrella = pkg.dependencies?.elcrm || pkg.devDependencies?.elcrm;
736
- if (umbrella && /^[\^~]?0\.9\./.test(umbrella)) {
737
- issues.push({
738
- level: "warn",
739
- code: "legacy-umbrella",
740
- message: `\u0423\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0438\u0439 \u043F\u0430\u043A\u0435\u0442 elcrm@${umbrella} \u2014 \u043F\u0435\u0440\u0435\u0445\u043E\u0434\u0438\u0442\u0435 \u043D\u0430 @elcrm/* / @elcrm/core`
741
- });
742
- }
743
- if (deps.length === 0 && !isLib) {
744
- issues.push({
745
- level: "info",
746
- code: "no-elcrm",
747
- message: "\u041D\u0435\u0442 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0435\u0439 @elcrm/*"
748
- });
749
- }
750
- const files = [];
751
- walkCode(join6(cwd, "src"), files);
752
- for (const name of ["bootstrap.tsx", "bootstrap.ts", "main.tsx", "main.ts", "index.tsx", "index.ts", "App.tsx"]) {
753
- const full = join6(cwd, name);
754
- if (existsSync6(full))
755
- files.push(full);
756
- const inSrc = join6(cwd, "src", name);
757
- if (existsSync6(inSrc) && !files.includes(inSrc))
758
- files.push(inSrc);
759
- }
380
+ return changes;
381
+ }
382
+ var VARS;
383
+ var init_field_border = __esm(() => {
384
+ init_walk();
385
+ VARS = [
386
+ "--field-border-focus",
387
+ "--field-border-hover",
388
+ "--field-border-error",
389
+ "--field-border-disabled"
390
+ ];
391
+ });
392
+
393
+ // src/lib/migrate/modal-create.ts
394
+ import { readFileSync as readFileSync6 } from "fs";
395
+ async function migrateModalCreate(cwd, dry) {
396
+ const changes = [];
397
+ const files = projectCodeRoots(cwd).flatMap((r) => walkCodeFiles(r)).filter((f) => /\.(tsx|jsx)$/.test(f));
398
+ const addRe = /Modal\.Add\s*\(\s*<([A-Za-z_][\w.]*)\s+key=\{Date\.now\(\)\}/;
760
399
  for (const file of files) {
761
- const text = readFileSync4(file, "utf8");
762
- const rel = relative(cwd, file);
763
- if (DEPRECATED_SOCKET_IMPORT_RE.test(text)) {
764
- issues.push({
765
- level: "error",
766
- code: "deprecated-import",
767
- message: "\u0438\u043C\u043F\u043E\u0440\u0442 @elcrm/socket/server \u0438\u043B\u0438 socket-server",
768
- file: rel,
769
- fixable: true
770
- });
400
+ const raw = readFileSync6(file, "utf8");
401
+ if (!addRe.test(raw) && !/Modal\.Add\s*\(/.test(raw))
402
+ continue;
403
+ if (/export\s+default\s+Modal\.Create\s*\(/.test(raw)) {
404
+ if (/Modal\.Add\s*\(/.test(raw)) {
405
+ changes.push({
406
+ file: rel(cwd, file),
407
+ detail: "\u043E\u0441\u0442\u0430\u043B\u0441\u044F Modal.Add \u0440\u044F\u0434\u043E\u043C \u0441 Create \u2014 \u0443\u0431\u0435\u0440\u0438\u0442\u0435 Add \u0432\u0440\u0443\u0447\u043D\u0443\u044E"
408
+ });
409
+ }
410
+ continue;
771
411
  }
772
- const themesRe = new RegExp(THEMES_CSS_IMPORT_RE.source, "g");
773
- let tm;
774
- while (tm = themesRe.exec(text)) {
775
- const line = text.slice(0, tm.index).split(/\r?\n/).length;
776
- issues.push({
777
- level: "error",
778
- code: "themes-css",
779
- 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)",
780
- file: rel,
781
- line
412
+ const simple = /function\s+Screen\b/.test(raw) || /const\s+Screen\s*=/.test(raw) || /function\s+\w+\s*\([^)]*\)\s*\{[\s\S]*Modal\.Main/.test(raw);
413
+ const openWrapper = /(?:const|function)\s+Open\s*=\s*(?:\([^)]*\)|[A-Za-z_]\w*)\s*=>\s*\{?\s*Modal\.Add/;
414
+ if (simple && openWrapper.test(raw) && /export\s+default/.test(raw)) {
415
+ let next = raw;
416
+ next = next.replace(/\n(?:const|function)\s+Open\s*=\s*[\s\S]*?(?=\n(?:const|function|export)\s)/, `
417
+ `);
418
+ next = next.replace(/\n(?:const|function)\s+Icon\s*=\s*[\s\S]*?(?=\n(?:const|function|export)\s)/, `
419
+ `);
420
+ next = next.replace(/\nconst\s+Add\s*=\s*\{[\s\S]*?\};\s*\nexport\s+default\s+Add\s*;?/, `
421
+ export default Modal.Create(Screen);
422
+ `);
423
+ next = next.replace(/export\s+default\s+\{\s*Open\s*,?\s*Icon?\s*,?\s*\}\s*;?/, "export default Modal.Create(Screen);");
424
+ next = next.replace(/export\s+function\s+open\w+\s*\([^)]*\)\s*\{[\s\S]*?Modal\.Add\([\s\S]*?\);\s*\}/, (block) => {
425
+ return `/* TODO: \u0437\u0430\u043C\u0435\u043D\u0438\u0442\u0435 \u0432\u044B\u0437\u043E\u0432\u044B \u043D\u0430 Modal.Open("Module","Name", props) \u0438\u043B\u0438 Create.Open */
426
+ ${block}`;
782
427
  });
783
- }
784
- if (LEGACY_PASSKEY_IMPORT_RE.test(text) && LEGACY_PASSKEY_SYMBOL_RE.test(text)) {
785
- const lines2 = text.split(/\r?\n/);
786
- lines2.forEach((line, i) => {
787
- if (/\bcreatePasskey\b/.test(line) || /\bauthenticateWithPasskey\b/.test(line)) {
788
- if (pkg.name === "@elcrm/passkey")
789
- return;
790
- issues.push({
791
- level: "error",
792
- code: "legacy-passkey-api",
793
- message: "createPasskey / authenticateWithPasskey \u043D\u0435 \u044D\u043A\u0441\u043F\u043E\u0440\u0442\u0438\u0440\u0443\u044E\u0442\u0441\u044F \u2014 createPasskeyClient",
794
- file: rel,
795
- line: i + 1
428
+ if (next !== raw && !/Modal\.Add/.test(next)) {
429
+ if (writeIfChanged(file, next, dry) || dry) {
430
+ changes.push({
431
+ file: rel(cwd, file),
432
+ detail: "Modal.Add \u2192 Modal.Create(Screen)"
796
433
  });
797
434
  }
798
- });
799
- }
800
- const lines = text.split(/\r?\n/);
801
- lines.forEach((line, i) => {
802
- const trimmed = line.trim();
803
- if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) {
804
- return;
805
- }
806
- if (/<(?:input|textarea|select)\b/.test(line)) {
807
- issues.push({
808
- level: "error",
809
- code: "native-html-input",
810
- message: "\u043D\u0430\u0442\u0438\u0432\u043D\u044B\u0439 <input|textarea|select> \u2014 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439\u0442\u0435 @elcrm/form (StringField, Select, \u2026)",
811
- file: rel,
812
- line: i + 1
813
- });
814
- }
815
- if (/(?:^|\s)native(?:\s|$|=|\/|>)/.test(line) && !/\bnavigator\b|\bnativeEvent\b|\bcreateNative/.test(line)) {
816
- issues.push({
817
- level: "warn",
818
- code: "form-native-prop",
819
- message: "prop native \u0443 \u043F\u043E\u043B\u044F \u0444\u043E\u0440\u043C\u044B \u2014 \u0443\u0431\u0435\u0440\u0438\u0442\u0435; \u0441\u0442\u0438\u043B\u0438 \u0447\u0435\u0440\u0435\u0437 \u0442\u043E\u043A\u0435\u043D\u044B @elcrm/form",
820
- file: rel,
821
- line: i + 1
822
- });
435
+ continue;
823
436
  }
437
+ }
438
+ const count = (raw.match(/Modal\.Add\s*\(/g) || []).length;
439
+ changes.push({
440
+ file: rel(cwd, file),
441
+ detail: `${count}\xD7 Modal.Add \u2014 \u043F\u0435\u0440\u0435\u0432\u0435\u0434\u0438\u0442\u0435 \u043D\u0430 Modal.Create + Open (\u0441\u043C. Projects/modal/Create)`
824
442
  });
825
443
  }
826
- return issues;
827
- }
828
- async function runPkgScript(cwd, script) {
829
- const pkg = JSON.parse(readFileSync4(join6(cwd, "package.json"), "utf8"));
830
- if (!pkg.scripts?.[script])
831
- return { ran: false, ok: true };
832
- console.log(`\u2192 bun run ${script}`);
833
- const proc = Bun.spawn(["bun", "run", script], {
834
- cwd,
835
- stdout: "inherit",
836
- stderr: "inherit"
837
- });
838
- const code = await proc.exited;
839
- return { ran: true, ok: code === 0 };
444
+ return changes;
840
445
  }
841
- async function runFrontScripts(cwd) {
842
- let failed = 0;
843
- for (const name of ["check:css", "check:tokens", "test"]) {
844
- const r = await runPkgScript(cwd, name);
845
- if (r.ran && !r.ok)
846
- failed++;
446
+ var init_modal_create = __esm(() => {
447
+ init_walk();
448
+ });
449
+
450
+ // src/lib/migrate/legacy-hacks.ts
451
+ import { existsSync as existsSync5, readFileSync as readFileSync7 } from "fs";
452
+ import { join as join5 } from "path";
453
+ async function migrateLegacyReactHack(cwd, dry) {
454
+ const changes = [];
455
+ const files = new Set(projectCodeRoots(cwd).flatMap((r) => walkCodeFiles(r)).filter((f) => /(^|\/)(main|bootstrap)\.(tsx?|jsx?)$/.test(f)));
456
+ for (const name of ["main.tsx", "main.ts", "src/main.tsx", "src/main.ts"]) {
457
+ const full = join5(cwd, name);
458
+ if (existsSync5(full))
459
+ files.add(full);
847
460
  }
848
- return failed;
461
+ for (const file of files) {
462
+ const raw = readFileSync7(file, "utf8");
463
+ if (!HACK_RE.test(raw))
464
+ continue;
465
+ const next = raw.replace(/\/\*\s*@elcrm\/modal[\s\S]*?\*\/\s*/m, "").replace(/\(globalThis\s+as\s+typeof\s+globalThis\s+&\s+\{\s*React:\s*typeof\s+React\s*\}\)\.React\s*=\s*React\s*;\s*/g, "").replace(/globalThis\.React\s*=\s*React\s*;\s*/g, "");
466
+ if (next === raw) {
467
+ changes.push({
468
+ file: rel(cwd, file),
469
+ detail: "globalThis.React \u2014 \u0443\u0434\u0430\u043B\u0438\u0442\u0435 \u0432\u0440\u0443\u0447\u043D\u0443\u044E \u043F\u043E\u0441\u043B\u0435 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u044F @elcrm/modal"
470
+ });
471
+ continue;
472
+ }
473
+ if (writeIfChanged(file, next, dry) || dry) {
474
+ changes.push({
475
+ file: rel(cwd, file),
476
+ detail: "\u0443\u0434\u0430\u043B\u0451\u043D globalThis.React \u0445\u0430\u043A"
477
+ });
478
+ }
479
+ }
480
+ return changes;
849
481
  }
482
+ var HACK_RE;
483
+ var init_legacy_hacks = __esm(() => {
484
+ init_walk();
485
+ HACK_RE = /\(globalThis\s+as\s+[^)]+\)\.React\s*=\s*React\s*;?|globalThis\.React\s*=\s*React\s*;?/;
486
+ });
850
487
 
851
- // src/lib/test/print.ts
852
- function printIssues(issues, title) {
853
- if (title)
854
- console.log(title);
855
- if (issues.length === 0) {
856
- console.log(c.green("ok \u2014 \u0437\u0430\u043C\u0435\u0447\u0430\u043D\u0438\u0439 \u043D\u0435\u0442"));
857
- return 0;
488
+ // src/lib/migrate/form-aliases.ts
489
+ import { readFileSync as readFileSync8 } from "fs";
490
+ function rewrite2(source) {
491
+ let next = source;
492
+ const labels = [];
493
+ for (const [re, repl, label] of REPLACEMENTS) {
494
+ let n = 0;
495
+ next = next.replace(re, () => {
496
+ n++;
497
+ return repl;
498
+ });
499
+ if (n > 0)
500
+ labels.push(`${n}\xD7 ${label}`);
858
501
  }
859
- let errors = 0;
860
- for (const issue of issues) {
861
- const mark = issue.level === "error" ? "E" : issue.level === "warn" ? "W" : "I";
862
- if (issue.level === "error")
863
- errors++;
864
- const loc = issue.file != null ? ` ${issue.file}${issue.line != null ? `:${issue.line}` : ""}` : "";
865
- const color = issue.level === "error" ? c.yellow : issue.level === "warn" ? c.yellow : c.gray;
866
- console.log(color(`[${mark}] ${issue.code}${loc}: ${issue.message}`));
502
+ return { next, labels };
503
+ }
504
+ async function migrateFormAliases(cwd, dry) {
505
+ const changes = [];
506
+ const files = projectCodeRoots(cwd).flatMap((r) => walkCodeFiles(r));
507
+ for (const file of files) {
508
+ if (file.endsWith(".css"))
509
+ continue;
510
+ const raw = readFileSync8(file, "utf8");
511
+ const { next, labels } = rewrite2(raw);
512
+ if (labels.length === 0)
513
+ continue;
514
+ if (writeIfChanged(file, next, dry) || dry) {
515
+ changes.push({
516
+ file: rel(cwd, file),
517
+ detail: labels.join(", ")
518
+ });
519
+ }
867
520
  }
868
- return errors;
521
+ return changes;
869
522
  }
523
+ var REPLACEMENTS;
524
+ var init_form_aliases = __esm(() => {
525
+ init_walk();
526
+ REPLACEMENTS = [
527
+ [/\bProgressField\b/g, "RangeField", "ProgressField\u2192RangeField"],
528
+ [/\bReadonlyField\b/g, "DisplayField", "ReadonlyField\u2192DisplayField"],
529
+ [/\bOtpField\b/g, "CodeField", "OtpField\u2192CodeField"],
530
+ [/\bSegmentField\b/g, "TabsField", "SegmentField\u2192TabsField"],
531
+ [/\bDragonDropField\b/g, "DragDropField", "DragonDropField\u2192DragDropField"],
532
+ [/\bTProgress\b/g, "TRange", "TProgress\u2192TRange"]
533
+ ];
534
+ });
870
535
 
871
- // src/lib/test/detect.ts
872
- import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync4 } from "fs";
873
- import { join as join7 } from "path";
874
- function hasElcrmServer(pkg) {
875
- for (const section of [
876
- "dependencies",
877
- "devDependencies",
878
- "peerDependencies"
879
- ]) {
880
- const map = pkg[section];
881
- if (map && typeof map === "object" && "@elcrm/server" in map)
882
- return true;
536
+ // src/lib/migrate/form-tokens.ts
537
+ import { readFileSync as readFileSync9 } from "fs";
538
+ function removeDecl(css, name) {
539
+ const re = new RegExp(`\\n?[ \\t]*${name.replace(/-/g, "\\-")}\\s*:[^;\\n}]+;?[ \\t]*`, "g");
540
+ let n = 0;
541
+ const next = css.replace(re, () => {
542
+ n++;
543
+ return "";
544
+ });
545
+ return { next: next.replace(/\n{3,}/g, `
546
+
547
+ `), n };
548
+ }
549
+ function migrateDatePopupShadowDecl(css) {
550
+ let n = 0;
551
+ let next = css;
552
+ const hasPopup = /--popup-shadow\s*:/.test(next);
553
+ const re = /(--date-popup-shadow\s*:\s*)(?!var\(\s*--popup-shadow)([^;\n}]+)/gi;
554
+ next = next.replace(re, (full, prefix, value) => {
555
+ n++;
556
+ const v = value.trim();
557
+ if (hasPopup) {
558
+ return `${prefix}var(--popup-shadow)`;
559
+ }
560
+ return `/*__POPUP_SHADOW__*/${v}/*__END__*/${prefix}var(--popup-shadow)`;
561
+ });
562
+ if (next.includes("/*__POPUP_SHADOW__*/")) {
563
+ next = next.replace(/\/\*__POPUP_SHADOW__\*\/([\s\S]*?)\/\*__END__\*\//, (_m, v) => `--popup-shadow: ${v.trim()};
564
+ `);
883
565
  }
884
- return false;
566
+ return { next, n };
885
567
  }
886
- function detectTestMode(pkg, force) {
887
- if (force === "server" || force === "front")
888
- return force;
889
- return hasElcrmServer(pkg) ? "server" : "front";
568
+ function expandFieldPaddingShorthand(css) {
569
+ let n = 0;
570
+ if (/--field-padding-block\s*:/.test(css)) {
571
+ return { next: css, n: 0 };
572
+ }
573
+ const re = /(--field-padding\s*:\s*)(?!var\(\s*--field-padding-block)(\S+)\s+(\S+)\s*;/g;
574
+ const next = css.replace(re, (full, prefix, block, inline) => {
575
+ if (block.includes("(") || inline.includes("("))
576
+ return full;
577
+ n++;
578
+ return `--field-padding-block: ${block};
579
+ ` + ` --field-padding-inline: ${inline};
580
+ ` + ` ${prefix}var(--field-padding-block) var(--field-padding-inline);`;
581
+ });
582
+ return { next, n };
890
583
  }
891
- function listWorkspaceDirs(cwd, pkg) {
892
- const raw = pkg.workspaces;
893
- let patterns = [];
894
- if (Array.isArray(raw)) {
895
- patterns = raw.filter((p) => typeof p === "string");
896
- } else if (raw && typeof raw === "object" && Array.isArray(raw.packages)) {
897
- patterns = raw.packages.filter((p) => typeof p === "string");
584
+ function rewriteFormTokens(source) {
585
+ let next = source;
586
+ let hits = 0;
587
+ for (const [re, to] of VAR_REPLACEMENTS) {
588
+ next = next.replace(re, () => {
589
+ hits++;
590
+ return to;
591
+ });
898
592
  }
899
- if (!patterns.length)
900
- return [];
901
- const out = [];
902
- for (const pat of patterns) {
903
- if (!pat.includes("*")) {
904
- const dir = join7(cwd, pat);
905
- if (existsSync7(join7(dir, "package.json")))
906
- out.push(dir);
593
+ const shadow = migrateDatePopupShadowDecl(next);
594
+ next = shadow.next;
595
+ hits += shadow.n;
596
+ const pad = expandFieldPaddingShorthand(next);
597
+ next = pad.next;
598
+ hits += pad.n;
599
+ for (const name of DECL_REMOVE) {
600
+ const stillUsed = new RegExp(`var\\(\\s*${name.replace(/-/g, "\\-")}\\s*`, "i").test(next);
601
+ if (stillUsed)
907
602
  continue;
908
- }
909
- const base = pat.replace(/\/\*$/, "").replace(/\*$/, "");
910
- const parent = join7(cwd, base);
911
- if (!existsSync7(parent) || !statSync4(parent).isDirectory())
603
+ const rem = removeDecl(next, name);
604
+ next = rem.next;
605
+ hits += rem.n;
606
+ }
607
+ return { next, hits };
608
+ }
609
+ async function migrateFormTokens(cwd, dry) {
610
+ const pkg = readPackageJson(cwd);
611
+ if (pkg?.name === "@elcrm/form")
612
+ return [];
613
+ const changes = [];
614
+ const files = projectCodeRoots(cwd).flatMap((r) => walkCodeFiles(r)).filter((f) => f.endsWith(".css"));
615
+ for (const file of files) {
616
+ const raw = readFileSync9(file, "utf8");
617
+ const { next, hits } = rewriteFormTokens(raw);
618
+ if (hits === 0 || next === raw)
912
619
  continue;
913
- for (const name of readdirSync4(parent)) {
914
- const dir = join7(parent, name);
915
- if (statSync4(dir).isDirectory() && existsSync7(join7(dir, "package.json"))) {
916
- out.push(dir);
917
- }
620
+ if (writeIfChanged(file, next, dry) || dry) {
621
+ changes.push({
622
+ file: rel(cwd, file),
623
+ detail: `${hits}\xD7 form-tokens (padding / popup-shadow / cleanup)`
624
+ });
918
625
  }
919
626
  }
920
- return out;
627
+ return changes;
921
628
  }
922
- function readPkgAt(dir) {
923
- const path = join7(dir, "package.json");
924
- if (!existsSync7(path))
925
- return null;
926
- try {
927
- return JSON.parse(readFileSync5(path, "utf8"));
629
+ function lineHasDeprecatedFormToken(line) {
630
+ const t = line.trim();
631
+ if (!t || t.startsWith("/*") || t.startsWith("*"))
632
+ return false;
633
+ if (DEPRECATED_VAR_RE.test(t))
634
+ return true;
635
+ if (!DEPRECATED_DECL_RE.test(t))
636
+ return false;
637
+ if (/:\s*var\(\s*--(?:field-padding|popup-shadow)/.test(t))
638
+ return false;
639
+ return true;
640
+ }
641
+ var VAR_REPLACEMENTS, DECL_REMOVE, FORM_TOKEN_DEPRECATED, DEPRECATED_VAR_RE, DEPRECATED_DECL_RE;
642
+ var init_form_tokens = __esm(() => {
643
+ init_pkg();
644
+ init_walk();
645
+ VAR_REPLACEMENTS = [
646
+ [
647
+ /var\(\s*--field-note-padding-top\s*(?:,[^)]+)?\)/gi,
648
+ "var(--field-padding-block)"
649
+ ],
650
+ [
651
+ /var\(\s*--field-note-padding\s*(?:,[^)]+)?\)/gi,
652
+ "var(--field-padding-block)"
653
+ ],
654
+ [
655
+ /var\(\s*--field-side-padding\s*(?:,[^)]+)?\)/gi,
656
+ "var(--field-padding-inline)"
657
+ ],
658
+ [
659
+ /var\(\s*--control-side-padding\s*(?:,[^)]+)?\)/gi,
660
+ "var(--field-padding-inline)"
661
+ ],
662
+ [
663
+ /var\(\s*--control-inner-padding\s*(?:,[^)]+)?\)/gi,
664
+ "var(--field-padding)"
665
+ ],
666
+ [
667
+ /var\(\s*--date-popup-shadow\s*(?:,[^)]+)?\)/gi,
668
+ "var(--popup-shadow)"
669
+ ]
670
+ ];
671
+ DECL_REMOVE = [
672
+ "--field-note-padding-top",
673
+ "--field-note-padding",
674
+ "--control-inner-padding",
675
+ "--control-side-padding",
676
+ "--field-side-padding",
677
+ "--date-popup-shadow"
678
+ ];
679
+ FORM_TOKEN_DEPRECATED = [
680
+ "--field-note-padding",
681
+ "--field-note-padding-top",
682
+ "--control-inner-padding",
683
+ "--control-side-padding",
684
+ "--field-side-padding",
685
+ "--date-popup-shadow"
686
+ ];
687
+ DEPRECATED_VAR_RE = new RegExp(`var\\(\\s*(?:${FORM_TOKEN_DEPRECATED.map((n) => n.replace(/-/g, "\\-")).join("|")})\\s*`, "i");
688
+ DEPRECATED_DECL_RE = new RegExp(`(?:${FORM_TOKEN_DEPRECATED.map((n) => n.replace(/-/g, "\\-")).join("|")})\\s*:`);
689
+ });
690
+
691
+ // src/lib/migrate/front.ts
692
+ var exports_front = {};
693
+ __export(exports_front, {
694
+ migrateFrontAll: () => migrateFrontAll,
695
+ FRONT_MIGRATIONS: () => FRONT_MIGRATIONS,
696
+ FRONT_MIGRATE_ORDER: () => FRONT_MIGRATE_ORDER
697
+ });
698
+ async function migrateFrontAll(cwd, dry) {
699
+ const all = [];
700
+ for (const name of FRONT_MIGRATE_ORDER) {
701
+ const part = await FRONT_MIGRATIONS[name](cwd, dry);
702
+ for (const c2 of part) {
703
+ all.push({ file: c2.file, detail: `[${name}] ${c2.detail}` });
704
+ }
705
+ }
706
+ return all;
707
+ }
708
+ var FRONT_MIGRATIONS, FRONT_MIGRATE_ORDER;
709
+ var init_front = __esm(() => {
710
+ init_socket_server();
711
+ init_size_sml();
712
+ init_field_border();
713
+ init_modal_create();
714
+ init_legacy_hacks();
715
+ init_form_aliases();
716
+ init_form_tokens();
717
+ FRONT_MIGRATIONS = {
718
+ "size-sml": migrateSizeSml,
719
+ "field-border": migrateFieldBorder,
720
+ "modal-create": migrateModalCreate,
721
+ "form-aliases": migrateFormAliases,
722
+ "form-tokens": migrateFormTokens,
723
+ "legacy-hacks": migrateLegacyReactHack,
724
+ "socket-server": migrateSocketServer
725
+ };
726
+ FRONT_MIGRATE_ORDER = [
727
+ "form-aliases",
728
+ "form-tokens",
729
+ "size-sml",
730
+ "field-border",
731
+ "modal-create",
732
+ "socket-server"
733
+ ];
734
+ });
735
+
736
+ // src/index.ts
737
+ import { readFileSync as readFileSync19 } from "fs";
738
+ import { dirname as dirname4, join as join18 } from "path";
739
+ import { fileURLToPath as fileURLToPath2 } from "url";
740
+
741
+ // src/parseArgs.ts
742
+ function parseArgs(argv = process.argv.slice(2)) {
743
+ const options = {};
744
+ const flags = [];
745
+ const positionals = [];
746
+ for (let i = 0;i < argv.length; i++) {
747
+ const arg = argv[i];
748
+ if (arg === "--") {
749
+ positionals.push(...argv.slice(i + 1));
750
+ break;
751
+ }
752
+ if (arg.startsWith("--")) {
753
+ const body = arg.slice(2);
754
+ if (body.includes("=")) {
755
+ const eq = body.indexOf("=");
756
+ const key = body.slice(0, eq);
757
+ const value = body.slice(eq + 1);
758
+ if (key)
759
+ options[key] = value;
760
+ continue;
761
+ }
762
+ const next = argv[i + 1];
763
+ if (next && !next.startsWith("-")) {
764
+ const valueOpts = new Set([
765
+ "template",
766
+ "url",
767
+ "name",
768
+ "dir",
769
+ "folder",
770
+ "out"
771
+ ]);
772
+ if (valueOpts.has(body)) {
773
+ options[body] = next;
774
+ i++;
775
+ continue;
776
+ }
777
+ }
778
+ if (body && !flags.includes(body))
779
+ flags.push(body);
780
+ continue;
781
+ }
782
+ if (arg.startsWith("-") && arg.length > 1) {
783
+ const short = arg.slice(1);
784
+ if (short === "t" && argv[i + 1] && !argv[i + 1].startsWith("-")) {
785
+ options.template = argv[i + 1];
786
+ i++;
787
+ continue;
788
+ }
789
+ if (short && !flags.includes(short))
790
+ flags.push(short);
791
+ continue;
792
+ }
793
+ positionals.push(arg);
794
+ }
795
+ const [command = "", ...rest] = positionals;
796
+ return {
797
+ command,
798
+ positionals: rest,
799
+ options,
800
+ flags
801
+ };
802
+ }
803
+ function hasFlag(parsed, ...names) {
804
+ return names.some((n) => parsed.flags.includes(n));
805
+ }
806
+
807
+ // src/lib/elcrmDeps.ts
808
+ var SECTIONS = [
809
+ "dependencies",
810
+ "devDependencies",
811
+ "peerDependencies"
812
+ ];
813
+ function listElcrmDeps(pkg) {
814
+ const out = [];
815
+ for (const section of SECTIONS) {
816
+ const map = pkg[section];
817
+ if (!map || typeof map !== "object")
818
+ continue;
819
+ for (const [name, version] of Object.entries(map)) {
820
+ if (name.startsWith("@elcrm/") && typeof version === "string") {
821
+ out.push({ name, version, section });
822
+ }
823
+ }
824
+ }
825
+ return out;
826
+ }
827
+ function uniqueElcrmNames(deps) {
828
+ return [...new Set(deps.map((d) => d.name))].sort();
829
+ }
830
+
831
+ // src/commands/update.ts
832
+ init_latest();
833
+
834
+ // src/lib/install.ts
835
+ import { existsSync } from "fs";
836
+ import { join } from "path";
837
+ import { spawnSync } from "child_process";
838
+ function detectPackageManager(cwd = process.cwd()) {
839
+ if (existsSync(join(cwd, "bun.lockb")) || existsSync(join(cwd, "bun.lock"))) {
840
+ return "bun";
841
+ }
842
+ const bun = spawnSync("bun", ["--version"], { encoding: "utf8" });
843
+ if (bun.status === 0)
844
+ return "bun";
845
+ return "npm";
846
+ }
847
+ function installDeps(cwd = process.cwd()) {
848
+ const pm = detectPackageManager(cwd);
849
+ console.log(`\u0423\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0435\u0439 (${pm} install)\u2026`);
850
+ const r = spawnSync(pm, ["install"], {
851
+ cwd,
852
+ stdio: "inherit",
853
+ env: process.env
854
+ });
855
+ return r.status === 0;
856
+ }
857
+
858
+ // src/commands/update.ts
859
+ init_pkg();
860
+
861
+ // src/lib/ansi.ts
862
+ var enabled = process.env.NO_COLOR == null && process.env.FORCE_COLOR !== "0" && Boolean(process.stdout.isTTY);
863
+ function wrap(code, text) {
864
+ if (!enabled)
865
+ return text;
866
+ return `\x1B[${code}m${text}\x1B[0m`;
867
+ }
868
+ var c = {
869
+ gray: (t) => wrap("90", t),
870
+ green: (t) => wrap("32", t),
871
+ blue: (t) => wrap("34", t),
872
+ yellow: (t) => wrap("33", t),
873
+ red: (t) => wrap("31", t),
874
+ cyan: (t) => wrap("36", t),
875
+ dim: (t) => wrap("2", t)
876
+ };
877
+
878
+ // src/commands/update.ts
879
+ init_front();
880
+
881
+ // src/lib/cssSync.ts
882
+ init_form_tokens();
883
+ import {
884
+ existsSync as existsSync6,
885
+ readdirSync as readdirSync3,
886
+ readFileSync as readFileSync10,
887
+ statSync as statSync3,
888
+ writeFileSync as writeFileSync4
889
+ } from "fs";
890
+ import { createRequire } from "module";
891
+ import { dirname, join as join6 } from "path";
892
+ var COLOR_VALUE = /#|rgba?\(|hsla?\(|oklch|oklab|color-mix|lab\(|lch\(|hwb\(/i;
893
+ var COLOR_NAME = /(?:^|-)(bg|fg|color|fill|track|thumb|overlay|shadow|border|ring|background|placeholder)(?:-|$)/i;
894
+ var GEOM_VALUE = /(?:^|\s)(\d*\.?\d+)(px|em|rem|%|vh|vw|dvh|ch|ex|s|ms|deg|fr|pt)\b|^[0-9.]+$|ease|linear|none/i;
895
+ function stripComments(css) {
896
+ return css.replace(/\/\*[\s\S]*?\*\//g, "");
897
+ }
898
+ function parseCustomProps(body) {
899
+ const out = new Map;
900
+ const src = stripComments(body);
901
+ const re = /(--[a-zA-Z0-9-]+)\s*:/g;
902
+ let m;
903
+ while (m = re.exec(src)) {
904
+ let depth = 0;
905
+ let end = re.lastIndex;
906
+ while (end < src.length) {
907
+ const ch = src[end];
908
+ if (ch === "(")
909
+ depth++;
910
+ else if (ch === ")")
911
+ depth--;
912
+ else if ((ch === ";" || ch === "}") && depth <= 0)
913
+ break;
914
+ end++;
915
+ }
916
+ const value = src.slice(re.lastIndex, end).trim().replace(/\s+/g, " ");
917
+ if (value)
918
+ out.set(m[1], value);
919
+ re.lastIndex = end;
920
+ }
921
+ return out;
922
+ }
923
+ function walkRules(css, fn) {
924
+ const s = stripComments(css);
925
+ let i = 0;
926
+ while (i < s.length) {
927
+ const open = s.indexOf("{", i);
928
+ if (open < 0)
929
+ break;
930
+ const sel = s.slice(i, open).trim();
931
+ let depth = 1;
932
+ let j = open + 1;
933
+ while (j < s.length && depth > 0) {
934
+ if (s[j] === "{")
935
+ depth++;
936
+ else if (s[j] === "}")
937
+ depth--;
938
+ j++;
939
+ }
940
+ const body = s.slice(open + 1, j - 1);
941
+ if (sel.startsWith("@"))
942
+ walkRules(body, fn);
943
+ else
944
+ fn(sel, body);
945
+ i = j;
946
+ }
947
+ }
948
+ function fileKind(file) {
949
+ const base = file.replace(/\\/g, "/").toLowerCase();
950
+ if (base.endsWith("light.css") || /\/light\.css$/.test(base))
951
+ return "light";
952
+ if (base.endsWith("dark.css") || /\/dark\.css$/.test(base))
953
+ return "dark";
954
+ if (base.endsWith("tokens.css") || base.endsWith("theme.css") || /\/tokens\.css$/.test(base)) {
955
+ return "geometry";
956
+ }
957
+ return "mixed";
958
+ }
959
+ function selectorBucket(sel) {
960
+ const s = sel.toLowerCase();
961
+ if (/data-theme\s*=\s*["']?light/.test(s))
962
+ return "light";
963
+ if (/data-theme\s*=\s*["']?dark/.test(s))
964
+ return "dark";
965
+ if (/(^|,)\s*:root\b/.test(s) || /(^|,)\s*html\b/.test(s))
966
+ return "geometry";
967
+ return null;
968
+ }
969
+ function isColorToken(name, value) {
970
+ if (COLOR_VALUE.test(value))
971
+ return true;
972
+ if (COLOR_NAME.test(name) && /transparent|currentcolor|var\(|\bnone\b/i.test(value)) {
973
+ return true;
974
+ }
975
+ if (COLOR_NAME.test(name) && !GEOM_VALUE.test(value))
976
+ return true;
977
+ return false;
978
+ }
979
+ function listCssFiles(dir, acc = []) {
980
+ if (!existsSync6(dir))
981
+ return acc;
982
+ for (const name of readdirSync3(dir)) {
983
+ if (name === "node_modules")
984
+ continue;
985
+ const full = join6(dir, name);
986
+ let st;
987
+ try {
988
+ st = statSync3(full);
989
+ } catch {
990
+ continue;
991
+ }
992
+ if (st.isDirectory())
993
+ listCssFiles(full, acc);
994
+ else if (name.endsWith(".css"))
995
+ acc.push(full);
996
+ }
997
+ return acc;
998
+ }
999
+ function resolvePkgRoot(pkgName, cwd) {
1000
+ try {
1001
+ const req = createRequire(join6(cwd, "package.json"));
1002
+ const pkgJson = req.resolve(`${pkgName}/package.json`);
1003
+ return dirname(pkgJson);
1004
+ } catch {
1005
+ const fallback = join6(cwd, "node_modules", ...pkgName.split("/"));
1006
+ return existsSync6(join6(fallback, "package.json")) ? fallback : null;
1007
+ }
1008
+ }
1009
+ function readPkgJson(dir) {
1010
+ try {
1011
+ return JSON.parse(readFileSync10(join6(dir, "package.json"), "utf8"));
1012
+ } catch {
1013
+ return null;
1014
+ }
1015
+ }
1016
+ function exportPath(exportsMap, key) {
1017
+ if (!exportsMap || typeof exportsMap !== "object")
1018
+ return;
1019
+ const ent = exportsMap[key];
1020
+ if (typeof ent === "string")
1021
+ return ent;
1022
+ if (ent && typeof ent === "object") {
1023
+ const o = ent;
1024
+ const v = o.import ?? o.default ?? o.require;
1025
+ return typeof v === "string" ? v : undefined;
1026
+ }
1027
+ return;
1028
+ }
1029
+ function inspectPkgCss(pkgName, cwd) {
1030
+ const root = resolvePkgRoot(pkgName, cwd);
1031
+ if (!root)
1032
+ return null;
1033
+ const pkg = readPkgJson(root);
1034
+ const exp = pkg?.exports;
1035
+ const styleRel = exportPath(exp, "./style.css");
1036
+ const lightRel = exportPath(exp, "./light.css");
1037
+ const darkRel = exportPath(exp, "./dark.css");
1038
+ const abs = (rel2) => rel2 ? join6(root, rel2.replace(/^\.\//, "")) : undefined;
1039
+ return {
1040
+ name: pkgName,
1041
+ root,
1042
+ style: abs(styleRel) && existsSync6(abs(styleRel)) ? abs(styleRel) : undefined,
1043
+ light: abs(lightRel) && existsSync6(abs(lightRel)) ? abs(lightRel) : undefined,
1044
+ dark: abs(darkRel) && existsSync6(abs(darkRel)) ? abs(darkRel) : undefined
1045
+ };
1046
+ }
1047
+ function addDef(acc, seen, pkg, bucket, name, value) {
1048
+ const key = `${bucket}:${name}`;
1049
+ if (seen.has(key))
1050
+ return;
1051
+ seen.add(key);
1052
+ acc.push({ name, value, bucket, pkg });
1053
+ }
1054
+ function collectPkgTokens(pkgName, cwd) {
1055
+ const root = resolvePkgRoot(pkgName, cwd);
1056
+ if (!root)
1057
+ return [];
1058
+ const files = [
1059
+ ...listCssFiles(join6(root, "dist")),
1060
+ ...listCssFiles(join6(root, "src/lib/styles")),
1061
+ ...listCssFiles(join6(root, "src/style")),
1062
+ join6(root, "test/main.css")
1063
+ ].filter((f) => existsSync6(f));
1064
+ const acc = [];
1065
+ const seen = new Set;
1066
+ for (const file of files) {
1067
+ const css = readFileSync10(file, "utf8");
1068
+ const kind = fileKind(file);
1069
+ if (kind !== "mixed") {
1070
+ for (const [name, value] of parseCustomProps(css)) {
1071
+ const color = isColorToken(name, value);
1072
+ if (kind === "geometry") {
1073
+ if (color) {
1074
+ addDef(acc, seen, pkgName, "light", name, value);
1075
+ addDef(acc, seen, pkgName, "dark", name, value);
1076
+ } else {
1077
+ addDef(acc, seen, pkgName, "geometry", name, value);
1078
+ }
1079
+ continue;
1080
+ }
1081
+ addDef(acc, seen, pkgName, color ? kind : "geometry", name, value);
1082
+ }
1083
+ continue;
1084
+ }
1085
+ walkRules(css, (sel, body) => {
1086
+ const bucket = selectorBucket(sel);
1087
+ if (!bucket)
1088
+ return;
1089
+ for (const [name, value] of parseCustomProps(body)) {
1090
+ const color = isColorToken(name, value);
1091
+ if (bucket === "geometry") {
1092
+ if (color) {
1093
+ addDef(acc, seen, pkgName, "light", name, value);
1094
+ addDef(acc, seen, pkgName, "dark", name, value);
1095
+ } else {
1096
+ addDef(acc, seen, pkgName, "geometry", name, value);
1097
+ }
1098
+ continue;
1099
+ }
1100
+ addDef(acc, seen, pkgName, color ? bucket : "geometry", name, value);
1101
+ }
1102
+ });
1103
+ }
1104
+ return acc;
1105
+ }
1106
+ function existingTokenNames(css) {
1107
+ return new Set(parseCustomProps(css).keys());
1108
+ }
1109
+ function detectIndent(css) {
1110
+ const m = css.match(/\n([ \t]+)--[a-zA-Z]/);
1111
+ return m?.[1] ?? " ";
1112
+ }
1113
+ function prefixOf(name) {
1114
+ const parts = name.slice(2).split("-");
1115
+ return parts[0] || "misc";
1116
+ }
1117
+ function formatTokenBlock(defs, indent) {
1118
+ const byPrefix = new Map;
1119
+ for (const d of defs) {
1120
+ const p = prefixOf(d.name);
1121
+ const list = byPrefix.get(p) ?? [];
1122
+ list.push(d);
1123
+ byPrefix.set(p, list);
1124
+ }
1125
+ const prefixes = [...byPrefix.keys()].sort();
1126
+ const lines = [];
1127
+ for (const p of prefixes) {
1128
+ const list = byPrefix.get(p);
1129
+ const pkgs = [...new Set(list.map((x) => x.pkg))].sort();
1130
+ const label = pkgs.map((n) => n.replace(/^@elcrm\//, "")).join(", ");
1131
+ lines.push("");
1132
+ lines.push(`${indent}/* \u2014\u2014 ${p} (${label}) \u2014\u2014 */`);
1133
+ list.sort((a, b) => a.name.localeCompare(b.name));
1134
+ for (const d of list) {
1135
+ lines.push(`${indent}${d.name}: ${d.value};`);
1136
+ }
1137
+ }
1138
+ lines.push("");
1139
+ return lines.join(`
1140
+ `);
1141
+ }
1142
+ function insertIntoRule(css, selectorTest, snippet) {
1143
+ const s = css;
1144
+ let i = 0;
1145
+ while (i < s.length) {
1146
+ const open = s.indexOf("{", i);
1147
+ if (open < 0)
1148
+ break;
1149
+ const sel = stripComments(s.slice(i, open)).trim();
1150
+ let depth = 1;
1151
+ let j = open + 1;
1152
+ while (j < s.length && depth > 0) {
1153
+ if (s[j] === "{")
1154
+ depth++;
1155
+ else if (s[j] === "}")
1156
+ depth--;
1157
+ j++;
1158
+ }
1159
+ if (selectorTest(sel)) {
1160
+ const before = s.slice(0, j - 1).replace(/\s*$/, "");
1161
+ const after = s.slice(j - 1);
1162
+ return `${before}
1163
+ ${snippet}${after}`;
1164
+ }
1165
+ i = j;
1166
+ }
1167
+ return null;
1168
+ }
1169
+ function findStyleDir(cwd) {
1170
+ for (const d of [join6(cwd, "src/style"), join6(cwd, "web/src/style")]) {
1171
+ if (existsSync6(join6(d, "theme-light.css")) || existsSync6(join6(d, "elcrm.css")) || existsSync6(join6(d, "theme.css"))) {
1172
+ return d;
1173
+ }
1174
+ }
1175
+ return null;
1176
+ }
1177
+ function ensureImport(css, spec) {
1178
+ const needle = `@import "${spec}"`;
1179
+ const alt = `@import '${spec}'`;
1180
+ if (css.includes(needle) || css.includes(alt)) {
1181
+ return { css, added: false };
1182
+ }
1183
+ const line = `${needle};`;
1184
+ const imports = [...css.matchAll(/@import[^;]+;/g)];
1185
+ if (imports.length) {
1186
+ const themeIdx = imports.findIndex((m) => /theme\.css/.test(m[0]));
1187
+ const anchor = themeIdx >= 0 ? imports[themeIdx] : imports[imports.length - 1];
1188
+ const idx = themeIdx >= 0 ? anchor.index : anchor.index + anchor[0].length;
1189
+ const pad = themeIdx >= 0 ? line + `
1190
+ ` : `
1191
+ ` + line;
1192
+ return {
1193
+ css: css.slice(0, idx) + pad + css.slice(idx),
1194
+ added: true
1195
+ };
1196
+ }
1197
+ return { css: line + `
1198
+ ` + css, added: true };
1199
+ }
1200
+ function syncElcrmCss(options) {
1201
+ const styleDir = findStyleDir(options.cwd);
1202
+ if (!styleDir) {
1203
+ throw new Error("\u041D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D src/style (theme.css / theme-light.css / elcrm.css).");
1204
+ }
1205
+ const paths = {
1206
+ geometry: join6(styleDir, "theme.css"),
1207
+ light: join6(styleDir, "theme-light.css"),
1208
+ dark: join6(styleDir, "theme-dark.css"),
1209
+ elcrm: join6(styleDir, "elcrm.css")
1210
+ };
1211
+ const allDefs = [];
1212
+ const skippedPkgs = [];
1213
+ const cssExports = [];
1214
+ for (const name of options.pkgNames) {
1215
+ const exp = inspectPkgCss(name, options.cwd);
1216
+ if (exp)
1217
+ cssExports.push(exp);
1218
+ const defs = collectPkgTokens(name, options.cwd);
1219
+ if (defs.length === 0)
1220
+ skippedPkgs.push(name);
1221
+ allDefs.push(...defs);
1222
+ }
1223
+ const added = {
1224
+ "theme.css": [],
1225
+ "theme-light.css": [],
1226
+ "theme-dark.css": []
1227
+ };
1228
+ const imports = [];
1229
+ const fileFor = (bucket) => bucket === "geometry" ? paths.geometry : bucket === "light" ? paths.light : paths.dark;
1230
+ const labelFor = (bucket) => bucket === "geometry" ? "theme.css" : bucket === "light" ? "theme-light.css" : "theme-dark.css";
1231
+ for (const bucket of ["geometry", "light", "dark"]) {
1232
+ const file = fileFor(bucket);
1233
+ if (!existsSync6(file))
1234
+ continue;
1235
+ let css = readFileSync10(file, "utf8");
1236
+ const have = existingTokenNames(css);
1237
+ const missing = allDefs.filter((d) => d.bucket === bucket && !have.has(d.name) && !FORM_TOKEN_DEPRECATED.includes(d.name));
1238
+ const uniq = [];
1239
+ const seen = new Set;
1240
+ for (const d of missing) {
1241
+ if (seen.has(d.name))
1242
+ continue;
1243
+ seen.add(d.name);
1244
+ uniq.push(d);
1245
+ }
1246
+ if (!uniq.length)
1247
+ continue;
1248
+ const indent = detectIndent(css);
1249
+ const snippet = formatTokenBlock(uniq, indent);
1250
+ const test = bucket === "geometry" ? (sel) => /(^|,)\s*:root\b/.test(sel) : bucket === "light" ? (sel) => /data-theme\s*=\s*["']?light/.test(sel.toLowerCase()) : (sel) => /data-theme\s*=\s*["']?dark/.test(sel.toLowerCase());
1251
+ const next = insertIntoRule(css, test, snippet);
1252
+ if (!next) {
1253
+ throw new Error(`\u041D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u0431\u043B\u043E\u043A \u0441\u0435\u043B\u0435\u043A\u0442\u043E\u0440\u0430 \u0432 ${labelFor(bucket)}`);
1254
+ }
1255
+ added[labelFor(bucket)] = uniq.map((d) => d.name);
1256
+ if (!options.dry)
1257
+ writeFileSync4(file, next, "utf8");
1258
+ }
1259
+ if (existsSync6(paths.elcrm)) {
1260
+ let css = readFileSync10(paths.elcrm, "utf8");
1261
+ for (const exp of cssExports) {
1262
+ const specs = [];
1263
+ if (exp.light)
1264
+ specs.push(`${exp.name}/light.css`);
1265
+ if (exp.dark)
1266
+ specs.push(`${exp.name}/dark.css`);
1267
+ const tokens = join6(exp.root, "dist/tokens.css");
1268
+ if (existsSync6(tokens))
1269
+ specs.push(`${exp.name}/tokens.css`);
1270
+ for (const spec of specs) {
1271
+ const r = ensureImport(css, spec);
1272
+ css = r.css;
1273
+ if (r.added)
1274
+ imports.push(spec);
1275
+ }
1276
+ }
1277
+ if (imports.length && !options.dry)
1278
+ writeFileSync4(paths.elcrm, css, "utf8");
1279
+ }
1280
+ return { styleDir, added, imports, skippedPkgs };
1281
+ }
1282
+
1283
+ // src/commands/test.ts
1284
+ import { resolve } from "path";
1285
+
1286
+ // src/lib/test/detect.ts
1287
+ import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync11, statSync as statSync4 } from "fs";
1288
+ import { join as join7 } from "path";
1289
+ function hasElcrmServer(pkg) {
1290
+ for (const section of [
1291
+ "dependencies",
1292
+ "devDependencies",
1293
+ "peerDependencies"
1294
+ ]) {
1295
+ const map = pkg[section];
1296
+ if (map && typeof map === "object" && "@elcrm/server" in map)
1297
+ return true;
1298
+ }
1299
+ return false;
1300
+ }
1301
+ function detectTestMode(pkg, force) {
1302
+ if (force === "server" || force === "front")
1303
+ return force;
1304
+ return hasElcrmServer(pkg) ? "server" : "front";
1305
+ }
1306
+ function listWorkspaceDirs(cwd, pkg) {
1307
+ const raw = pkg.workspaces;
1308
+ let patterns = [];
1309
+ if (Array.isArray(raw)) {
1310
+ patterns = raw.filter((p) => typeof p === "string");
1311
+ } else if (raw && typeof raw === "object" && Array.isArray(raw.packages)) {
1312
+ patterns = raw.packages.filter((p) => typeof p === "string");
1313
+ }
1314
+ if (!patterns.length)
1315
+ return [];
1316
+ const out = [];
1317
+ for (const pat of patterns) {
1318
+ if (!pat.includes("*")) {
1319
+ const dir = join7(cwd, pat);
1320
+ if (existsSync7(join7(dir, "package.json")))
1321
+ out.push(dir);
1322
+ continue;
1323
+ }
1324
+ const base = pat.replace(/\/\*$/, "").replace(/\*$/, "");
1325
+ const parent = join7(cwd, base);
1326
+ if (!existsSync7(parent) || !statSync4(parent).isDirectory())
1327
+ continue;
1328
+ for (const name of readdirSync4(parent)) {
1329
+ const dir = join7(parent, name);
1330
+ if (statSync4(dir).isDirectory() && existsSync7(join7(dir, "package.json"))) {
1331
+ out.push(dir);
1332
+ }
1333
+ }
1334
+ }
1335
+ return out;
1336
+ }
1337
+ function readPkgAt(dir) {
1338
+ const path = join7(dir, "package.json");
1339
+ if (!existsSync7(path))
1340
+ return null;
1341
+ try {
1342
+ return JSON.parse(readFileSync11(path, "utf8"));
928
1343
  } catch {
929
1344
  return null;
930
1345
  }
931
1346
  }
932
1347
 
1348
+ // src/lib/test/front.ts
1349
+ import { existsSync as existsSync8, readdirSync as readdirSync5, readFileSync as readFileSync12, statSync as statSync5 } from "fs";
1350
+ import { join as join8, relative as relative2 } from "path";
1351
+ init_form_tokens();
1352
+ var SKIP = new Set(["node_modules", "dist", ".git", "coverage", ".bun"]);
1353
+ var DEPRECATED_SOCKET_IMPORT_RE = /(?:from\s+|require\s*\(\s*)["']@elcrm\/(?:socket\/server|socket-server)["']/;
1354
+ var THEMES_CSS_IMPORT_RE = /(?:from\s+|import\s+)["']@elcrm\/[^"']+\/themes\.css["']|import\s+["']@elcrm\/[^"']+\/themes\.css["']/;
1355
+ var LEGACY_PASSKEY_IMPORT_RE = /(?:from\s+|require\s*\(\s*)["']@elcrm\/passkey["']/;
1356
+ var LEGACY_PASSKEY_SYMBOL_RE = /\b(?:createPasskey|authenticateWithPasskey)\b/;
1357
+ function walkCode(dir, out) {
1358
+ if (!existsSync8(dir))
1359
+ return;
1360
+ for (const name of readdirSync5(dir)) {
1361
+ if (SKIP.has(name))
1362
+ continue;
1363
+ const full = join8(dir, name);
1364
+ let st;
1365
+ try {
1366
+ st = statSync5(full);
1367
+ } catch {
1368
+ continue;
1369
+ }
1370
+ if (st.isDirectory())
1371
+ walkCode(full, out);
1372
+ else if (/\.(tsx?|jsx?|mjs|cjs|css)$/.test(name))
1373
+ out.push(full);
1374
+ }
1375
+ }
1376
+ function collectFrontIssues(cwd, pkg) {
1377
+ const issues = [];
1378
+ const isLib = Boolean(pkg.name?.startsWith("@elcrm/"));
1379
+ const scripts = pkg.scripts ?? {};
1380
+ if (pkg.type !== "module") {
1381
+ issues.push({
1382
+ level: "warn",
1383
+ code: "type-module",
1384
+ message: '\u041D\u0435\u0442 "type": "module"',
1385
+ fixable: true
1386
+ });
1387
+ }
1388
+ if (!pkg.engines?.node && !pkg.engines?.bun) {
1389
+ issues.push({
1390
+ level: "warn",
1391
+ code: "engines",
1392
+ message: "\u041D\u0435\u0442 engines.node (\u043E\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044F >=18) \u0438\u043B\u0438 engines.bun",
1393
+ fixable: true
1394
+ });
1395
+ }
1396
+ if (isLib) {
1397
+ if (!scripts.build && !scripts["pack:check"]) {
1398
+ issues.push({
1399
+ level: "warn",
1400
+ code: "scripts",
1401
+ message: "\u0414\u043B\u044F \u0431\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0438 \u0436\u0435\u043B\u0430\u0442\u0435\u043B\u044C\u043D\u044B scripts.build \u0438/\u0438\u043B\u0438 pack:check"
1402
+ });
1403
+ }
1404
+ if (!scripts["pack:check"]) {
1405
+ issues.push({
1406
+ level: "info",
1407
+ code: "pack-check",
1408
+ message: "\u041D\u0435\u0442 scripts.pack:check",
1409
+ fixable: true
1410
+ });
1411
+ }
1412
+ if (existsSync8(join8(cwd, "src/lib")) && !scripts["check:css"]) {
1413
+ issues.push({
1414
+ level: "info",
1415
+ code: "check-css",
1416
+ message: "\u041D\u0435\u0442 scripts.check:css (css-scoped) \u2014 \u0440\u0435\u043A\u043E\u043C\u0435\u043D\u0434\u0443\u0435\u0442\u0441\u044F \u0434\u043B\u044F UI-lib"
1417
+ });
1418
+ }
1419
+ if (existsSync8(join8(cwd, "src/lib")) && !scripts["check:tokens"]) {
1420
+ issues.push({
1421
+ level: "info",
1422
+ code: "check-tokens",
1423
+ message: "\u041D\u0435\u0442 scripts.check:tokens \u2014 \u0440\u0435\u043A\u043E\u043C\u0435\u043D\u0434\u0443\u0435\u0442\u0441\u044F \u0434\u043B\u044F UI-lib \u0441 CSS Modules"
1424
+ });
1425
+ }
1426
+ }
1427
+ const deps = listElcrmDeps(pkg);
1428
+ if (deps.some((d) => d.name === "@elcrm/socket-server")) {
1429
+ issues.push({
1430
+ level: "error",
1431
+ code: "deprecated-socket-server",
1432
+ message: "\u0417\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u044C @elcrm/socket-server \u0443\u0441\u0442\u0430\u0440\u0435\u043B\u0430 \u2014 elcrm migrate socket-server",
1433
+ fixable: true
1434
+ });
1435
+ }
1436
+ for (const name of ["@elcrm/db", "@elcrm/jwt", "@elcrm/cron", "@elcrm/tb"]) {
1437
+ if (deps.some((d) => d.name === name)) {
1438
+ issues.push({
1439
+ level: "error",
1440
+ code: `pkg-${name}`,
1441
+ message: `${name} \u2014 \u0441\u0435\u0440\u0432\u0435\u0440\u043D\u044B\u0439 \u043F\u0430\u043A\u0435\u0442; \u043D\u0430 \u0444\u0440\u043E\u043D\u0442\u0435 \u043D\u0435 \u043D\u0443\u0436\u0435\u043D (server: @elcrm/server/\u2026)`
1442
+ });
1443
+ }
1444
+ }
1445
+ const umbrella = pkg.dependencies?.elcrm || pkg.devDependencies?.elcrm;
1446
+ if (umbrella && /^[\^~]?0\.9\./.test(umbrella)) {
1447
+ issues.push({
1448
+ level: "warn",
1449
+ code: "legacy-umbrella",
1450
+ message: `\u0423\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0438\u0439 \u043F\u0430\u043A\u0435\u0442 elcrm@${umbrella} \u2014 \u043F\u0435\u0440\u0435\u0445\u043E\u0434\u0438\u0442\u0435 \u043D\u0430 @elcrm/* / @elcrm/core`
1451
+ });
1452
+ }
1453
+ if (deps.length === 0 && !isLib) {
1454
+ issues.push({
1455
+ level: "info",
1456
+ code: "no-elcrm",
1457
+ message: "\u041D\u0435\u0442 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0435\u0439 @elcrm/*"
1458
+ });
1459
+ }
1460
+ const files = [];
1461
+ walkCode(join8(cwd, "src"), files);
1462
+ walkCode(join8(cwd, "style"), files);
1463
+ for (const name of ["bootstrap.tsx", "bootstrap.ts", "main.tsx", "main.ts", "index.tsx", "index.ts", "App.tsx"]) {
1464
+ const full = join8(cwd, name);
1465
+ if (existsSync8(full))
1466
+ files.push(full);
1467
+ const inSrc = join8(cwd, "src", name);
1468
+ if (existsSync8(inSrc) && !files.includes(inSrc))
1469
+ files.push(inSrc);
1470
+ }
1471
+ for (const file of files) {
1472
+ const text = readFileSync12(file, "utf8");
1473
+ const rel2 = relative2(cwd, file);
1474
+ if (DEPRECATED_SOCKET_IMPORT_RE.test(text)) {
1475
+ issues.push({
1476
+ level: "error",
1477
+ code: "deprecated-import",
1478
+ message: "\u0438\u043C\u043F\u043E\u0440\u0442 @elcrm/socket/server \u0438\u043B\u0438 socket-server",
1479
+ file: rel2,
1480
+ fixable: true
1481
+ });
1482
+ }
1483
+ const themesRe = new RegExp(THEMES_CSS_IMPORT_RE.source, "g");
1484
+ let tm;
1485
+ while (tm = themesRe.exec(text)) {
1486
+ const line = text.slice(0, tm.index).split(/\r?\n/).length;
1487
+ issues.push({
1488
+ level: "error",
1489
+ code: "themes-css",
1490
+ 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)",
1491
+ file: rel2,
1492
+ line
1493
+ });
1494
+ }
1495
+ if (LEGACY_PASSKEY_IMPORT_RE.test(text) && LEGACY_PASSKEY_SYMBOL_RE.test(text)) {
1496
+ const lines2 = text.split(/\r?\n/);
1497
+ lines2.forEach((line, i) => {
1498
+ if (/\bcreatePasskey\b/.test(line) || /\bauthenticateWithPasskey\b/.test(line)) {
1499
+ if (pkg.name === "@elcrm/passkey")
1500
+ return;
1501
+ issues.push({
1502
+ level: "error",
1503
+ code: "legacy-passkey-api",
1504
+ message: "createPasskey / authenticateWithPasskey \u043D\u0435 \u044D\u043A\u0441\u043F\u043E\u0440\u0442\u0438\u0440\u0443\u044E\u0442\u0441\u044F \u2014 createPasskeyClient",
1505
+ file: rel2,
1506
+ line: i + 1
1507
+ });
1508
+ }
1509
+ });
1510
+ }
1511
+ const lines = text.split(/\r?\n/);
1512
+ lines.forEach((line, i) => {
1513
+ const trimmed = line.trim();
1514
+ if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) {
1515
+ return;
1516
+ }
1517
+ if (/<(?:input|textarea|select)\b/.test(line)) {
1518
+ if (pkg.name === "@elcrm/form")
1519
+ return;
1520
+ issues.push({
1521
+ level: "error",
1522
+ code: "native-html-input",
1523
+ message: "\u043D\u0430\u0442\u0438\u0432\u043D\u044B\u0439 <input|textarea|select> \u2014 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439\u0442\u0435 @elcrm/form (StringField, Select, \u2026)",
1524
+ file: rel2,
1525
+ line: i + 1
1526
+ });
1527
+ }
1528
+ if (/(?:^|\s)native(?:\s|$|=|\/|>)/.test(line) && !/\bnavigator\b|\bnativeEvent\b|\bcreateNative/.test(line)) {
1529
+ if (pkg.name === "@elcrm/form")
1530
+ return;
1531
+ issues.push({
1532
+ level: "warn",
1533
+ code: "form-native-prop",
1534
+ message: "prop native \u0443 \u043F\u043E\u043B\u044F \u0444\u043E\u0440\u043C\u044B \u2014 \u0443\u0431\u0435\u0440\u0438\u0442\u0435; \u0441\u0442\u0438\u043B\u0438 \u0447\u0435\u0440\u0435\u0437 \u0442\u043E\u043A\u0435\u043D\u044B @elcrm/form",
1535
+ file: rel2,
1536
+ line: i + 1
1537
+ });
1538
+ }
1539
+ if (/size\s*=\s*["'](?:sm|md)["']/.test(line) && !/titleSize/.test(line)) {
1540
+ issues.push({
1541
+ level: "warn",
1542
+ code: "size-legacy",
1543
+ message: 'size="sm"|"md" \u0443\u0441\u0442\u0430\u0440\u0435\u043B\u043E \u2014 "s"|"m" (elcrm migrate size-sml)',
1544
+ file: rel2,
1545
+ line: i + 1,
1546
+ fixable: true
1547
+ });
1548
+ }
1549
+ if (/Modal\.Add\s*\(/.test(line)) {
1550
+ issues.push({
1551
+ level: "warn",
1552
+ code: "modal-add",
1553
+ message: "Modal.Add \u2014 Modal.Create + Open (elcrm migrate modal-create)",
1554
+ file: rel2,
1555
+ line: i + 1,
1556
+ fixable: true
1557
+ });
1558
+ }
1559
+ if (/--field-border-(?:focus|hover|error|disabled)\s*:\s*\d/.test(line) && /solid|dashed|dotted/i.test(line)) {
1560
+ issues.push({
1561
+ level: "error",
1562
+ code: "field-border-shorthand",
1563
+ message: "field-border hover/focus/disabled \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u0446\u0432\u0435\u0442 (elcrm migrate field-border)",
1564
+ file: rel2,
1565
+ line: i + 1,
1566
+ fixable: true
1567
+ });
1568
+ }
1569
+ if (lineHasDeprecatedFormToken(line)) {
1570
+ issues.push({
1571
+ level: "warn",
1572
+ code: "form-token-legacy",
1573
+ message: "legacy padding/shadow-\u0442\u043E\u043A\u0435\u043D \u2192 --field-padding* / --popup-shadow (elcrm migrate form-tokens)",
1574
+ file: rel2,
1575
+ line: i + 1,
1576
+ fixable: true
1577
+ });
1578
+ }
1579
+ });
1580
+ }
1581
+ return issues;
1582
+ }
1583
+ async function runPkgScript(cwd, script) {
1584
+ const pkg = JSON.parse(readFileSync12(join8(cwd, "package.json"), "utf8"));
1585
+ if (!pkg.scripts?.[script])
1586
+ return { ran: false, ok: true };
1587
+ console.log(`\u2192 bun run ${script}`);
1588
+ const proc = Bun.spawn(["bun", "run", script], {
1589
+ cwd,
1590
+ stdout: "inherit",
1591
+ stderr: "inherit"
1592
+ });
1593
+ const code = await proc.exited;
1594
+ return { ran: true, ok: code === 0 };
1595
+ }
1596
+ async function runFrontScripts(cwd) {
1597
+ let failed = 0;
1598
+ for (const name of ["check:css", "check:tokens", "test"]) {
1599
+ const r = await runPkgScript(cwd, name);
1600
+ if (r.ran && !r.ok)
1601
+ failed++;
1602
+ }
1603
+ return failed;
1604
+ }
1605
+
1606
+ // src/lib/test/print.ts
1607
+ function printIssues(issues, title) {
1608
+ if (title)
1609
+ console.log(title);
1610
+ if (issues.length === 0) {
1611
+ console.log(c.green("ok \u2014 \u0437\u0430\u043C\u0435\u0447\u0430\u043D\u0438\u0439 \u043D\u0435\u0442"));
1612
+ return 0;
1613
+ }
1614
+ let errors = 0;
1615
+ for (const issue of issues) {
1616
+ const mark = issue.level === "error" ? "E" : issue.level === "warn" ? "W" : "I";
1617
+ if (issue.level === "error")
1618
+ errors++;
1619
+ const loc = issue.file != null ? ` ${issue.file}${issue.line != null ? `:${issue.line}` : ""}` : "";
1620
+ const color = issue.level === "error" ? c.yellow : issue.level === "warn" ? c.yellow : c.gray;
1621
+ console.log(color(`[${mark}] ${issue.code}${loc}: ${issue.message}`));
1622
+ }
1623
+ return errors;
1624
+ }
1625
+
933
1626
  // src/lib/test/serverScan.ts
934
- import { existsSync as existsSync8, readdirSync as readdirSync5, readFileSync as readFileSync6, statSync as statSync5 } from "fs";
935
- import { join as join8, relative as relative2 } from "path";
1627
+ import { existsSync as existsSync9, readdirSync as readdirSync6, readFileSync as readFileSync13, statSync as statSync6 } from "fs";
1628
+ import { join as join9, relative as relative3 } from "path";
936
1629
 
937
1630
  // src/lib/test/serverRules.ts
938
1631
  var CODE_RULES = [
@@ -1054,6 +1747,14 @@ var CODE_RULES = [
1054
1747
  }
1055
1748
  ]
1056
1749
  },
1750
+ {
1751
+ id: "manual-loadEnvAtStartup",
1752
+ severity: "error",
1753
+ test: /\bloadEnvAtStartup\s*\(/,
1754
+ message: "\u0440\u0443\u0447\u043D\u043E\u0439 loadEnvAtStartup() \u2014 \u043B\u0435\u0433\u0430\u0441\u0438; \u0434\u043E createServer \u2014 ensureEnvAutoLoaded()",
1755
+ fix: "await ensureEnvAutoLoaded() (\u043E\u0434\u0438\u043D \u0440\u0430\u0437 \u0432 start); createServer/mail \u0441\u0430\u043C\u0438 \u0434\u043E\u0433\u0440\u0443\u0436\u0430\u044E\u0442. \u041D\u0435 top-level await \u0432 \u043F\u0430\u043A\u0435\u0442\u0435 (bun build \u043B\u043E\u043C\u0430\u0435\u0442)",
1756
+ auto: false
1757
+ },
1057
1758
  {
1058
1759
  id: "createJwtFromEnv",
1059
1760
  severity: "warn",
@@ -1229,7 +1930,7 @@ var PKG_LEGACY = [
1229
1930
  ];
1230
1931
 
1231
1932
  // src/lib/test/serverScan.ts
1232
- var SKIP3 = new Set([
1933
+ var SKIP2 = new Set([
1233
1934
  "node_modules",
1234
1935
  "dist",
1235
1936
  ".git",
@@ -1238,141 +1939,581 @@ var SKIP3 = new Set([
1238
1939
  "vendor"
1239
1940
  ]);
1240
1941
  function walkFiles2(root, dir, out) {
1241
- if (!existsSync8(dir))
1942
+ if (!existsSync9(dir))
1943
+ return;
1944
+ for (const name of readdirSync6(dir)) {
1945
+ if (SKIP2.has(name))
1946
+ continue;
1947
+ const full = join9(dir, name);
1948
+ let st;
1949
+ try {
1950
+ st = statSync6(full);
1951
+ } catch {
1952
+ continue;
1953
+ }
1954
+ if (st.isDirectory())
1955
+ walkFiles2(root, full, out);
1956
+ else if (/\.(tsx?|jsx?|mjs|cjs)$/.test(name) || name === "package.json") {
1957
+ out.push(full);
1958
+ }
1959
+ }
1960
+ }
1961
+ function scanCodeFile(root, file, text) {
1962
+ const findings = [];
1963
+ const lines = text.split(/\r?\n/);
1964
+ const rel2 = relative3(root, file) || file;
1965
+ for (const rule of CODE_RULES) {
1966
+ lines.forEach((line, i) => {
1967
+ rule.test.lastIndex = 0;
1968
+ if (!rule.test.test(line))
1969
+ return;
1970
+ rule.test.lastIndex = 0;
1971
+ const m = line.match(rule.test);
1972
+ findings.push({
1973
+ ruleId: rule.id,
1974
+ severity: rule.severity,
1975
+ file: rel2,
1976
+ line: i + 1,
1977
+ match: (m?.[0] ?? line).trim().slice(0, 120),
1978
+ message: rule.message,
1979
+ fix: rule.fix,
1980
+ auto: rule.auto
1981
+ });
1982
+ });
1983
+ }
1984
+ return findings;
1985
+ }
1986
+ function scanPackageJson(root, file, text) {
1987
+ const findings = [];
1988
+ const rel2 = relative3(root, file) || file;
1989
+ let pkg;
1990
+ try {
1991
+ pkg = JSON.parse(text);
1992
+ } catch {
1993
+ return findings;
1994
+ }
1995
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1996
+ for (const name of PKG_LEGACY) {
1997
+ if (deps[name]) {
1998
+ findings.push({
1999
+ ruleId: `pkg-${name}`,
2000
+ severity: "error",
2001
+ file: rel2,
2002
+ line: 1,
2003
+ match: `"${name}": "${deps[name]}"`,
2004
+ 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)`,
2005
+ fix: `\u0423\u0434\u0430\u043B\u0438\u0442\u044C ${name}, \u0434\u043E\u0431\u0430\u0432\u0438\u0442\u044C/\u043E\u0431\u043D\u043E\u0432\u0438\u0442\u044C "@elcrm/server": ">=0.6.0"`,
2006
+ auto: true
2007
+ });
2008
+ }
2009
+ }
2010
+ if ((deps["@elcrm/db"] || deps["@elcrm/jwt"] || deps["@elcrm/cron"] || deps["@elcrm/tb"]) && !deps["@elcrm/server"]) {
2011
+ findings.push({
2012
+ ruleId: "pkg-need-server",
2013
+ severity: "error",
2014
+ file: rel2,
2015
+ line: 1,
2016
+ match: "missing @elcrm/server",
2017
+ message: "\u041D\u0443\u0436\u043D\u0430 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u044C @elcrm/server",
2018
+ fix: `"@elcrm/server": ">=0.6.0"`,
2019
+ auto: true
2020
+ });
2021
+ }
2022
+ return findings;
2023
+ }
2024
+ function scanServerProject(root) {
2025
+ const files = [];
2026
+ walkFiles2(root, root, files);
2027
+ const findings = [];
2028
+ for (const file of files) {
2029
+ const text = readFileSync13(file, "utf8");
2030
+ if (file.endsWith("package.json")) {
2031
+ findings.push(...scanPackageJson(root, file, text));
2032
+ } else {
2033
+ findings.push(...scanCodeFile(root, file, text));
2034
+ }
2035
+ }
2036
+ return { root, findings, filesScanned: files.length };
2037
+ }
2038
+ function printServerFindings(result) {
2039
+ const { findings, filesScanned, root } = result;
2040
+ const errors = findings.filter((f) => f.severity === "error");
2041
+ const warns = findings.filter((f) => f.severity === "warn");
2042
+ const infos = findings.filter((f) => f.severity === "info");
2043
+ console.log(`
2044
+ elcrm test (server) \u2014 ${root}`);
2045
+ console.log(`\u0444\u0430\u0439\u043B\u043E\u0432: ${filesScanned}, \u043F\u0440\u043E\u0431\u043B\u0435\u043C: ${findings.length}`);
2046
+ console.log(` error: ${errors.length} warn: ${warns.length} info: ${infos.length}
2047
+ `);
2048
+ const byFile = new Map;
2049
+ for (const f of findings) {
2050
+ const list = byFile.get(f.file) ?? [];
2051
+ list.push(f);
2052
+ byFile.set(f.file, list);
2053
+ }
2054
+ for (const [file, list] of [...byFile.entries()].sort()) {
2055
+ console.log(`${file}`);
2056
+ for (const f of list) {
2057
+ const tag = f.severity === "error" ? "ERR " : f.severity === "warn" ? "WARN" : "INFO";
2058
+ console.log(` ${tag} L${f.line} [${f.ruleId}] ${f.message}`);
2059
+ console.log(` ${f.match}`);
2060
+ if (f.fix)
2061
+ console.log(` \u2192 ${f.fix}`);
2062
+ if (f.auto)
2063
+ console.log(` (auto: bunx elcrm-server update)`);
2064
+ else
2065
+ console.log(` (\u0432\u0440\u0443\u0447\u043D\u0443\u044E \u2014 MPC.md \u0432 @elcrm/server)`);
2066
+ }
2067
+ console.log("");
2068
+ }
2069
+ if (findings.length === 0) {
2070
+ console.log(`ok \u2014 \u0441\u0435\u0440\u0432\u0435\u0440\u043D\u043E\u0435 \u043B\u0435\u0433\u0430\u0441\u0438 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043E
2071
+ `);
2072
+ }
2073
+ return errors.length;
2074
+ }
2075
+
2076
+ // src/commands/test.ts
2077
+ async function testOneDir(cwd, force, runScripts) {
2078
+ const pkg = readPkgAt(cwd);
2079
+ if (!pkg) {
2080
+ console.error(`package.json \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D: ${cwd}`);
2081
+ return 1;
2082
+ }
2083
+ const mode = detectTestMode(pkg, force);
2084
+ console.log(c.blue(`
2085
+ elcrm test \u2014 ${cwd}
2086
+ \u0440\u0435\u0436\u0438\u043C: ${mode}${force ? " (\u0444\u043B\u0430\u0433)" : " (\u043F\u043E package.json)"}`));
2087
+ if (mode === "server") {
2088
+ const result = scanServerProject(cwd);
2089
+ return printServerFindings(result);
2090
+ }
2091
+ const issues = collectFrontIssues(cwd, pkg);
2092
+ let errors = printIssues(issues, `
2093
+ \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438\u0439 \u0430\u043D\u0430\u043B\u0438\u0437:`);
2094
+ if (runScripts) {
2095
+ console.log("");
2096
+ const scriptFails = await runFrontScripts(cwd);
2097
+ errors += scriptFails;
2098
+ } else {
2099
+ console.log(c.gray(`
2100
+ (--no-scripts: check:css / check:tokens / test \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D\u044B)`));
2101
+ }
2102
+ return errors;
2103
+ }
2104
+ async function cmdTest(parsed) {
2105
+ const target = resolve(parsed.positionals[0] ?? process.cwd());
2106
+ const force = hasFlag(parsed, "server") ? "server" : hasFlag(parsed, "front") ? "front" : null;
2107
+ const runScripts = !hasFlag(parsed, "no-scripts");
2108
+ const rootPkg = readPkgAt(target);
2109
+ if (!rootPkg) {
2110
+ console.error("package.json \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D.");
2111
+ process.exit(1);
2112
+ }
2113
+ const workspaces = listWorkspaceDirs(target, rootPkg);
2114
+ let totalErrors = 0;
2115
+ if (workspaces.length > 0 && !force) {
2116
+ console.log(c.dim(`workspaces: ${workspaces.map((d) => d.replace(target + "/", "")).join(", ")}`));
2117
+ const rootHasElcrm = Boolean(rootPkg.dependencies && Object.keys(rootPkg.dependencies).some((k) => k.startsWith("@elcrm/"))) || Boolean(rootPkg.devDependencies && Object.keys(rootPkg.devDependencies).some((k) => k.startsWith("@elcrm/")));
2118
+ if (rootHasElcrm || hasFlag(parsed, "root")) {
2119
+ totalErrors += await testOneDir(target, force, runScripts);
2120
+ }
2121
+ for (const dir of workspaces) {
2122
+ totalErrors += await testOneDir(dir, force, runScripts);
2123
+ }
2124
+ } else {
2125
+ totalErrors += await testOneDir(target, force, runScripts);
2126
+ }
2127
+ if (totalErrors > 0) {
2128
+ console.log(c.yellow(`
2129
+ elcrm test: ${totalErrors} error(s)`));
2130
+ process.exit(1);
2131
+ }
2132
+ console.log(c.green(`
2133
+ elcrm test: ok`));
2134
+ }
2135
+
2136
+ // src/commands/update.ts
2137
+ async function cmdUpdate(parsed) {
2138
+ const cwd = process.cwd();
2139
+ const pkg = readPackageJson(cwd);
2140
+ if (!pkg) {
2141
+ console.error("package.json \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u0432 \u0442\u0435\u043A\u0443\u0449\u0435\u043C \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0435.");
2142
+ process.exit(1);
2143
+ }
2144
+ const deps = listElcrmDeps(pkg);
2145
+ if (deps.length === 0) {
2146
+ console.log("\u0417\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0435\u0439 @elcrm/* \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043E.");
2147
+ return;
2148
+ }
2149
+ const dry = hasFlag(parsed, "dry");
2150
+ const useCore = hasFlag(parsed, "core");
2151
+ const noInstall = hasFlag(parsed, "no-install");
2152
+ const doFix = hasFlag(parsed, "fix");
2153
+ const doTest = hasFlag(parsed, "test");
2154
+ const names = uniqueElcrmNames(deps);
2155
+ console.log(`\u041D\u0430\u0439\u0434\u0435\u043D\u043E @elcrm/*: ${names.length}`);
2156
+ let versions;
2157
+ if (useCore) {
2158
+ console.log("\u0420\u0435\u0436\u0438\u043C --core: \u0432\u0435\u0440\u0441\u0438\u0438 \u0438\u0437 \u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D\u043D\u043E\u0433\u043E @elcrm/core\u2026");
2159
+ versions = await fetchCorePinnedVersions();
2160
+ const missing = names.filter((n) => !versions.has(n));
2161
+ if (missing.length) {
2162
+ const extra = await fetchLatestMap(missing);
2163
+ for (const [k, v] of extra)
2164
+ versions.set(k, v);
2165
+ }
2166
+ } else {
2167
+ console.log("\u0417\u0430\u043F\u0440\u043E\u0441 latest \u0441 npm registry\u2026");
2168
+ versions = await fetchLatestMap(names);
2169
+ }
2170
+ let changed = 0;
2171
+ const rows = [];
2172
+ for (const dep of deps) {
2173
+ const latest = versions.get(dep.name);
2174
+ if (!latest) {
2175
+ rows.push(` ${c.yellow("?")} ${dep.name}: ${c.gray(dep.version)} \u2192 ${c.yellow("(\u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043E \u043D\u0430 npm)")}`);
2176
+ continue;
2177
+ }
2178
+ const current = dep.version.replace(/^[\^~]/, "");
2179
+ if (current === latest) {
2180
+ rows.push(` ${c.gray("=")} ${dep.name}: ${c.gray(dep.version)}`);
2181
+ continue;
2182
+ }
2183
+ const prefix = dep.version.startsWith("^") ? "^" : dep.version.startsWith("~") ? "~" : "";
2184
+ const next = `${prefix}${latest}`;
2185
+ rows.push(` ${c.blue("\u2191")} ${dep.name}: ${c.gray(dep.version)} \u2192 ${c.green(next)}`);
2186
+ if (!dry) {
2187
+ const section = pkg[dep.section];
2188
+ section[dep.name] = next;
2189
+ changed++;
2190
+ } else {
2191
+ changed++;
2192
+ }
2193
+ }
2194
+ console.log(rows.join(`
2195
+ `));
2196
+ if (dry) {
2197
+ console.log(`
2198
+ --dry: \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0439 \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043D\u043E (\u043A \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u044E: ${changed}).`);
2199
+ if (doFix) {
2200
+ console.log(`
2201
+ --fix + --dry: \u043F\u0440\u0435\u0432\u044C\u044E migrate front\u2026`);
2202
+ const front = await migrateFrontAll(cwd, true);
2203
+ if (front.length === 0)
2204
+ console.log(" \u043C\u0438\u0433\u0440\u0430\u0446\u0438\u0439 \u043A\u043E\u0434\u0430 \u043D\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044F.");
2205
+ else
2206
+ for (const c2 of front)
2207
+ console.log(` ${c2.file}: ${c2.detail}`);
2208
+ console.log(`
2209
+ --fix + --dry: \u043F\u0440\u0435\u0432\u044C\u044E CSS-\u0442\u043E\u043A\u0435\u043D\u043E\u0432\u2026`);
2210
+ try {
2211
+ const css = syncElcrmCss({ cwd, pkgNames: names, dry: true });
2212
+ let n = 0;
2213
+ for (const [file, tokens] of Object.entries(css.added)) {
2214
+ if (!tokens.length)
2215
+ continue;
2216
+ n += tokens.length;
2217
+ console.log(` ${file}: ${tokens.join(", ")}`);
2218
+ }
2219
+ if (n === 0)
2220
+ console.log(" \u041D\u043E\u0432\u044B\u0445 \u0442\u043E\u043A\u0435\u043D\u043E\u0432 \u043D\u0435\u0442.");
2221
+ } catch (e) {
2222
+ console.log(` ${c.gray("css:")} ${e instanceof Error ? e.message : e}`);
2223
+ }
2224
+ }
1242
2225
  return;
1243
- for (const name of readdirSync5(dir)) {
1244
- if (SKIP3.has(name))
1245
- continue;
1246
- const full = join8(dir, name);
1247
- let st;
2226
+ }
2227
+ if (changed === 0) {
2228
+ console.log(`
2229
+ \u0412\u0441\u0435 @elcrm/* \u0443\u0436\u0435 \u043D\u0430 \u0430\u043A\u0442\u0443\u0430\u043B\u044C\u043D\u044B\u0445 \u0432\u0435\u0440\u0441\u0438\u044F\u0445.`);
2230
+ } else {
2231
+ writePackageJson(pkg, cwd);
2232
+ console.log(`
2233
+ package.json \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D (${changed}).`);
2234
+ if (!noInstall) {
2235
+ const ok = installDeps(cwd);
2236
+ if (!ok) {
2237
+ console.error("\u0423\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0435\u0439 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0430\u0441\u044C \u0441 \u043E\u0448\u0438\u0431\u043A\u043E\u0439.");
2238
+ process.exit(1);
2239
+ }
2240
+ } else {
2241
+ console.log("\u041F\u0440\u043E\u043F\u0443\u0441\u043A install (--no-install). \u0417\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u0435 bun/npm install \u0432\u0440\u0443\u0447\u043D\u0443\u044E.");
2242
+ }
2243
+ }
2244
+ if (doFix) {
2245
+ console.log(`
2246
+ --fix: migrate front (form-aliases, size, \u2026)\u2026`);
2247
+ const front = await migrateFrontAll(cwd, false);
2248
+ if (front.length === 0) {
2249
+ console.log(" \u0418\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0439 \u043A\u043E\u0434\u0430 \u043D\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044F.");
2250
+ } else {
2251
+ for (const ch of front)
2252
+ console.log(` ${ch.file}: ${ch.detail}`);
2253
+ console.log(` \u041F\u0440\u0438\u043C\u0435\u043D\u0435\u043D\u043E: ${front.length}.`);
2254
+ }
2255
+ console.log(`
2256
+ --fix: sync CSS-\u0442\u043E\u043A\u0435\u043D\u043E\u0432 (elcrm css)\u2026`);
1248
2257
  try {
1249
- st = statSync5(full);
1250
- } catch {
1251
- continue;
2258
+ const pkgAfter = readPackageJson(cwd) ?? pkg;
2259
+ const cssNames = uniqueElcrmNames(listElcrmDeps(pkgAfter));
2260
+ const css = syncElcrmCss({ cwd, pkgNames: cssNames, dry: false });
2261
+ let added = 0;
2262
+ for (const [file, tokens] of Object.entries(css.added)) {
2263
+ if (!tokens.length)
2264
+ continue;
2265
+ added += tokens.length;
2266
+ console.log(` ${c.green("+")} ${file}: ${tokens.join(", ")}`);
2267
+ }
2268
+ if (css.imports.length) {
2269
+ console.log(` ${c.green("+")} elcrm.css @import: ${css.imports.join(", ")}`);
2270
+ }
2271
+ if (added === 0 && css.imports.length === 0) {
2272
+ console.log(" \u0422\u043E\u043A\u0435\u043D\u044B \u0443\u0436\u0435 \u043D\u0430 \u043C\u0435\u0441\u0442\u0435.");
2273
+ }
2274
+ } catch (e) {
2275
+ console.log(` ${c.gray("css sync \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D:")} ${e instanceof Error ? e.message : e}`);
1252
2276
  }
1253
- if (st.isDirectory())
1254
- walkFiles2(root, full, out);
1255
- else if (/\.(tsx?|jsx?|mjs|cjs)$/.test(name) || name === "package.json") {
1256
- out.push(full);
2277
+ }
2278
+ if (doTest) {
2279
+ console.log(`
2280
+ --test: elcrm test\u2026`);
2281
+ await cmdTest(parseArgs(["test"]));
2282
+ } else if (doFix) {
2283
+ console.log(`
2284
+ \u0414\u0430\u043B\u044C\u0448\u0435: elcrm test (\u0438\u043B\u0438 elcrm update --fix --test)`);
2285
+ } else if (changed > 0) {
2286
+ console.log(`
2287
+ \u041F\u043E\u0441\u043B\u0435 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u044F @elcrm/form: elcrm update --fix --test`);
2288
+ console.log(" (\u0430\u043B\u0438\u0430\u0441\u044B \u043F\u043E\u043B\u0435\u0439 + form-tokens + \u043D\u0435\u0434\u043E\u0441\u0442\u0430\u044E\u0449\u0438\u0435 CSS-\u0442\u043E\u043A\u0435\u043D\u044B)");
2289
+ }
2290
+ }
2291
+
2292
+ // src/lib/copyTemplate.ts
2293
+ import {
2294
+ cpSync,
2295
+ existsSync as existsSync11,
2296
+ mkdirSync,
2297
+ readdirSync as readdirSync7,
2298
+ readFileSync as readFileSync14,
2299
+ statSync as statSync7,
2300
+ writeFileSync as writeFileSync5
2301
+ } from "fs";
2302
+ import { join as join11, resolve as resolve2 } from "path";
2303
+
2304
+ // src/lib/paths.ts
2305
+ import { existsSync as existsSync10 } from "fs";
2306
+ import { dirname as dirname2, join as join10 } from "path";
2307
+ import { fileURLToPath } from "url";
2308
+ function packageRoot() {
2309
+ const here = dirname2(fileURLToPath(import.meta.url));
2310
+ const candidates = [here, join10(here, ".."), join10(here, "..", "..")];
2311
+ for (const dir of candidates) {
2312
+ if (existsSync10(join10(dir, "package.json")) && existsSync10(join10(dir, "templates"))) {
2313
+ return dir;
1257
2314
  }
1258
2315
  }
2316
+ for (const dir of candidates) {
2317
+ if (existsSync10(join10(dir, "package.json")))
2318
+ return dir;
2319
+ }
2320
+ return join10(here, "..");
1259
2321
  }
1260
- function scanCodeFile(root, file, text) {
1261
- const findings = [];
1262
- const lines = text.split(/\r?\n/);
1263
- const rel = relative2(root, file) || file;
1264
- for (const rule of CODE_RULES) {
1265
- lines.forEach((line, i) => {
1266
- rule.test.lastIndex = 0;
1267
- if (!rule.test.test(line))
1268
- return;
1269
- rule.test.lastIndex = 0;
1270
- const m = line.match(rule.test);
1271
- findings.push({
1272
- ruleId: rule.id,
1273
- severity: rule.severity,
1274
- file: rel,
1275
- line: i + 1,
1276
- match: (m?.[0] ?? line).trim().slice(0, 120),
1277
- message: rule.message,
1278
- fix: rule.fix,
1279
- auto: rule.auto
1280
- });
1281
- });
2322
+ function templatesDir() {
2323
+ return join10(packageRoot(), "templates");
2324
+ }
2325
+
2326
+ // src/lib/copyTemplate.ts
2327
+ var SKIP3 = new Set(["node_modules", "dist", ".git"]);
2328
+ function folderToPackageName(projectPath) {
2329
+ const base = projectPath.replace(/\\/g, "/").split("/").filter(Boolean).pop() ?? "app";
2330
+ const slug = base.toLowerCase().replace(/[^a-z0-9-._~/]/g, "-").replace(/^[-_.]+|[-_.]+$/g, "");
2331
+ return slug || "app";
2332
+ }
2333
+ function isEmptyDir(dir) {
2334
+ if (!existsSync11(dir))
2335
+ return true;
2336
+ const entries = readdirSync7(dir).filter((e) => e !== ".git" && e !== ".DS_Store");
2337
+ return entries.length === 0;
2338
+ }
2339
+ function writePkgName(pkgPath, name) {
2340
+ if (!existsSync11(pkgPath))
2341
+ return;
2342
+ const pkg = JSON.parse(readFileSync14(pkgPath, "utf8"));
2343
+ pkg.name = name;
2344
+ pkg.version = pkg.version ?? "0.0.0";
2345
+ writeFileSync5(pkgPath, JSON.stringify(pkg, null, 2) + `
2346
+ `, "utf8");
2347
+ }
2348
+ function finalizePanelNames(targetDir, baseName) {
2349
+ writePkgName(join11(targetDir, "package.json"), baseName);
2350
+ writePkgName(join11(targetDir, "web", "package.json"), `${baseName}-web`);
2351
+ writePkgName(join11(targetDir, "api", "package.json"), `${baseName}-api`);
2352
+ }
2353
+ function copyTemplate(templateName, targetDir, packageName) {
2354
+ const src = join11(templatesDir(), templateName);
2355
+ if (!existsSync11(src) || !statSync7(src).isDirectory()) {
2356
+ throw new Error(`\u0428\u0430\u0431\u043B\u043E\u043D \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D: ${templateName}
2357
+ \u041E\u0436\u0438\u0434\u0430\u043B\u0441\u044F \u043A\u0430\u0442\u0430\u043B\u043E\u0433: ${src}`);
1282
2358
  }
1283
- return findings;
2359
+ mkdirSync(targetDir, { recursive: true });
2360
+ for (const entry of readdirSync7(src)) {
2361
+ if (SKIP3.has(entry))
2362
+ continue;
2363
+ const from = join11(src, entry);
2364
+ const to = join11(targetDir, entry);
2365
+ cpSync(from, to, { recursive: true });
2366
+ }
2367
+ const name = packageName ?? folderToPackageName(targetDir);
2368
+ if (templateName === "panel") {
2369
+ finalizePanelNames(targetDir, name);
2370
+ return;
2371
+ }
2372
+ writePkgName(join11(targetDir, "package.json"), name);
1284
2373
  }
1285
- function scanPackageJson(root, file, text) {
1286
- const findings = [];
1287
- const rel = relative2(root, file) || file;
1288
- let pkg;
1289
- try {
1290
- pkg = JSON.parse(text);
1291
- } catch {
1292
- return findings;
2374
+ function listTemplates() {
2375
+ const dir = templatesDir();
2376
+ if (!existsSync11(dir))
2377
+ return [];
2378
+ return readdirSync7(dir).filter((name) => {
2379
+ const p = join11(dir, name);
2380
+ return statSync7(p).isDirectory() && !SKIP3.has(name);
2381
+ });
2382
+ }
2383
+ function resolveTarget(pathOrName, cwd = process.cwd()) {
2384
+ return resolve2(cwd, pathOrName);
2385
+ }
2386
+
2387
+ // src/commands/create.ts
2388
+ function libPackageName(pathOrName) {
2389
+ let slug = folderToPackageName(pathOrName);
2390
+ slug = slug.replace(/^@elcrm\//, "").replace(/^elcrm[._-]/, "");
2391
+ return `@elcrm/${slug || "lib"}`;
2392
+ }
2393
+ function doneHint(template, cdPath) {
2394
+ const prefix = cdPath ? ` cd ${cdPath}
2395
+ ` : "";
2396
+ if (template === "panel") {
2397
+ console.log(`
2398
+ \u0413\u043E\u0442\u043E\u0432\u043E (panel: web + api).
2399
+
2400
+ ${prefix} bun run dev
2401
+
2402
+ Web \u2192 http://localhost:5173
2403
+ API \u2192 http://localhost:4545
2404
+ \u0412\u0445\u043E\u0434: admin / admin
2405
+ `);
2406
+ return;
1293
2407
  }
1294
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1295
- for (const name of PKG_LEGACY) {
1296
- if (deps[name]) {
1297
- findings.push({
1298
- ruleId: `pkg-${name}`,
1299
- severity: "error",
1300
- file: rel,
1301
- line: 1,
1302
- match: `"${name}": "${deps[name]}"`,
1303
- 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)`,
1304
- fix: `\u0423\u0434\u0430\u043B\u0438\u0442\u044C ${name}, \u0434\u043E\u0431\u0430\u0432\u0438\u0442\u044C/\u043E\u0431\u043D\u043E\u0432\u0438\u0442\u044C "@elcrm/server": ">=0.6.0"`,
1305
- auto: true
1306
- });
1307
- }
2408
+ console.log(`
2409
+ \u0413\u043E\u0442\u043E\u0432\u043E.
2410
+
2411
+ ${prefix} bun run dev
2412
+ `);
2413
+ }
2414
+ async function cmdCreate(parsed) {
2415
+ const name = parsed.positionals[0];
2416
+ if (!name) {
2417
+ console.error("\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u0438\u043C\u044F \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0430: elcrm create <name>");
2418
+ console.error(`\u0428\u0430\u0431\u043B\u043E\u043D\u044B: ${listTemplates().join(", ") || "(\u043D\u0435\u0442)"}`);
2419
+ process.exit(1);
1308
2420
  }
1309
- if ((deps["@elcrm/db"] || deps["@elcrm/jwt"] || deps["@elcrm/cron"] || deps["@elcrm/tb"]) && !deps["@elcrm/server"]) {
1310
- findings.push({
1311
- ruleId: "pkg-need-server",
1312
- severity: "error",
1313
- file: rel,
1314
- line: 1,
1315
- match: "missing @elcrm/server",
1316
- message: "\u041D\u0443\u0436\u043D\u0430 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u044C @elcrm/server",
1317
- fix: `"@elcrm/server": ">=0.6.0"`,
1318
- auto: true
1319
- });
2421
+ const template = parsed.options.template ?? "app";
2422
+ const target = resolveTarget(name);
2423
+ const noInstall = hasFlag(parsed, "no-install");
2424
+ if (!isEmptyDir(target)) {
2425
+ console.error(`\u041A\u0430\u0442\u0430\u043B\u043E\u0433 \u043D\u0435 \u043F\u0443\u0441\u0442: ${target}`);
2426
+ process.exit(1);
1320
2427
  }
1321
- return findings;
2428
+ const pkgName = template === "lib" && !name.startsWith("@") ? libPackageName(name) : folderToPackageName(name);
2429
+ console.log(`\u0428\u0430\u0431\u043B\u043E\u043D: ${template}`);
2430
+ console.log(`\u041A\u0430\u0442\u0430\u043B\u043E\u0433: ${target}`);
2431
+ console.log(`package name: ${pkgName}`);
2432
+ copyTemplate(template, target, pkgName);
2433
+ console.log("\u0424\u0430\u0439\u043B\u044B \u0441\u043A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u043D\u044B.");
2434
+ if (!noInstall) {
2435
+ const ok = installDeps(target);
2436
+ if (!ok)
2437
+ process.exit(1);
2438
+ }
2439
+ doneHint(template, name);
1322
2440
  }
1323
- function scanServerProject(root) {
1324
- const files = [];
1325
- walkFiles2(root, root, files);
1326
- const findings = [];
1327
- for (const file of files) {
1328
- const text = readFileSync6(file, "utf8");
1329
- if (file.endsWith("package.json")) {
1330
- findings.push(...scanPackageJson(root, file, text));
1331
- } else {
1332
- findings.push(...scanCodeFile(root, file, text));
2441
+ async function cmdInit(parsed) {
2442
+ const template = parsed.options.template ?? "app";
2443
+ const cwd = process.cwd();
2444
+ const force = hasFlag(parsed, "force");
2445
+ const noInstall = hasFlag(parsed, "no-install");
2446
+ if (!force && !isEmptyDir(cwd)) {
2447
+ console.error("\u0422\u0435\u043A\u0443\u0449\u0438\u0439 \u043A\u0430\u0442\u0430\u043B\u043E\u0433 \u043D\u0435 \u043F\u0443\u0441\u0442. \u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439\u0442\u0435 --force \u0438\u043B\u0438 elcrm create <name>.");
2448
+ process.exit(1);
2449
+ }
2450
+ const pkgName = template === "lib" ? libPackageName(cwd) : folderToPackageName(cwd);
2451
+ console.log(`\u0428\u0430\u0431\u043B\u043E\u043D: ${template}`);
2452
+ console.log(`\u041A\u0430\u0442\u0430\u043B\u043E\u0433: ${cwd}`);
2453
+ copyTemplate(template, cwd, pkgName);
2454
+ console.log("\u041F\u0440\u043E\u0435\u043A\u0442 \u0438\u043D\u0438\u0446\u0438\u0430\u043B\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u043D.");
2455
+ if (!noInstall) {
2456
+ const ok = installDeps(cwd);
2457
+ if (!ok)
2458
+ process.exit(1);
2459
+ }
2460
+ doneHint(template);
2461
+ }
2462
+
2463
+ // src/commands/migrate.ts
2464
+ init_socket_server();
2465
+ init_front();
2466
+ var MIGRATIONS = {
2467
+ "socket-server": migrateSocketServer,
2468
+ ...FRONT_MIGRATIONS,
2469
+ front: migrateFrontAll,
2470
+ ui: migrateFrontAll
2471
+ };
2472
+ async function cmdMigrate(parsed) {
2473
+ if (hasFlag(parsed, "list") || parsed.positionals[0] === "list") {
2474
+ console.log("\u0414\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0435 \u043C\u0438\u0433\u0440\u0430\u0446\u0438\u0438:");
2475
+ 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:");
2476
+ for (const name2 of FRONT_MIGRATE_ORDER) {
2477
+ console.log(` - ${name2}`);
1333
2478
  }
2479
+ console.log(" socket-server (\u0442\u0430\u043A\u0436\u0435 \u0432 front)");
2480
+ return;
2481
+ }
2482
+ const name = parsed.positionals[0];
2483
+ if (!name) {
2484
+ console.error("\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u043C\u0438\u0433\u0440\u0430\u0446\u0438\u044E: elcrm migrate <name>");
2485
+ console.error(`\u0414\u043E\u0441\u0442\u0443\u043F\u043D\u043E: ${Object.keys(MIGRATIONS).join(", ")}`);
2486
+ console.error("\u0421\u043F\u0438\u0441\u043E\u043A: elcrm migrate --list");
2487
+ process.exit(1);
2488
+ }
2489
+ const fn = MIGRATIONS[name];
2490
+ if (!fn) {
2491
+ console.error(`\u041D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043C\u0438\u0433\u0440\u0430\u0446\u0438\u044F: ${name}`);
2492
+ console.error(`\u0414\u043E\u0441\u0442\u0443\u043F\u043D\u043E: ${Object.keys(MIGRATIONS).join(", ")}`);
2493
+ process.exit(1);
1334
2494
  }
1335
- return { root, findings, filesScanned: files.length };
1336
- }
1337
- function printServerFindings(result) {
1338
- const { findings, filesScanned, root } = result;
1339
- const errors = findings.filter((f) => f.severity === "error");
1340
- const warns = findings.filter((f) => f.severity === "warn");
1341
- const infos = findings.filter((f) => f.severity === "info");
1342
- console.log(`
1343
- elcrm test (server) \u2014 ${root}`);
1344
- console.log(`\u0444\u0430\u0439\u043B\u043E\u0432: ${filesScanned}, \u043F\u0440\u043E\u0431\u043B\u0435\u043C: ${findings.length}`);
1345
- console.log(` error: ${errors.length} warn: ${warns.length} info: ${infos.length}
1346
- `);
1347
- const byFile = new Map;
1348
- for (const f of findings) {
1349
- const list = byFile.get(f.file) ?? [];
1350
- list.push(f);
1351
- byFile.set(f.file, list);
2495
+ const dry = hasFlag(parsed, "dry");
2496
+ const cwd = process.cwd();
2497
+ console.log(`\u041C\u0438\u0433\u0440\u0430\u0446\u0438\u044F: ${name}${dry ? " (--dry)" : ""}`);
2498
+ const changes = await fn(cwd, dry);
2499
+ if (changes.length === 0) {
2500
+ console.log("\u0418\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0439 \u043D\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044F.");
2501
+ return;
1352
2502
  }
1353
- for (const [file, list] of [...byFile.entries()].sort()) {
1354
- console.log(`${file}`);
1355
- for (const f of list) {
1356
- const tag = f.severity === "error" ? "ERR " : f.severity === "warn" ? "WARN" : "INFO";
1357
- console.log(` ${tag} L${f.line} [${f.ruleId}] ${f.message}`);
1358
- console.log(` ${f.match}`);
1359
- if (f.fix)
1360
- console.log(` \u2192 ${f.fix}`);
1361
- if (f.auto)
1362
- console.log(` (auto: bunx elcrm-server update)`);
1363
- else
1364
- console.log(` (\u0432\u0440\u0443\u0447\u043D\u0443\u044E \u2014 MPC.md \u0432 @elcrm/server)`);
1365
- }
1366
- console.log("");
2503
+ for (const c2 of changes) {
2504
+ console.log(` ${c2.file}: ${c2.detail}`);
1367
2505
  }
1368
- if (findings.length === 0) {
1369
- console.log(`ok \u2014 \u0441\u0435\u0440\u0432\u0435\u0440\u043D\u043E\u0435 \u043B\u0435\u0433\u0430\u0441\u0438 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043E
1370
- `);
2506
+ console.log(dry ? `
2507
+ --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.` : `
2508
+ \u041F\u0440\u0438\u043C\u0435\u043D\u0435\u043D\u043E: ${changes.length}.`);
2509
+ if (name === "front" || name === "ui" || name === "size-sml" || name === "form-tokens") {
2510
+ console.log("\u0414\u0430\u043B\u044C\u0448\u0435: elcrm css && elcrm audit");
1371
2511
  }
1372
- return errors.length;
1373
2512
  }
1374
2513
 
1375
2514
  // src/commands/doctor.ts
2515
+ init_socket_server();
2516
+ init_pkg();
1376
2517
  async function cmdDoctor(parsed) {
1377
2518
  const cwd = process.cwd();
1378
2519
  const fix = hasFlag(parsed, "fix");
@@ -1440,14 +2581,20 @@ async function cmdDoctor(parsed) {
1440
2581
  if (mig.length) {
1441
2582
  console.log(`\u041C\u0438\u0433\u0440\u0430\u0446\u0438\u044F socket-server: ${mig.length} \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0439.`);
1442
2583
  }
2584
+ const { migrateFrontAll: migrateFrontAll2 } = await Promise.resolve().then(() => (init_front(), exports_front));
2585
+ const front = await migrateFrontAll2(cwd, false);
2586
+ if (front.length) {
2587
+ console.log(`migrate front: ${front.length} \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0439.`);
2588
+ }
1443
2589
  console.log("doctor --fix: \u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u044B\u0435 \u043F\u0440\u0430\u0432\u043A\u0438 \u043F\u0440\u0438\u043C\u0435\u043D\u0435\u043D\u044B.");
1444
- console.log("\u0420\u0435\u043A\u043E\u043C\u0435\u043D\u0434\u0443\u0435\u0442\u0441\u044F: elcrm update");
2590
+ console.log("\u0420\u0435\u043A\u043E\u043C\u0435\u043D\u0434\u0443\u0435\u0442\u0441\u044F: elcrm update --fix --test");
1445
2591
  }
1446
2592
 
1447
2593
  // src/commands/build.ts
1448
- import { existsSync as existsSync9 } from "fs";
2594
+ import { existsSync as existsSync12 } from "fs";
1449
2595
 
1450
2596
  // src/lib/nextVersion.ts
2597
+ init_pkg();
1451
2598
  async function nextVersion() {
1452
2599
  const pkg = readPackageJson();
1453
2600
  if (!pkg?.version) {
@@ -1468,9 +2615,9 @@ async function nextVersion() {
1468
2615
 
1469
2616
  // src/commands/build.ts
1470
2617
  function resolveEntrypoint() {
1471
- if (existsSync9("./src/index.js"))
2618
+ if (existsSync12("./src/index.js"))
1472
2619
  return "./src/index.js";
1473
- if (existsSync9("./src/index.ts"))
2620
+ if (existsSync12("./src/index.ts"))
1474
2621
  return "./src/index.ts";
1475
2622
  return "./src/index.js";
1476
2623
  }
@@ -1547,91 +2694,30 @@ async function cmdUuid() {
1547
2694
  console.log(crypto.randomUUID());
1548
2695
  }
1549
2696
 
1550
- // src/commands/test.ts
1551
- import { resolve as resolve2 } from "path";
1552
- async function testOneDir(cwd, force, runScripts) {
1553
- const pkg = readPkgAt(cwd);
1554
- if (!pkg) {
1555
- console.error(`package.json \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D: ${cwd}`);
1556
- return 1;
1557
- }
1558
- const mode = detectTestMode(pkg, force);
1559
- console.log(c.blue(`
1560
- elcrm test \u2014 ${cwd}
1561
- \u0440\u0435\u0436\u0438\u043C: ${mode}${force ? " (\u0444\u043B\u0430\u0433)" : " (\u043F\u043E package.json)"}`));
1562
- if (mode === "server") {
1563
- const result = scanServerProject(cwd);
1564
- return printServerFindings(result);
1565
- }
1566
- const issues = collectFrontIssues(cwd, pkg);
1567
- let errors = printIssues(issues, `
1568
- \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438\u0439 \u0430\u043D\u0430\u043B\u0438\u0437:`);
1569
- if (runScripts) {
1570
- console.log("");
1571
- const scriptFails = await runFrontScripts(cwd);
1572
- errors += scriptFails;
1573
- } else {
1574
- console.log(c.gray(`
1575
- (--no-scripts: check:css / check:tokens / test \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D\u044B)`));
1576
- }
1577
- return errors;
1578
- }
1579
- async function cmdTest(parsed) {
1580
- const target = resolve2(parsed.positionals[0] ?? process.cwd());
1581
- const force = hasFlag(parsed, "server") ? "server" : hasFlag(parsed, "front") ? "front" : null;
1582
- const runScripts = !hasFlag(parsed, "no-scripts");
1583
- const rootPkg = readPkgAt(target);
1584
- if (!rootPkg) {
1585
- console.error("package.json \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D.");
1586
- process.exit(1);
1587
- }
1588
- const workspaces = listWorkspaceDirs(target, rootPkg);
1589
- let totalErrors = 0;
1590
- if (workspaces.length > 0 && !force) {
1591
- console.log(c.dim(`workspaces: ${workspaces.map((d) => d.replace(target + "/", "")).join(", ")}`));
1592
- const rootHasElcrm = Boolean(rootPkg.dependencies && Object.keys(rootPkg.dependencies).some((k) => k.startsWith("@elcrm/"))) || Boolean(rootPkg.devDependencies && Object.keys(rootPkg.devDependencies).some((k) => k.startsWith("@elcrm/")));
1593
- if (rootHasElcrm || hasFlag(parsed, "root")) {
1594
- totalErrors += await testOneDir(target, force, runScripts);
1595
- }
1596
- for (const dir of workspaces) {
1597
- totalErrors += await testOneDir(dir, force, runScripts);
1598
- }
1599
- } else {
1600
- totalErrors += await testOneDir(target, force, runScripts);
1601
- }
1602
- if (totalErrors > 0) {
1603
- console.log(c.yellow(`
1604
- elcrm test: ${totalErrors} error(s)`));
1605
- process.exit(1);
1606
- }
1607
- console.log(c.green(`
1608
- elcrm test: ok`));
1609
- }
1610
-
1611
2697
  // src/commands/postbuild.ts
1612
2698
  import { resolve as resolve3 } from "path";
1613
2699
 
1614
2700
  // src/vite/postbuild.ts
1615
2701
  import {
1616
- existsSync as existsSync10,
1617
- readdirSync as readdirSync7,
1618
- readFileSync as readFileSync8,
2702
+ existsSync as existsSync13,
2703
+ readdirSync as readdirSync9,
2704
+ readFileSync as readFileSync16,
1619
2705
  rmSync,
1620
- statSync as statSync6,
1621
- writeFileSync as writeFileSync5
2706
+ statSync as statSync8,
2707
+ writeFileSync as writeFileSync7
1622
2708
  } from "fs";
1623
- import { join as join10 } from "path";
2709
+ import { join as join13 } from "path";
1624
2710
 
1625
2711
  // src/vite/concat-css.ts
1626
- import { readdirSync as readdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
1627
- import { join as join9 } from "path";
2712
+ import { readdirSync as readdirSync8, readFileSync as readFileSync15, writeFileSync as writeFileSync6 } from "fs";
2713
+ import { join as join12 } from "path";
1628
2714
  function concatDistCss(options) {
1629
2715
  const outFile = options.outFile ?? "index.css";
1630
- const files = readdirSync6(options.distDir).filter((f) => f.endsWith(".css") && f !== outFile).sort();
1631
- const css = files.map((f) => readFileSync7(join9(options.distDir, f), "utf8")).join(`
2716
+ const files = readdirSync8(options.distDir).filter((f) => f.endsWith(".css") && f !== outFile).sort();
2717
+ const css = files.map((f) => readFileSync15(join12(options.distDir, f), "utf8")).join(`
1632
2718
  `);
1633
- const outPath = join9(options.distDir, outFile);
1634
- writeFileSync4(outPath, css);
2719
+ const outPath = join12(options.distDir, outFile);
2720
+ writeFileSync6(outPath, css);
1635
2721
  console.log(`[concat-css] ${files.length} \u0444\u0430\u0439\u043B\u043E\u0432 \u2192 ${outFile}`);
1636
2722
  return outPath;
1637
2723
  }
@@ -1648,19 +2734,19 @@ function postbuildLib(options) {
1648
2734
  distDir: dist,
1649
2735
  outFile: options.cssOutFile ?? "index.css"
1650
2736
  });
1651
- for (const name of readdirSync7(dist)) {
1652
- const dir = join10(dist, name);
1653
- if (!statSync6(dir).isDirectory())
2737
+ for (const name of readdirSync9(dist)) {
2738
+ const dir = join13(dist, name);
2739
+ if (!statSync8(dir).isDirectory())
1654
2740
  continue;
1655
2741
  if (keep.has(name))
1656
2742
  continue;
1657
- const impl = join10(dir, `${name}.d.ts`);
1658
- const nestedIndex = join10(dir, "index.d.ts");
1659
- const flat = join10(dist, `${name}.d.ts`);
1660
- const src = existsSync10(impl) ? impl : existsSync10(nestedIndex) ? nestedIndex : null;
2743
+ const impl = join13(dir, `${name}.d.ts`);
2744
+ const nestedIndex = join13(dir, "index.d.ts");
2745
+ const flat = join13(dist, `${name}.d.ts`);
2746
+ const src = existsSync13(impl) ? impl : existsSync13(nestedIndex) ? nestedIndex : null;
1661
2747
  if (src) {
1662
- const text = readFileSync8(src, "utf8").replace(/from ['"]\.\.\/([^'"]+)['"]/g, `from './$1'`);
1663
- writeFileSync5(flat, text);
2748
+ const text = readFileSync16(src, "utf8").replace(/from ['"]\.\.\/([^'"]+)['"]/g, `from './$1'`);
2749
+ writeFileSync7(flat, text);
1664
2750
  console.log(`[postbuild] types \u2192 ${name}.d.ts`);
1665
2751
  }
1666
2752
  rmSync(dir, { recursive: true, force: true });
@@ -1676,15 +2762,444 @@ async function cmdPostbuild(parsed) {
1676
2762
  console.log(`[postbuild] \u0433\u043E\u0442\u043E\u0432\u043E: ${distDir}`);
1677
2763
  }
1678
2764
 
2765
+ // src/commands/css.ts
2766
+ init_pkg();
2767
+ async function cmdCss(parsed) {
2768
+ const cwd = parsed.options.dir || process.cwd();
2769
+ const pkg = readPackageJson(cwd);
2770
+ if (!pkg) {
2771
+ console.error("package.json \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D.");
2772
+ process.exit(1);
2773
+ }
2774
+ const names = uniqueElcrmNames(listElcrmDeps(pkg));
2775
+ if (!names.length) {
2776
+ console.log("\u0417\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0435\u0439 @elcrm/* \u043D\u0435\u0442.");
2777
+ return;
2778
+ }
2779
+ const dry = hasFlag(parsed, "dry");
2780
+ console.log(`${dry ? "[dry] " : ""}\u041F\u0430\u043A\u0435\u0442\u044B: ${names.join(", ")}`);
2781
+ try {
2782
+ const result = syncElcrmCss({ cwd, pkgNames: names, dry });
2783
+ console.log(`\u041A\u0430\u0442\u0430\u043B\u043E\u0433 \u0441\u0442\u0438\u043B\u0435\u0439: ${result.styleDir}`);
2784
+ let total = 0;
2785
+ for (const [file, tokens] of Object.entries(result.added)) {
2786
+ if (!tokens.length) {
2787
+ console.log(` ${c.gray(file)}: \u0431\u0435\u0437 \u043D\u043E\u0432\u044B\u0445 \u0442\u043E\u043A\u0435\u043D\u043E\u0432`);
2788
+ continue;
2789
+ }
2790
+ total += tokens.length;
2791
+ console.log(` ${c.green("+")} ${file}: ${tokens.length}`);
2792
+ for (const t of tokens)
2793
+ console.log(` ${c.dim(t)}`);
2794
+ }
2795
+ if (result.imports.length) {
2796
+ console.log(` ${c.green("+")} elcrm.css @import:`);
2797
+ for (const spec of result.imports)
2798
+ console.log(` ${c.dim(spec)}`);
2799
+ }
2800
+ if (result.skippedPkgs.length) {
2801
+ console.log(` ${c.gray("\u0431\u0435\u0437 CSS-\u0442\u043E\u043A\u0435\u043D\u043E\u0432:")} ${result.skippedPkgs.join(", ")}`);
2802
+ }
2803
+ if (total === 0 && result.imports.length === 0) {
2804
+ console.log("\u0412\u0441\u0451 \u0443\u0436\u0435 \u043D\u0430 \u043C\u0435\u0441\u0442\u0435.");
2805
+ } else if (dry) {
2806
+ console.log("\u0417\u0430\u043F\u0438\u0441\u044C \u043D\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u043B\u0430\u0441\u044C (--dry).");
2807
+ }
2808
+ } catch (e) {
2809
+ console.error(e instanceof Error ? e.message : e);
2810
+ process.exit(1);
2811
+ }
2812
+ }
2813
+
2814
+ // src/commands/audit.ts
2815
+ import { existsSync as existsSync14, readFileSync as readFileSync17 } from "fs";
2816
+ import { join as join14, relative as relative4 } from "path";
2817
+ init_pkg();
2818
+ init_walk();
2819
+ init_front();
2820
+ init_form_tokens();
2821
+ function scan(cwd) {
2822
+ const out = [];
2823
+ const pkg = readPackageJson(cwd);
2824
+ if (!pkg) {
2825
+ out.push({
2826
+ level: "error",
2827
+ code: "no-pkg",
2828
+ message: "package.json \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D"
2829
+ });
2830
+ return out;
2831
+ }
2832
+ for (const section of ["dependencies", "devDependencies"]) {
2833
+ const map = pkg[section];
2834
+ if (!map)
2835
+ continue;
2836
+ for (const [name, ver] of Object.entries(map)) {
2837
+ if (!name.startsWith("@elcrm/"))
2838
+ continue;
2839
+ if (typeof ver === "string" && ver.startsWith("file:")) {
2840
+ out.push({
2841
+ level: "warn",
2842
+ code: "file-dep",
2843
+ message: `${section}["${name}"] = ${ver} \u2014 \u043E\u043F\u0443\u0431\u043B\u0438\u043A\u0443\u0439\u0442\u0435 \u0438\u043B\u0438 \u0437\u0430\u0444\u0438\u043A\u0441\u0438\u0440\u0443\u0439\u0442\u0435 sibling \u0432 README`,
2844
+ fix: "elcrm update / publish \u043F\u0430\u043A\u0435\u0442\u0430"
2845
+ });
2846
+ }
2847
+ }
2848
+ }
2849
+ const files = projectCodeRoots(cwd).flatMap((r) => walkCodeFiles(r));
2850
+ for (const file of files) {
2851
+ const text = readFileSync17(file, "utf8");
2852
+ const rel2 = relative4(cwd, file);
2853
+ const lines = text.split(/\r?\n/);
2854
+ lines.forEach((line, i) => {
2855
+ const n = i + 1;
2856
+ if (/size\s*=\s*["']sm["']/.test(line) || /size\s*=\s*["']md["']/.test(line)) {
2857
+ if (/titleSize/.test(line))
2858
+ return;
2859
+ out.push({
2860
+ level: "warn",
2861
+ code: "size-legacy",
2862
+ message: 'size="sm"|"md" \u2192 "s"|"m"',
2863
+ file: rel2,
2864
+ line: n,
2865
+ fix: "elcrm migrate size-sml"
2866
+ });
2867
+ }
2868
+ if (/Modal\.Add\s*\(/.test(line)) {
2869
+ out.push({
2870
+ level: "warn",
2871
+ code: "modal-add",
2872
+ message: "Modal.Add \u2014 \u043F\u0440\u0435\u0434\u043F\u043E\u0447\u0442\u0438\u0442\u0435\u043B\u0435\u043D Modal.Create + Open",
2873
+ file: rel2,
2874
+ line: n,
2875
+ fix: "elcrm migrate modal-create"
2876
+ });
2877
+ }
2878
+ if (/key=\{Date\.now\(\)\}/.test(line) && /Modal/.test(text)) {
2879
+ out.push({
2880
+ level: "info",
2881
+ code: "modal-date-key",
2882
+ 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",
2883
+ file: rel2,
2884
+ line: n
2885
+ });
2886
+ }
2887
+ if (/--field-border-(?:focus|hover|error|disabled)\s*:\s*\d/.test(line) && /solid|dashed|dotted/i.test(line)) {
2888
+ out.push({
2889
+ level: "error",
2890
+ code: "field-border-shorthand",
2891
+ message: "hover/focus border \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u0446\u0432\u0435\u0442, \u043D\u0435 shorthand",
2892
+ file: rel2,
2893
+ line: n,
2894
+ fix: "elcrm migrate field-border"
2895
+ });
2896
+ }
2897
+ if (/!important/.test(line) && /drag|file-upload|DragDrop/i.test(rel2 + line)) {
2898
+ out.push({
2899
+ level: "info",
2900
+ code: "dragdrop-important",
2901
+ message: '!important \u0443 DragDrop \u2014 layout="fill"|"overlay" \u0432 @elcrm/form',
2902
+ file: rel2,
2903
+ line: n
2904
+ });
2905
+ }
2906
+ if (/globalThis\.React\s*=/.test(line)) {
2907
+ out.push({
2908
+ level: "warn",
2909
+ code: "react-global",
2910
+ message: "globalThis.React \u0445\u0430\u043A \u2014 migrate legacy-hacks / \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u0435 modal",
2911
+ file: rel2,
2912
+ line: n,
2913
+ fix: "elcrm migrate legacy-hacks"
2914
+ });
2915
+ }
2916
+ if (/\b(ProgressField|ReadonlyField|OtpField|SegmentField|DragonDropField|TProgress)\b/.test(line)) {
2917
+ out.push({
2918
+ level: "error",
2919
+ code: "form-alias",
2920
+ 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",
2921
+ file: rel2,
2922
+ line: n,
2923
+ fix: "elcrm migrate form-aliases"
2924
+ });
2925
+ }
2926
+ });
2927
+ }
2928
+ const toolbarMarkers = files.filter((f) => {
2929
+ if (!f.endsWith(".css"))
2930
+ return false;
2931
+ if (/theme(-light|-dark)?\.css$/.test(f))
2932
+ return false;
2933
+ const t = readFileSync17(f, "utf8");
2934
+ return t.includes("--search-border:") && t.includes("--button-secondary-bg:") && t.includes("transparent");
2935
+ });
2936
+ if (toolbarMarkers.length > 1) {
2937
+ out.push({
2938
+ level: "info",
2939
+ code: "toolbar-dup",
2940
+ message: `\u043E\u0434\u0438\u043D\u0430\u043A\u043E\u0432\u044B\u0435 toolbar-\u0442\u043E\u043A\u0435\u043D\u044B \u0432 ${toolbarMarkers.length} \u0444\u0430\u0439\u043B\u0430\u0445 \u2014 \u043A\u043B\u0430\u0441\u0441 .el-toolbar-controls`,
2941
+ fix: "\u043E\u0431\u0449\u0438\u0439 \u043A\u043B\u0430\u0441\u0441 + theme tokens"
2942
+ });
2943
+ }
2944
+ const styleDir = ["src/style", "src/styles", "style"].map((d) => join14(cwd, d)).find(existsSync14);
2945
+ if (styleDir && !existsSync14(join14(styleDir, "elcrm.css"))) {
2946
+ out.push({
2947
+ level: "info",
2948
+ code: "elcrm-css",
2949
+ message: "\u043D\u0435\u0442 elcrm.css \u2014 elcrm css \u0441\u043E\u0437\u0434\u0430\u0441\u0442 imports + \u0442\u043E\u043A\u0435\u043D\u044B",
2950
+ fix: "elcrm css"
2951
+ });
2952
+ }
2953
+ const depsForm = pkg.dependencies?.["@elcrm/form"] || pkg.devDependencies?.["@elcrm/form"];
2954
+ if (depsForm && styleDir) {
2955
+ const themeFiles = [
2956
+ "theme-light.css",
2957
+ "theme-dark.css",
2958
+ "theme.css"
2959
+ ].map((f) => join14(styleDir, f));
2960
+ const themeText = themeFiles.filter(existsSync14).map((f) => readFileSync17(f, "utf8")).join(`
2961
+ `);
2962
+ if (themeText && !/--popup-shadow\s*:/.test(themeText) && /--date-popup-shadow\s*:|--select-background\s*:/.test(themeText)) {
2963
+ out.push({
2964
+ level: "info",
2965
+ code: "popup-shadow",
2966
+ message: "\u043D\u0435\u0442 --popup-shadow \u0432 theme-* (\u043E\u0431\u0449\u0430\u044F \u0442\u0435\u043D\u044C \u043F\u043E\u043F\u0430\u043F\u043E\u0432 Date/Select/Color/Time)",
2967
+ fix: "elcrm css \u0438\u043B\u0438 elcrm update --fix"
2968
+ });
2969
+ }
2970
+ if (themeText && !/--field-padding-block\s*:/.test(themeText) && /--field-note-padding\s*:|--control-inner-padding\s*:/.test(themeText)) {
2971
+ out.push({
2972
+ level: "warn",
2973
+ code: "field-padding",
2974
+ message: "\u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0438\u0435 padding-\u0442\u043E\u043A\u0435\u043D\u044B \u2014 \u0435\u0434\u0438\u043D\u044B\u0439 --field-padding / --field-padding-block",
2975
+ fix: "elcrm migrate form-tokens"
2976
+ });
2977
+ }
2978
+ }
2979
+ for (const file of files) {
2980
+ if (!file.endsWith(".css"))
2981
+ continue;
2982
+ const text = readFileSync17(file, "utf8");
2983
+ const relPath = relative4(cwd, file);
2984
+ const lines = text.split(/\r?\n/);
2985
+ lines.forEach((line, i) => {
2986
+ if (/:\s*var\(\s*--field-padding/.test(line))
2987
+ return;
2988
+ if (lineHasDeprecatedFormToken(line)) {
2989
+ out.push({
2990
+ level: "warn",
2991
+ code: "form-token-legacy",
2992
+ message: "legacy form-\u0442\u043E\u043A\u0435\u043D \u2192 --field-padding* / --popup-shadow",
2993
+ file: relPath,
2994
+ line: i + 1,
2995
+ fix: "elcrm migrate form-tokens"
2996
+ });
2997
+ }
2998
+ });
2999
+ }
3000
+ return out;
3001
+ }
3002
+ function printFindings(findings) {
3003
+ if (!findings.length) {
3004
+ console.log(`${c.green("audit")}: \u0437\u0430\u043C\u0435\u0447\u0430\u043D\u0438\u0439 \u043D\u0435\u0442.`);
3005
+ return;
3006
+ }
3007
+ const icon = { error: c.red("\u2715"), warn: c.yellow("!"), info: c.cyan("i") };
3008
+ for (const f of findings) {
3009
+ const loc = f.file ? f.line ? `${f.file}:${f.line}` : f.file : "";
3010
+ console.log(`${icon[f.level]} [${f.code}] ${f.message}${loc ? ` ${c.dim(loc)}` : ""}`);
3011
+ if (f.fix)
3012
+ console.log(` ${c.dim("\u2192 " + f.fix)}`);
3013
+ }
3014
+ const err = findings.filter((f) => f.level === "error").length;
3015
+ const warn = findings.filter((f) => f.level === "warn").length;
3016
+ console.log(`
3017
+ \u0418\u0442\u043E\u0433\u043E: ${findings.length} (${err} error, ${warn} warn). \u0418\u0441\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0435: elcrm audit --fix / elcrm migrate front`);
3018
+ }
3019
+ async function cmdAudit(parsed) {
3020
+ const cwd = parsed.options.dir || process.cwd();
3021
+ const fix = hasFlag(parsed, "fix");
3022
+ const findings = scan(cwd);
3023
+ printFindings(findings);
3024
+ if (!fix) {
3025
+ if (findings.some((f) => f.level === "error"))
3026
+ process.exit(1);
3027
+ return;
3028
+ }
3029
+ console.log(`
3030
+ ${c.cyan("\u2192")} elcrm migrate front${hasFlag(parsed, "dry") ? " --dry" : ""}`);
3031
+ const changes = await migrateFrontAll(cwd, hasFlag(parsed, "dry"));
3032
+ if (!changes.length) {
3033
+ console.log("\u0410\u0432\u0442\u043E\u043C\u0438\u0433\u0440\u0430\u0446\u0438\u0439 \u043D\u0435 \u043F\u043E\u0442\u0440\u0435\u0431\u043E\u0432\u0430\u043B\u043E\u0441\u044C.");
3034
+ } else {
3035
+ for (const ch of changes) {
3036
+ console.log(` ${ch.file}: ${ch.detail}`);
3037
+ }
3038
+ console.log(hasFlag(parsed, "dry") ? `
3039
+ --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.` : `
3040
+ \u041F\u0440\u0438\u043C\u0435\u043D\u0435\u043D\u043E: ${changes.length}.`);
3041
+ }
3042
+ console.log("\u0420\u0435\u043A\u043E\u043C\u0435\u043D\u0434\u0443\u0435\u0442\u0441\u044F: elcrm css && elcrm doctor");
3043
+ }
3044
+
3045
+ // src/commands/docs.ts
3046
+ import { join as join16, resolve as resolve4 } from "path";
3047
+
3048
+ // src/lib/projectKind.ts
3049
+ function detectProjectKind(cwd) {
3050
+ const pkg = readPkgAt(cwd);
3051
+ if (!pkg)
3052
+ return "front";
3053
+ const workspaces = listWorkspaceDirs(cwd, pkg);
3054
+ if (workspaces.length) {
3055
+ const modes = new Set;
3056
+ for (const dir of workspaces) {
3057
+ const w = readPkgAt(dir);
3058
+ if (w)
3059
+ modes.add(detectTestMode(w));
3060
+ }
3061
+ if (modes.has("front") && modes.has("server"))
3062
+ return "mixed";
3063
+ if (modes.has("server") && !modes.has("front"))
3064
+ return "server";
3065
+ if (modes.has("front"))
3066
+ return "front";
3067
+ }
3068
+ return hasElcrmServer(pkg) ? "server" : "front";
3069
+ }
3070
+ function isElcrmLibPackage(cwd) {
3071
+ const pkg = readPkgAt(cwd);
3072
+ const name = pkg?.name;
3073
+ return typeof name === "string" && name.startsWith("@elcrm/");
3074
+ }
3075
+
3076
+ // src/lib/elcrmDocsCatalog.ts
3077
+ var ALL_ELCRM_DOCS = [
3078
+ "CLI.elCRM.md",
3079
+ "API.elCRM.md",
3080
+ "ALERT.elCRM.md",
3081
+ "BUTTON.elCRM.md",
3082
+ "MODAL.elCRM.md",
3083
+ "NOTICE.elCRM.md",
3084
+ "NOTIFICATIONS.elCRM.md",
3085
+ "PASSKEY.elCRM.md",
3086
+ "ROUTER.elCRM.md",
3087
+ "SOCKET.elCRM.md",
3088
+ "TOOLTIP.elCRM.md",
3089
+ "ICONS.elCRM.md",
3090
+ "FORM.elCRM.md",
3091
+ "COMPONENTS.elCRM.md",
3092
+ "SERVER.elCRM.md"
3093
+ ];
3094
+ var SERVER_DOCS = [
3095
+ "SERVER.elCRM.md",
3096
+ "CLI.elCRM.md",
3097
+ "API.elCRM.md",
3098
+ "SOCKET.elCRM.md"
3099
+ ];
3100
+ function docsForProject(kind, lib) {
3101
+ if (lib || kind === "front" || kind === "mixed")
3102
+ return ALL_ELCRM_DOCS;
3103
+ return SERVER_DOCS;
3104
+ }
3105
+
3106
+ // src/lib/scaffoldFiles.ts
3107
+ import {
3108
+ existsSync as existsSync15,
3109
+ mkdirSync as mkdirSync2,
3110
+ readdirSync as readdirSync10,
3111
+ readFileSync as readFileSync18,
3112
+ writeFileSync as writeFileSync8
3113
+ } from "fs";
3114
+ import { dirname as dirname3, join as join15 } from "path";
3115
+ function writeScaffoldFile(dest, body, opts) {
3116
+ if (opts.dry)
3117
+ return "dry";
3118
+ if (!opts.force && existsSync15(dest))
3119
+ return "skip";
3120
+ mkdirSync2(dirname3(dest), { recursive: true });
3121
+ writeFileSync8(dest, body, "utf8");
3122
+ return "write";
3123
+ }
3124
+ function readBundled(relPath) {
3125
+ const full = join15(packageRoot(), "templates", relPath);
3126
+ if (!existsSync15(full)) {
3127
+ throw new Error(`\u0428\u0430\u0431\u043B\u043E\u043D \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D: ${relPath}`);
3128
+ }
3129
+ return readFileSync18(full, "utf8");
3130
+ }
3131
+
3132
+ // src/commands/docs.ts
3133
+ async function cmdDocs(parsed) {
3134
+ const cwd = resolve4(parsed.options.dir || process.cwd());
3135
+ const dry = hasFlag(parsed, "dry");
3136
+ const force = hasFlag(parsed, "force") || !hasFlag(parsed, "keep");
3137
+ const lib = hasFlag(parsed, "lib") || isElcrmLibPackage(cwd);
3138
+ const kind = detectProjectKind(cwd);
3139
+ const names = docsForProject(kind, lib);
3140
+ console.log(`${dry ? "[dry] " : ""}elcrm docs \u2192 ${cwd}/docs (${kind}${lib ? ", lib" : ""})`);
3141
+ for (const name of names) {
3142
+ const body = readBundled(join16("elcrm-docs", name));
3143
+ const dest = join16(cwd, "docs", name);
3144
+ const action = writeScaffoldFile(dest, body, { dry, force });
3145
+ const mark = action === "write" ? c.green("+") : action === "skip" ? c.gray("=") : c.cyan("~");
3146
+ console.log(` ${mark} docs/${name}`);
3147
+ }
3148
+ if (kind === "server" && !lib) {
3149
+ console.log(c.gray("\u0441\u0435\u0440\u0432\u0435\u0440: SERVER, CLI, API, SOCKET"));
3150
+ }
3151
+ console.log("\u0414\u0430\u043B\u044C\u0448\u0435: elcrm cursor \u2014 \u043F\u0440\u0430\u0432\u0438\u043B\u0430 \u0432 .cursor/rules (+ .cursor/docs)");
3152
+ }
3153
+
3154
+ // src/commands/cursor.ts
3155
+ import { join as join17, resolve as resolve5 } from "path";
3156
+ var ALWAYS = ["elcrm.mdc", "elcrm-packages.mdc"];
3157
+ var UI = ["elcrm-ui.mdc", "elcrm-module.mdc", "elcrm-theme.mdc"];
3158
+ var SERVER = ["elcrm-server.mdc", "elcrm-rpc.mdc"];
3159
+ var LIB = ["elcrm-lib.mdc"];
3160
+ function writeOne(dest, body, dry, force, label) {
3161
+ const action = writeScaffoldFile(dest, body, { dry, force });
3162
+ const mark = action === "write" ? c.green("+") : action === "skip" ? c.gray("=") : c.cyan("~");
3163
+ console.log(` ${mark} ${label}`);
3164
+ }
3165
+ async function cmdCursor(parsed) {
3166
+ const cwd = resolve5(parsed.options.dir || process.cwd());
3167
+ const dry = hasFlag(parsed, "dry");
3168
+ const force = hasFlag(parsed, "force") || !hasFlag(parsed, "keep");
3169
+ const lib = hasFlag(parsed, "lib") || isElcrmLibPackage(cwd);
3170
+ const kind = detectProjectKind(cwd);
3171
+ const ruleNames = [
3172
+ ...ALWAYS,
3173
+ ...kind === "server" ? [] : UI,
3174
+ ...kind === "front" ? [] : SERVER,
3175
+ ...lib ? LIB : []
3176
+ ];
3177
+ console.log(`${dry ? "[dry] " : ""}elcrm cursor \u2192 ${cwd}/.cursor (${kind}${lib ? ", lib" : ""})`);
3178
+ for (const name of ruleNames) {
3179
+ let body = readBundled(join17("elcrm-cursor", "rules", name));
3180
+ if (kind === "server" && (name === "elcrm-server.mdc" || name === "elcrm-rpc.mdc")) {
3181
+ body = body.replace("alwaysApply: false", "alwaysApply: true").replace(/^globs:.*\n/m, "");
3182
+ }
3183
+ writeOne(join17(cwd, ".cursor", "rules", name), body, dry, force, `.cursor/rules/${name}`);
3184
+ }
3185
+ const docs = docsForProject(kind, lib);
3186
+ for (const name of docs) {
3187
+ const body = readBundled(join17("elcrm-docs", name));
3188
+ writeOne(join17(cwd, ".cursor", "docs", name), body, dry, force, `.cursor/docs/${name}`);
3189
+ }
3190
+ writeOne(join17(cwd, "AGENTS.md"), readBundled(join17("elcrm-cursor", "AGENTS.md")), dry, force, "AGENTS.md");
3191
+ console.log(c.gray("\u0410\u0433\u0435\u043D\u0442: AGENTS.md + rules (\u0443\u0437\u043A\u0438\u0435 glob) + .cursor/docs. \u0421\u0434\u0430\u0447\u0430: elcrm test."));
3192
+ }
3193
+
1679
3194
  // src/index.ts
1680
3195
  function cliVersion() {
1681
- const here = dirname2(fileURLToPath2(import.meta.url));
3196
+ const here = dirname4(fileURLToPath2(import.meta.url));
1682
3197
  for (const p of [
1683
- join11(here, "..", "package.json"),
1684
- join11(here, "package.json")
3198
+ join18(here, "..", "package.json"),
3199
+ join18(here, "package.json")
1685
3200
  ]) {
1686
3201
  try {
1687
- const v = JSON.parse(readFileSync9(p, "utf8")).version;
3202
+ const v = JSON.parse(readFileSync19(p, "utf8")).version;
1688
3203
  if (typeof v === "string" && v)
1689
3204
  return v;
1690
3205
  } catch {}
@@ -1701,8 +3216,9 @@ function showHelp() {
1701
3216
  elcrm <\u043A\u043E\u043C\u0430\u043D\u0434\u0430> [\u0430\u0440\u0433\u0443\u043C\u0435\u043D\u0442\u044B] [\u0444\u043B\u0430\u0433\u0438]
1702
3217
 
1703
3218
  \u041A\u043E\u043C\u0430\u043D\u0434\u044B:
1704
- update [--dry] [--core] [--no-install]
1705
- \u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u0432\u0441\u0435 @elcrm/* \u0432 package.json \u0434\u043E latest (\u0438\u043B\u0438 pinned \u0438\u0437 @elcrm/core)
3219
+ update [--dry] [--core] [--no-install] [--fix] [--test]
3220
+ \u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u0432\u0441\u0435 @elcrm/* \u0434\u043E latest; --fix = migrate front; --test = elcrm test
3221
+ \u0420\u0435\u043A\u043E\u043C\u0435\u043D\u0434\u0443\u0435\u0442\u0441\u044F: elcrm update --fix --test
1706
3222
 
1707
3223
  create <name> [--template app|lib|panel] [--no-install]
1708
3224
  \u0421\u043E\u0437\u0434\u0430\u0442\u044C \u043F\u0440\u043E\u0435\u043A\u0442 \u0438\u0437 \u0448\u0430\u0431\u043B\u043E\u043D\u0430 (\u043A\u0430\u043A create-react-app)
@@ -1713,7 +3229,11 @@ function showHelp() {
1713
3229
 
1714
3230
  migrate <name> [--dry]
1715
3231
  migrate --list
1716
- \u041C\u0438\u0433\u0440\u0430\u0446\u0438\u0438 \u043A\u043E\u0434\u0430/\u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0435\u0439 (\u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 socket-server)
3232
+ \u041C\u0438\u0433\u0440\u0430\u0446\u0438\u0438: front|ui, form-aliases, form-tokens, size-sml, field-border, modal-create, legacy-hacks, socket-server
3233
+
3234
+ audit [--fix] [--dry] [--dir=\u2026]
3235
+ \u0423\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0438\u0439 API + CSS-\u0442\u043E\u043A\u0435\u043D\u044B form (padding-\u0430\u043B\u0438\u0430\u0441\u044B, popup-shadow, border shorthand)
3236
+ --fix \u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0435\u0442 migrate front
1717
3237
 
1718
3238
  doctor [--fix]
1719
3239
  \u041F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u043F\u0440\u043E\u0435\u043A\u0442 \u0438 \u043F\u0440\u0438 \u043D\u0435\u043E\u0431\u0445\u043E\u0434\u0438\u043C\u043E\u0441\u0442\u0438 \u043F\u0440\u0438\u0432\u0435\u0441\u0442\u0438 \u043A \u0441\u0442\u0430\u043D\u0434\u0430\u0440\u0442\u0443
@@ -1729,6 +3249,18 @@ function showHelp() {
1729
3249
  \u041F\u043E\u0441\u043B\u0435 vite build lib-\u043F\u0430\u043A\u0435\u0442\u0430: \u0441\u043A\u043B\u0435\u0438\u0442\u044C CSS + \u0441\u043F\u043B\u044E\u0449\u0438\u0442\u044C .d.ts
1730
3250
  (package.json: "build": "vite build && elcrm postbuild")
1731
3251
 
3252
+ css [--dry] [--dir=\u2026]
3253
+ \u0414\u0435\u0444\u043E\u043B\u0442\u043D\u044B\u0435 \u0442\u043E\u043A\u0435\u043D\u044B @elcrm/* \u2192 src/style/theme.css, theme-light/dark, elcrm.css
3254
+ \u0423\u0436\u0435 \u0437\u0430\u0434\u0430\u043D\u043D\u044B\u0435 \u043F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u044B\u0435 \u043D\u0435 \u0437\u0430\u0442\u0438\u0440\u0430\u044E\u0442\u0441\u044F, \u0442\u043E\u043B\u044C\u043A\u043E \u043D\u0435\u0434\u043E\u0441\u0442\u0430\u044E\u0449\u0438\u0435 (\u0433\u0440\u0443\u043F\u043F\u044B \u0447\u0435\u0440\u0435\u0437 \u043F\u0443\u0441\u0442\u0443\u044E \u0441\u0442\u0440\u043E\u043A\u0443)
3255
+
3256
+ docs [--dry] [--keep] [--dir=\u2026] [--lib]
3257
+ docs/*.elCRM.md \u043F\u043E \u043F\u0430\u043A\u0435\u0442\u0430\u043C (cli, api, alert, button, modal, notice, \u2026)
3258
+ \u0441\u0435\u0440\u0432\u0435\u0440-only \u2192 SERVER, CLI, API, SOCKET; --lib \u0438\u043B\u0438 \u043F\u0430\u043A\u0435\u0442 @elcrm/* \u2192 \u0432\u0441\u0435 \u0444\u0430\u0439\u043B\u044B
3259
+
3260
+ cursor [--dry] [--keep] [--dir=\u2026] [--lib|-lib]
3261
+ .cursor/rules + .cursor/docs + AGENTS.md
3262
+ --lib: \u0447\u0435\u043A\u043B\u0438\u0441\u0442 \u0430\u0432\u0442\u043E\u0440\u0430 \u0431\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0438 (\u043F\u043E\u0441\u043B\u0435 \u043F\u0440\u0430\u0432\u043E\u043A src \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u044C CLI-\u0448\u043F\u0430\u0440\u0433\u0430\u043B\u043A\u0438)
3263
+
1732
3264
  uuid
1733
3265
  \u0421\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C UUID
1734
3266
 
@@ -1743,15 +3275,26 @@ function showHelp() {
1743
3275
  elcrm -v
1744
3276
  elcrm update
1745
3277
  elcrm update --dry
3278
+ elcrm update --fix --test
3279
+ elcrm migrate form-aliases --dry
3280
+ elcrm migrate form-tokens --dry
1746
3281
  elcrm create my-app --template app
1747
3282
  elcrm init --template panel
1748
3283
  elcrm init --template lib
1749
- elcrm migrate socket-server --dry
3284
+ elcrm migrate front --dry
3285
+ elcrm migrate size-sml
3286
+ elcrm audit
3287
+ elcrm audit --fix
1750
3288
  elcrm doctor --fix
1751
3289
  elcrm test
1752
3290
  elcrm test --server
1753
3291
  elcrm build --client --url=https://example.com/deploy
1754
3292
  elcrm postbuild
3293
+ elcrm css
3294
+ elcrm css --dry
3295
+ elcrm docs
3296
+ elcrm cursor
3297
+ elcrm cursor --lib
1755
3298
  `);
1756
3299
  }
1757
3300
  async function main() {
@@ -1792,6 +3335,18 @@ async function main() {
1792
3335
  case "postbuild":
1793
3336
  await cmdPostbuild(parsed);
1794
3337
  break;
3338
+ case "css":
3339
+ await cmdCss(parsed);
3340
+ break;
3341
+ case "audit":
3342
+ await cmdAudit(parsed);
3343
+ break;
3344
+ case "docs":
3345
+ await cmdDocs(parsed);
3346
+ break;
3347
+ case "cursor":
3348
+ await cmdCursor(parsed);
3349
+ break;
1795
3350
  case "uuid":
1796
3351
  await cmdUuid();
1797
3352
  break;