elcrm 1.0.0 → 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 (47) hide show
  1. package/README.md +10 -0
  2. package/dist/index.js +2828 -425
  3. package/dist/vite/discover-lib.js +4 -4
  4. package/dist/vite/plugin-css-scoped.js +2 -2
  5. package/dist/vite/postbuild.d.ts +5 -0
  6. package/dist/vite/postbuild.js +59 -0
  7. package/package.json +10 -4
  8. package/templates/elcrm-cursor/AGENTS.md +13 -0
  9. package/templates/elcrm-cursor/rules/elcrm-lib.mdc +101 -0
  10. package/templates/elcrm-cursor/rules/elcrm-module.mdc +25 -0
  11. package/templates/elcrm-cursor/rules/elcrm-packages.mdc +43 -0
  12. package/templates/elcrm-cursor/rules/elcrm-rpc.mdc +23 -0
  13. package/templates/elcrm-cursor/rules/elcrm-server.mdc +30 -0
  14. package/templates/elcrm-cursor/rules/elcrm-theme.mdc +13 -0
  15. package/templates/elcrm-cursor/rules/elcrm-ui.mdc +41 -0
  16. package/templates/elcrm-cursor/rules/elcrm.mdc +43 -0
  17. package/templates/elcrm-docs/ALERT.elCRM.md +26 -0
  18. package/templates/elcrm-docs/API.elCRM.md +38 -0
  19. package/templates/elcrm-docs/BUTTON.elCRM.md +36 -0
  20. package/templates/elcrm-docs/CLI.elCRM.md +27 -0
  21. package/templates/elcrm-docs/COMPONENTS.elCRM.md +68 -0
  22. package/templates/elcrm-docs/FORM.elCRM.md +39 -0
  23. package/templates/elcrm-docs/ICONS.elCRM.md +40 -0
  24. package/templates/elcrm-docs/MODAL.elCRM.md +41 -0
  25. package/templates/elcrm-docs/NOTICE.elCRM.md +31 -0
  26. package/templates/elcrm-docs/NOTIFICATIONS.elCRM.md +39 -0
  27. package/templates/elcrm-docs/PASSKEY.elCRM.md +26 -0
  28. package/templates/elcrm-docs/ROUTER.elCRM.md +34 -0
  29. package/templates/elcrm-docs/SERVER.elCRM.md +53 -0
  30. package/templates/elcrm-docs/SOCKET.elCRM.md +34 -0
  31. package/templates/elcrm-docs/TOOLTIP.elCRM.md +25 -0
  32. package/templates/panel/README.md +2 -26
  33. package/templates/panel/api/package.json +2 -5
  34. package/templates/panel/api/src/index.ts +11 -20
  35. package/templates/panel/api/src/lib/cron.ts +10 -7
  36. package/templates/panel/api/src/lib/db.ts +3 -5
  37. package/templates/panel/api/src/lib/jwt.ts +1 -1
  38. package/templates/panel/api/src/routes/auth.ts +20 -8
  39. package/templates/panel/web/package.json +4 -4
  40. package/templates/panel/web/src/modules/Account/sections/Profile.tsx +0 -1
  41. package/templates/panel/web/src/modules/Settings/sections/General.tsx +0 -2
  42. package/templates/panel/web/src/modules/Settings/sections/Profile.tsx +0 -1
  43. package/templates/panel/web/src/pages/Auth.tsx +0 -1
  44. package/templates/panel/web/src/style/elcrm.css +4 -1
  45. package/templates/panel/web/src/style/theme-dark.css +65 -3
  46. package/templates/panel/web/src/style/theme-light.css +65 -3
  47. package/templates/panel/web/src/style/theme.css +73 -3
package/dist/index.js CHANGED
@@ -1,5 +1,742 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
+ var __defProp = Object.defineProperty;
4
+ var __returnValue = (v) => v;
5
+ function __exportSetter(name, newValue) {
6
+ this[name] = __returnValue.bind(null, newValue);
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);
18
+
19
+ // src/lib/latest.ts
20
+ async function fetchLatestVersion(name) {
21
+ if (CACHE.has(name))
22
+ return CACHE.get(name);
23
+ const url = `https://registry.npmjs.org/${encodeURIComponent(name)}/latest`;
24
+ try {
25
+ const res = await fetch(url, {
26
+ headers: { Accept: "application/json" }
27
+ });
28
+ if (!res.ok)
29
+ return null;
30
+ const data = await res.json();
31
+ if (!data.version)
32
+ return null;
33
+ CACHE.set(name, data.version);
34
+ return data.version;
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+ async function fetchLatestMap(names) {
40
+ const map = new Map;
41
+ const results = await Promise.all(names.map(async (name) => {
42
+ const v = await fetchLatestVersion(name);
43
+ return [name, v];
44
+ }));
45
+ for (const [name, v] of results) {
46
+ if (v)
47
+ map.set(name, v);
48
+ }
49
+ return map;
50
+ }
51
+ async function fetchCorePinnedVersions() {
52
+ const url = "https://registry.npmjs.org/@elcrm%2Fcore/latest";
53
+ const map = new Map;
54
+ try {
55
+ const res = await fetch(url, {
56
+ headers: { Accept: "application/json" }
57
+ });
58
+ if (!res.ok)
59
+ return map;
60
+ const data = await res.json();
61
+ if (data.version)
62
+ map.set("@elcrm/core", data.version);
63
+ for (const [name, ver] of Object.entries(data.dependencies ?? {})) {
64
+ if (name.startsWith("@elcrm/")) {
65
+ map.set(name, ver.replace(/^[\^~]/, ""));
66
+ }
67
+ }
68
+ } catch {}
69
+ return map;
70
+ }
71
+ var CACHE;
72
+ var init_latest = __esm(() => {
73
+ CACHE = new Map;
74
+ });
75
+
76
+ // src/lib/pkg.ts
77
+ import { existsSync as existsSync2, readFileSync, writeFileSync } from "fs";
78
+ import { join as join2 } from "path";
79
+ function packageJsonPath(cwd = process.cwd()) {
80
+ return join2(cwd, "package.json");
81
+ }
82
+ function readPackageJson(cwd = process.cwd()) {
83
+ const path = packageJsonPath(cwd);
84
+ if (!existsSync2(path))
85
+ return null;
86
+ try {
87
+ return JSON.parse(readFileSync(path, "utf8"));
88
+ } catch {
89
+ return null;
90
+ }
91
+ }
92
+ function writePackageJson(pkg, cwd = process.cwd()) {
93
+ writeFileSync(packageJsonPath(cwd), JSON.stringify(pkg, null, 2) + `
94
+ `, "utf8");
95
+ }
96
+ var init_pkg = () => {};
97
+
98
+ // src/lib/migrate/socket-server.ts
99
+ import {
100
+ existsSync as existsSync3,
101
+ readdirSync,
102
+ readFileSync as readFileSync2,
103
+ statSync,
104
+ writeFileSync as writeFileSync2
105
+ } from "fs";
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);
121
+ }
122
+ }
123
+ return out;
124
+ }
125
+ function rewriteImports(source) {
126
+ let hits = 0;
127
+ let next = source;
128
+ const patterns = [
129
+ [
130
+ /from\s+["']@elcrm\/socket\/server["']/g,
131
+ 'from "@elcrm/server"'
132
+ ],
133
+ [
134
+ /from\s+["']@elcrm\/socket-server["']/g,
135
+ 'from "@elcrm/server"'
136
+ ],
137
+ [
138
+ /require\s*\(\s*["']@elcrm\/socket\/server["']\s*\)/g,
139
+ 'require("@elcrm/server")'
140
+ ],
141
+ [
142
+ /require\s*\(\s*["']@elcrm\/socket-server["']\s*\)/g,
143
+ 'require("@elcrm/server")'
144
+ ]
145
+ ];
146
+ for (const [re, repl] of patterns) {
147
+ const before = next;
148
+ next = next.replace(re, () => {
149
+ hits++;
150
+ return repl;
151
+ });
152
+ }
153
+ return { next, hits };
154
+ }
155
+ async function migrateSocketServer(cwd, dry) {
156
+ const changes = [];
157
+ const pkg = readPackageJson(cwd);
158
+ let removedSocketServer = false;
159
+ if (pkg) {
160
+ for (const section of [
161
+ "dependencies",
162
+ "devDependencies",
163
+ "peerDependencies"
164
+ ]) {
165
+ const map = pkg[section];
166
+ if (!map)
167
+ continue;
168
+ if (map["@elcrm/socket-server"]) {
169
+ removedSocketServer = true;
170
+ changes.push({
171
+ file: "package.json",
172
+ detail: `\u0443\u0434\u0430\u043B\u0438\u0442\u044C ${section}["@elcrm/socket-server"]`
173
+ });
174
+ if (!dry)
175
+ delete map["@elcrm/socket-server"];
176
+ }
177
+ }
178
+ const hasServer = Boolean(pkg.dependencies?.["@elcrm/server"]) || Boolean(pkg.devDependencies?.["@elcrm/server"]);
179
+ if (removedSocketServer && !hasServer) {
180
+ const latest = dry ? null : await fetchLatestVersion("@elcrm/server");
181
+ const ver = latest ? `^${latest}` : "^0.5.3";
182
+ changes.push({
183
+ file: "package.json",
184
+ detail: `\u0434\u043E\u0431\u0430\u0432\u0438\u0442\u044C dependencies["@elcrm/server"] = ${ver}`
185
+ });
186
+ if (!dry) {
187
+ if (!pkg.dependencies)
188
+ pkg.dependencies = {};
189
+ pkg.dependencies["@elcrm/server"] = ver;
190
+ }
191
+ }
192
+ if (!dry && changes.some((c2) => c2.file === "package.json")) {
193
+ writePackageJson(pkg, cwd);
194
+ }
195
+ }
196
+ const roots = ["src", "app", "server", "lib"].map((d) => join3(cwd, d));
197
+ const files = roots.flatMap((r) => walkFiles(r));
198
+ for (const file of files) {
199
+ const raw = readFileSync2(file, "utf8");
200
+ const { next, hits } = rewriteImports(raw);
201
+ if (hits > 0) {
202
+ changes.push({
203
+ file: file.replace(cwd + "/", ""),
204
+ detail: `${hits} \u0437\u0430\u043C\u0435\u043D(\u044B) \u0438\u043C\u043F\u043E\u0440\u0442\u043E\u0432 \u2192 @elcrm/server`
205
+ });
206
+ if (!dry)
207
+ writeFileSync2(file, next, "utf8");
208
+ }
209
+ }
210
+ return changes;
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
+ });
234
+
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))
249
+ continue;
250
+ const full = join4(dir, name);
251
+ let st;
252
+ try {
253
+ st = statSync2(full);
254
+ } catch {
255
+ continue;
256
+ }
257
+ if (st.isDirectory())
258
+ walkCodeFiles(full, out);
259
+ else if (/\.(tsx?|jsx?|mjs|cjs|css)$/.test(name))
260
+ out.push(full);
261
+ }
262
+ return out;
263
+ }
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;
300
+ });
301
+ }
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`
318
+ });
319
+ }
320
+ }
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}`;
361
+ });
362
+ }
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`
377
+ });
378
+ }
379
+ }
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\(\)\}/;
399
+ for (const file of files) {
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;
411
+ }
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}`;
427
+ });
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)"
433
+ });
434
+ }
435
+ continue;
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)`
442
+ });
443
+ }
444
+ return changes;
445
+ }
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);
460
+ }
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;
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
+ });
487
+
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}`);
501
+ }
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
+ }
520
+ }
521
+ return changes;
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
+ });
535
+
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
+ `);
565
+ }
566
+ return { next, n };
567
+ }
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 };
583
+ }
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
+ });
592
+ }
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)
602
+ continue;
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)
619
+ continue;
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
+ });
625
+ }
626
+ }
627
+ return changes;
628
+ }
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";
3
740
 
4
741
  // src/parseArgs.ts
5
742
  function parseArgs(argv = process.argv.slice(2)) {
@@ -38,156 +775,1362 @@ function parseArgs(argv = process.argv.slice(2)) {
38
775
  continue;
39
776
  }
40
777
  }
41
- if (body && !flags.includes(body))
42
- flags.push(body);
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
+ }
43
1083
  continue;
44
1084
  }
45
- if (arg.startsWith("-") && arg.length > 1) {
46
- const short = arg.slice(1);
47
- if (short === "t" && argv[i + 1] && !argv[i + 1].startsWith("-")) {
48
- options.template = argv[i + 1];
49
- i++;
50
- continue;
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);
51
1101
  }
52
- if (short && !flags.includes(short))
53
- flags.push(short);
54
- continue;
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};`);
55
1136
  }
