@hyphaene/hexa-ts-kit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +169 -0
- package/bin/hexa-ts-mcp.js +2 -0
- package/bin/hexa-ts.js +2 -0
- package/dist/chunk-56VIQG3N.js +1434 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +26 -0
- package/dist/mcp-server.d.ts +1 -0
- package/dist/mcp-server.js +104 -0
- package/package.json +81 -0
|
@@ -0,0 +1,1434 @@
|
|
|
1
|
+
// src/commands/lint.ts
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
|
+
|
|
5
|
+
// src/lint/checkers/structure/colocation.ts
|
|
6
|
+
import fg from "fast-glob";
|
|
7
|
+
import { existsSync } from "fs";
|
|
8
|
+
import { basename, dirname, join } from "path";
|
|
9
|
+
var colocationChecker = {
|
|
10
|
+
name: "colocation",
|
|
11
|
+
rules: [
|
|
12
|
+
"COL-001",
|
|
13
|
+
"COL-002",
|
|
14
|
+
"COL-003",
|
|
15
|
+
"COL-004",
|
|
16
|
+
"COL-005",
|
|
17
|
+
"COL-006",
|
|
18
|
+
"COL-007",
|
|
19
|
+
"COL-008",
|
|
20
|
+
"COL-010",
|
|
21
|
+
"COL-011",
|
|
22
|
+
"COL-012"
|
|
23
|
+
],
|
|
24
|
+
async check(ctx) {
|
|
25
|
+
const results = [];
|
|
26
|
+
const forbiddenDirs = await checkForbiddenRootDirs(ctx.cwd);
|
|
27
|
+
results.push(...forbiddenDirs);
|
|
28
|
+
const defaultIgnore = [
|
|
29
|
+
"**/node_modules/**",
|
|
30
|
+
"**/__tests__/**",
|
|
31
|
+
"**/worktrees/**",
|
|
32
|
+
"**/dist/**",
|
|
33
|
+
"**/coverage/**"
|
|
34
|
+
];
|
|
35
|
+
const vueFiles = await fg("**/src/domains/**/*.vue", {
|
|
36
|
+
cwd: ctx.cwd,
|
|
37
|
+
ignore: defaultIgnore
|
|
38
|
+
});
|
|
39
|
+
const vueFilesOutsideDomains = await fg("**/src/**/*.vue", {
|
|
40
|
+
cwd: ctx.cwd,
|
|
41
|
+
ignore: [...defaultIgnore, "**/src/domains/**", "**/src/shared/**"]
|
|
42
|
+
});
|
|
43
|
+
for (const file of vueFilesOutsideDomains) {
|
|
44
|
+
results.push({
|
|
45
|
+
ruleId: "COL-001",
|
|
46
|
+
severity: "error",
|
|
47
|
+
message: `Vue component outside domains/: ${file}`,
|
|
48
|
+
file,
|
|
49
|
+
suggestion: "Move to src/domains/{domain}/{feature}/"
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
for (const vueFile of vueFiles) {
|
|
53
|
+
const fileResults = await checkVueFileColocation(ctx.cwd, vueFile);
|
|
54
|
+
results.push(...fileResults);
|
|
55
|
+
}
|
|
56
|
+
return results;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
async function checkForbiddenRootDirs(cwd) {
|
|
60
|
+
const results = [];
|
|
61
|
+
const forbidden = [
|
|
62
|
+
{ pattern: "src/components", ruleId: "COL-005" },
|
|
63
|
+
{ pattern: "src/hooks", ruleId: "COL-006" },
|
|
64
|
+
{ pattern: "src/composables", ruleId: "COL-006" },
|
|
65
|
+
{ pattern: "src/services", ruleId: "COL-007" },
|
|
66
|
+
{ pattern: "src/types", ruleId: "COL-008" }
|
|
67
|
+
];
|
|
68
|
+
for (const { pattern, ruleId } of forbidden) {
|
|
69
|
+
const dir = join(cwd, pattern);
|
|
70
|
+
if (existsSync(dir)) {
|
|
71
|
+
results.push({
|
|
72
|
+
ruleId,
|
|
73
|
+
severity: "error",
|
|
74
|
+
message: `Forbidden directory at root: ${pattern}/`,
|
|
75
|
+
file: pattern,
|
|
76
|
+
suggestion: "Move contents to src/domains/{domain}/ or src/shared/"
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return results;
|
|
81
|
+
}
|
|
82
|
+
async function checkVueFileColocation(cwd, vueFile) {
|
|
83
|
+
const results = [];
|
|
84
|
+
const dir = dirname(vueFile);
|
|
85
|
+
const name = basename(vueFile, ".vue");
|
|
86
|
+
const featureName = name.charAt(0).toLowerCase() + name.slice(1);
|
|
87
|
+
const fullDir = join(cwd, dir);
|
|
88
|
+
const composablePath = join(fullDir, `${featureName}.composable.ts`);
|
|
89
|
+
if (!existsSync(composablePath)) {
|
|
90
|
+
results.push({
|
|
91
|
+
ruleId: "COL-002",
|
|
92
|
+
severity: "error",
|
|
93
|
+
message: `Missing colocated composable: ${featureName}.composable.ts`,
|
|
94
|
+
file: vueFile,
|
|
95
|
+
suggestion: `Create ${dir}/${featureName}.composable.ts`
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const testsDir = join(fullDir, "__tests__");
|
|
99
|
+
if (!existsSync(testsDir)) {
|
|
100
|
+
results.push({
|
|
101
|
+
ruleId: "COL-004",
|
|
102
|
+
severity: "error",
|
|
103
|
+
message: `Missing __tests__/ directory`,
|
|
104
|
+
file: vueFile,
|
|
105
|
+
suggestion: `Create ${dir}/__tests__/`
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
const typesPath = join(fullDir, `${featureName}.types.ts`);
|
|
109
|
+
if (!existsSync(typesPath)) {
|
|
110
|
+
results.push({
|
|
111
|
+
ruleId: "COL-010",
|
|
112
|
+
severity: "warning",
|
|
113
|
+
message: `Missing colocated types: ${featureName}.types.ts`,
|
|
114
|
+
file: vueFile,
|
|
115
|
+
suggestion: `Create ${dir}/${featureName}.types.ts if types are needed`
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const translationsPath = join(fullDir, `${featureName}.translations.ts`);
|
|
119
|
+
if (!existsSync(translationsPath)) {
|
|
120
|
+
results.push({
|
|
121
|
+
ruleId: "COL-011",
|
|
122
|
+
severity: "warning",
|
|
123
|
+
message: `Missing colocated translations: ${featureName}.translations.ts`,
|
|
124
|
+
file: vueFile,
|
|
125
|
+
suggestion: `Create ${dir}/${featureName}.translations.ts`
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return results;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/lint/checkers/structure/naming.ts
|
|
132
|
+
import fg2 from "fast-glob";
|
|
133
|
+
import { readFile } from "fs/promises";
|
|
134
|
+
import { basename as basename2 } from "path";
|
|
135
|
+
var namingChecker = {
|
|
136
|
+
name: "naming",
|
|
137
|
+
rules: [
|
|
138
|
+
"NAM-001",
|
|
139
|
+
"NAM-002",
|
|
140
|
+
"NAM-003",
|
|
141
|
+
"NAM-004",
|
|
142
|
+
"NAM-005",
|
|
143
|
+
"NAM-006",
|
|
144
|
+
"NAM-007",
|
|
145
|
+
"NAM-008",
|
|
146
|
+
"NAM-009",
|
|
147
|
+
"NAM-010",
|
|
148
|
+
"NAM-011"
|
|
149
|
+
],
|
|
150
|
+
async check(ctx) {
|
|
151
|
+
const results = [];
|
|
152
|
+
const vueFiles = await fg2("**/*.vue", {
|
|
153
|
+
cwd: ctx.cwd,
|
|
154
|
+
ignore: ["**/node_modules/**"]
|
|
155
|
+
});
|
|
156
|
+
for (const file of vueFiles) {
|
|
157
|
+
const name = basename2(file, ".vue");
|
|
158
|
+
if (!isPascalCase(name)) {
|
|
159
|
+
results.push({
|
|
160
|
+
ruleId: "NAM-001",
|
|
161
|
+
severity: "error",
|
|
162
|
+
message: `Vue component must be PascalCase: ${name}.vue`,
|
|
163
|
+
file,
|
|
164
|
+
suggestion: `Rename to ${toPascalCase(name)}.vue`
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const tsFiles = await fg2("**/src/domains/**/*.ts", {
|
|
169
|
+
cwd: ctx.cwd,
|
|
170
|
+
ignore: ["**/node_modules/**", "**/__tests__/**", "**/*.d.ts"]
|
|
171
|
+
});
|
|
172
|
+
for (const file of tsFiles) {
|
|
173
|
+
const name = basename2(file);
|
|
174
|
+
if (name.endsWith(".composable.ts")) {
|
|
175
|
+
const featureName = name.replace(".composable.ts", "");
|
|
176
|
+
if (!isKebabOrCamelCase(featureName)) {
|
|
177
|
+
results.push({
|
|
178
|
+
ruleId: "NAM-003",
|
|
179
|
+
severity: "error",
|
|
180
|
+
message: `Composable file must be camelCase: ${name}`,
|
|
181
|
+
file
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
const content = await readFile(`${ctx.cwd}/${file}`, "utf-8");
|
|
185
|
+
const exportResults = checkComposableExport(file, content);
|
|
186
|
+
results.push(...exportResults);
|
|
187
|
+
}
|
|
188
|
+
if (name.endsWith(".rules.ts")) {
|
|
189
|
+
const featureName = name.replace(".rules.ts", "");
|
|
190
|
+
if (!isKebabOrCamelCase(featureName)) {
|
|
191
|
+
results.push({
|
|
192
|
+
ruleId: "NAM-004",
|
|
193
|
+
severity: "error",
|
|
194
|
+
message: `Rules file must be camelCase: ${name}`,
|
|
195
|
+
file
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
const content = await readFile(`${ctx.cwd}/${file}`, "utf-8");
|
|
199
|
+
const exportResults = checkRulesExport(
|
|
200
|
+
file,
|
|
201
|
+
content,
|
|
202
|
+
featureName
|
|
203
|
+
);
|
|
204
|
+
results.push(...exportResults);
|
|
205
|
+
}
|
|
206
|
+
if (name.endsWith(".types.ts")) {
|
|
207
|
+
const featureName = name.replace(".types.ts", "");
|
|
208
|
+
if (!isKebabOrCamelCase(featureName)) {
|
|
209
|
+
results.push({
|
|
210
|
+
ruleId: "NAM-005",
|
|
211
|
+
severity: "error",
|
|
212
|
+
message: `Types file must be camelCase: ${name}`,
|
|
213
|
+
file
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (name.endsWith(".query.ts")) {
|
|
218
|
+
const featureName = name.replace(".query.ts", "");
|
|
219
|
+
if (!isKebabOrCamelCase(featureName)) {
|
|
220
|
+
results.push({
|
|
221
|
+
ruleId: "NAM-006",
|
|
222
|
+
severity: "error",
|
|
223
|
+
message: `Query file must be camelCase: ${name}`,
|
|
224
|
+
file
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
const content = await readFile(`${ctx.cwd}/${file}`, "utf-8");
|
|
228
|
+
const exportResults = checkQueryKeysExport(
|
|
229
|
+
file,
|
|
230
|
+
content,
|
|
231
|
+
featureName
|
|
232
|
+
);
|
|
233
|
+
results.push(...exportResults);
|
|
234
|
+
}
|
|
235
|
+
if (name.endsWith(".translations.ts")) {
|
|
236
|
+
const featureName = name.replace(".translations.ts", "");
|
|
237
|
+
if (!isKebabOrCamelCase(featureName)) {
|
|
238
|
+
results.push({
|
|
239
|
+
ruleId: "NAM-007",
|
|
240
|
+
severity: "error",
|
|
241
|
+
message: `Translations file must be camelCase: ${name}`,
|
|
242
|
+
file
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
const content = await readFile(`${ctx.cwd}/${file}`, "utf-8");
|
|
246
|
+
const exportResults = checkTranslationsExport(
|
|
247
|
+
file,
|
|
248
|
+
content,
|
|
249
|
+
featureName
|
|
250
|
+
);
|
|
251
|
+
results.push(...exportResults);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const domainDirs = await fg2("src/domains/*", {
|
|
255
|
+
cwd: ctx.cwd,
|
|
256
|
+
onlyDirectories: true
|
|
257
|
+
});
|
|
258
|
+
for (const domainPath of domainDirs) {
|
|
259
|
+
const domainName = basename2(domainPath);
|
|
260
|
+
if (!isKebabCase(domainName)) {
|
|
261
|
+
results.push({
|
|
262
|
+
ruleId: "NAM-011",
|
|
263
|
+
severity: "error",
|
|
264
|
+
message: `Domain must be kebab-case: ${domainName}`,
|
|
265
|
+
file: domainPath
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
if (!looksPlural(domainName)) {
|
|
269
|
+
results.push({
|
|
270
|
+
ruleId: "NAM-011",
|
|
271
|
+
severity: "warning",
|
|
272
|
+
message: `Domain should be plural: ${domainName}`,
|
|
273
|
+
file: domainPath,
|
|
274
|
+
suggestion: `Consider renaming to ${domainName}s`
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return results;
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
function isPascalCase(str) {
|
|
282
|
+
return /^[A-Z][a-zA-Z0-9]*$/.test(str);
|
|
283
|
+
}
|
|
284
|
+
function toPascalCase(str) {
|
|
285
|
+
return str.split(/[-_]/).map(
|
|
286
|
+
(part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()
|
|
287
|
+
).join("");
|
|
288
|
+
}
|
|
289
|
+
function isKebabOrCamelCase(str) {
|
|
290
|
+
return /^[a-z][a-zA-Z0-9]*$/.test(str) || /^[a-z][a-z0-9-]*$/.test(str);
|
|
291
|
+
}
|
|
292
|
+
function isKebabCase(str) {
|
|
293
|
+
return /^[a-z][a-z0-9-]*$/.test(str);
|
|
294
|
+
}
|
|
295
|
+
function looksPlural(str) {
|
|
296
|
+
return str.endsWith("s") || str.endsWith("ies") || str === "shared" || str === "data";
|
|
297
|
+
}
|
|
298
|
+
function checkComposableExport(file, content) {
|
|
299
|
+
const results = [];
|
|
300
|
+
const hasUseExport = /export\s+function\s+use[A-Z]/.test(content);
|
|
301
|
+
if (!hasUseExport) {
|
|
302
|
+
results.push({
|
|
303
|
+
ruleId: "NAM-002",
|
|
304
|
+
severity: "error",
|
|
305
|
+
message: `Composable must export function with 'use' prefix`,
|
|
306
|
+
file,
|
|
307
|
+
suggestion: `Export function useFeatureName()`
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
return results;
|
|
311
|
+
}
|
|
312
|
+
function checkRulesExport(file, content, featureName) {
|
|
313
|
+
const results = [];
|
|
314
|
+
const expectedClass = `${toPascalCase(featureName)}Rules`;
|
|
315
|
+
const expectedSingleton = `${featureName}Rules`;
|
|
316
|
+
const hasClass = new RegExp(`class\\s+${expectedClass}\\b`).test(content);
|
|
317
|
+
const hasSingleton = new RegExp(
|
|
318
|
+
`export\\s+const\\s+${expectedSingleton}\\s*=`
|
|
319
|
+
).test(content);
|
|
320
|
+
if (!hasClass) {
|
|
321
|
+
results.push({
|
|
322
|
+
ruleId: "NAM-008",
|
|
323
|
+
severity: "error",
|
|
324
|
+
message: `Rules file must have class ${expectedClass}`,
|
|
325
|
+
file
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
if (!hasSingleton) {
|
|
329
|
+
results.push({
|
|
330
|
+
ruleId: "NAM-008",
|
|
331
|
+
severity: "error",
|
|
332
|
+
message: `Rules file must export singleton ${expectedSingleton}`,
|
|
333
|
+
file,
|
|
334
|
+
suggestion: `Add: export const ${expectedSingleton} = new ${expectedClass}()`
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
return results;
|
|
338
|
+
}
|
|
339
|
+
function checkQueryKeysExport(file, content, featureName) {
|
|
340
|
+
const results = [];
|
|
341
|
+
const expectedKeys = `${featureName}Keys`;
|
|
342
|
+
const hasKeysExport = new RegExp(
|
|
343
|
+
`export\\s+const\\s+${expectedKeys}\\s*=`
|
|
344
|
+
).test(content);
|
|
345
|
+
if (!hasKeysExport) {
|
|
346
|
+
results.push({
|
|
347
|
+
ruleId: "NAM-010",
|
|
348
|
+
severity: "error",
|
|
349
|
+
message: `Query file must export ${expectedKeys}`,
|
|
350
|
+
file,
|
|
351
|
+
suggestion: `Add: export const ${expectedKeys} = { all: ['${featureName}'] as const, ... }`
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
return results;
|
|
355
|
+
}
|
|
356
|
+
function checkTranslationsExport(file, content, featureName) {
|
|
357
|
+
const results = [];
|
|
358
|
+
const expectedExport = `${featureName}Translations`;
|
|
359
|
+
const hasExport = new RegExp(
|
|
360
|
+
`export\\s+const\\s+${expectedExport}\\s*=`
|
|
361
|
+
).test(content);
|
|
362
|
+
if (!hasExport) {
|
|
363
|
+
results.push({
|
|
364
|
+
ruleId: "NAM-009",
|
|
365
|
+
severity: "error",
|
|
366
|
+
message: `Translations file must export ${expectedExport}`,
|
|
367
|
+
file,
|
|
368
|
+
suggestion: `Add: export const ${expectedExport} = { ... }`
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
return results;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// src/lint/checkers/structure/domains.ts
|
|
375
|
+
import fg3 from "fast-glob";
|
|
376
|
+
import { basename as basename3 } from "path";
|
|
377
|
+
var domainsChecker = {
|
|
378
|
+
name: "domains",
|
|
379
|
+
rules: ["DOM-001", "DOM-004"],
|
|
380
|
+
async check(ctx) {
|
|
381
|
+
const results = [];
|
|
382
|
+
const domainDirs = await fg3("src/domains/*", {
|
|
383
|
+
cwd: ctx.cwd,
|
|
384
|
+
onlyDirectories: true
|
|
385
|
+
});
|
|
386
|
+
for (const domainPath of domainDirs) {
|
|
387
|
+
const domainName = basename3(domainPath);
|
|
388
|
+
if (domainName === "shared") continue;
|
|
389
|
+
const features = await fg3(`${domainPath}/*`, {
|
|
390
|
+
cwd: ctx.cwd,
|
|
391
|
+
onlyDirectories: true
|
|
392
|
+
});
|
|
393
|
+
const featureDirs = features.filter(
|
|
394
|
+
(f) => !basename3(f).startsWith("__") && !basename3(f).startsWith(".")
|
|
395
|
+
);
|
|
396
|
+
if (featureDirs.length < 2) {
|
|
397
|
+
results.push({
|
|
398
|
+
ruleId: "DOM-001",
|
|
399
|
+
severity: "warning",
|
|
400
|
+
message: `Domain "${domainName}" has only ${featureDirs.length} feature(s), expected at least 2`,
|
|
401
|
+
file: domainPath,
|
|
402
|
+
suggestion: featureDirs.length === 1 ? "Consider merging into parent domain or wait for more features" : "Add features or merge into another domain"
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
const deepDirs = await fg3(`${domainPath}/*/*`, {
|
|
406
|
+
cwd: ctx.cwd,
|
|
407
|
+
onlyDirectories: true
|
|
408
|
+
});
|
|
409
|
+
for (const deepDir of deepDirs) {
|
|
410
|
+
const parts = deepDir.split("/");
|
|
411
|
+
const deepName = basename3(deepDir);
|
|
412
|
+
const hasVueFiles = await fg3(`${deepDir}/*.vue`, {
|
|
413
|
+
cwd: ctx.cwd
|
|
414
|
+
});
|
|
415
|
+
if (hasVueFiles.length > 0 && !deepName.startsWith("__") && deepName !== "components") {
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
const subFeatures = await fg3(`${deepDir}/*`, {
|
|
419
|
+
cwd: ctx.cwd,
|
|
420
|
+
onlyDirectories: true
|
|
421
|
+
});
|
|
422
|
+
if (subFeatures.length >= 3) {
|
|
423
|
+
results.push({
|
|
424
|
+
ruleId: "DOM-004",
|
|
425
|
+
severity: "warning",
|
|
426
|
+
message: `Nested directory "${deepName}" has ${subFeatures.length} subdirs - consider if it should be a separate domain`,
|
|
427
|
+
file: deepDir,
|
|
428
|
+
suggestion: `If "${deepName}" is a distinct business concept, move to src/domains/${deepName}/`
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return results;
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
// src/lint/checkers/ast/vue-component.ts
|
|
438
|
+
import fg4 from "fast-glob";
|
|
439
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
440
|
+
var FORBIDDEN_CALLS = [
|
|
441
|
+
{ name: "ref", ruleId: "VUE-001" },
|
|
442
|
+
{ name: "reactive", ruleId: "VUE-002" },
|
|
443
|
+
{ name: "computed", ruleId: "VUE-003" },
|
|
444
|
+
{ name: "watch", ruleId: "VUE-004" },
|
|
445
|
+
{ name: "watchEffect", ruleId: "VUE-005" }
|
|
446
|
+
];
|
|
447
|
+
var vueComponentChecker = {
|
|
448
|
+
name: "vue-component",
|
|
449
|
+
rules: [
|
|
450
|
+
"VUE-001",
|
|
451
|
+
"VUE-002",
|
|
452
|
+
"VUE-003",
|
|
453
|
+
"VUE-004",
|
|
454
|
+
"VUE-005",
|
|
455
|
+
"VUE-006",
|
|
456
|
+
"VUE-007"
|
|
457
|
+
],
|
|
458
|
+
async check(ctx) {
|
|
459
|
+
const results = [];
|
|
460
|
+
const vueFiles = await fg4("**/*.vue", {
|
|
461
|
+
cwd: ctx.cwd,
|
|
462
|
+
ignore: ["**/node_modules/**", "**/worktrees/**", "**/dist/**"]
|
|
463
|
+
});
|
|
464
|
+
for (const file of vueFiles) {
|
|
465
|
+
const content = await readFile2(`${ctx.cwd}/${file}`, "utf-8");
|
|
466
|
+
const fileResults = checkVueComponent(file, content);
|
|
467
|
+
results.push(...fileResults);
|
|
468
|
+
}
|
|
469
|
+
return results;
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
function checkVueComponent(file, content) {
|
|
473
|
+
const results = [];
|
|
474
|
+
const scriptContent = getScriptContent(content);
|
|
475
|
+
if (!scriptContent) return results;
|
|
476
|
+
const lines = scriptContent.split("\n");
|
|
477
|
+
const scriptStartLine = getScriptStartLine(content);
|
|
478
|
+
for (let i = 0; i < lines.length; i++) {
|
|
479
|
+
const line = lines[i];
|
|
480
|
+
if (!line) continue;
|
|
481
|
+
if (line.trim().startsWith("import")) continue;
|
|
482
|
+
if (line.trim().startsWith("//")) continue;
|
|
483
|
+
for (const { name, ruleId } of FORBIDDEN_CALLS) {
|
|
484
|
+
const pattern = new RegExp(`\\b${name}\\s*\\(`);
|
|
485
|
+
if (pattern.test(line)) {
|
|
486
|
+
results.push({
|
|
487
|
+
ruleId,
|
|
488
|
+
severity: "error",
|
|
489
|
+
message: `${name}() is forbidden in Vue component - move to composable`,
|
|
490
|
+
file,
|
|
491
|
+
line: scriptStartLine + i,
|
|
492
|
+
suggestion: `Move ${name}() call to the composable file`
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
const templateResults = checkTemplateInlineLogic(file, content);
|
|
498
|
+
results.push(...templateResults);
|
|
499
|
+
return results;
|
|
500
|
+
}
|
|
501
|
+
function getScriptContent(content) {
|
|
502
|
+
const match = content.match(/<script[^>]*setup[^>]*>([\s\S]*?)<\/script>/);
|
|
503
|
+
return match?.[1] ?? null;
|
|
504
|
+
}
|
|
505
|
+
function getScriptStartLine(content) {
|
|
506
|
+
const lines = content.split("\n");
|
|
507
|
+
for (let i = 0; i < lines.length; i++) {
|
|
508
|
+
if (/<script[^>]*setup[^>]*>/.test(lines[i] ?? "")) {
|
|
509
|
+
return i + 2;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return 1;
|
|
513
|
+
}
|
|
514
|
+
function checkTemplateInlineLogic(file, content) {
|
|
515
|
+
const results = [];
|
|
516
|
+
const templateMatch = content.match(/<template>([\s\S]*?)<\/template>/);
|
|
517
|
+
if (!templateMatch) return results;
|
|
518
|
+
const template = templateMatch[1];
|
|
519
|
+
if (!template) return results;
|
|
520
|
+
const templateStartLine = getTemplateStartLine(content);
|
|
521
|
+
const lines = template.split("\n");
|
|
522
|
+
for (let i = 0; i < lines.length; i++) {
|
|
523
|
+
const line = lines[i];
|
|
524
|
+
if (!line) continue;
|
|
525
|
+
const inlineMethods = [
|
|
526
|
+
".reduce(",
|
|
527
|
+
".filter(",
|
|
528
|
+
".map(",
|
|
529
|
+
".find(",
|
|
530
|
+
".some(",
|
|
531
|
+
".every("
|
|
532
|
+
];
|
|
533
|
+
for (const method of inlineMethods) {
|
|
534
|
+
if (line.includes("{{") && line.includes(method)) {
|
|
535
|
+
results.push({
|
|
536
|
+
ruleId: "VUE-006",
|
|
537
|
+
severity: "error",
|
|
538
|
+
message: `Inline ${method.slice(1, -1)}() in template - move to computed in composable`,
|
|
539
|
+
file,
|
|
540
|
+
line: templateStartLine + i,
|
|
541
|
+
suggestion: "Pre-calculate in composable and expose as computed"
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
const bindingMatch = line.match(/:[a-z-]+="([^"]+)"/gi);
|
|
546
|
+
if (bindingMatch) {
|
|
547
|
+
for (const binding of bindingMatch) {
|
|
548
|
+
const value = binding.match(/="([^"]+)"/)?.[1] ?? "";
|
|
549
|
+
const operators = (value.match(/&&|\|\||\?|:/g) ?? []).length;
|
|
550
|
+
if (operators >= 3) {
|
|
551
|
+
results.push({
|
|
552
|
+
ruleId: "VUE-007",
|
|
553
|
+
severity: "warning",
|
|
554
|
+
message: "Complex condition in binding - extract to computed",
|
|
555
|
+
file,
|
|
556
|
+
line: templateStartLine + i,
|
|
557
|
+
suggestion: "Move complex logic to composable computed property"
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return results;
|
|
564
|
+
}
|
|
565
|
+
function getTemplateStartLine(content) {
|
|
566
|
+
const lines = content.split("\n");
|
|
567
|
+
for (let i = 0; i < lines.length; i++) {
|
|
568
|
+
if (/<template>/.test(lines[i] ?? "")) {
|
|
569
|
+
return i + 2;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return 1;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// src/lint/checkers/ast/rules-file.ts
|
|
576
|
+
import fg5 from "fast-glob";
|
|
577
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
578
|
+
var FORBIDDEN_IMPORTS = [
|
|
579
|
+
{ pattern: /from\s+['"]vue['"]/, ruleId: "RUL-001", name: "vue" },
|
|
580
|
+
{
|
|
581
|
+
pattern: /from\s+['"]@vue\//,
|
|
582
|
+
ruleId: "RUL-001",
|
|
583
|
+
name: "@vue/*"
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
pattern: /from\s+['"]pinia['"]/,
|
|
587
|
+
ruleId: "RUL-002",
|
|
588
|
+
name: "pinia"
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
pattern: /from\s+['"]vue-router['"]/,
|
|
592
|
+
ruleId: "RUL-002",
|
|
593
|
+
name: "vue-router"
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
pattern: /from\s+['"]@tanstack/,
|
|
597
|
+
ruleId: "RUL-002",
|
|
598
|
+
name: "@tanstack/*"
|
|
599
|
+
}
|
|
600
|
+
];
|
|
601
|
+
var FORBIDDEN_PATTERNS = [
|
|
602
|
+
{
|
|
603
|
+
pattern: /\basync\s+\w+\s*\(/,
|
|
604
|
+
ruleId: "RUL-008",
|
|
605
|
+
message: "async function"
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
pattern: /\bawait\s+/,
|
|
609
|
+
ruleId: "RUL-008",
|
|
610
|
+
message: "await keyword"
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
pattern: /\bconsole\.(log|warn|error|info)\s*\(/,
|
|
614
|
+
ruleId: "RUL-003",
|
|
615
|
+
message: "console.*"
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
pattern: /\blocalStorage\b/,
|
|
619
|
+
ruleId: "RUL-003",
|
|
620
|
+
message: "localStorage"
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
pattern: /\bsessionStorage\b/,
|
|
624
|
+
ruleId: "RUL-003",
|
|
625
|
+
message: "sessionStorage"
|
|
626
|
+
},
|
|
627
|
+
{ pattern: /\bfetch\s*\(/, ruleId: "RUL-003", message: "fetch()" },
|
|
628
|
+
{
|
|
629
|
+
pattern: /\bnew\s+Date\s*\(\s*\)/,
|
|
630
|
+
ruleId: "RUL-007",
|
|
631
|
+
message: "new Date() without injection"
|
|
632
|
+
}
|
|
633
|
+
];
|
|
634
|
+
var rulesFileChecker = {
|
|
635
|
+
name: "rules-file",
|
|
636
|
+
rules: ["RUL-001", "RUL-002", "RUL-003", "RUL-007", "RUL-008"],
|
|
637
|
+
async check(ctx) {
|
|
638
|
+
const results = [];
|
|
639
|
+
const rulesFiles = await fg5("**/*.rules.ts", {
|
|
640
|
+
cwd: ctx.cwd,
|
|
641
|
+
ignore: [
|
|
642
|
+
"**/node_modules/**",
|
|
643
|
+
"**/worktrees/**",
|
|
644
|
+
"**/dist/**",
|
|
645
|
+
"**/__tests__/**"
|
|
646
|
+
]
|
|
647
|
+
});
|
|
648
|
+
for (const file of rulesFiles) {
|
|
649
|
+
const content = await readFile3(`${ctx.cwd}/${file}`, "utf-8");
|
|
650
|
+
const fileResults = checkRulesFile(file, content);
|
|
651
|
+
results.push(...fileResults);
|
|
652
|
+
}
|
|
653
|
+
return results;
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
function checkRulesFile(file, content) {
|
|
657
|
+
const results = [];
|
|
658
|
+
const lines = content.split("\n");
|
|
659
|
+
for (let i = 0; i < lines.length; i++) {
|
|
660
|
+
const line = lines[i];
|
|
661
|
+
if (!line) continue;
|
|
662
|
+
for (const { pattern, ruleId, name } of FORBIDDEN_IMPORTS) {
|
|
663
|
+
if (pattern.test(line)) {
|
|
664
|
+
results.push({
|
|
665
|
+
ruleId,
|
|
666
|
+
severity: "error",
|
|
667
|
+
message: `Import from "${name}" is forbidden in rules files`,
|
|
668
|
+
file,
|
|
669
|
+
line: i + 1,
|
|
670
|
+
suggestion: "Rules must be pure - no framework dependencies"
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
const trimmed = line.trim();
|
|
675
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
|
|
676
|
+
for (const { pattern, ruleId, message } of FORBIDDEN_PATTERNS) {
|
|
677
|
+
if (pattern.test(line)) {
|
|
678
|
+
if (ruleId === "RUL-007") {
|
|
679
|
+
if (line.includes("= new Date()") && !line.includes(": Date")) {
|
|
680
|
+
results.push({
|
|
681
|
+
ruleId,
|
|
682
|
+
severity: "warning",
|
|
683
|
+
message: `${message} - inject as parameter with default`,
|
|
684
|
+
file,
|
|
685
|
+
line: i + 1,
|
|
686
|
+
suggestion: "Use: (now: Date = new Date()) for testability"
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
} else {
|
|
690
|
+
results.push({
|
|
691
|
+
ruleId,
|
|
692
|
+
severity: "error",
|
|
693
|
+
message: `${message} is forbidden in rules files`,
|
|
694
|
+
file,
|
|
695
|
+
line: i + 1,
|
|
696
|
+
suggestion: ruleId === "RUL-008" ? "Move async operations to query.ts" : "Rules must be pure - no side effects"
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
const hasClass = /\bclass\s+\w+Rules\b/.test(content);
|
|
703
|
+
if (!hasClass) {
|
|
704
|
+
results.push({
|
|
705
|
+
ruleId: "RUL-009",
|
|
706
|
+
severity: "error",
|
|
707
|
+
message: "Rules file must export a class with 'Rules' suffix",
|
|
708
|
+
file,
|
|
709
|
+
suggestion: "Create: class FeatureRules { ... }"
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
const hasSingleton = /export\s+const\s+\w+Rules\s*=\s*new\s+\w+Rules\s*\(\s*\)/.test(
|
|
713
|
+
content
|
|
714
|
+
);
|
|
715
|
+
if (!hasSingleton) {
|
|
716
|
+
results.push({
|
|
717
|
+
ruleId: "RUL-010",
|
|
718
|
+
severity: "error",
|
|
719
|
+
message: "Rules file must export a singleton instance",
|
|
720
|
+
file,
|
|
721
|
+
suggestion: "Add: export const featureRules = new FeatureRules()"
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
return results;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// src/lint/checkers/ast/typescript.ts
|
|
728
|
+
import fg6 from "fast-glob";
|
|
729
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
730
|
+
var typescriptChecker = {
|
|
731
|
+
name: "typescript",
|
|
732
|
+
rules: ["TSP-002", "TSP-004", "TSP-009"],
|
|
733
|
+
async check(ctx) {
|
|
734
|
+
const results = [];
|
|
735
|
+
const tsFiles = await fg6("**/*.ts", {
|
|
736
|
+
cwd: ctx.cwd,
|
|
737
|
+
ignore: [
|
|
738
|
+
"**/node_modules/**",
|
|
739
|
+
"**/worktrees/**",
|
|
740
|
+
"**/dist/**",
|
|
741
|
+
"**/*.d.ts",
|
|
742
|
+
"**/*.spec.ts",
|
|
743
|
+
"**/*.test.ts"
|
|
744
|
+
]
|
|
745
|
+
});
|
|
746
|
+
for (const file of tsFiles) {
|
|
747
|
+
const content = await readFile4(`${ctx.cwd}/${file}`, "utf-8");
|
|
748
|
+
const fileResults = checkTypeScript(file, content);
|
|
749
|
+
results.push(...fileResults);
|
|
750
|
+
}
|
|
751
|
+
return results;
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
function checkTypeScript(file, content) {
|
|
755
|
+
const results = [];
|
|
756
|
+
const lines = content.split("\n");
|
|
757
|
+
for (let i = 0; i < lines.length; i++) {
|
|
758
|
+
const line = lines[i];
|
|
759
|
+
if (!line) continue;
|
|
760
|
+
const trimmed = line.trim();
|
|
761
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
|
|
762
|
+
if (/\benum\s+\w+/.test(line)) {
|
|
763
|
+
results.push({
|
|
764
|
+
ruleId: "TSP-002",
|
|
765
|
+
severity: "error",
|
|
766
|
+
message: "TypeScript enum is forbidden - use 'as const' or Zod",
|
|
767
|
+
file,
|
|
768
|
+
line: i + 1,
|
|
769
|
+
suggestion: "const Status = { Active: 'ACTIVE' } as const"
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
if (/:\s*any\b|as\s+any\b|<any>/.test(line)) {
|
|
773
|
+
results.push({
|
|
774
|
+
ruleId: "TSP-004",
|
|
775
|
+
severity: "error",
|
|
776
|
+
message: "Explicit 'any' type is forbidden",
|
|
777
|
+
file,
|
|
778
|
+
line: i + 1,
|
|
779
|
+
suggestion: "Use 'unknown' and narrow the type, or define proper types"
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
if (/\w+!\.\w+!/.test(line)) {
|
|
783
|
+
results.push({
|
|
784
|
+
ruleId: "TSP-009",
|
|
785
|
+
severity: "warning",
|
|
786
|
+
message: "Chained non-null assertions (!.!) - use proper narrowing",
|
|
787
|
+
file,
|
|
788
|
+
line: i + 1,
|
|
789
|
+
suggestion: "Use optional chaining (?.) or add null checks"
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return results;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// src/lint/reporters/console.ts
|
|
797
|
+
import pc from "picocolors";
|
|
798
|
+
var severityColors = {
|
|
799
|
+
error: pc.red,
|
|
800
|
+
warning: pc.yellow,
|
|
801
|
+
info: pc.blue
|
|
802
|
+
};
|
|
803
|
+
var severityIcons = {
|
|
804
|
+
error: "\u2716",
|
|
805
|
+
warning: "\u26A0",
|
|
806
|
+
info: "\u2139"
|
|
807
|
+
};
|
|
808
|
+
function formatConsole(results) {
|
|
809
|
+
if (results.length === 0) {
|
|
810
|
+
return pc.green("\u2713 No issues found");
|
|
811
|
+
}
|
|
812
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
813
|
+
for (const result of results) {
|
|
814
|
+
const existing = byFile.get(result.file) ?? [];
|
|
815
|
+
existing.push(result);
|
|
816
|
+
byFile.set(result.file, existing);
|
|
817
|
+
}
|
|
818
|
+
const lines = [];
|
|
819
|
+
for (const [file, fileResults] of byFile) {
|
|
820
|
+
lines.push("");
|
|
821
|
+
lines.push(pc.underline(file));
|
|
822
|
+
for (const result of fileResults) {
|
|
823
|
+
const color = severityColors[result.severity];
|
|
824
|
+
const icon = severityIcons[result.severity];
|
|
825
|
+
const location = result.line !== void 0 ? `:${result.line}:${result.column ?? 0}` : "";
|
|
826
|
+
lines.push(
|
|
827
|
+
` ${color(icon)} ${result.message} ${pc.dim(`[${result.ruleId}]`)}`
|
|
828
|
+
);
|
|
829
|
+
if (result.suggestion) {
|
|
830
|
+
lines.push(` ${pc.dim("\u2192")} ${pc.cyan(result.suggestion)}`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
const errorCount = results.filter((r) => r.severity === "error").length;
|
|
835
|
+
const warningCount = results.filter((r) => r.severity === "warning").length;
|
|
836
|
+
const infoCount = results.filter((r) => r.severity === "info").length;
|
|
837
|
+
lines.push("");
|
|
838
|
+
lines.push(
|
|
839
|
+
pc.bold(
|
|
840
|
+
`${pc.red(`${errorCount} error${errorCount !== 1 ? "s" : ""}`)}, ${pc.yellow(`${warningCount} warning${warningCount !== 1 ? "s" : ""}`)}, ${pc.blue(`${infoCount} info`)}`
|
|
841
|
+
)
|
|
842
|
+
);
|
|
843
|
+
return lines.join("\n");
|
|
844
|
+
}
|
|
845
|
+
function formatSummary(results) {
|
|
846
|
+
return {
|
|
847
|
+
errors: results.filter((r) => r.severity === "error").length,
|
|
848
|
+
warnings: results.filter((r) => r.severity === "warning").length,
|
|
849
|
+
info: results.filter((r) => r.severity === "info").length,
|
|
850
|
+
total: results.length
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// src/commands/lint.ts
|
|
855
|
+
var structureCheckers = [
|
|
856
|
+
colocationChecker,
|
|
857
|
+
namingChecker,
|
|
858
|
+
domainsChecker
|
|
859
|
+
];
|
|
860
|
+
var astCheckers = [
|
|
861
|
+
vueComponentChecker,
|
|
862
|
+
rulesFileChecker,
|
|
863
|
+
typescriptChecker
|
|
864
|
+
];
|
|
865
|
+
var allCheckers = [...structureCheckers, ...astCheckers];
|
|
866
|
+
function getChangedFiles(cwd) {
|
|
867
|
+
try {
|
|
868
|
+
const output = execSync("git diff --name-only HEAD", {
|
|
869
|
+
cwd,
|
|
870
|
+
encoding: "utf-8"
|
|
871
|
+
});
|
|
872
|
+
const stagedOutput = execSync("git diff --name-only --cached", {
|
|
873
|
+
cwd,
|
|
874
|
+
encoding: "utf-8"
|
|
875
|
+
});
|
|
876
|
+
const allFiles = [...output.split("\n"), ...stagedOutput.split("\n")].filter(Boolean).filter((f) => f.endsWith(".ts") || f.endsWith(".vue"));
|
|
877
|
+
return [...new Set(allFiles)];
|
|
878
|
+
} catch {
|
|
879
|
+
return [];
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
async function lintCore(options) {
|
|
883
|
+
const cwd = resolve(options.path || ".");
|
|
884
|
+
let files = [];
|
|
885
|
+
if (options.changed) {
|
|
886
|
+
files = getChangedFiles(cwd);
|
|
887
|
+
if (files.length === 0) {
|
|
888
|
+
return {
|
|
889
|
+
success: true,
|
|
890
|
+
command: "lint",
|
|
891
|
+
message: "No changed files to lint",
|
|
892
|
+
files: [],
|
|
893
|
+
results: [],
|
|
894
|
+
summary: { errors: 0, warnings: 0, info: 0, total: 0 }
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
let checkersToRun = allCheckers;
|
|
899
|
+
if (options.rules) {
|
|
900
|
+
const prefixes = options.rules.split(",").map((r) => r.trim().toUpperCase());
|
|
901
|
+
checkersToRun = allCheckers.filter(
|
|
902
|
+
(checker) => checker.rules.some(
|
|
903
|
+
(rule) => prefixes.some((p) => rule.startsWith(p))
|
|
904
|
+
)
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
const allResults = [];
|
|
908
|
+
for (const checker of checkersToRun) {
|
|
909
|
+
const results = await checker.check({ cwd, files });
|
|
910
|
+
if (options.changed && files.length > 0) {
|
|
911
|
+
const filteredResults2 = results.filter(
|
|
912
|
+
(r) => files.some((f) => r.file.endsWith(f))
|
|
913
|
+
);
|
|
914
|
+
allResults.push(...filteredResults2);
|
|
915
|
+
} else {
|
|
916
|
+
allResults.push(...results);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
const filteredResults = options.quiet ? allResults.filter((r) => r.severity === "error") : allResults;
|
|
920
|
+
const summary = formatSummary(allResults);
|
|
921
|
+
return {
|
|
922
|
+
success: summary.errors === 0,
|
|
923
|
+
command: "lint",
|
|
924
|
+
files: options.changed ? files : void 0,
|
|
925
|
+
results: filteredResults,
|
|
926
|
+
summary
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
async function lintCommand(path = ".", options) {
|
|
930
|
+
const startTime = performance.now();
|
|
931
|
+
if (options.debug) {
|
|
932
|
+
console.log(`Linting: ${resolve(path)}`);
|
|
933
|
+
console.log(`Checkers: ${allCheckers.map((c) => c.name).join(", ")}`);
|
|
934
|
+
}
|
|
935
|
+
const result = await lintCore({
|
|
936
|
+
path,
|
|
937
|
+
changed: options.changed,
|
|
938
|
+
rules: options.rules,
|
|
939
|
+
quiet: options.quiet
|
|
940
|
+
});
|
|
941
|
+
if (options.format === "json") {
|
|
942
|
+
console.log(JSON.stringify(result, null, 2));
|
|
943
|
+
} else {
|
|
944
|
+
if (result.message) {
|
|
945
|
+
console.log(result.message);
|
|
946
|
+
} else {
|
|
947
|
+
console.log(formatConsole(result.results));
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
if (options.debug) {
|
|
951
|
+
const elapsed = (performance.now() - startTime).toFixed(0);
|
|
952
|
+
console.log(`
|
|
953
|
+
Completed in ${elapsed}ms`);
|
|
954
|
+
}
|
|
955
|
+
process.exit(result.summary.errors > 0 ? 1 : 0);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// src/commands/analyze.ts
|
|
959
|
+
import { basename as basename4 } from "path";
|
|
960
|
+
import { execSync as execSync2 } from "child_process";
|
|
961
|
+
import { readFileSync, existsSync as existsSync2 } from "fs";
|
|
962
|
+
import fg7 from "fast-glob";
|
|
963
|
+
import matter from "gray-matter";
|
|
964
|
+
import { minimatch } from "minimatch";
|
|
965
|
+
function expandPath(p) {
|
|
966
|
+
if (p.startsWith("~")) {
|
|
967
|
+
return p.replace("~", process.env.HOME || "");
|
|
968
|
+
}
|
|
969
|
+
return p;
|
|
970
|
+
}
|
|
971
|
+
function getChangedFiles2(cwd) {
|
|
972
|
+
try {
|
|
973
|
+
const output = execSync2("git diff --name-only HEAD", {
|
|
974
|
+
cwd,
|
|
975
|
+
encoding: "utf-8"
|
|
976
|
+
});
|
|
977
|
+
const stagedOutput = execSync2("git diff --name-only --cached", {
|
|
978
|
+
cwd,
|
|
979
|
+
encoding: "utf-8"
|
|
980
|
+
});
|
|
981
|
+
return [...output.split("\n"), ...stagedOutput.split("\n")].filter(Boolean).filter((f) => f.endsWith(".ts") || f.endsWith(".vue"));
|
|
982
|
+
} catch {
|
|
983
|
+
return [];
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
function loadKnowledgeMappings(knowledgePath) {
|
|
987
|
+
const expandedPath = expandPath(knowledgePath);
|
|
988
|
+
if (!existsSync2(expandedPath)) {
|
|
989
|
+
return [];
|
|
990
|
+
}
|
|
991
|
+
const knowledgeFiles = fg7.sync("**/*.knowledge.md", {
|
|
992
|
+
cwd: expandedPath,
|
|
993
|
+
absolute: true
|
|
994
|
+
});
|
|
995
|
+
const mappings = [];
|
|
996
|
+
for (const file of knowledgeFiles) {
|
|
997
|
+
try {
|
|
998
|
+
const content = readFileSync(file, "utf-8");
|
|
999
|
+
const { data } = matter(content);
|
|
1000
|
+
if (data.match) {
|
|
1001
|
+
mappings.push({
|
|
1002
|
+
name: data.name || basename4(file, ".knowledge.md"),
|
|
1003
|
+
path: file,
|
|
1004
|
+
match: data.match,
|
|
1005
|
+
description: data.description
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
} catch {
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
return mappings;
|
|
1012
|
+
}
|
|
1013
|
+
function matchFileToKnowledges(file, mappings) {
|
|
1014
|
+
const results = [];
|
|
1015
|
+
const fileName = basename4(file);
|
|
1016
|
+
for (const mapping of mappings) {
|
|
1017
|
+
if (minimatch(fileName, mapping.match) || minimatch(file, mapping.match)) {
|
|
1018
|
+
results.push({
|
|
1019
|
+
file,
|
|
1020
|
+
knowledge: mapping.name,
|
|
1021
|
+
path: mapping.path
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
return results;
|
|
1026
|
+
}
|
|
1027
|
+
var DEFAULT_KNOWLEDGE_PATH = "~/.claude/marketplace/shared/knowledge";
|
|
1028
|
+
async function analyzeCore(options) {
|
|
1029
|
+
const cwd = process.cwd();
|
|
1030
|
+
const knowledgePath = options.knowledgePath || DEFAULT_KNOWLEDGE_PATH;
|
|
1031
|
+
let filesToAnalyze = options.files || [];
|
|
1032
|
+
if (options.changed) {
|
|
1033
|
+
filesToAnalyze = getChangedFiles2(cwd);
|
|
1034
|
+
}
|
|
1035
|
+
if (filesToAnalyze.length === 0) {
|
|
1036
|
+
return {
|
|
1037
|
+
success: true,
|
|
1038
|
+
command: "analyze",
|
|
1039
|
+
message: "No files to analyze",
|
|
1040
|
+
files: [],
|
|
1041
|
+
knowledges: []
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
const mappings = loadKnowledgeMappings(knowledgePath);
|
|
1045
|
+
if (mappings.length === 0) {
|
|
1046
|
+
return {
|
|
1047
|
+
success: false,
|
|
1048
|
+
command: "analyze",
|
|
1049
|
+
error: `No knowledge files with 'match' pattern found in ${knowledgePath}`,
|
|
1050
|
+
files: filesToAnalyze,
|
|
1051
|
+
knowledges: []
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
const allResults = [];
|
|
1055
|
+
for (const file of filesToAnalyze) {
|
|
1056
|
+
const matches = matchFileToKnowledges(file, mappings);
|
|
1057
|
+
allResults.push(...matches);
|
|
1058
|
+
}
|
|
1059
|
+
return {
|
|
1060
|
+
success: true,
|
|
1061
|
+
command: "analyze",
|
|
1062
|
+
files: filesToAnalyze,
|
|
1063
|
+
knowledges: allResults,
|
|
1064
|
+
availableMappings: mappings.length
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
async function analyzeCommand(files = [], options) {
|
|
1068
|
+
const result = await analyzeCore({
|
|
1069
|
+
files,
|
|
1070
|
+
changed: options.changed,
|
|
1071
|
+
knowledgePath: options.knowledgePath
|
|
1072
|
+
});
|
|
1073
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// src/commands/scaffold.ts
|
|
1077
|
+
import { resolve as resolve2, dirname as dirname3, basename as basename5 } from "path";
|
|
1078
|
+
import { mkdirSync, writeFileSync, existsSync as existsSync3 } from "fs";
|
|
1079
|
+
function toPascalCase2(str) {
|
|
1080
|
+
return str.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
1081
|
+
}
|
|
1082
|
+
function toCamelCase(str) {
|
|
1083
|
+
const pascal = toPascalCase2(str);
|
|
1084
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
1085
|
+
}
|
|
1086
|
+
function generateVueFeature(featurePath) {
|
|
1087
|
+
const featureName = basename5(featurePath);
|
|
1088
|
+
const pascalName = toPascalCase2(featureName);
|
|
1089
|
+
const camelName = toCamelCase(featureName);
|
|
1090
|
+
return [
|
|
1091
|
+
{
|
|
1092
|
+
path: `${featurePath}/${pascalName}.vue`,
|
|
1093
|
+
content: `<script setup lang="ts">
|
|
1094
|
+
import { use${pascalName} } from './${camelName}.composable';
|
|
1095
|
+
|
|
1096
|
+
const { /* state and methods */ } = use${pascalName}();
|
|
1097
|
+
</script>
|
|
1098
|
+
|
|
1099
|
+
<template>
|
|
1100
|
+
<div class="${featureName}">
|
|
1101
|
+
<!-- Template -->
|
|
1102
|
+
</div>
|
|
1103
|
+
</template>
|
|
1104
|
+
`,
|
|
1105
|
+
knowledge: "vue-component",
|
|
1106
|
+
rules: ["COL-001", "VUE-001"]
|
|
1107
|
+
},
|
|
1108
|
+
{
|
|
1109
|
+
path: `${featurePath}/${camelName}.composable.ts`,
|
|
1110
|
+
content: `import { ref, computed } from 'vue';
|
|
1111
|
+
import { ${camelName}Rules } from './${camelName}.rules';
|
|
1112
|
+
import type { ${pascalName}State } from './${camelName}.types';
|
|
1113
|
+
|
|
1114
|
+
export function use${pascalName}() {
|
|
1115
|
+
// State
|
|
1116
|
+
|
|
1117
|
+
// Computed
|
|
1118
|
+
|
|
1119
|
+
// Methods
|
|
1120
|
+
|
|
1121
|
+
return {
|
|
1122
|
+
// Expose state and methods
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
`,
|
|
1126
|
+
knowledge: "vue-composable",
|
|
1127
|
+
rules: ["COL-002", "NAM-002", "NAM-003"]
|
|
1128
|
+
},
|
|
1129
|
+
{
|
|
1130
|
+
path: `${featurePath}/${camelName}.rules.ts`,
|
|
1131
|
+
content: `// Pure functions only - no framework imports, no side effects
|
|
1132
|
+
|
|
1133
|
+
export class ${pascalName}Rules {
|
|
1134
|
+
// Validation, calculation, transformation functions
|
|
1135
|
+
|
|
1136
|
+
static validate(input: unknown): boolean {
|
|
1137
|
+
// Implement validation logic
|
|
1138
|
+
return true;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
export const ${camelName}Rules = ${pascalName}Rules;
|
|
1143
|
+
`,
|
|
1144
|
+
knowledge: "vue-rules",
|
|
1145
|
+
rules: ["RUL-001", "RUL-002", "NAM-008"]
|
|
1146
|
+
},
|
|
1147
|
+
{
|
|
1148
|
+
path: `${featurePath}/${camelName}.types.ts`,
|
|
1149
|
+
content: `export interface ${pascalName}State {
|
|
1150
|
+
// Define state types
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
export interface ${pascalName}Props {
|
|
1154
|
+
// Define component props
|
|
1155
|
+
}
|
|
1156
|
+
`,
|
|
1157
|
+
knowledge: "typescript-types",
|
|
1158
|
+
rules: ["COL-010", "NAM-005"]
|
|
1159
|
+
},
|
|
1160
|
+
{
|
|
1161
|
+
path: `${featurePath}/${camelName}.query.ts`,
|
|
1162
|
+
content: `import { useQuery, useMutation } from '@tanstack/vue-query';
|
|
1163
|
+
|
|
1164
|
+
export function use${pascalName}Query() {
|
|
1165
|
+
// Implement TanStack Query hooks
|
|
1166
|
+
}
|
|
1167
|
+
`,
|
|
1168
|
+
knowledge: "tanstack-query",
|
|
1169
|
+
rules: ["NAM-007"]
|
|
1170
|
+
},
|
|
1171
|
+
{
|
|
1172
|
+
path: `${featurePath}/${camelName}.translations.ts`,
|
|
1173
|
+
content: `export const ${camelName}Translations = {
|
|
1174
|
+
fr: {
|
|
1175
|
+
// French translations
|
|
1176
|
+
},
|
|
1177
|
+
en: {
|
|
1178
|
+
// English translations
|
|
1179
|
+
},
|
|
1180
|
+
} as const;
|
|
1181
|
+
`,
|
|
1182
|
+
knowledge: "translations",
|
|
1183
|
+
rules: ["COL-011", "NAM-009"]
|
|
1184
|
+
},
|
|
1185
|
+
{
|
|
1186
|
+
path: `${featurePath}/__tests__/${camelName}.rules.test.ts`,
|
|
1187
|
+
content: `import { describe, it, expect } from 'vitest';
|
|
1188
|
+
import { ${pascalName}Rules } from '../${camelName}.rules';
|
|
1189
|
+
|
|
1190
|
+
describe('${pascalName}Rules', () => {
|
|
1191
|
+
describe('validate', () => {
|
|
1192
|
+
it('should validate correct input', () => {
|
|
1193
|
+
expect(${pascalName}Rules.validate({})).toBe(true);
|
|
1194
|
+
});
|
|
1195
|
+
});
|
|
1196
|
+
});
|
|
1197
|
+
`,
|
|
1198
|
+
knowledge: "testing-rules",
|
|
1199
|
+
rules: ["COL-004"]
|
|
1200
|
+
}
|
|
1201
|
+
];
|
|
1202
|
+
}
|
|
1203
|
+
function generateNestJSFeature(featurePath) {
|
|
1204
|
+
const featureName = basename5(featurePath);
|
|
1205
|
+
const pascalName = toPascalCase2(featureName);
|
|
1206
|
+
const camelName = toCamelCase(featureName);
|
|
1207
|
+
return [
|
|
1208
|
+
{
|
|
1209
|
+
path: `${featurePath}/${camelName}.module.ts`,
|
|
1210
|
+
content: `import { Module } from '@nestjs/common';
|
|
1211
|
+
import { ${pascalName}Controller } from './${camelName}.controller';
|
|
1212
|
+
import { ${pascalName}Service } from './${camelName}.service';
|
|
1213
|
+
|
|
1214
|
+
@Module({
|
|
1215
|
+
controllers: [${pascalName}Controller],
|
|
1216
|
+
providers: [${pascalName}Service],
|
|
1217
|
+
exports: [${pascalName}Service],
|
|
1218
|
+
})
|
|
1219
|
+
export class ${pascalName}Module {}
|
|
1220
|
+
`,
|
|
1221
|
+
knowledge: "nestjs-module"
|
|
1222
|
+
},
|
|
1223
|
+
{
|
|
1224
|
+
path: `${featurePath}/${camelName}.controller.ts`,
|
|
1225
|
+
content: `import { Controller, Get, Post, Body, Param } from '@nestjs/common';
|
|
1226
|
+
import { ${pascalName}Service } from './${camelName}.service';
|
|
1227
|
+
|
|
1228
|
+
@Controller('${featureName}')
|
|
1229
|
+
export class ${pascalName}Controller {
|
|
1230
|
+
constructor(private readonly ${camelName}Service: ${pascalName}Service) {}
|
|
1231
|
+
|
|
1232
|
+
@Get()
|
|
1233
|
+
findAll() {
|
|
1234
|
+
return this.${camelName}Service.findAll();
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
`,
|
|
1238
|
+
knowledge: "nestjs-controller"
|
|
1239
|
+
},
|
|
1240
|
+
{
|
|
1241
|
+
path: `${featurePath}/${camelName}.service.ts`,
|
|
1242
|
+
content: `import { Injectable } from '@nestjs/common';
|
|
1243
|
+
|
|
1244
|
+
@Injectable()
|
|
1245
|
+
export class ${pascalName}Service {
|
|
1246
|
+
findAll() {
|
|
1247
|
+
// Implement service logic
|
|
1248
|
+
return [];
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
`,
|
|
1252
|
+
knowledge: "nestjs-service"
|
|
1253
|
+
},
|
|
1254
|
+
{
|
|
1255
|
+
path: `${featurePath}/${camelName}.types.ts`,
|
|
1256
|
+
content: `export interface ${pascalName}Entity {
|
|
1257
|
+
id: string;
|
|
1258
|
+
// Define entity properties
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
export interface Create${pascalName}Dto {
|
|
1262
|
+
// Define creation DTO
|
|
1263
|
+
}
|
|
1264
|
+
`,
|
|
1265
|
+
knowledge: "typescript-types"
|
|
1266
|
+
},
|
|
1267
|
+
{
|
|
1268
|
+
path: `${featurePath}/__tests__/${camelName}.controller.e2e.spec.ts`,
|
|
1269
|
+
content: `import { Test, TestingModule } from '@nestjs/testing';
|
|
1270
|
+
import { ${pascalName}Controller } from '../${camelName}.controller';
|
|
1271
|
+
import { ${pascalName}Service } from '../${camelName}.service';
|
|
1272
|
+
|
|
1273
|
+
describe('${pascalName}Controller', () => {
|
|
1274
|
+
let controller: ${pascalName}Controller;
|
|
1275
|
+
|
|
1276
|
+
beforeEach(async () => {
|
|
1277
|
+
const module: TestingModule = await Test.createTestingModule({
|
|
1278
|
+
controllers: [${pascalName}Controller],
|
|
1279
|
+
providers: [${pascalName}Service],
|
|
1280
|
+
}).compile();
|
|
1281
|
+
|
|
1282
|
+
controller = module.get<${pascalName}Controller>(${pascalName}Controller);
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
it('should be defined', () => {
|
|
1286
|
+
expect(controller).toBeDefined();
|
|
1287
|
+
});
|
|
1288
|
+
});
|
|
1289
|
+
`,
|
|
1290
|
+
knowledge: "nestjs-testing"
|
|
1291
|
+
}
|
|
1292
|
+
];
|
|
1293
|
+
}
|
|
1294
|
+
function generatePlaywrightFeature(featurePath) {
|
|
1295
|
+
const featureName = basename5(featurePath);
|
|
1296
|
+
const pascalName = toPascalCase2(featureName);
|
|
1297
|
+
const camelName = toCamelCase(featureName);
|
|
1298
|
+
return [
|
|
1299
|
+
{
|
|
1300
|
+
path: `${featurePath}/${camelName}.spec.ts`,
|
|
1301
|
+
content: `import { test, expect } from '@playwright/test';
|
|
1302
|
+
import { ${pascalName}Page } from './${camelName}.page';
|
|
1303
|
+
|
|
1304
|
+
test.describe('${pascalName}', () => {
|
|
1305
|
+
test('should load correctly', async ({ page }) => {
|
|
1306
|
+
const ${camelName}Page = new ${pascalName}Page(page);
|
|
1307
|
+
await ${camelName}Page.goto();
|
|
1308
|
+
// Add assertions
|
|
1309
|
+
});
|
|
1310
|
+
});
|
|
1311
|
+
`,
|
|
1312
|
+
knowledge: "playwright-spec"
|
|
1313
|
+
},
|
|
1314
|
+
{
|
|
1315
|
+
path: `${featurePath}/${camelName}.page.ts`,
|
|
1316
|
+
content: `import type { Page, Locator } from '@playwright/test';
|
|
1317
|
+
|
|
1318
|
+
export class ${pascalName}Page {
|
|
1319
|
+
readonly page: Page;
|
|
1320
|
+
|
|
1321
|
+
constructor(page: Page) {
|
|
1322
|
+
this.page = page;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
async goto() {
|
|
1326
|
+
await this.page.goto('/${featureName}');
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// Add page object methods
|
|
1330
|
+
}
|
|
1331
|
+
`,
|
|
1332
|
+
knowledge: "playwright-page-object"
|
|
1333
|
+
},
|
|
1334
|
+
{
|
|
1335
|
+
path: `${featurePath}/${camelName}.fixtures.ts`,
|
|
1336
|
+
content: `import { test as base } from '@playwright/test';
|
|
1337
|
+
import { ${pascalName}Page } from './${camelName}.page';
|
|
1338
|
+
|
|
1339
|
+
export const test = base.extend<{ ${camelName}Page: ${pascalName}Page }>({
|
|
1340
|
+
${camelName}Page: async ({ page }, use) => {
|
|
1341
|
+
const ${camelName}Page = new ${pascalName}Page(page);
|
|
1342
|
+
await use(${camelName}Page);
|
|
1343
|
+
},
|
|
1344
|
+
});
|
|
1345
|
+
|
|
1346
|
+
export { expect } from '@playwright/test';
|
|
1347
|
+
`,
|
|
1348
|
+
knowledge: "playwright-fixtures"
|
|
1349
|
+
}
|
|
1350
|
+
];
|
|
1351
|
+
}
|
|
1352
|
+
var generators = {
|
|
1353
|
+
"vue-feature": generateVueFeature,
|
|
1354
|
+
"nestjs-feature": generateNestJSFeature,
|
|
1355
|
+
"playwright-feature": generatePlaywrightFeature
|
|
1356
|
+
};
|
|
1357
|
+
async function scaffoldCore(options) {
|
|
1358
|
+
const featureType = options.type;
|
|
1359
|
+
const generator = generators[featureType];
|
|
1360
|
+
if (!generator) {
|
|
1361
|
+
return {
|
|
1362
|
+
success: false,
|
|
1363
|
+
command: "scaffold",
|
|
1364
|
+
error: `Unknown feature type: ${options.type}`,
|
|
1365
|
+
availableTypes: Object.keys(generators)
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
const absolutePath = resolve2(options.path);
|
|
1369
|
+
const files = generator(absolutePath);
|
|
1370
|
+
if (options.dryRun) {
|
|
1371
|
+
return {
|
|
1372
|
+
success: true,
|
|
1373
|
+
command: "scaffold",
|
|
1374
|
+
dryRun: true,
|
|
1375
|
+
type: featureType,
|
|
1376
|
+
path: absolutePath,
|
|
1377
|
+
files: files.map((f) => ({
|
|
1378
|
+
path: f.path,
|
|
1379
|
+
knowledge: f.knowledge,
|
|
1380
|
+
rules: f.rules
|
|
1381
|
+
}))
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
const created = [];
|
|
1385
|
+
const errors = [];
|
|
1386
|
+
for (const file of files) {
|
|
1387
|
+
try {
|
|
1388
|
+
const dir = dirname3(file.path);
|
|
1389
|
+
if (!existsSync3(dir)) {
|
|
1390
|
+
mkdirSync(dir, { recursive: true });
|
|
1391
|
+
}
|
|
1392
|
+
writeFileSync(file.path, file.content, "utf-8");
|
|
1393
|
+
created.push(file.path);
|
|
1394
|
+
} catch (err) {
|
|
1395
|
+
errors.push({
|
|
1396
|
+
path: file.path,
|
|
1397
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
return {
|
|
1402
|
+
success: errors.length === 0,
|
|
1403
|
+
command: "scaffold",
|
|
1404
|
+
type: featureType,
|
|
1405
|
+
path: absolutePath,
|
|
1406
|
+
created,
|
|
1407
|
+
errors: errors.length > 0 ? errors : void 0,
|
|
1408
|
+
knowledges: files.filter((f) => f.knowledge).map((f) => ({
|
|
1409
|
+
path: f.path,
|
|
1410
|
+
knowledge: f.knowledge,
|
|
1411
|
+
rules: f.rules
|
|
1412
|
+
}))
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
async function scaffoldCommand(type, path, options) {
|
|
1416
|
+
const result = await scaffoldCore({
|
|
1417
|
+
type,
|
|
1418
|
+
path,
|
|
1419
|
+
dryRun: options.dryRun
|
|
1420
|
+
});
|
|
1421
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1422
|
+
if (!result.success) {
|
|
1423
|
+
process.exit(1);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
export {
|
|
1428
|
+
lintCore,
|
|
1429
|
+
lintCommand,
|
|
1430
|
+
analyzeCore,
|
|
1431
|
+
analyzeCommand,
|
|
1432
|
+
scaffoldCore,
|
|
1433
|
+
scaffoldCommand
|
|
1434
|
+
};
|