viberails 0.1.0 → 0.2.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/dist/index.cjs +732 -231
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +729 -228
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
package/dist/index.cjs
CHANGED
|
@@ -34,7 +34,7 @@ __export(index_exports, {
|
|
|
34
34
|
VERSION: () => VERSION
|
|
35
35
|
});
|
|
36
36
|
module.exports = __toCommonJS(index_exports);
|
|
37
|
-
var
|
|
37
|
+
var import_chalk7 = __toESM(require("chalk"), 1);
|
|
38
38
|
var import_commander = require("commander");
|
|
39
39
|
|
|
40
40
|
// src/commands/boundaries.ts
|
|
@@ -67,11 +67,11 @@ async function confirm(message) {
|
|
|
67
67
|
input: process.stdin,
|
|
68
68
|
output: process.stdout
|
|
69
69
|
});
|
|
70
|
-
return new Promise((
|
|
70
|
+
return new Promise((resolve4) => {
|
|
71
71
|
rl.question(`${message} (Y/n) `, (answer) => {
|
|
72
72
|
rl.close();
|
|
73
73
|
const trimmed = answer.trim().toLowerCase();
|
|
74
|
-
|
|
74
|
+
resolve4(trimmed === "" || trimmed === "y" || trimmed === "yes");
|
|
75
75
|
});
|
|
76
76
|
});
|
|
77
77
|
}
|
|
@@ -220,12 +220,42 @@ ${import_chalk.default.yellow("Cycles detected:")}`);
|
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
// src/commands/check.ts
|
|
223
|
+
var fs6 = __toESM(require("fs"), 1);
|
|
224
|
+
var path6 = __toESM(require("path"), 1);
|
|
225
|
+
var import_config2 = require("@viberails/config");
|
|
226
|
+
var import_chalk2 = __toESM(require("chalk"), 1);
|
|
227
|
+
|
|
228
|
+
// src/commands/check-config.ts
|
|
229
|
+
function resolveConfigForFile(relPath, config) {
|
|
230
|
+
if (!config.packages || config.packages.length === 0) {
|
|
231
|
+
return { rules: config.rules, conventions: config.conventions };
|
|
232
|
+
}
|
|
233
|
+
const sortedPackages = [...config.packages].sort((a, b) => b.path.length - a.path.length);
|
|
234
|
+
for (const pkg of sortedPackages) {
|
|
235
|
+
if (relPath.startsWith(`${pkg.path}/`) || relPath === pkg.path) {
|
|
236
|
+
return {
|
|
237
|
+
rules: { ...config.rules, ...pkg.rules },
|
|
238
|
+
conventions: { ...config.conventions, ...pkg.conventions }
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return { rules: config.rules, conventions: config.conventions };
|
|
243
|
+
}
|
|
244
|
+
function resolveIgnoreForFile(relPath, config) {
|
|
245
|
+
const globalIgnore = config.ignore;
|
|
246
|
+
if (!config.packages) return globalIgnore;
|
|
247
|
+
for (const pkg of config.packages) {
|
|
248
|
+
if (pkg.ignore && relPath.startsWith(`${pkg.path}/`)) {
|
|
249
|
+
return [...globalIgnore, ...pkg.ignore];
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return globalIgnore;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/commands/check-files.ts
|
|
223
256
|
var import_node_child_process = require("child_process");
|
|
224
257
|
var fs4 = __toESM(require("fs"), 1);
|
|
225
258
|
var path4 = __toESM(require("path"), 1);
|
|
226
|
-
var import_config2 = require("@viberails/config");
|
|
227
|
-
var import_chalk2 = __toESM(require("chalk"), 1);
|
|
228
|
-
var CONFIG_FILE2 = "viberails.config.json";
|
|
229
259
|
var SOURCE_EXTS = /* @__PURE__ */ new Set([
|
|
230
260
|
".ts",
|
|
231
261
|
".tsx",
|
|
@@ -243,6 +273,160 @@ var NAMING_PATTERNS = {
|
|
|
243
273
|
PascalCase: /^[A-Z][a-zA-Z0-9]*$/,
|
|
244
274
|
snake_case: /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/
|
|
245
275
|
};
|
|
276
|
+
function isIgnored(relPath, ignorePatterns) {
|
|
277
|
+
for (const pattern of ignorePatterns) {
|
|
278
|
+
if (pattern.endsWith("/**")) {
|
|
279
|
+
const prefix = pattern.slice(0, -3);
|
|
280
|
+
if (relPath.startsWith(`${prefix}/`) || relPath === prefix) return true;
|
|
281
|
+
} else if (pattern.startsWith("**/")) {
|
|
282
|
+
const suffix = pattern.slice(3);
|
|
283
|
+
if (relPath.endsWith(suffix)) return true;
|
|
284
|
+
} else if (relPath === pattern || relPath.startsWith(`${pattern}/`)) {
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
function countFileLines(filePath) {
|
|
291
|
+
try {
|
|
292
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
293
|
+
if (content.length === 0) return 0;
|
|
294
|
+
let count = 1;
|
|
295
|
+
for (let i = 0; i < content.length; i++) {
|
|
296
|
+
if (content.charCodeAt(i) === 10) count++;
|
|
297
|
+
}
|
|
298
|
+
return count;
|
|
299
|
+
} catch {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function checkNaming(relPath, conventions) {
|
|
304
|
+
const filename = path4.basename(relPath);
|
|
305
|
+
const ext = path4.extname(filename);
|
|
306
|
+
if (!SOURCE_EXTS.has(ext)) return void 0;
|
|
307
|
+
if (filename.startsWith("index.") || filename.includes(".config.") || filename.includes(".test.") || filename.includes(".spec.") || filename.startsWith(".")) {
|
|
308
|
+
return void 0;
|
|
309
|
+
}
|
|
310
|
+
const bare = filename.slice(0, filename.indexOf("."));
|
|
311
|
+
const convention = typeof conventions.fileNaming === "string" ? conventions.fileNaming : conventions.fileNaming?.value;
|
|
312
|
+
if (!convention) return void 0;
|
|
313
|
+
const pattern = NAMING_PATTERNS[convention];
|
|
314
|
+
if (!pattern || pattern.test(bare)) return void 0;
|
|
315
|
+
return `File name "${filename}" does not follow ${convention} convention.`;
|
|
316
|
+
}
|
|
317
|
+
function getStagedFiles(projectRoot) {
|
|
318
|
+
try {
|
|
319
|
+
const output = (0, import_node_child_process.execSync)("git diff --cached --name-only --diff-filter=ACM", {
|
|
320
|
+
cwd: projectRoot,
|
|
321
|
+
encoding: "utf-8"
|
|
322
|
+
});
|
|
323
|
+
return output.trim().split("\n").filter(Boolean);
|
|
324
|
+
} catch {
|
|
325
|
+
return [];
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function getAllSourceFiles(projectRoot, config) {
|
|
329
|
+
const files = [];
|
|
330
|
+
const walk = (dir) => {
|
|
331
|
+
let entries;
|
|
332
|
+
try {
|
|
333
|
+
entries = fs4.readdirSync(dir, { withFileTypes: true });
|
|
334
|
+
} catch {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
for (const entry of entries) {
|
|
338
|
+
const rel = path4.relative(projectRoot, path4.join(dir, entry.name));
|
|
339
|
+
if (entry.isDirectory()) {
|
|
340
|
+
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist") {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (isIgnored(rel, config.ignore)) continue;
|
|
344
|
+
walk(path4.join(dir, entry.name));
|
|
345
|
+
} else if (entry.isFile()) {
|
|
346
|
+
const ext = path4.extname(entry.name);
|
|
347
|
+
if (SOURCE_EXTS.has(ext) && !isIgnored(rel, config.ignore)) {
|
|
348
|
+
files.push(rel);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
walk(projectRoot);
|
|
354
|
+
return files;
|
|
355
|
+
}
|
|
356
|
+
function collectSourceFiles(dir, projectRoot) {
|
|
357
|
+
const files = [];
|
|
358
|
+
const walk = (d) => {
|
|
359
|
+
let entries;
|
|
360
|
+
try {
|
|
361
|
+
entries = fs4.readdirSync(d, { withFileTypes: true });
|
|
362
|
+
} catch {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
for (const entry of entries) {
|
|
366
|
+
if (entry.isDirectory()) {
|
|
367
|
+
if (entry.name === "node_modules") continue;
|
|
368
|
+
walk(path4.join(d, entry.name));
|
|
369
|
+
} else if (entry.isFile()) {
|
|
370
|
+
files.push(path4.relative(projectRoot, path4.join(d, entry.name)));
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
walk(dir);
|
|
375
|
+
return files;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// src/commands/check-tests.ts
|
|
379
|
+
var fs5 = __toESM(require("fs"), 1);
|
|
380
|
+
var path5 = __toESM(require("path"), 1);
|
|
381
|
+
var SOURCE_EXTS2 = /* @__PURE__ */ new Set([
|
|
382
|
+
".ts",
|
|
383
|
+
".tsx",
|
|
384
|
+
".js",
|
|
385
|
+
".jsx",
|
|
386
|
+
".mjs",
|
|
387
|
+
".cjs",
|
|
388
|
+
".vue",
|
|
389
|
+
".svelte",
|
|
390
|
+
".astro"
|
|
391
|
+
]);
|
|
392
|
+
function checkMissingTests(projectRoot, config, severity) {
|
|
393
|
+
const violations = [];
|
|
394
|
+
const { testPattern } = config.structure;
|
|
395
|
+
if (!testPattern) return violations;
|
|
396
|
+
const srcDir = config.structure.srcDir;
|
|
397
|
+
if (!srcDir) return violations;
|
|
398
|
+
const srcPath = path5.join(projectRoot, srcDir);
|
|
399
|
+
if (!fs5.existsSync(srcPath)) return violations;
|
|
400
|
+
const testSuffix = testPattern.replace("*", "");
|
|
401
|
+
const sourceFiles = collectSourceFiles(srcPath, projectRoot);
|
|
402
|
+
for (const relFile of sourceFiles) {
|
|
403
|
+
const basename6 = path5.basename(relFile);
|
|
404
|
+
if (basename6.includes(".test.") || basename6.includes(".spec.") || basename6.startsWith("index.") || basename6.endsWith(".d.ts")) {
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
const ext = path5.extname(basename6);
|
|
408
|
+
if (!SOURCE_EXTS2.has(ext)) continue;
|
|
409
|
+
const stem = basename6.slice(0, basename6.indexOf("."));
|
|
410
|
+
const expectedTestFile = `${stem}${testSuffix}`;
|
|
411
|
+
const dir = path5.dirname(path5.join(projectRoot, relFile));
|
|
412
|
+
const colocatedTest = path5.join(dir, expectedTestFile);
|
|
413
|
+
const testsDir = config.structure.tests;
|
|
414
|
+
const dedicatedTest = testsDir ? path5.join(projectRoot, testsDir, expectedTestFile) : null;
|
|
415
|
+
const hasTest = fs5.existsSync(colocatedTest) || dedicatedTest !== null && fs5.existsSync(dedicatedTest);
|
|
416
|
+
if (!hasTest) {
|
|
417
|
+
violations.push({
|
|
418
|
+
file: relFile,
|
|
419
|
+
rule: "missing-test",
|
|
420
|
+
message: `No test file found. Expected \`${expectedTestFile}\`.`,
|
|
421
|
+
severity
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return violations;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// src/commands/check.ts
|
|
429
|
+
var CONFIG_FILE2 = "viberails.config.json";
|
|
246
430
|
async function checkCommand(options, cwd) {
|
|
247
431
|
const startDir = cwd ?? process.cwd();
|
|
248
432
|
const projectRoot = findProjectRoot(startDir);
|
|
@@ -250,8 +434,8 @@ async function checkCommand(options, cwd) {
|
|
|
250
434
|
console.error(`${import_chalk2.default.red("Error:")} No package.json found. Are you in a JS/TS project?`);
|
|
251
435
|
return 1;
|
|
252
436
|
}
|
|
253
|
-
const configPath =
|
|
254
|
-
if (!
|
|
437
|
+
const configPath = path6.join(projectRoot, CONFIG_FILE2);
|
|
438
|
+
if (!fs6.existsSync(configPath)) {
|
|
255
439
|
console.error(
|
|
256
440
|
`${import_chalk2.default.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
|
|
257
441
|
);
|
|
@@ -273,23 +457,25 @@ async function checkCommand(options, cwd) {
|
|
|
273
457
|
const violations = [];
|
|
274
458
|
const severity = config.enforcement === "enforce" ? "error" : "warn";
|
|
275
459
|
for (const file of filesToCheck) {
|
|
276
|
-
const absPath =
|
|
277
|
-
const relPath =
|
|
278
|
-
|
|
279
|
-
if (
|
|
280
|
-
if (
|
|
460
|
+
const absPath = path6.isAbsolute(file) ? file : path6.join(projectRoot, file);
|
|
461
|
+
const relPath = path6.relative(projectRoot, absPath);
|
|
462
|
+
const effectiveIgnore = resolveIgnoreForFile(relPath, config);
|
|
463
|
+
if (isIgnored(relPath, effectiveIgnore)) continue;
|
|
464
|
+
if (!fs6.existsSync(absPath)) continue;
|
|
465
|
+
const resolved = resolveConfigForFile(relPath, config);
|
|
466
|
+
if (resolved.rules.maxFileLines > 0) {
|
|
281
467
|
const lines = countFileLines(absPath);
|
|
282
|
-
if (lines !== null && lines >
|
|
468
|
+
if (lines !== null && lines > resolved.rules.maxFileLines) {
|
|
283
469
|
violations.push({
|
|
284
470
|
file: relPath,
|
|
285
471
|
rule: "file-size",
|
|
286
|
-
message: `${lines} lines (max ${
|
|
472
|
+
message: `${lines} lines (max ${resolved.rules.maxFileLines}). Split into focused modules.`,
|
|
287
473
|
severity
|
|
288
474
|
});
|
|
289
475
|
}
|
|
290
476
|
}
|
|
291
|
-
if (
|
|
292
|
-
const namingViolation = checkNaming(relPath,
|
|
477
|
+
if (resolved.rules.enforceNaming && resolved.conventions.fileNaming) {
|
|
478
|
+
const namingViolation = checkNaming(relPath, resolved.conventions);
|
|
293
479
|
if (namingViolation) {
|
|
294
480
|
violations.push({
|
|
295
481
|
file: relPath,
|
|
@@ -313,10 +499,10 @@ async function checkCommand(options, cwd) {
|
|
|
313
499
|
ignore: config.ignore
|
|
314
500
|
});
|
|
315
501
|
const boundaryViolations = checkBoundaries(graph, config.boundaries);
|
|
316
|
-
const filterSet = options.staged || options.files ? new Set(filesToCheck.map((f) =>
|
|
502
|
+
const filterSet = options.staged || options.files ? new Set(filesToCheck.map((f) => path6.resolve(projectRoot, f))) : null;
|
|
317
503
|
for (const bv of boundaryViolations) {
|
|
318
504
|
if (filterSet && !filterSet.has(bv.file)) continue;
|
|
319
|
-
const relFile =
|
|
505
|
+
const relFile = path6.relative(projectRoot, bv.file);
|
|
320
506
|
violations.push({
|
|
321
507
|
file: relFile,
|
|
322
508
|
rule: "boundary-violation",
|
|
@@ -344,153 +530,378 @@ ${violations.length} ${word} found.`);
|
|
|
344
530
|
}
|
|
345
531
|
return 0;
|
|
346
532
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
533
|
+
|
|
534
|
+
// src/commands/fix.ts
|
|
535
|
+
var import_node_child_process2 = require("child_process");
|
|
536
|
+
var fs9 = __toESM(require("fs"), 1);
|
|
537
|
+
var path10 = __toESM(require("path"), 1);
|
|
538
|
+
var import_node_readline = require("readline");
|
|
539
|
+
var import_config3 = require("@viberails/config");
|
|
540
|
+
var import_chalk3 = __toESM(require("chalk"), 1);
|
|
541
|
+
|
|
542
|
+
// src/commands/fix-imports.ts
|
|
543
|
+
var path7 = __toESM(require("path"), 1);
|
|
544
|
+
function stripExtension(filePath) {
|
|
545
|
+
return filePath.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
|
|
359
546
|
}
|
|
360
|
-
function
|
|
361
|
-
const
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
}
|
|
367
|
-
const bare = filename.slice(0, filename.indexOf("."));
|
|
368
|
-
const convention = typeof config.conventions.fileNaming === "string" ? config.conventions.fileNaming : config.conventions.fileNaming?.value;
|
|
369
|
-
if (!convention) return void 0;
|
|
370
|
-
const pattern = NAMING_PATTERNS[convention];
|
|
371
|
-
if (!pattern || pattern.test(bare)) return void 0;
|
|
372
|
-
return `File name "${filename}" does not follow ${convention} convention.`;
|
|
547
|
+
function computeNewSpecifier(oldSpecifier, newBare) {
|
|
548
|
+
const hasJsExt = oldSpecifier.endsWith(".js");
|
|
549
|
+
const base = hasJsExt ? oldSpecifier.slice(0, -3) : oldSpecifier;
|
|
550
|
+
const dir = base.lastIndexOf("/");
|
|
551
|
+
const prefix = dir >= 0 ? base.slice(0, dir + 1) : "";
|
|
552
|
+
const newSpec = prefix + newBare;
|
|
553
|
+
return hasJsExt ? `${newSpec}.js` : newSpec;
|
|
373
554
|
}
|
|
374
|
-
function
|
|
375
|
-
|
|
376
|
-
const {
|
|
377
|
-
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
555
|
+
async function updateImportsAfterRenames(renames, projectRoot) {
|
|
556
|
+
if (renames.length === 0) return [];
|
|
557
|
+
const { Project, SyntaxKind } = await import("ts-morph");
|
|
558
|
+
const renameMap = /* @__PURE__ */ new Map();
|
|
559
|
+
for (const r of renames) {
|
|
560
|
+
const oldStripped = stripExtension(r.oldAbsPath);
|
|
561
|
+
const newFilename = path7.basename(r.newPath);
|
|
562
|
+
const newName = newFilename.slice(0, newFilename.indexOf("."));
|
|
563
|
+
renameMap.set(oldStripped, { newBare: newName });
|
|
564
|
+
}
|
|
565
|
+
const project = new Project({
|
|
566
|
+
tsConfigFilePath: void 0,
|
|
567
|
+
skipAddingFilesFromTsConfig: true
|
|
568
|
+
});
|
|
569
|
+
project.addSourceFilesAtPaths(path7.join(projectRoot, "**/*.{ts,tsx,js,jsx,mjs,cjs}"));
|
|
570
|
+
const updates = [];
|
|
571
|
+
const extensions = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
|
|
572
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
573
|
+
const filePath = sourceFile.getFilePath();
|
|
574
|
+
if (filePath.includes("/node_modules/") || filePath.includes("/dist/")) continue;
|
|
575
|
+
const fileDir = path7.dirname(filePath);
|
|
576
|
+
for (const decl of sourceFile.getImportDeclarations()) {
|
|
577
|
+
const specifier = decl.getModuleSpecifierValue();
|
|
578
|
+
if (!specifier.startsWith(".")) continue;
|
|
579
|
+
const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
|
|
580
|
+
if (!match) continue;
|
|
581
|
+
const newSpec = computeNewSpecifier(specifier, match.newBare);
|
|
582
|
+
updates.push({
|
|
583
|
+
file: filePath,
|
|
584
|
+
oldSpecifier: specifier,
|
|
585
|
+
newSpecifier: newSpec,
|
|
586
|
+
line: decl.getStartLineNumber()
|
|
587
|
+
});
|
|
588
|
+
decl.setModuleSpecifier(newSpec);
|
|
388
589
|
}
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
file: relFile,
|
|
401
|
-
rule: "missing-test",
|
|
402
|
-
message: `No test file found. Expected \`${expectedTestFile}\`.`,
|
|
403
|
-
severity
|
|
590
|
+
for (const decl of sourceFile.getExportDeclarations()) {
|
|
591
|
+
const specifier = decl.getModuleSpecifierValue();
|
|
592
|
+
if (!specifier || !specifier.startsWith(".")) continue;
|
|
593
|
+
const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
|
|
594
|
+
if (!match) continue;
|
|
595
|
+
const newSpec = computeNewSpecifier(specifier, match.newBare);
|
|
596
|
+
updates.push({
|
|
597
|
+
file: filePath,
|
|
598
|
+
oldSpecifier: specifier,
|
|
599
|
+
newSpecifier: newSpec,
|
|
600
|
+
line: decl.getStartLineNumber()
|
|
404
601
|
});
|
|
602
|
+
decl.setModuleSpecifier(newSpec);
|
|
603
|
+
}
|
|
604
|
+
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
605
|
+
if (call.getExpression().getKind() !== SyntaxKind.ImportKeyword) continue;
|
|
606
|
+
const args = call.getArguments();
|
|
607
|
+
if (args.length === 0) continue;
|
|
608
|
+
const arg = args[0];
|
|
609
|
+
if (arg.getKind() !== SyntaxKind.StringLiteral) continue;
|
|
610
|
+
const specifier = arg.getText().slice(1, -1);
|
|
611
|
+
if (!specifier.startsWith(".")) continue;
|
|
612
|
+
const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
|
|
613
|
+
if (!match) continue;
|
|
614
|
+
const newSpec = computeNewSpecifier(specifier, match.newBare);
|
|
615
|
+
updates.push({
|
|
616
|
+
file: filePath,
|
|
617
|
+
oldSpecifier: specifier,
|
|
618
|
+
newSpecifier: newSpec,
|
|
619
|
+
line: call.getStartLineNumber()
|
|
620
|
+
});
|
|
621
|
+
const quote = arg.getText()[0];
|
|
622
|
+
arg.replaceWithText(`${quote}${newSpec}${quote}`);
|
|
405
623
|
}
|
|
406
624
|
}
|
|
407
|
-
|
|
625
|
+
if (updates.length > 0) {
|
|
626
|
+
await project.save();
|
|
627
|
+
}
|
|
628
|
+
return updates;
|
|
408
629
|
}
|
|
409
|
-
function
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
return [];
|
|
630
|
+
function resolveToRenamedFile(specifier, fromDir, renameMap, extensions) {
|
|
631
|
+
const cleanSpec = specifier.endsWith(".js") ? specifier.slice(0, -3) : specifier;
|
|
632
|
+
const resolved = path7.resolve(fromDir, cleanSpec);
|
|
633
|
+
for (const ext of extensions) {
|
|
634
|
+
const candidate = resolved + ext;
|
|
635
|
+
const stripped = stripExtension(candidate);
|
|
636
|
+
const match = renameMap.get(stripped);
|
|
637
|
+
if (match) return match;
|
|
418
638
|
}
|
|
639
|
+
return void 0;
|
|
419
640
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
641
|
+
|
|
642
|
+
// src/commands/fix-naming.ts
|
|
643
|
+
var fs7 = __toESM(require("fs"), 1);
|
|
644
|
+
var path8 = __toESM(require("path"), 1);
|
|
645
|
+
|
|
646
|
+
// src/commands/convert-name.ts
|
|
647
|
+
function splitIntoWords(name) {
|
|
648
|
+
const parts = name.split(/[-_]/);
|
|
649
|
+
const words = [];
|
|
650
|
+
for (const part of parts) {
|
|
651
|
+
if (part === "") continue;
|
|
652
|
+
let current = "";
|
|
653
|
+
for (let i = 0; i < part.length; i++) {
|
|
654
|
+
const ch = part[i];
|
|
655
|
+
const isUpper = ch >= "A" && ch <= "Z";
|
|
656
|
+
if (isUpper && current.length > 0) {
|
|
657
|
+
const prevIsUpper = current[current.length - 1] >= "A" && current[current.length - 1] <= "Z";
|
|
658
|
+
const nextIsLower = i + 1 < part.length && part[i + 1] >= "a" && part[i + 1] <= "z";
|
|
659
|
+
if (!prevIsUpper || nextIsLower) {
|
|
660
|
+
words.push(current.toLowerCase());
|
|
661
|
+
current = "";
|
|
441
662
|
}
|
|
442
663
|
}
|
|
664
|
+
current += ch;
|
|
443
665
|
}
|
|
666
|
+
if (current) words.push(current.toLowerCase());
|
|
667
|
+
}
|
|
668
|
+
return words;
|
|
669
|
+
}
|
|
670
|
+
function convertName(bare, target) {
|
|
671
|
+
const words = splitIntoWords(bare);
|
|
672
|
+
if (words.length === 0) return bare;
|
|
673
|
+
switch (target) {
|
|
674
|
+
case "kebab-case":
|
|
675
|
+
return words.join("-");
|
|
676
|
+
case "camelCase":
|
|
677
|
+
return words[0] + words.slice(1).map(capitalize).join("");
|
|
678
|
+
case "PascalCase":
|
|
679
|
+
return words.map(capitalize).join("");
|
|
680
|
+
case "snake_case":
|
|
681
|
+
return words.join("_");
|
|
682
|
+
default:
|
|
683
|
+
return bare;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
function capitalize(word) {
|
|
687
|
+
if (word.length === 0) return word;
|
|
688
|
+
return word[0].toUpperCase() + word.slice(1);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// src/commands/fix-naming.ts
|
|
692
|
+
function computeRename(relPath, targetConvention, projectRoot) {
|
|
693
|
+
const filename = path8.basename(relPath);
|
|
694
|
+
const dir = path8.dirname(relPath);
|
|
695
|
+
const dotIndex = filename.indexOf(".");
|
|
696
|
+
if (dotIndex === -1) return null;
|
|
697
|
+
const bare = filename.slice(0, dotIndex);
|
|
698
|
+
const suffix = filename.slice(dotIndex);
|
|
699
|
+
const newBare = convertName(bare, targetConvention);
|
|
700
|
+
if (newBare === bare) return null;
|
|
701
|
+
const newFilename = newBare + suffix;
|
|
702
|
+
const newRelPath = path8.join(dir, newFilename);
|
|
703
|
+
const oldAbsPath = path8.join(projectRoot, relPath);
|
|
704
|
+
const newAbsPath = path8.join(projectRoot, newRelPath);
|
|
705
|
+
if (fs7.existsSync(newAbsPath)) return null;
|
|
706
|
+
return { oldPath: relPath, newPath: newRelPath, oldAbsPath, newAbsPath };
|
|
707
|
+
}
|
|
708
|
+
function executeRename(rename) {
|
|
709
|
+
if (fs7.existsSync(rename.newAbsPath)) return false;
|
|
710
|
+
fs7.renameSync(rename.oldAbsPath, rename.newAbsPath);
|
|
711
|
+
return true;
|
|
712
|
+
}
|
|
713
|
+
function deduplicateRenames(renames) {
|
|
714
|
+
const seen = /* @__PURE__ */ new Set();
|
|
715
|
+
const result = [];
|
|
716
|
+
for (const r of renames) {
|
|
717
|
+
if (seen.has(r.newAbsPath)) continue;
|
|
718
|
+
seen.add(r.newAbsPath);
|
|
719
|
+
result.push(r);
|
|
720
|
+
}
|
|
721
|
+
return result;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// src/commands/fix-tests.ts
|
|
725
|
+
var fs8 = __toESM(require("fs"), 1);
|
|
726
|
+
var path9 = __toESM(require("path"), 1);
|
|
727
|
+
function generateTestStub(sourceRelPath, config, projectRoot) {
|
|
728
|
+
const { testPattern } = config.structure;
|
|
729
|
+
if (!testPattern) return null;
|
|
730
|
+
const basename6 = path9.basename(sourceRelPath);
|
|
731
|
+
const stem = basename6.slice(0, basename6.indexOf("."));
|
|
732
|
+
const testSuffix = testPattern.replace("*", "");
|
|
733
|
+
const testFilename = `${stem}${testSuffix}`;
|
|
734
|
+
const dir = path9.dirname(path9.join(projectRoot, sourceRelPath));
|
|
735
|
+
const testAbsPath = path9.join(dir, testFilename);
|
|
736
|
+
if (fs8.existsSync(testAbsPath)) return null;
|
|
737
|
+
return {
|
|
738
|
+
path: path9.relative(projectRoot, testAbsPath),
|
|
739
|
+
absPath: testAbsPath,
|
|
740
|
+
moduleName: stem
|
|
444
741
|
};
|
|
445
|
-
walk(projectRoot);
|
|
446
|
-
return files;
|
|
447
742
|
}
|
|
448
|
-
function
|
|
449
|
-
const
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
743
|
+
function writeTestStub(stub, config) {
|
|
744
|
+
const runner = config.stack.testRunner === "jest" ? "jest" : "vitest";
|
|
745
|
+
const importLine = runner === "jest" ? "" : "import { describe, it, expect } from 'vitest';\n\n";
|
|
746
|
+
const content = `${importLine}describe('${stub.moduleName}', () => {
|
|
747
|
+
it.todo('add tests');
|
|
748
|
+
});
|
|
749
|
+
`;
|
|
750
|
+
fs8.mkdirSync(path9.dirname(stub.absPath), { recursive: true });
|
|
751
|
+
fs8.writeFileSync(stub.absPath, content);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// src/commands/fix.ts
|
|
755
|
+
var CONFIG_FILE3 = "viberails.config.json";
|
|
756
|
+
async function fixCommand(options, cwd) {
|
|
757
|
+
const startDir = cwd ?? process.cwd();
|
|
758
|
+
const projectRoot = findProjectRoot(startDir);
|
|
759
|
+
if (!projectRoot) {
|
|
760
|
+
console.error(`${import_chalk3.default.red("Error:")} No package.json found. Are you in a JS/TS project?`);
|
|
761
|
+
return 1;
|
|
762
|
+
}
|
|
763
|
+
const configPath = path10.join(projectRoot, CONFIG_FILE3);
|
|
764
|
+
if (!fs9.existsSync(configPath)) {
|
|
765
|
+
console.error(
|
|
766
|
+
`${import_chalk3.default.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
|
|
767
|
+
);
|
|
768
|
+
return 1;
|
|
769
|
+
}
|
|
770
|
+
const config = await (0, import_config3.loadConfig)(configPath);
|
|
771
|
+
if (!options.yes && !options.dryRun) {
|
|
772
|
+
const isDirty = checkGitDirty(projectRoot);
|
|
773
|
+
if (isDirty) {
|
|
774
|
+
console.log(
|
|
775
|
+
import_chalk3.default.yellow("Warning: You have uncommitted changes. Consider committing first.")
|
|
776
|
+
);
|
|
456
777
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
778
|
+
}
|
|
779
|
+
const shouldFixNaming = !options.rule || options.rule.includes("file-naming");
|
|
780
|
+
const shouldFixTests = !options.rule || options.rule.includes("missing-test");
|
|
781
|
+
const allFiles = getAllSourceFiles(projectRoot, config);
|
|
782
|
+
const renames = [];
|
|
783
|
+
if (shouldFixNaming) {
|
|
784
|
+
for (const file of allFiles) {
|
|
785
|
+
const resolved = resolveConfigForFile(file, config);
|
|
786
|
+
if (!resolved.rules.enforceNaming || !resolved.conventions.fileNaming) continue;
|
|
787
|
+
const violation = checkNaming(file, resolved.conventions);
|
|
788
|
+
if (!violation) continue;
|
|
789
|
+
const convention = getConventionValue(resolved.conventions.fileNaming);
|
|
790
|
+
if (!convention) continue;
|
|
791
|
+
const rename = computeRename(file, convention, projectRoot);
|
|
792
|
+
if (rename) renames.push(rename);
|
|
464
793
|
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
|
|
794
|
+
}
|
|
795
|
+
const dedupedRenames = deduplicateRenames(renames);
|
|
796
|
+
const testStubs = [];
|
|
797
|
+
if (shouldFixTests && config.rules.requireTests) {
|
|
798
|
+
const testViolations = checkMissingTests(projectRoot, config, "warn");
|
|
799
|
+
for (const v of testViolations) {
|
|
800
|
+
const stub = generateTestStub(v.file, config, projectRoot);
|
|
801
|
+
if (stub) testStubs.push(stub);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
if (dedupedRenames.length === 0 && testStubs.length === 0) {
|
|
805
|
+
console.log(`${import_chalk3.default.green("\u2713")} No fixable violations found.`);
|
|
806
|
+
return 0;
|
|
807
|
+
}
|
|
808
|
+
printPlan(dedupedRenames, testStubs);
|
|
809
|
+
if (options.dryRun) {
|
|
810
|
+
console.log(import_chalk3.default.dim("\nDry run \u2014 no changes applied."));
|
|
811
|
+
return 0;
|
|
812
|
+
}
|
|
813
|
+
if (!options.yes) {
|
|
814
|
+
const confirmed = await promptConfirm("Apply these fixes?");
|
|
815
|
+
if (!confirmed) {
|
|
816
|
+
console.log("Aborted.");
|
|
817
|
+
return 0;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
let renameCount = 0;
|
|
821
|
+
for (const rename of dedupedRenames) {
|
|
822
|
+
if (executeRename(rename)) {
|
|
823
|
+
renameCount++;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
let importUpdateCount = 0;
|
|
827
|
+
if (renameCount > 0) {
|
|
828
|
+
const appliedRenames = dedupedRenames.filter((r) => fs9.existsSync(r.newAbsPath));
|
|
829
|
+
const updates = await updateImportsAfterRenames(appliedRenames, projectRoot);
|
|
830
|
+
importUpdateCount = updates.length;
|
|
831
|
+
}
|
|
832
|
+
let stubCount = 0;
|
|
833
|
+
for (const stub of testStubs) {
|
|
834
|
+
if (!fs9.existsSync(stub.absPath)) {
|
|
835
|
+
writeTestStub(stub, config);
|
|
836
|
+
stubCount++;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
console.log("");
|
|
840
|
+
if (renameCount > 0) {
|
|
841
|
+
console.log(`${import_chalk3.default.green("\u2713")} Renamed ${renameCount} file${renameCount > 1 ? "s" : ""}`);
|
|
842
|
+
}
|
|
843
|
+
if (importUpdateCount > 0) {
|
|
844
|
+
console.log(
|
|
845
|
+
`${import_chalk3.default.green("\u2713")} Updated ${importUpdateCount} import${importUpdateCount > 1 ? "s" : ""}`
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
if (stubCount > 0) {
|
|
849
|
+
console.log(`${import_chalk3.default.green("\u2713")} Generated ${stubCount} test stub${stubCount > 1 ? "s" : ""}`);
|
|
850
|
+
}
|
|
851
|
+
return 0;
|
|
468
852
|
}
|
|
469
|
-
function
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
} else if (pattern.startsWith("**/")) {
|
|
475
|
-
const suffix = pattern.slice(3);
|
|
476
|
-
if (relPath.endsWith(suffix)) return true;
|
|
477
|
-
} else if (relPath === pattern || relPath.startsWith(`${pattern}/`)) {
|
|
478
|
-
return true;
|
|
853
|
+
function printPlan(renames, stubs) {
|
|
854
|
+
if (renames.length > 0) {
|
|
855
|
+
console.log(import_chalk3.default.bold("\nFile renames:"));
|
|
856
|
+
for (const r of renames) {
|
|
857
|
+
console.log(` ${import_chalk3.default.red(r.oldPath)} \u2192 ${import_chalk3.default.green(r.newPath)}`);
|
|
479
858
|
}
|
|
480
859
|
}
|
|
481
|
-
|
|
860
|
+
if (stubs.length > 0) {
|
|
861
|
+
console.log(import_chalk3.default.bold("\nTest stubs to create:"));
|
|
862
|
+
for (const s of stubs) {
|
|
863
|
+
console.log(` ${import_chalk3.default.green("+")} ${s.path}`);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
function checkGitDirty(projectRoot) {
|
|
868
|
+
try {
|
|
869
|
+
const output = (0, import_node_child_process2.execSync)("git status --porcelain", {
|
|
870
|
+
cwd: projectRoot,
|
|
871
|
+
encoding: "utf-8"
|
|
872
|
+
});
|
|
873
|
+
return output.trim().length > 0;
|
|
874
|
+
} catch {
|
|
875
|
+
return false;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
function getConventionValue(convention) {
|
|
879
|
+
if (typeof convention === "string") return convention;
|
|
880
|
+
if (convention && typeof convention === "object" && "value" in convention) {
|
|
881
|
+
return convention.value;
|
|
882
|
+
}
|
|
883
|
+
return void 0;
|
|
884
|
+
}
|
|
885
|
+
function promptConfirm(question) {
|
|
886
|
+
const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
|
|
887
|
+
return new Promise((resolve4) => {
|
|
888
|
+
rl.question(`${question} (y/N) `, (answer) => {
|
|
889
|
+
rl.close();
|
|
890
|
+
resolve4(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
891
|
+
});
|
|
892
|
+
});
|
|
482
893
|
}
|
|
483
894
|
|
|
484
895
|
// src/commands/init.ts
|
|
485
|
-
var
|
|
486
|
-
var
|
|
487
|
-
var
|
|
896
|
+
var fs11 = __toESM(require("fs"), 1);
|
|
897
|
+
var path12 = __toESM(require("path"), 1);
|
|
898
|
+
var import_config4 = require("@viberails/config");
|
|
488
899
|
var import_scanner = require("@viberails/scanner");
|
|
489
|
-
var
|
|
900
|
+
var import_chalk5 = __toESM(require("chalk"), 1);
|
|
490
901
|
|
|
491
902
|
// src/display.ts
|
|
492
903
|
var import_types = require("@viberails/types");
|
|
493
|
-
var
|
|
904
|
+
var import_chalk4 = __toESM(require("chalk"), 1);
|
|
494
905
|
var CONVENTION_LABELS = {
|
|
495
906
|
fileNaming: "File naming",
|
|
496
907
|
componentNaming: "Component naming",
|
|
@@ -508,53 +919,128 @@ function confidenceLabel(convention) {
|
|
|
508
919
|
}
|
|
509
920
|
return `${pct}% \u2014 medium confidence, suggested only`;
|
|
510
921
|
}
|
|
922
|
+
function formatPackageSummary(pkg) {
|
|
923
|
+
const parts = [];
|
|
924
|
+
if (pkg.stack.framework) {
|
|
925
|
+
parts.push(formatItem(pkg.stack.framework, import_types.FRAMEWORK_NAMES));
|
|
926
|
+
}
|
|
927
|
+
if (pkg.stack.styling) {
|
|
928
|
+
parts.push(formatItem(pkg.stack.styling, import_types.STYLING_NAMES));
|
|
929
|
+
}
|
|
930
|
+
const files = `${pkg.statistics.totalFiles} files`;
|
|
931
|
+
const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
|
|
932
|
+
return ` ${pkg.relativePath} \u2014 ${detail}`;
|
|
933
|
+
}
|
|
934
|
+
function displayMonorepoResults(scanResult) {
|
|
935
|
+
const { stack, packages } = scanResult;
|
|
936
|
+
console.log(`
|
|
937
|
+
${import_chalk4.default.bold(`Detected: (monorepo, ${packages.length} packages)`)}`);
|
|
938
|
+
console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.language)}`);
|
|
939
|
+
if (stack.packageManager) {
|
|
940
|
+
console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.packageManager)}`);
|
|
941
|
+
}
|
|
942
|
+
if (stack.linter) {
|
|
943
|
+
console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.linter)}`);
|
|
944
|
+
}
|
|
945
|
+
if (stack.testRunner) {
|
|
946
|
+
console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
|
|
947
|
+
}
|
|
948
|
+
console.log("");
|
|
949
|
+
for (const pkg of packages) {
|
|
950
|
+
console.log(formatPackageSummary(pkg));
|
|
951
|
+
}
|
|
952
|
+
const packagesWithDirs = packages.filter(
|
|
953
|
+
(pkg) => pkg.structure.directories.some((d) => d.role !== "unknown")
|
|
954
|
+
);
|
|
955
|
+
if (packagesWithDirs.length > 0) {
|
|
956
|
+
console.log(`
|
|
957
|
+
${import_chalk4.default.bold("Structure:")}`);
|
|
958
|
+
for (const pkg of packagesWithDirs) {
|
|
959
|
+
const meaningfulDirs = pkg.structure.directories.filter((d) => d.role !== "unknown");
|
|
960
|
+
if (meaningfulDirs.length === 0) continue;
|
|
961
|
+
console.log(` ${pkg.relativePath}:`);
|
|
962
|
+
for (const dir of meaningfulDirs) {
|
|
963
|
+
const label = import_types.ROLE_DESCRIPTIONS[dir.role] ?? dir.role;
|
|
964
|
+
const files = dir.fileCount === 1 ? "1 file" : `${dir.fileCount} files`;
|
|
965
|
+
console.log(` ${import_chalk4.default.green("\u2713")} ${dir.path} \u2014 ${label} (${files})`);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
const conventionEntries = Object.entries(scanResult.conventions);
|
|
970
|
+
if (conventionEntries.length > 0) {
|
|
971
|
+
console.log(`
|
|
972
|
+
${import_chalk4.default.bold("Conventions:")}`);
|
|
973
|
+
for (const [key, convention] of conventionEntries) {
|
|
974
|
+
if (convention.confidence === "low") continue;
|
|
975
|
+
const label = CONVENTION_LABELS[key] ?? key;
|
|
976
|
+
const pkgValues = packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
|
|
977
|
+
const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
|
|
978
|
+
if (allSame || pkgValues.length <= 1) {
|
|
979
|
+
const ind = convention.confidence === "high" ? import_chalk4.default.green("\u2713") : import_chalk4.default.yellow("~");
|
|
980
|
+
const detail = import_chalk4.default.dim(`(${confidenceLabel(convention)})`);
|
|
981
|
+
console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
|
|
982
|
+
} else {
|
|
983
|
+
console.log(` ${import_chalk4.default.yellow("~")} ${label}: varies by package`);
|
|
984
|
+
for (const pv of pkgValues) {
|
|
985
|
+
const pct = Math.round(pv.convention.consistency);
|
|
986
|
+
console.log(` ${pv.relativePath}: ${pv.convention.value} (${pct}%)`);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
console.log("");
|
|
992
|
+
}
|
|
511
993
|
function displayScanResults(scanResult) {
|
|
994
|
+
if (scanResult.packages.length > 1) {
|
|
995
|
+
displayMonorepoResults(scanResult);
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
512
998
|
const { stack, conventions } = scanResult;
|
|
513
999
|
console.log(`
|
|
514
|
-
${
|
|
1000
|
+
${import_chalk4.default.bold("Detected:")}`);
|
|
515
1001
|
if (stack.framework) {
|
|
516
|
-
console.log(` ${
|
|
1002
|
+
console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.framework, import_types.FRAMEWORK_NAMES)}`);
|
|
517
1003
|
}
|
|
518
|
-
console.log(` ${
|
|
1004
|
+
console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.language)}`);
|
|
519
1005
|
if (stack.styling) {
|
|
520
|
-
console.log(` ${
|
|
1006
|
+
console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.styling, import_types.STYLING_NAMES)}`);
|
|
521
1007
|
}
|
|
522
1008
|
if (stack.backend) {
|
|
523
|
-
console.log(` ${
|
|
1009
|
+
console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.backend, import_types.FRAMEWORK_NAMES)}`);
|
|
524
1010
|
}
|
|
525
1011
|
if (stack.linter) {
|
|
526
|
-
console.log(` ${
|
|
1012
|
+
console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.linter)}`);
|
|
527
1013
|
}
|
|
528
1014
|
if (stack.testRunner) {
|
|
529
|
-
console.log(` ${
|
|
1015
|
+
console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
|
|
530
1016
|
}
|
|
531
1017
|
if (stack.packageManager) {
|
|
532
|
-
console.log(` ${
|
|
1018
|
+
console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.packageManager)}`);
|
|
533
1019
|
}
|
|
534
1020
|
if (stack.libraries.length > 0) {
|
|
535
1021
|
for (const lib of stack.libraries) {
|
|
536
|
-
console.log(` ${
|
|
1022
|
+
console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(lib, import_types.LIBRARY_NAMES)}`);
|
|
537
1023
|
}
|
|
538
1024
|
}
|
|
539
1025
|
const meaningfulDirs = scanResult.structure.directories.filter((d) => d.role !== "unknown");
|
|
540
1026
|
if (meaningfulDirs.length > 0) {
|
|
541
1027
|
console.log(`
|
|
542
|
-
${
|
|
1028
|
+
${import_chalk4.default.bold("Structure:")}`);
|
|
543
1029
|
for (const dir of meaningfulDirs) {
|
|
544
1030
|
const label = import_types.ROLE_DESCRIPTIONS[dir.role] ?? dir.role;
|
|
545
1031
|
const files = dir.fileCount === 1 ? "1 file" : `${dir.fileCount} files`;
|
|
546
|
-
console.log(` ${
|
|
1032
|
+
console.log(` ${import_chalk4.default.green("\u2713")} ${dir.path} \u2014 ${label} (${files})`);
|
|
547
1033
|
}
|
|
548
1034
|
}
|
|
549
1035
|
const conventionEntries = Object.entries(conventions);
|
|
550
1036
|
if (conventionEntries.length > 0) {
|
|
551
1037
|
console.log(`
|
|
552
|
-
${
|
|
1038
|
+
${import_chalk4.default.bold("Conventions:")}`);
|
|
553
1039
|
for (const [key, convention] of conventionEntries) {
|
|
554
1040
|
if (convention.confidence === "low") continue;
|
|
555
1041
|
const label = CONVENTION_LABELS[key] ?? key;
|
|
556
|
-
const ind = convention.confidence === "high" ?
|
|
557
|
-
const detail =
|
|
1042
|
+
const ind = convention.confidence === "high" ? import_chalk4.default.green("\u2713") : import_chalk4.default.yellow("~");
|
|
1043
|
+
const detail = import_chalk4.default.dim(`(${confidenceLabel(convention)})`);
|
|
558
1044
|
console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
|
|
559
1045
|
}
|
|
560
1046
|
}
|
|
@@ -562,28 +1048,33 @@ ${import_chalk3.default.bold("Conventions:")}`);
|
|
|
562
1048
|
}
|
|
563
1049
|
|
|
564
1050
|
// src/utils/write-generated-files.ts
|
|
565
|
-
var
|
|
566
|
-
var
|
|
1051
|
+
var fs10 = __toESM(require("fs"), 1);
|
|
1052
|
+
var path11 = __toESM(require("path"), 1);
|
|
567
1053
|
var import_context = require("@viberails/context");
|
|
568
1054
|
var CONTEXT_DIR = ".viberails";
|
|
569
1055
|
var CONTEXT_FILE = "context.md";
|
|
570
1056
|
var SCAN_RESULT_FILE = "scan-result.json";
|
|
571
1057
|
function writeGeneratedFiles(projectRoot, config, scanResult) {
|
|
572
|
-
const contextDir =
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
1058
|
+
const contextDir = path11.join(projectRoot, CONTEXT_DIR);
|
|
1059
|
+
try {
|
|
1060
|
+
if (!fs10.existsSync(contextDir)) {
|
|
1061
|
+
fs10.mkdirSync(contextDir, { recursive: true });
|
|
1062
|
+
}
|
|
1063
|
+
const context = (0, import_context.generateContext)(config);
|
|
1064
|
+
fs10.writeFileSync(path11.join(contextDir, CONTEXT_FILE), context);
|
|
1065
|
+
fs10.writeFileSync(
|
|
1066
|
+
path11.join(contextDir, SCAN_RESULT_FILE),
|
|
1067
|
+
`${JSON.stringify(scanResult, null, 2)}
|
|
581
1068
|
`
|
|
582
|
-
|
|
1069
|
+
);
|
|
1070
|
+
} catch (err) {
|
|
1071
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1072
|
+
throw new Error(`Failed to write generated files to ${contextDir}: ${message}`);
|
|
1073
|
+
}
|
|
583
1074
|
}
|
|
584
1075
|
|
|
585
1076
|
// src/commands/init.ts
|
|
586
|
-
var
|
|
1077
|
+
var CONFIG_FILE4 = "viberails.config.json";
|
|
587
1078
|
function filterHighConfidence(conventions) {
|
|
588
1079
|
const filtered = {};
|
|
589
1080
|
for (const [key, value] of Object.entries(conventions)) {
|
|
@@ -604,19 +1095,19 @@ async function initCommand(options, cwd) {
|
|
|
604
1095
|
"No package.json found in this directory or any parent.\n\nMake sure you are inside a JavaScript or TypeScript project, then run:\n npx viberails"
|
|
605
1096
|
);
|
|
606
1097
|
}
|
|
607
|
-
const configPath =
|
|
608
|
-
if (
|
|
1098
|
+
const configPath = path12.join(projectRoot, CONFIG_FILE4);
|
|
1099
|
+
if (fs11.existsSync(configPath)) {
|
|
609
1100
|
console.log(
|
|
610
|
-
|
|
1101
|
+
import_chalk5.default.yellow("!") + " viberails is already initialized in this project.\n Run " + import_chalk5.default.cyan("viberails sync") + " to update the generated files."
|
|
611
1102
|
);
|
|
612
1103
|
return;
|
|
613
1104
|
}
|
|
614
|
-
console.log(
|
|
1105
|
+
console.log(import_chalk5.default.dim("Scanning project..."));
|
|
615
1106
|
const scanResult = await (0, import_scanner.scan)(projectRoot);
|
|
616
1107
|
displayScanResults(scanResult);
|
|
617
1108
|
if (scanResult.statistics.totalFiles === 0) {
|
|
618
1109
|
console.log(
|
|
619
|
-
|
|
1110
|
+
import_chalk5.default.yellow("!") + " No source files detected. viberails will generate context with minimal content.\n Run " + import_chalk5.default.cyan("viberails sync") + " after adding source files.\n"
|
|
620
1111
|
);
|
|
621
1112
|
}
|
|
622
1113
|
if (!options.yes) {
|
|
@@ -626,7 +1117,7 @@ async function initCommand(options, cwd) {
|
|
|
626
1117
|
return;
|
|
627
1118
|
}
|
|
628
1119
|
}
|
|
629
|
-
const config = (0,
|
|
1120
|
+
const config = (0, import_config4.generateConfig)(scanResult);
|
|
630
1121
|
if (options.yes) {
|
|
631
1122
|
config.conventions = filterHighConfidence(config.conventions);
|
|
632
1123
|
}
|
|
@@ -636,7 +1127,7 @@ async function initCommand(options, cwd) {
|
|
|
636
1127
|
shouldInfer = await confirm("Infer boundary rules from import patterns?");
|
|
637
1128
|
}
|
|
638
1129
|
if (shouldInfer) {
|
|
639
|
-
console.log(
|
|
1130
|
+
console.log(import_chalk5.default.dim("Building import graph..."));
|
|
640
1131
|
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
641
1132
|
const packages = resolveWorkspacePackages(projectRoot, config.workspace);
|
|
642
1133
|
const graph = await buildImportGraph(projectRoot, { packages, ignore: config.ignore });
|
|
@@ -644,69 +1135,69 @@ async function initCommand(options, cwd) {
|
|
|
644
1135
|
if (inferred.length > 0) {
|
|
645
1136
|
config.boundaries = inferred;
|
|
646
1137
|
config.rules.enforceBoundaries = true;
|
|
647
|
-
console.log(` ${
|
|
1138
|
+
console.log(` ${import_chalk5.default.green("\u2713")} Inferred ${inferred.length} boundary rules`);
|
|
648
1139
|
}
|
|
649
1140
|
}
|
|
650
1141
|
}
|
|
651
|
-
|
|
1142
|
+
fs11.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
652
1143
|
`);
|
|
653
1144
|
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
654
1145
|
updateGitignore(projectRoot);
|
|
655
1146
|
setupPreCommitHook(projectRoot);
|
|
656
1147
|
console.log(`
|
|
657
|
-
${
|
|
658
|
-
console.log(` ${
|
|
659
|
-
console.log(` ${
|
|
660
|
-
console.log(` ${
|
|
1148
|
+
${import_chalk5.default.bold("Created:")}`);
|
|
1149
|
+
console.log(` ${import_chalk5.default.green("\u2713")} ${CONFIG_FILE4}`);
|
|
1150
|
+
console.log(` ${import_chalk5.default.green("\u2713")} .viberails/context.md`);
|
|
1151
|
+
console.log(` ${import_chalk5.default.green("\u2713")} .viberails/scan-result.json`);
|
|
661
1152
|
console.log(`
|
|
662
|
-
${
|
|
663
|
-
console.log(` 1. Review ${
|
|
1153
|
+
${import_chalk5.default.bold("Next steps:")}`);
|
|
1154
|
+
console.log(` 1. Review ${import_chalk5.default.cyan("viberails.config.json")} and adjust rules`);
|
|
664
1155
|
console.log(
|
|
665
|
-
` 2. Commit ${
|
|
1156
|
+
` 2. Commit ${import_chalk5.default.cyan("viberails.config.json")} and ${import_chalk5.default.cyan(".viberails/context.md")}`
|
|
666
1157
|
);
|
|
667
|
-
console.log(` 3. Run ${
|
|
1158
|
+
console.log(` 3. Run ${import_chalk5.default.cyan("viberails check")} to verify your project passes`);
|
|
668
1159
|
}
|
|
669
1160
|
function updateGitignore(projectRoot) {
|
|
670
|
-
const gitignorePath =
|
|
1161
|
+
const gitignorePath = path12.join(projectRoot, ".gitignore");
|
|
671
1162
|
let content = "";
|
|
672
|
-
if (
|
|
673
|
-
content =
|
|
1163
|
+
if (fs11.existsSync(gitignorePath)) {
|
|
1164
|
+
content = fs11.readFileSync(gitignorePath, "utf-8");
|
|
674
1165
|
}
|
|
675
1166
|
if (!content.includes(".viberails/scan-result.json")) {
|
|
676
1167
|
const block = "\n# viberails\n.viberails/scan-result.json\n";
|
|
677
|
-
|
|
1168
|
+
fs11.writeFileSync(gitignorePath, `${content.trimEnd()}
|
|
678
1169
|
${block}`);
|
|
679
1170
|
}
|
|
680
1171
|
}
|
|
681
1172
|
function setupPreCommitHook(projectRoot) {
|
|
682
|
-
const lefthookPath =
|
|
683
|
-
if (
|
|
1173
|
+
const lefthookPath = path12.join(projectRoot, "lefthook.yml");
|
|
1174
|
+
if (fs11.existsSync(lefthookPath)) {
|
|
684
1175
|
addLefthookPreCommit(lefthookPath);
|
|
685
|
-
console.log(` ${
|
|
1176
|
+
console.log(` ${import_chalk5.default.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
|
|
686
1177
|
return;
|
|
687
1178
|
}
|
|
688
|
-
const huskyDir =
|
|
689
|
-
if (
|
|
1179
|
+
const huskyDir = path12.join(projectRoot, ".husky");
|
|
1180
|
+
if (fs11.existsSync(huskyDir)) {
|
|
690
1181
|
writeHuskyPreCommit(huskyDir);
|
|
691
|
-
console.log(` ${
|
|
1182
|
+
console.log(` ${import_chalk5.default.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
|
|
692
1183
|
return;
|
|
693
1184
|
}
|
|
694
|
-
const gitDir =
|
|
695
|
-
if (
|
|
696
|
-
const hooksDir =
|
|
697
|
-
if (!
|
|
698
|
-
|
|
1185
|
+
const gitDir = path12.join(projectRoot, ".git");
|
|
1186
|
+
if (fs11.existsSync(gitDir)) {
|
|
1187
|
+
const hooksDir = path12.join(gitDir, "hooks");
|
|
1188
|
+
if (!fs11.existsSync(hooksDir)) {
|
|
1189
|
+
fs11.mkdirSync(hooksDir, { recursive: true });
|
|
699
1190
|
}
|
|
700
1191
|
writeGitHookPreCommit(hooksDir);
|
|
701
|
-
console.log(` ${
|
|
1192
|
+
console.log(` ${import_chalk5.default.green("\u2713")} .git/hooks/pre-commit`);
|
|
702
1193
|
}
|
|
703
1194
|
}
|
|
704
1195
|
function writeGitHookPreCommit(hooksDir) {
|
|
705
|
-
const hookPath =
|
|
706
|
-
if (
|
|
707
|
-
const existing =
|
|
1196
|
+
const hookPath = path12.join(hooksDir, "pre-commit");
|
|
1197
|
+
if (fs11.existsSync(hookPath)) {
|
|
1198
|
+
const existing = fs11.readFileSync(hookPath, "utf-8");
|
|
708
1199
|
if (existing.includes("viberails")) return;
|
|
709
|
-
|
|
1200
|
+
fs11.writeFileSync(
|
|
710
1201
|
hookPath,
|
|
711
1202
|
`${existing.trimEnd()}
|
|
712
1203
|
|
|
@@ -723,37 +1214,37 @@ npx viberails check --staged
|
|
|
723
1214
|
"npx viberails check --staged",
|
|
724
1215
|
""
|
|
725
1216
|
].join("\n");
|
|
726
|
-
|
|
1217
|
+
fs11.writeFileSync(hookPath, script, { mode: 493 });
|
|
727
1218
|
}
|
|
728
1219
|
function addLefthookPreCommit(lefthookPath) {
|
|
729
|
-
const content =
|
|
1220
|
+
const content = fs11.readFileSync(lefthookPath, "utf-8");
|
|
730
1221
|
if (content.includes("viberails")) return;
|
|
731
1222
|
const addition = ["", " viberails:", " run: npx viberails check --staged"].join("\n");
|
|
732
|
-
|
|
1223
|
+
fs11.writeFileSync(lefthookPath, `${content.trimEnd()}
|
|
733
1224
|
${addition}
|
|
734
1225
|
`);
|
|
735
1226
|
}
|
|
736
1227
|
function writeHuskyPreCommit(huskyDir) {
|
|
737
|
-
const hookPath =
|
|
738
|
-
if (
|
|
739
|
-
const existing =
|
|
1228
|
+
const hookPath = path12.join(huskyDir, "pre-commit");
|
|
1229
|
+
if (fs11.existsSync(hookPath)) {
|
|
1230
|
+
const existing = fs11.readFileSync(hookPath, "utf-8");
|
|
740
1231
|
if (!existing.includes("viberails")) {
|
|
741
|
-
|
|
1232
|
+
fs11.writeFileSync(hookPath, `${existing.trimEnd()}
|
|
742
1233
|
npx viberails check --staged
|
|
743
1234
|
`);
|
|
744
1235
|
}
|
|
745
1236
|
return;
|
|
746
1237
|
}
|
|
747
|
-
|
|
1238
|
+
fs11.writeFileSync(hookPath, "#!/bin/sh\nnpx viberails check --staged\n", { mode: 493 });
|
|
748
1239
|
}
|
|
749
1240
|
|
|
750
1241
|
// src/commands/sync.ts
|
|
751
|
-
var
|
|
752
|
-
var
|
|
753
|
-
var
|
|
1242
|
+
var fs12 = __toESM(require("fs"), 1);
|
|
1243
|
+
var path13 = __toESM(require("path"), 1);
|
|
1244
|
+
var import_config5 = require("@viberails/config");
|
|
754
1245
|
var import_scanner2 = require("@viberails/scanner");
|
|
755
|
-
var
|
|
756
|
-
var
|
|
1246
|
+
var import_chalk6 = __toESM(require("chalk"), 1);
|
|
1247
|
+
var CONFIG_FILE5 = "viberails.config.json";
|
|
757
1248
|
async function syncCommand(cwd) {
|
|
758
1249
|
const startDir = cwd ?? process.cwd();
|
|
759
1250
|
const projectRoot = findProjectRoot(startDir);
|
|
@@ -762,23 +1253,23 @@ async function syncCommand(cwd) {
|
|
|
762
1253
|
"No package.json found in this directory or any parent.\n\nMake sure you are inside a JavaScript or TypeScript project, then run:\n npx viberails"
|
|
763
1254
|
);
|
|
764
1255
|
}
|
|
765
|
-
const configPath =
|
|
766
|
-
const existing = await (0,
|
|
767
|
-
console.log(
|
|
1256
|
+
const configPath = path13.join(projectRoot, CONFIG_FILE5);
|
|
1257
|
+
const existing = await (0, import_config5.loadConfig)(configPath);
|
|
1258
|
+
console.log(import_chalk6.default.dim("Scanning project..."));
|
|
768
1259
|
const scanResult = await (0, import_scanner2.scan)(projectRoot);
|
|
769
|
-
const merged = (0,
|
|
770
|
-
|
|
1260
|
+
const merged = (0, import_config5.mergeConfig)(existing, scanResult);
|
|
1261
|
+
fs12.writeFileSync(configPath, `${JSON.stringify(merged, null, 2)}
|
|
771
1262
|
`);
|
|
772
1263
|
writeGeneratedFiles(projectRoot, merged, scanResult);
|
|
773
1264
|
console.log(`
|
|
774
|
-
${
|
|
775
|
-
console.log(` ${
|
|
776
|
-
console.log(` ${
|
|
777
|
-
console.log(` ${
|
|
1265
|
+
${import_chalk6.default.bold("Synced:")}`);
|
|
1266
|
+
console.log(` ${import_chalk6.default.green("\u2713")} ${CONFIG_FILE5} \u2014 updated`);
|
|
1267
|
+
console.log(` ${import_chalk6.default.green("\u2713")} .viberails/context.md \u2014 regenerated`);
|
|
1268
|
+
console.log(` ${import_chalk6.default.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
|
|
778
1269
|
}
|
|
779
1270
|
|
|
780
1271
|
// src/index.ts
|
|
781
|
-
var VERSION = "0.
|
|
1272
|
+
var VERSION = "0.2.0";
|
|
782
1273
|
var program = new import_commander.Command();
|
|
783
1274
|
program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
|
|
784
1275
|
program.command("init", { isDefault: true }).description("Scan your project and set up enforcement guardrails").option("-y, --yes", "Non-interactive mode (use defaults, high-confidence only)").action(async (options) => {
|
|
@@ -786,7 +1277,7 @@ program.command("init", { isDefault: true }).description("Scan your project and
|
|
|
786
1277
|
await initCommand(options);
|
|
787
1278
|
} catch (err) {
|
|
788
1279
|
const message = err instanceof Error ? err.message : String(err);
|
|
789
|
-
console.error(`${
|
|
1280
|
+
console.error(`${import_chalk7.default.red("Error:")} ${message}`);
|
|
790
1281
|
process.exit(1);
|
|
791
1282
|
}
|
|
792
1283
|
});
|
|
@@ -795,7 +1286,7 @@ program.command("sync").description("Re-scan and update generated files").action
|
|
|
795
1286
|
await syncCommand();
|
|
796
1287
|
} catch (err) {
|
|
797
1288
|
const message = err instanceof Error ? err.message : String(err);
|
|
798
|
-
console.error(`${
|
|
1289
|
+
console.error(`${import_chalk7.default.red("Error:")} ${message}`);
|
|
799
1290
|
process.exit(1);
|
|
800
1291
|
}
|
|
801
1292
|
});
|
|
@@ -808,7 +1299,17 @@ program.command("check").description("Check files against enforced rules").optio
|
|
|
808
1299
|
process.exit(exitCode);
|
|
809
1300
|
} catch (err) {
|
|
810
1301
|
const message = err instanceof Error ? err.message : String(err);
|
|
811
|
-
console.error(`${
|
|
1302
|
+
console.error(`${import_chalk7.default.red("Error:")} ${message}`);
|
|
1303
|
+
process.exit(1);
|
|
1304
|
+
}
|
|
1305
|
+
});
|
|
1306
|
+
program.command("fix").description("Auto-fix file naming violations and generate missing test stubs").option("--dry-run", "Show planned fixes without applying them").option("--rule <rules...>", "Fix only specific rules (file-naming, missing-test)").option("-y, --yes", "Skip confirmation prompt").action(async (options) => {
|
|
1307
|
+
try {
|
|
1308
|
+
const exitCode = await fixCommand(options);
|
|
1309
|
+
process.exit(exitCode);
|
|
1310
|
+
} catch (err) {
|
|
1311
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1312
|
+
console.error(`${import_chalk7.default.red("Error:")} ${message}`);
|
|
812
1313
|
process.exit(1);
|
|
813
1314
|
}
|
|
814
1315
|
});
|
|
@@ -817,7 +1318,7 @@ program.command("boundaries").description("Display, infer, or inspect import bou
|
|
|
817
1318
|
await boundariesCommand(options);
|
|
818
1319
|
} catch (err) {
|
|
819
1320
|
const message = err instanceof Error ? err.message : String(err);
|
|
820
|
-
console.error(`${
|
|
1321
|
+
console.error(`${import_chalk7.default.red("Error:")} ${message}`);
|
|
821
1322
|
process.exit(1);
|
|
822
1323
|
}
|
|
823
1324
|
});
|