56
- positionals.push(arg);
57
1137
  }
58
- const [command = "", ...rest] = positionals;
59
- return {
60
- command,
61
- positionals: rest,
62
- options,
63
- flags
64
- };
1138
+ lines.push("");
1139
+ return lines.join(`
1140
+ `);
65
1141
  }
66
- function hasFlag(parsed, ...names) {
67
- return names.some((n) => parsed.flags.includes(n));
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 };
68
1281
  }
69
1282
 
70
- // src/lib/elcrmDeps.ts
71
- var SECTIONS = [
72
- "dependencies",
73
- "devDependencies",
74
- "peerDependencies"
75
- ];
76
- function listElcrmDeps(pkg) {
77
- const out = [];
78
- for (const section of SECTIONS) {
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
+ ]) {
79
1295
  const map = pkg[section];
80
- if (!map || typeof map !== "object")
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);
81
1322
  continue;
82
- for (const [name, version] of Object.entries(map)) {
83
- if (name.startsWith("@elcrm/") && typeof version === "string") {
84
- out.push({ name, version, section });
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);
85
1332
  }
86
1333
  }
87
1334
  }
88
1335
  return out;
89
1336
  }
90
- function uniqueElcrmNames(deps) {
91
- return [...new Set(deps.map((d) => d.name))].sort();
92
- }
93
-
94
- // src/lib/latest.ts
95
- var CACHE = new Map;
96
- async function fetchLatestVersion(name) {
97
- if (CACHE.has(name))
98
- return CACHE.get(name);
99
- const url = `https://registry.npmjs.org/${encodeURIComponent(name)}/latest`;
1337
+ function readPkgAt(dir) {
1338
+ const path = join7(dir, "package.json");
1339
+ if (!existsSync7(path))
1340
+ return null;
100
1341
  try {
101
- const res = await fetch(url, {
102
- headers: { Accept: "application/json" }
103
- });
104
- if (!res.ok)
105
- return null;
106
- const data = await res.json();
107
- if (!data.version)
108
- return null;
109
- CACHE.set(name, data.version);
110
- return data.version;
1342
+ return JSON.parse(readFileSync11(path, "utf8"));
111
1343
  } catch {
112
1344
  return null;
113
1345
  }
114
1346
  }
115
- async function fetchLatestMap(names) {
116
- const map = new Map;
117
- const results = await Promise.all(names.map(async (name) => {
118
- const v = await fetchLatestVersion(name);
119
- return [name, v];
120
- }));
121
- for (const [name, v] of results) {
122
- if (v)
123
- map.set(name, v);
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
+
1626
+ // src/lib/test/serverScan.ts
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";
1629
+
1630
+ // src/lib/test/serverRules.ts
1631
+ var CODE_RULES = [
1632
+ {
1633
+ id: "import-elcrm-db",
1634
+ severity: "error",
1635
+ test: /from\s+["']@elcrm\/db["']/,
1636
+ message: "\u041F\u0430\u043A\u0435\u0442 @elcrm/db \u0432\u043B\u0438\u0442 \u0432 @elcrm/server/db",
1637
+ fix: `import { \u2026 } from "@elcrm/server/db"`,
1638
+ auto: true,
1639
+ replace: [
1640
+ {
1641
+ from: /from\s+["']@elcrm\/db["']/g,
1642
+ to: 'from "@elcrm/server/db"'
1643
+ }
1644
+ ]
1645
+ },
1646
+ {
1647
+ id: "import-elcrm-jwt",
1648
+ severity: "error",
1649
+ test: /from\s+["']@elcrm\/jwt["']/,
1650
+ message: "\u041F\u0430\u043A\u0435\u0442 @elcrm/jwt \u0432\u043B\u0438\u0442 \u0432 @elcrm/server/jwt",
1651
+ fix: `import { \u2026 } from "@elcrm/server/jwt"`,
1652
+ auto: true,
1653
+ replace: [
1654
+ {
1655
+ from: /from\s+["']@elcrm\/jwt["']/g,
1656
+ to: 'from "@elcrm/server/jwt"'
1657
+ }
1658
+ ]
1659
+ },
1660
+ {
1661
+ id: "import-elcrm-cron",
1662
+ severity: "error",
1663
+ test: /from\s+["']@elcrm\/cron["']/,
1664
+ message: "\u041F\u0430\u043A\u0435\u0442 @elcrm/cron \u0432\u043B\u0438\u0442 \u0432 @elcrm/server/cron",
1665
+ fix: `import { cron, createCron } from "@elcrm/server/cron"`,
1666
+ auto: true,
1667
+ replace: [
1668
+ {
1669
+ from: /from\s+["']@elcrm\/cron["']/g,
1670
+ to: 'from "@elcrm/server/cron"'
1671
+ }
1672
+ ]
1673
+ },
1674
+ {
1675
+ id: "import-elcrm-tb",
1676
+ severity: "error",
1677
+ test: /from\s+["']@elcrm\/tb["']/,
1678
+ message: "\u041F\u0430\u043A\u0435\u0442 @elcrm/tb \u0432\u043B\u0438\u0442 \u0432 @elcrm/server/tb",
1679
+ fix: `import { tb, createTb } from "@elcrm/server/tb"`,
1680
+ auto: true,
1681
+ replace: [
1682
+ {
1683
+ from: /from\s+["']@elcrm\/tb["']/g,
1684
+ to: 'from "@elcrm/server/tb"'
1685
+ }
1686
+ ]
1687
+ },
1688
+ {
1689
+ id: "createWsHub",
1690
+ severity: "error",
1691
+ test: /\bcreateWsHub\b/,
1692
+ message: "createWsHub \u0443\u0434\u0430\u043B\u0451\u043D",
1693
+ fix: 'createSocket("ws").use(\u2026) + sendSocket \u2014 \u0441\u043C. MPC.md',
1694
+ auto: false
1695
+ },
1696
+ {
1697
+ id: "serverConnect",
1698
+ severity: "error",
1699
+ test: /\bserverConnect\b/,
1700
+ message: "serverConnect \u0443\u0434\u0430\u043B\u0451\u043D",
1701
+ fix: 'createSocket("ws").use("auth"|name, handler) \u2014 \u0441\u043C. MPC.md',
1702
+ auto: false
1703
+ },
1704
+ {
1705
+ id: "sendListen",
1706
+ severity: "error",
1707
+ test: /\.sendListen\b/,
1708
+ message: "hub.sendListen \u2192 sendSocket",
1709
+ fix: `import { sendSocket } from "@elcrm/server/ws"; sendSocket(uid, listen, data)`,
1710
+ auto: true,
1711
+ replace: [
1712
+ {
1713
+ from: /(\w+)\.sendListen\(/g,
1714
+ to: "sendSocket("
1715
+ },
1716
+ {
1717
+ from: /(\w+)\.sendListenAll\(/g,
1718
+ to: "sendSocketAll("
1719
+ }
1720
+ ]
1721
+ },
1722
+ {
1723
+ id: "sendListenAll",
1724
+ severity: "error",
1725
+ test: /\.sendListenAll\b/,
1726
+ message: "hub.sendListenAll \u2192 sendSocketAll",
1727
+ fix: `import { sendSocketAll } from "@elcrm/server/ws"`,
1728
+ auto: true,
1729
+ replace: [
1730
+ {
1731
+ from: /(\w+)\.sendListenAll\(/g,
1732
+ to: "sendSocketAll("
1733
+ }
1734
+ ]
1735
+ },
1736
+ {
1737
+ id: "createServerFromEnv",
1738
+ severity: "error",
1739
+ test: /\bcreateServerFromEnv\b/,
1740
+ message: "createServerFromEnv \u0443\u0434\u0430\u043B\u0451\u043D",
1741
+ fix: "createServer() \u2014 env \u0432\u043D\u0443\u0442\u0440\u0438",
1742
+ auto: true,
1743
+ replace: [
1744
+ {
1745
+ from: /\bcreateServerFromEnv\b/g,
1746
+ to: "createServer"
1747
+ }
1748
+ ]
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
+ },
1758
+ {
1759
+ id: "createJwtFromEnv",
1760
+ severity: "warn",
1761
+ test: /\bcreateJwtFromEnv\b/,
1762
+ message: "createJwtFromEnv \u0443\u0434\u0430\u043B\u0451\u043D",
1763
+ fix: "createJWT()",
1764
+ auto: true,
1765
+ replace: [{ from: /\bcreateJwtFromEnv\b/g, to: "createJWT" }]
1766
+ },
1767
+ {
1768
+ id: "createCronFromEnv",
1769
+ severity: "warn",
1770
+ test: /\bcreateCronFromEnv\b/,
1771
+ message: "createCronFromEnv \u0443\u0434\u0430\u043B\u0451\u043D",
1772
+ fix: "createCron() \u0438\u043B\u0438 cron",
1773
+ auto: true,
1774
+ replace: [{ from: /\bcreateCronFromEnv\b/g, to: "createCron" }]
1775
+ },
1776
+ {
1777
+ id: "createTbFromEnv",
1778
+ severity: "warn",
1779
+ test: /\bcreateTbFromEnv\b/,
1780
+ message: "createTbFromEnv \u0443\u0434\u0430\u043B\u0451\u043D",
1781
+ fix: "createTb() \u0438\u043B\u0438 TELEGRAM= + createServer()",
1782
+ auto: true,
1783
+ replace: [{ from: /\bcreateTbFromEnv\b/g, to: "createTb" }]
1784
+ },
1785
+ {
1786
+ id: "registerOpsRouters",
1787
+ severity: "warn",
1788
+ test: /\bregisterOpsRouters\b/,
1789
+ message: "registerOpsRouters \u0443\u0434\u0430\u043B\u0451\u043D",
1790
+ fix: 'createRouter("health"|"system"|"logs").use(\u2026)',
1791
+ auto: false
1792
+ },
1793
+ {
1794
+ id: "Cron-useJob",
1795
+ severity: "info",
1796
+ test: /\.useJob\s*\(/,
1797
+ message: "useJob \u0443\u0441\u0442\u0430\u0440\u0435\u043B",
1798
+ fix: "cron.use(\u2026)",
1799
+ auto: true,
1800
+ replace: [{ from: /\.useJob\s*\(/g, to: ".use(" }]
1801
+ },
1802
+ {
1803
+ id: "WsHubApi-type",
1804
+ severity: "warn",
1805
+ test: /\bWsHubApi\b/,
1806
+ message: "\u0442\u0438\u043F WsHubApi \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u044D\u043A\u0441\u043F\u043E\u0440\u0442\u0438\u0440\u0443\u0435\u0442\u0441\u044F",
1807
+ fix: "sendSocket / getSocket(); \u0442\u0438\u043F \u043D\u0435 \u043D\u0443\u0436\u0435\u043D",
1808
+ auto: false
1809
+ },
1810
+ {
1811
+ id: "homemade-smtp-auth-login",
1812
+ severity: "error",
1813
+ test: /\bAUTH LOGIN\b/,
1814
+ message: "\u0421\u0430\u043C\u043E\u043F\u0438\u0441\u043D\u044B\u0439 SMTP (AUTH LOGIN) \u2014 \u043B\u0435\u0433\u0430\u0441\u0438",
1815
+ fix: 'createMailer / sendMail \u0438\u0437 "@elcrm/server/mail" \u2014 \u0441\u043C. MPC.md',
1816
+ auto: false
1817
+ },
1818
+ {
1819
+ id: "import-nodemailer",
1820
+ severity: "error",
1821
+ test: /from\s+["']nodemailer["']|require\s*\(\s*["']nodemailer["']\s*\)/,
1822
+ message: "nodemailer \u2192 @elcrm/server/mail",
1823
+ fix: 'import { createMailer, sendMail } from "@elcrm/server/mail"',
1824
+ auto: false
1825
+ },
1826
+ {
1827
+ id: "import-web-push",
1828
+ severity: "error",
1829
+ test: /from\s+["']web-push["']|require\s*\(\s*["']web-push["']\s*\)/,
1830
+ message: "web-push \u2192 @elcrm/server/notifications",
1831
+ fix: 'import { \u2026 } from "@elcrm/server/notifications"',
1832
+ auto: false
1833
+ },
1834
+ {
1835
+ id: "cors-origin-singular",
1836
+ severity: "warn",
1837
+ test: /process\.env\.CORS_ORIGIN\b(?!S)/,
1838
+ message: "process.env.CORS_ORIGIN \u2192 CORS_ORIGINS (\u043F\u0430\u043A\u0435\u0442 createServer)",
1839
+ fix: "CORS_ORIGINS=https://a.example,https://b.example \u2014 \u0431\u0435\u0437 path; \u0441\u0432\u043E\u0439 cors({ origin }) \u2014 \u043E\u043A",
1840
+ auto: false
1841
+ },
1842
+ {
1843
+ id: "rest-path-equals-api",
1844
+ severity: "error",
1845
+ test: /path\s*===\s*["']\/api\//,
1846
+ message: 'REST if (path === "/api/\u2026") \u2014 \u043B\u0435\u0433\u0430\u0441\u0438',
1847
+ fix: 'createRouter("name").use("method", handler) + rpcOk/rpcErr \u2014 \u0441\u043C. MPC.md',
1848
+ auto: false
1849
+ },
1850
+ {
1851
+ id: "rest-handle-export",
1852
+ severity: "error",
1853
+ test: /export\s+async\s+function\s+handle\w+\s*\([^)]*\)\s*:\s*Promise<\s*Response\b/,
1854
+ message: "export handle*(ctx): Promise<Response> \u2014 REST-\u043B\u0435\u0433\u0430\u0441\u0438",
1855
+ fix: "\u043B\u043E\u0433\u0438\u043A\u0430 \u0432 createRouter(\u2026).use(\u2026); Response \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F git/hooks/static",
1856
+ auto: false
1857
+ },
1858
+ {
1859
+ id: "rpc-bridge-invoke-handle",
1860
+ severity: "warn",
1861
+ test: /invoke(?:Authed|Public|Repo)\s*\(\s*handle\w+/,
1862
+ message: "RPC-\u043C\u043E\u0441\u0442 invoke*(handle*) \u043F\u043E\u0432\u0435\u0440\u0445 REST \u2014 \u043F\u0440\u043E\u043C\u0435\u0436\u0443\u0442\u043E\u0447\u043D\u044B\u0439 \u043B\u0435\u0433\u0430\u0441\u0438",
1863
+ fix: "\u043F\u0435\u0440\u0435\u043D\u0435\u0441\u0442\u0438 \u0442\u0435\u043B\u043E handler \u0432 createRouter().use(); \u0443\u0431\u0440\u0430\u0442\u044C routes/api",
1864
+ auto: false
1865
+ },
1866
+ {
1867
+ id: "routes-api-import",
1868
+ severity: "warn",
1869
+ test: /from\s+["'][^"']*\/routes\/api\/[^"']+["']/,
1870
+ message: "\u0438\u043C\u043F\u043E\u0440\u0442 \u0438\u0437 routes/api \u2014 REST-\u043B\u0435\u0433\u0430\u0441\u0438",
1871
+ fix: "createRouter \u0432 src/rpc (\u0438\u043B\u0438 \u0440\u044F\u0434\u043E\u043C \u0441 createServer); \u0431\u0435\u0437 handle* \u043C\u043E\u0441\u0442\u0430",
1872
+ auto: false
1873
+ },
1874
+ {
1875
+ id: "cron-useJob",
1876
+ severity: "warn",
1877
+ test: /\b(?:Cron\.)?useJob\s*\(/,
1878
+ message: "Cron.useJob deprecated",
1879
+ fix: 'import { cron } from "@elcrm/server/cron"; cron.use(fn, interval?, name?)',
1880
+ auto: false
1881
+ },
1882
+ {
1883
+ id: "cron-namespace",
1884
+ severity: "info",
1885
+ test: /import\s*\{[^}]*\bCron\b[^}]*\}\s*from\s*["']@elcrm\/server\/cron["']/,
1886
+ message: "namespace Cron deprecated \u2014 singleton cron",
1887
+ fix: 'import { cron } from "@elcrm/server/cron"',
1888
+ auto: false
1889
+ },
1890
+ {
1891
+ id: "manual-jwt-init-handler",
1892
+ severity: "warn",
1893
+ test: /\bjwt\.init\s*\(/,
1894
+ message: "jwt.init \u0432 handler \u2014 createServer \u0443\u0436\u0435 \u043A\u043B\u0430\u0434\u0451\u0442 user; \u043D\u0435 \u043F\u0430\u0440\u0441\u0438\u0442\u044C ssid \u043F\u043E\u0432\u0442\u043E\u0440\u043D\u043E",
1895
+ fix: "\u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439\u0442\u0435 params.user; \u0434\u043B\u044F \u0433\u0435\u0439\u0442\u0430 \u2014 middleware (if (!user?.id) return rpcLogout())",
1896
+ auto: false
1897
+ },
1898
+ {
1899
+ id: "createSessionMiddleware-reparse",
1900
+ severity: "info",
1901
+ test: /createSessionMiddleware\s*\(\s*\{[^}]*resolve\s*:\s*async/,
1902
+ message: "createSessionMiddleware \u0441 resolve \u0447\u0430\u0441\u0442\u043E \u0441\u043D\u043E\u0432\u0430 \u0437\u043E\u0432\u0451\u0442 jwt.init \u2014 \u0434\u0443\u0431\u043B\u044C",
1903
+ fix: "\u0435\u0441\u043B\u0438 createServer({ jwt }) \u0443\u0436\u0435 \u0435\u0441\u0442\u044C: middleware \u0442\u043E\u043B\u044C\u043A\u043E if (!user?.id) return rpcLogout()",
1904
+ auto: false
1905
+ },
1906
+ {
1907
+ id: "node-fs-import",
1908
+ severity: "warn",
1909
+ test: /from\s+["'](?:node:)?fs(?:\/promises)?["']/,
1910
+ message: "import node:fs / fs \u2014 \u0432 Bun-\u0441\u0435\u0440\u0432\u0435\u0440\u0435 \u043D\u0435 \u043D\u0443\u0436\u0435\u043D \u0434\u043B\u044F \u0442\u0438\u043F\u043E\u0432\u044B\u0445 \u0437\u0430\u0434\u0430\u0447",
1911
+ fix: "SQLite: initDB \u0441\u0430\u043C \u0441\u043E\u0437\u0434\u0430\u0451\u0442 \u043A\u0430\u0442\u0430\u043B\u043E\u0433; \u0444\u0430\u0439\u043B\u044B \u2014 Bun.file / Bun.write; \u043F\u0443\u0442\u0438 \u2014 \u0431\u0435\u0437 node:path",
1912
+ auto: false
1913
+ },
1914
+ {
1915
+ id: "node-path-import",
1916
+ severity: "warn",
1917
+ test: /from\s+["'](?:node:)?path["']/,
1918
+ message: "import node:path / path \u2014 \u0438\u0437\u0431\u0435\u0433\u0430\u0439\u0442\u0435 \u0432 app \u043D\u0430 @elcrm/server",
1919
+ fix: "\u043E\u0442\u043D\u043E\u0441\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0435 \u0441\u0442\u0440\u043E\u043A\u0438 / URL; mkdir \u0434\u043B\u044F sqlite \u0434\u0435\u043B\u0430\u0435\u0442 @elcrm/server/db",
1920
+ auto: false
1921
+ }
1922
+ ];
1923
+ var PKG_LEGACY = [
1924
+ "@elcrm/db",
1925
+ "@elcrm/jwt",
1926
+ "@elcrm/cron",
1927
+ "@elcrm/tb",
1928
+ "nodemailer",
1929
+ "web-push"
1930
+ ];
1931
+
1932
+ // src/lib/test/serverScan.ts
1933
+ var SKIP2 = new Set([
1934
+ "node_modules",
1935
+ "dist",
1936
+ ".git",
1937
+ "coverage",
1938
+ ".bun",
1939
+ "vendor"
1940
+ ]);
1941
+ function walkFiles2(root, dir, out) {
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
+ });
124
1983
  }
125
- return map;
1984
+ return findings;
126
1985
  }
127
- async function fetchCorePinnedVersions() {
128
- const url = "https://registry.npmjs.org/@elcrm%2Fcore/latest";
129
- const map = new Map;
1986
+ function scanPackageJson(root, file, text) {
1987
+ const findings = [];
1988
+ const rel2 = relative3(root, file) || file;
1989
+ let pkg;
130
1990
  try {
131
- const res = await fetch(url, {
132
- headers: { Accept: "application/json" }
133
- });
134
- if (!res.ok)
135
- return map;
136
- const data = await res.json();
137
- if (data.version)
138
- map.set("@elcrm/core", data.version);
139
- for (const [name, ver] of Object.entries(data.dependencies ?? {})) {
140
- if (name.startsWith("@elcrm/")) {
141
- map.set(name, ver.replace(/^[\^~]/, ""));
142
- }
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
+ });
143
2008
  }
144
- } catch {}
145
- return map;
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;
146
2023
  }
147
-
148
- // src/lib/install.ts
149
- import { existsSync } from "fs";
150
- import { join } from "path";
151
- import { spawnSync } from "child_process";
152
- function detectPackageManager(cwd = process.cwd()) {
153
- if (existsSync(join(cwd, "bun.lockb")) || existsSync(join(cwd, "bun.lock"))) {
154
- return "bun";
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
+ }
155
2035
  }
156
- const bun = spawnSync("bun", ["--version"], { encoding: "utf8" });
157
- if (bun.status === 0)
158
- return "bun";
159
- return "npm";
2036
+ return { root, findings, filesScanned: files.length };
160
2037
  }
161
- function installDeps(cwd = process.cwd()) {
162
- const pm = detectPackageManager(cwd);
163
- 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`);
164
- const r = spawnSync(pm, ["install"], {
165
- cwd,
166
- stdio: "inherit",
167
- env: process.env
168
- });
169
- return r.status === 0;
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;
170
2074
  }
171
2075
 
172
- // src/lib/pkg.ts
173
- import { existsSync as existsSync2, readFileSync, writeFileSync } from "fs";
174
- import { join as join2 } from "path";
175
- function packageJsonPath(cwd = process.cwd()) {
176
- return join2(cwd, "package.json");
177
- }
178
- function readPackageJson(cwd = process.cwd()) {
179
- const path = packageJsonPath(cwd);
180
- if (!existsSync2(path))
181
- return null;
182
- try {
183
- return JSON.parse(readFileSync(path, "utf8"));
184
- } catch {
185
- return null;
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;
186
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;
187
2103
  }
188
- function writePackageJson(pkg, cwd = process.cwd()) {
189
- writeFileSync(packageJsonPath(cwd), JSON.stringify(pkg, null, 2) + `
190
- `, "utf8");
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`));
191
2134
  }
192
2135
 
193
2136
  // src/commands/update.ts
@@ -206,6 +2149,8 @@ async function cmdUpdate(parsed) {
206
2149
  const dry = hasFlag(parsed, "dry");
207
2150
  const useCore = hasFlag(parsed, "core");
208
2151
  const noInstall = hasFlag(parsed, "no-install");
2152
+ const doFix = hasFlag(parsed, "fix");
2153
+ const doTest = hasFlag(parsed, "test");
209
2154
  const names = uniqueElcrmNames(deps);
210
2155
  console.log(`\u041D\u0430\u0439\u0434\u0435\u043D\u043E @elcrm/*: ${names.length}`);
211
2156
  let versions;
@@ -227,17 +2172,17 @@ async function cmdUpdate(parsed) {
227
2172
  for (const dep of deps) {
228
2173
  const latest = versions.get(dep.name);
229
2174
  if (!latest) {
230
- rows.push(` ? ${dep.name}: ${dep.version} \u2192 (\u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043E \u043D\u0430 npm)`);
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)")}`);
231
2176
  continue;
232
2177
  }
233
2178
  const current = dep.version.replace(/^[\^~]/, "");
234
2179
  if (current === latest) {
235
- rows.push(` = ${dep.name}: ${dep.version}`);
2180
+ rows.push(` ${c.gray("=")} ${dep.name}: ${c.gray(dep.version)}`);
236
2181
  continue;
237
2182
  }
238
2183
  const prefix = dep.version.startsWith("^") ? "^" : dep.version.startsWith("~") ? "~" : "";
239
2184
  const next = `${prefix}${latest}`;
240
- rows.push(` \u2191 ${dep.name}: ${dep.version} \u2192 ${next}`);
2185
+ rows.push(` ${c.blue("\u2191")} ${dep.name}: ${c.gray(dep.version)} \u2192 ${c.green(next)}`);
241
2186
  if (!dry) {
242
2187
  const section = pkg[dep.section];
243
2188
  section[dep.name] = next;
@@ -251,100 +2196,172 @@ async function cmdUpdate(parsed) {
251
2196
  if (dry) {
252
2197
  console.log(`
253
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
+ }
254
2225
  return;
255
2226
  }
256
2227
  if (changed === 0) {
257
2228
  console.log(`
258
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.`);
259
- return;
260
- }
261
- writePackageJson(pkg, cwd);
262
- console.log(`
2230
+ } else {
2231
+ writePackageJson(pkg, cwd);
2232
+ console.log(`
263
2233
  package.json \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D (${changed}).`);
264
- if (!noInstall) {
265
- const ok = installDeps(cwd);
266
- if (!ok) {
267
- 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.");
268
- process.exit(1);
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.");
269
2242
  }
270
- } else {
271
- 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.");
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`);
2257
+ try {
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}`);
2276
+ }
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)");
272
2289
  }
273
2290
  }
274
2291
 
275
2292
  // src/lib/copyTemplate.ts
276
2293
  import {
277
2294
  cpSync,
278
- existsSync as existsSync4,
2295
+ existsSync as existsSync11,
279
2296
  mkdirSync,
280
- readdirSync,
281
- readFileSync as readFileSync2,
282
- statSync,
283
- writeFileSync as writeFileSync2
2297
+ readdirSync as readdirSync7,
2298
+ readFileSync as readFileSync14,
2299
+ statSync as statSync7,
2300
+ writeFileSync as writeFileSync5
284
2301
  } from "fs";
285
- import { join as join4, resolve } from "path";
2302
+ import { join as join11, resolve as resolve2 } from "path";
286
2303
 
287
2304
  // src/lib/paths.ts
288
- import { existsSync as existsSync3 } from "fs";
289
- import { dirname, join as join3 } from "path";
2305
+ import { existsSync as existsSync10 } from "fs";
2306
+ import { dirname as dirname2, join as join10 } from "path";
290
2307
  import { fileURLToPath } from "url";
291
2308
  function packageRoot() {
292
- const here = dirname(fileURLToPath(import.meta.url));
293
- const candidates = [here, join3(here, ".."), join3(here, "..", "..")];
2309
+ const here = dirname2(fileURLToPath(import.meta.url));
2310
+ const candidates = [here, join10(here, ".."), join10(here, "..", "..")];
294
2311
  for (const dir of candidates) {
295
- if (existsSync3(join3(dir, "package.json")) && existsSync3(join3(dir, "templates"))) {
2312
+ if (existsSync10(join10(dir, "package.json")) && existsSync10(join10(dir, "templates"))) {
296
2313
  return dir;
297
2314
  }
298
2315
  }
299
2316
  for (const dir of candidates) {
300
- if (existsSync3(join3(dir, "package.json")))
2317
+ if (existsSync10(join10(dir, "package.json")))
301
2318
  return dir;
302
2319
  }
303
- return join3(here, "..");
2320
+ return join10(here, "..");
304
2321
  }
305
2322
  function templatesDir() {
306
- return join3(packageRoot(), "templates");
2323
+ return join10(packageRoot(), "templates");
307
2324
  }
308
2325
 
309
2326
  // src/lib/copyTemplate.ts
310
- var SKIP = new Set(["node_modules", "dist", ".git"]);
2327
+ var SKIP3 = new Set(["node_modules", "dist", ".git"]);
311
2328
  function folderToPackageName(projectPath) {
312
2329
  const base = projectPath.replace(/\\/g, "/").split("/").filter(Boolean).pop() ?? "app";
313
2330
  const slug = base.toLowerCase().replace(/[^a-z0-9-._~/]/g, "-").replace(/^[-_.]+|[-_.]+$/g, "");
314
2331
  return slug || "app";
315
2332
  }
316
2333
  function isEmptyDir(dir) {
317
- if (!existsSync4(dir))
2334
+ if (!existsSync11(dir))
318
2335
  return true;
319
- const entries = readdirSync(dir).filter((e) => e !== ".git" && e !== ".DS_Store");
2336
+ const entries = readdirSync7(dir).filter((e) => e !== ".git" && e !== ".DS_Store");
320
2337
  return entries.length === 0;
321
2338
  }
322
2339
  function writePkgName(pkgPath, name) {
323
- if (!existsSync4(pkgPath))
2340
+ if (!existsSync11(pkgPath))
324
2341
  return;
325
- const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
2342
+ const pkg = JSON.parse(readFileSync14(pkgPath, "utf8"));
326
2343
  pkg.name = name;
327
2344
  pkg.version = pkg.version ?? "0.0.0";
328
- writeFileSync2(pkgPath, JSON.stringify(pkg, null, 2) + `
2345
+ writeFileSync5(pkgPath, JSON.stringify(pkg, null, 2) + `
329
2346
  `, "utf8");
330
2347
  }
331
2348
  function finalizePanelNames(targetDir, baseName) {
332
- writePkgName(join4(targetDir, "package.json"), baseName);
333
- writePkgName(join4(targetDir, "web", "package.json"), `${baseName}-web`);
334
- writePkgName(join4(targetDir, "api", "package.json"), `${baseName}-api`);
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`);
335
2352
  }
336
2353
  function copyTemplate(templateName, targetDir, packageName) {
337
- const src = join4(templatesDir(), templateName);
338
- if (!existsSync4(src) || !statSync(src).isDirectory()) {
2354
+ const src = join11(templatesDir(), templateName);
2355
+ if (!existsSync11(src) || !statSync7(src).isDirectory()) {
339
2356
  throw new Error(`\u0428\u0430\u0431\u043B\u043E\u043D \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D: ${templateName}
340
2357
  \u041E\u0436\u0438\u0434\u0430\u043B\u0441\u044F \u043A\u0430\u0442\u0430\u043B\u043E\u0433: ${src}`);
341
2358
  }
342
2359
  mkdirSync(targetDir, { recursive: true });
343
- for (const entry of readdirSync(src)) {
344
- if (SKIP.has(entry))
2360
+ for (const entry of readdirSync7(src)) {
2361
+ if (SKIP3.has(entry))
345
2362
  continue;
346
- const from = join4(src, entry);
347
- const to = join4(targetDir, entry);
2363
+ const from = join11(src, entry);
2364
+ const to = join11(targetDir, entry);
348
2365
  cpSync(from, to, { recursive: true });
349
2366
  }
350
2367
  const name = packageName ?? folderToPackageName(targetDir);
@@ -352,19 +2369,19 @@ function copyTemplate(templateName, targetDir, packageName) {
352
2369
  finalizePanelNames(targetDir, name);
353
2370
  return;
354
2371
  }
355
- writePkgName(join4(targetDir, "package.json"), name);
2372
+ writePkgName(join11(targetDir, "package.json"), name);
356
2373
  }
357
2374
  function listTemplates() {
358
2375
  const dir = templatesDir();
359
- if (!existsSync4(dir))
2376
+ if (!existsSync11(dir))
360
2377
  return [];
361
- return readdirSync(dir).filter((name) => {
362
- const p = join4(dir, name);
363
- return statSync(p).isDirectory() && !SKIP.has(name);
2378
+ return readdirSync7(dir).filter((name) => {
2379
+ const p = join11(dir, name);
2380
+ return statSync7(p).isDirectory() && !SKIP3.has(name);
364
2381
  });
365
2382
  }
366
2383
  function resolveTarget(pathOrName, cwd = process.cwd()) {
367
- return resolve(cwd, pathOrName);
2384
+ return resolve2(cwd, pathOrName);
368
2385
  }
369
2386
 
370
2387
  // src/commands/create.ts
@@ -426,165 +2443,40 @@ async function cmdInit(parsed) {
426
2443
  const cwd = process.cwd();
427
2444
  const force = hasFlag(parsed, "force");
428
2445
  const noInstall = hasFlag(parsed, "no-install");
429
- if (!force && !isEmptyDir(cwd)) {
430
- 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>.");
431
- process.exit(1);
432
- }
433
- const pkgName = template === "lib" ? libPackageName(cwd) : folderToPackageName(cwd);
434
- console.log(`\u0428\u0430\u0431\u043B\u043E\u043D: ${template}`);
435
- console.log(`\u041A\u0430\u0442\u0430\u043B\u043E\u0433: ${cwd}`);
436
- copyTemplate(template, cwd, pkgName);
437
- console.log("\u041F\u0440\u043E\u0435\u043A\u0442 \u0438\u043D\u0438\u0446\u0438\u0430\u043B\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u043D.");
438
- if (!noInstall) {
439
- const ok = installDeps(cwd);
440
- if (!ok)
441
- process.exit(1);
442
- }
443
- doneHint(template);
444
- }
445
-
446
- // src/lib/migrate/socket-server.ts
447
- import {
448
- existsSync as existsSync5,
449
- readdirSync as readdirSync2,
450
- readFileSync as readFileSync3,
451
- statSync as statSync2,
452
- writeFileSync as writeFileSync3
453
- } from "fs";
454
- import { join as join5 } from "path";
455
- var SKIP_DIRS = new Set([
456
- "node_modules",
457
- "dist",
458
- ".git",
459
- "coverage",
460
- ".turbo"
461
- ]);
462
- var TEXT_EXT = new Set([
463
- ".ts",
464
- ".tsx",
465
- ".js",
466
- ".jsx",
467
- ".mjs",
468
- ".cjs",
469
- ".mts",
470
- ".cts"
471
- ]);
472
- function walkFiles(dir, out = []) {
473
- if (!existsSync5(dir))
474
- return out;
475
- for (const name of readdirSync2(dir)) {
476
- if (SKIP_DIRS.has(name))
477
- continue;
478
- const full = join5(dir, name);
479
- const st = statSync2(full);
480
- if (st.isDirectory())
481
- walkFiles(full, out);
482
- else {
483
- const ext = name.includes(".") ? `.${name.split(".").pop()}` : "";
484
- if (TEXT_EXT.has(ext))
485
- out.push(full);
486
- }
487
- }
488
- return out;
489
- }
490
- function rewriteImports(source) {
491
- let hits = 0;
492
- let next = source;
493
- const patterns = [
494
- [
495
- /from\s+["']@elcrm\/socket\/server["']/g,
496
- 'from "@elcrm/server"'
497
- ],
498
- [
499
- /from\s+["']@elcrm\/socket-server["']/g,
500
- 'from "@elcrm/server"'
501
- ],
502
- [
503
- /require\s*\(\s*["']@elcrm\/socket\/server["']\s*\)/g,
504
- 'require("@elcrm/server")'
505
- ],
506
- [
507
- /require\s*\(\s*["']@elcrm\/socket-server["']\s*\)/g,
508
- 'require("@elcrm/server")'
509
- ]
510
- ];
511
- for (const [re, repl] of patterns) {
512
- const before = next;
513
- next = next.replace(re, () => {
514
- hits++;
515
- return repl;
516
- });
517
- }
518
- return { next, hits };
519
- }
520
- async function migrateSocketServer(cwd, dry) {
521
- const changes = [];
522
- const pkg = readPackageJson(cwd);
523
- let removedSocketServer = false;
524
- if (pkg) {
525
- for (const section of [
526
- "dependencies",
527
- "devDependencies",
528
- "peerDependencies"
529
- ]) {
530
- const map = pkg[section];
531
- if (!map)
532
- continue;
533
- if (map["@elcrm/socket-server"]) {
534
- removedSocketServer = true;
535
- changes.push({
536
- file: "package.json",
537
- detail: `\u0443\u0434\u0430\u043B\u0438\u0442\u044C ${section}["@elcrm/socket-server"]`
538
- });
539
- if (!dry)
540
- delete map["@elcrm/socket-server"];
541
- }
542
- }
543
- const hasServer = Boolean(pkg.dependencies?.["@elcrm/server"]) || Boolean(pkg.devDependencies?.["@elcrm/server"]);
544
- if (removedSocketServer && !hasServer) {
545
- const latest = dry ? null : await fetchLatestVersion("@elcrm/server");
546
- const ver = latest ? `^${latest}` : "^0.5.3";
547
- changes.push({
548
- file: "package.json",
549
- detail: `\u0434\u043E\u0431\u0430\u0432\u0438\u0442\u044C dependencies["@elcrm/server"] = ${ver}`
550
- });
551
- if (!dry) {
552
- if (!pkg.dependencies)
553
- pkg.dependencies = {};
554
- pkg.dependencies["@elcrm/server"] = ver;
555
- }
556
- }
557
- if (!dry && changes.some((c) => c.file === "package.json")) {
558
- writePackageJson(pkg, cwd);
559
- }
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);
560
2449
  }
561
- const roots = ["src", "app", "server", "lib"].map((d) => join5(cwd, d));
562
- const files = roots.flatMap((r) => walkFiles(r));
563
- for (const file of files) {
564
- const raw = readFileSync3(file, "utf8");
565
- const { next, hits } = rewriteImports(raw);
566
- if (hits > 0) {
567
- changes.push({
568
- file: file.replace(cwd + "/", ""),
569
- detail: `${hits} \u0437\u0430\u043C\u0435\u043D(\u044B) \u0438\u043C\u043F\u043E\u0440\u0442\u043E\u0432 \u2192 @elcrm/server`
570
- });
571
- if (!dry)
572
- writeFileSync3(file, next, "utf8");
573
- }
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);
574
2459
  }
575
- return changes;
2460
+ doneHint(template);
576
2461
  }
577
2462
 
578
2463
  // src/commands/migrate.ts
2464
+ init_socket_server();
2465
+ init_front();
579
2466
  var MIGRATIONS = {
580
- "socket-server": migrateSocketServer
2467
+ "socket-server": migrateSocketServer,
2468
+ ...FRONT_MIGRATIONS,
2469
+ front: migrateFrontAll,
2470
+ ui: migrateFrontAll
581
2471
  };
582
2472
  async function cmdMigrate(parsed) {
583
2473
  if (hasFlag(parsed, "list") || parsed.positionals[0] === "list") {
584
2474
  console.log("\u0414\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0435 \u043C\u0438\u0433\u0440\u0430\u0446\u0438\u0438:");
585
- for (const name2 of Object.keys(MIGRATIONS)) {
586
- console.log(` ${name2}`);
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}`);
587
2478
  }
2479
+ console.log(" socket-server (\u0442\u0430\u043A\u0436\u0435 \u0432 front)");
588
2480
  return;
589
2481
  }
590
2482
  const name = parsed.positionals[0];
@@ -608,127 +2500,51 @@ async function cmdMigrate(parsed) {
608
2500
  console.log("\u0418\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0439 \u043D\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044F.");
609
2501
  return;
610
2502
  }
611
- for (const c of changes) {
612
- console.log(` ${c.file}: ${c.detail}`);
2503
+ for (const c2 of changes) {
2504
+ console.log(` ${c2.file}: ${c2.detail}`);
613
2505
  }
614
2506
  console.log(dry ? `
615
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.` : `
616
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");
2511
+ }
617
2512
  }
618
2513
 
619
2514
  // src/commands/doctor.ts
620
- import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync4, statSync as statSync3 } from "fs";
621
- import { join as join6 } from "path";
622
- var DEPRECATED_IMPORT_RE = /(?:from\s+|require\s*\(\s*)["']@elcrm\/(?:socket\/server|socket-server)["']/;
623
- function scanDeprecatedImports(cwd) {
624
- const hits = [];
625
- const SKIP2 = new Set(["node_modules", "dist", ".git"]);
626
- const walk = (dir) => {
627
- if (!existsSync6(dir))
628
- return;
629
- for (const name of readdirSync3(dir)) {
630
- if (SKIP2.has(name))
631
- continue;
632
- const full = join6(dir, name);
633
- const st = statSync3(full);
634
- if (st.isDirectory())
635
- walk(full);
636
- else if (/\.(tsx?|jsx?|mjs|cjs)$/.test(name)) {
637
- const text = readFileSync4(full, "utf8");
638
- if (DEPRECATED_IMPORT_RE.test(text)) {
639
- hits.push(full.replace(cwd + "/", ""));
640
- }
641
- }
642
- }
643
- };
644
- walk(join6(cwd, "src"));
645
- return hits;
646
- }
2515
+ init_socket_server();
2516
+ init_pkg();
647
2517
  async function cmdDoctor(parsed) {
648
2518
  const cwd = process.cwd();
649
2519
  const fix = hasFlag(parsed, "fix");
650
- const issues = [];
651
2520
  const pkg = readPackageJson(cwd);
652
2521
  if (!pkg) {
653
2522
  console.error("package.json \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D.");
654
2523
  process.exit(1);
655
2524
  }
656
- if (pkg.type !== "module") {
657
- issues.push({
658
- level: "warn",
659
- code: "type-module",
660
- message: '\u041D\u0435\u0442 "type": "module"',
661
- fixable: true
662
- });
663
- }
664
- if (!pkg.engines?.node) {
665
- issues.push({
666
- level: "warn",
667
- code: "engines",
668
- message: "\u041D\u0435\u0442 engines.node (\u043E\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044F >=18)",
669
- fixable: true
670
- });
671
- }
672
- const scripts = pkg.scripts ?? {};
673
- const isLib = Boolean(pkg.name?.startsWith("@elcrm/"));
674
- if (isLib) {
675
- if (!scripts.build && !scripts["pack:check"]) {
676
- issues.push({
677
- level: "warn",
678
- code: "scripts",
679
- 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"
680
- });
681
- }
682
- if (!scripts["pack:check"]) {
683
- issues.push({
684
- level: "info",
685
- code: "pack-check",
686
- message: "\u041D\u0435\u0442 scripts.pack:check",
687
- fixable: true
688
- });
2525
+ if (hasElcrmServer(pkg)) {
2526
+ const result = scanServerProject(cwd);
2527
+ const errors = printServerFindings(result);
2528
+ if (!fix) {
2529
+ if (result.findings.some((f) => f.auto)) {
2530
+ console.log(`
2531
+ \u0410\u0432\u0442\u043E\u0437\u0430\u043C\u0435\u043D\u044B: bunx elcrm-server update (\u0438\u043B\u0438 elcrm migrate \u2026)`);
2532
+ }
2533
+ if (errors)
2534
+ process.exit(1);
2535
+ return;
689
2536
  }
2537
+ console.log("doctor --fix \u043D\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0435: \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439\u0442\u0435 bunx elcrm-server update");
2538
+ if (errors)
2539
+ process.exit(1);
2540
+ return;
690
2541
  }
691
- const deps = listElcrmDeps(pkg);
692
- if (deps.some((d) => d.name === "@elcrm/socket-server")) {
693
- issues.push({
694
- level: "error",
695
- code: "deprecated-socket-server",
696
- 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",
697
- fixable: true
698
- });
699
- }
700
- const umbrella = pkg.dependencies?.elcrm || pkg.devDependencies?.elcrm;
701
- if (umbrella && /^[\^~]?0\.9\./.test(umbrella)) {
702
- issues.push({
703
- level: "warn",
704
- code: "legacy-umbrella",
705
- 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`
706
- });
707
- }
708
- const importHits = scanDeprecatedImports(cwd);
709
- for (const file of importHits) {
710
- issues.push({
711
- level: "error",
712
- code: "deprecated-import",
713
- message: `${file}: \u0438\u043C\u043F\u043E\u0440\u0442 @elcrm/socket/server \u0438\u043B\u0438 socket-server`,
714
- fixable: true
715
- });
716
- }
717
- if (deps.length === 0 && !isLib) {
718
- issues.push({
719
- level: "info",
720
- code: "no-elcrm",
721
- message: "\u041D\u0435\u0442 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0435\u0439 @elcrm/*"
722
- });
723
- }
2542
+ const issues = collectFrontIssues(cwd, pkg);
724
2543
  if (issues.length === 0) {
725
2544
  console.log("doctor: \u0437\u0430\u043C\u0435\u0447\u0430\u043D\u0438\u0439 \u043D\u0435\u0442.");
726
2545
  return;
727
2546
  }
728
- for (const issue of issues) {
729
- const mark = issue.level === "error" ? "E" : issue.level === "warn" ? "W" : "I";
730
- console.log(`[${mark}] ${issue.code}: ${issue.message}`);
731
- }
2547
+ printIssues(issues);
732
2548
  if (!fix) {
733
2549
  const fixable = issues.filter((i) => i.fixable).length;
734
2550
  if (fixable) {
@@ -746,10 +2562,11 @@ async function cmdDoctor(parsed) {
746
2562
  }
747
2563
  if (!pkg.engines)
748
2564
  pkg.engines = {};
749
- if (!pkg.engines.node) {
2565
+ if (!pkg.engines.node && !pkg.engines.bun) {
750
2566
  pkg.engines.node = ">=18";
751
2567
  pkgDirty = true;
752
2568
  }
2569
+ const isLib = Boolean(pkg.name?.startsWith("@elcrm/"));
753
2570
  if (isLib) {
754
2571
  if (!pkg.scripts)
755
2572
  pkg.scripts = {};
@@ -764,14 +2581,20 @@ async function cmdDoctor(parsed) {
764
2581
  if (mig.length) {
765
2582
  console.log(`\u041C\u0438\u0433\u0440\u0430\u0446\u0438\u044F socket-server: ${mig.length} \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0439.`);
766
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
+ }
767
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.");
768
- 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");
769
2591
  }
770
2592
 
771
2593
  // src/commands/build.ts
772
- import { existsSync as existsSync7 } from "fs";
2594
+ import { existsSync as existsSync12 } from "fs";
773
2595
 
774
2596
  // src/lib/nextVersion.ts
2597
+ init_pkg();
775
2598
  async function nextVersion() {
776
2599
  const pkg = readPackageJson();
777
2600
  if (!pkg?.version) {
@@ -792,9 +2615,9 @@ async function nextVersion() {
792
2615
 
793
2616
  // src/commands/build.ts
794
2617
  function resolveEntrypoint() {
795
- if (existsSync7("./src/index.js"))
2618
+ if (existsSync12("./src/index.js"))
796
2619
  return "./src/index.js";
797
- if (existsSync7("./src/index.ts"))
2620
+ if (existsSync12("./src/index.ts"))
798
2621
  return "./src/index.ts";
799
2622
  return "./src/index.js";
800
2623
  }
@@ -871,7 +2694,521 @@ async function cmdUuid() {
871
2694
  console.log(crypto.randomUUID());
872
2695
  }
873
2696
 
2697
+ // src/commands/postbuild.ts
2698
+ import { resolve as resolve3 } from "path";
2699
+
2700
+ // src/vite/postbuild.ts
2701
+ import {
2702
+ existsSync as existsSync13,
2703
+ readdirSync as readdirSync9,
2704
+ readFileSync as readFileSync16,
2705
+ rmSync,
2706
+ statSync as statSync8,
2707
+ writeFileSync as writeFileSync7
2708
+ } from "fs";
2709
+ import { join as join13 } from "path";
2710
+
2711
+ // src/vite/concat-css.ts
2712
+ import { readdirSync as readdirSync8, readFileSync as readFileSync15, writeFileSync as writeFileSync6 } from "fs";
2713
+ import { join as join12 } from "path";
2714
+ function concatDistCss(options) {
2715
+ const outFile = options.outFile ?? "index.css";
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(`
2718
+ `);
2719
+ const outPath = join12(options.distDir, outFile);
2720
+ writeFileSync6(outPath, css);
2721
+ console.log(`[concat-css] ${files.length} \u0444\u0430\u0439\u043B\u043E\u0432 \u2192 ${outFile}`);
2722
+ return outPath;
2723
+ }
2724
+
2725
+ // src/vite/postbuild.ts
2726
+ function postbuildLib(options) {
2727
+ const dist = options.distDir;
2728
+ const keep = new Set([
2729
+ "chunks",
2730
+ "assets",
2731
+ ...options.keepDirs ?? []
2732
+ ]);
2733
+ concatDistCss({
2734
+ distDir: dist,
2735
+ outFile: options.cssOutFile ?? "index.css"
2736
+ });
2737
+ for (const name of readdirSync9(dist)) {
2738
+ const dir = join13(dist, name);
2739
+ if (!statSync8(dir).isDirectory())
2740
+ continue;
2741
+ if (keep.has(name))
2742
+ continue;
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;
2747
+ if (src) {
2748
+ const text = readFileSync16(src, "utf8").replace(/from ['"]\.\.\/([^'"]+)['"]/g, `from './$1'`);
2749
+ writeFileSync7(flat, text);
2750
+ console.log(`[postbuild] types \u2192 ${name}.d.ts`);
2751
+ }
2752
+ rmSync(dir, { recursive: true, force: true });
2753
+ console.log(`[postbuild] removed nested dir: ${name}/`);
2754
+ }
2755
+ }
2756
+
2757
+ // src/commands/postbuild.ts
2758
+ async function cmdPostbuild(parsed) {
2759
+ const dir = parsed.options.dir || "./dist";
2760
+ const distDir = resolve3(process.cwd(), dir);
2761
+ postbuildLib({ distDir });
2762
+ console.log(`[postbuild] \u0433\u043E\u0442\u043E\u0432\u043E: ${distDir}`);
2763
+ }
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
+
874
3194
  // src/index.ts
3195
+ function cliVersion() {
3196
+ const here = dirname4(fileURLToPath2(import.meta.url));
3197
+ for (const p of [
3198
+ join18(here, "..", "package.json"),
3199
+ join18(here, "package.json")
3200
+ ]) {
3201
+ try {
3202
+ const v = JSON.parse(readFileSync19(p, "utf8")).version;
3203
+ if (typeof v === "string" && v)
3204
+ return v;
3205
+ } catch {}
3206
+ }
3207
+ return "0.0.0";
3208
+ }
3209
+ function showVersion() {
3210
+ console.log(`elcrm ${cliVersion()}`);
3211
+ }
875
3212
  function showHelp() {
876
3213
  console.log(`elcrm \u2014 CLI \u0434\u043B\u044F \u044D\u043A\u043E\u0441\u0438\u0441\u0442\u0435\u043C\u044B @elcrm/*
877
3214
 
@@ -879,8 +3216,9 @@ function showHelp() {
879
3216
  elcrm <\u043A\u043E\u043C\u0430\u043D\u0434\u0430> [\u0430\u0440\u0433\u0443\u043C\u0435\u043D\u0442\u044B] [\u0444\u043B\u0430\u0433\u0438]
880
3217
 
881
3218
  \u041A\u043E\u043C\u0430\u043D\u0434\u044B:
882
- update [--dry] [--core] [--no-install]
883
- \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
884
3222
 
885
3223
  create <name> [--template app|lib|panel] [--no-install]
886
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)
@@ -891,33 +3229,80 @@ function showHelp() {
891
3229
 
892
3230
  migrate <name> [--dry]
893
3231
  migrate --list
894
- \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
895
3237
 
896
3238
  doctor [--fix]
897
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
898
3240
 
3241
+ test [dir] [--front|--server] [--no-scripts]
3242
+ \u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0438: @elcrm/server \u0432 package.json \u2192 server, \u0438\u043D\u0430\u0447\u0435 \u2192 front
3243
+ (workspaces \u2014 \u043A\u0430\u0436\u0434\u044B\u0439 \u043F\u0430\u043A\u0435\u0442 \u0441\u0432\u043E\u0438\u043C \u0440\u0435\u0436\u0438\u043C\u043E\u043C)
3244
+
899
3245
  build [--client] [--url=\u2026] [--name=\u2026] [--dir=\u2026] [--folder=\u2026]
900
3246
  \u0421\u0431\u043E\u0440\u043A\u0430 \u0438 \u0434\u0435\u043F\u043B\u043E\u0439 (\u0438\u0437 @elcrm/deploy)
901
3247
 
3248
+ postbuild [--dir=./dist]
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
3250
+ (package.json: "build": "vite build && elcrm postbuild")
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
+
902
3264
  uuid
903
3265
  \u0421\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C UUID
904
3266
 
905
3267
  help
906
3268
  \u042D\u0442\u0430 \u0441\u043F\u0440\u0430\u0432\u043A\u0430
907
3269
 
3270
+ \u0424\u043B\u0430\u0433\u0438:
3271
+ -v, --version \u0412\u0435\u0440\u0441\u0438\u044F CLI
3272
+ -h, --help \u0421\u043F\u0440\u0430\u0432\u043A\u0430
3273
+
908
3274
  \u041F\u0440\u0438\u043C\u0435\u0440\u044B:
3275
+ elcrm -v
909
3276
  elcrm update
910
3277
  elcrm update --dry
3278
+ elcrm update --fix --test
3279
+ elcrm migrate form-aliases --dry
3280
+ elcrm migrate form-tokens --dry
911
3281
  elcrm create my-app --template app
912
3282
  elcrm init --template panel
913
3283
  elcrm init --template lib
914
- elcrm migrate socket-server --dry
3284
+ elcrm migrate front --dry
3285
+ elcrm migrate size-sml
3286
+ elcrm audit
3287
+ elcrm audit --fix
915
3288
  elcrm doctor --fix
3289
+ elcrm test
3290
+ elcrm test --server
916
3291
  elcrm build --client --url=https://example.com/deploy
3292
+ elcrm postbuild
3293
+ elcrm css
3294
+ elcrm css --dry
3295
+ elcrm docs
3296
+ elcrm cursor
3297
+ elcrm cursor --lib
917
3298
  `);
918
3299
  }
919
3300
  async function main() {
920
3301
  const parsed = parseArgs();
3302
+ if (hasFlag(parsed, "version", "v") || parsed.command === "version") {
3303
+ showVersion();
3304
+ return;
3305
+ }
921
3306
  if (hasFlag(parsed, "help", "h") || parsed.command === "help" || !parsed.command) {
922
3307
  showHelp();
923
3308
  return;
@@ -941,9 +3326,27 @@ async function main() {
941
3326
  case "doctor":
942
3327
  await cmdDoctor(parsed);
943
3328
  break;
3329
+ case "test":
3330
+ await cmdTest(parsed);
3331
+ break;
944
3332
  case "build":
945
3333
  await cmdBuild(parsed);
946
3334
  break;
3335
+ case "postbuild":
3336
+ await cmdPostbuild(parsed);
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;
947
3350
  case "uuid":
948
3351
  await cmdUuid();
949
3352
  break;