viberails 0.1.0 → 0.2.1
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 +862 -287
- 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 +860 -285
- 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_chalk10 = __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,488 @@ ${violations.length} ${word} found.`);
|
|
|
344
530
|
}
|
|
345
531
|
return 0;
|
|
346
532
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
533
|
+
|
|
534
|
+
// src/commands/fix.ts
|
|
535
|
+
var fs9 = __toESM(require("fs"), 1);
|
|
536
|
+
var path10 = __toESM(require("path"), 1);
|
|
537
|
+
var import_config3 = require("@viberails/config");
|
|
538
|
+
var import_chalk4 = __toESM(require("chalk"), 1);
|
|
539
|
+
|
|
540
|
+
// src/commands/fix-helpers.ts
|
|
541
|
+
var import_node_child_process2 = require("child_process");
|
|
542
|
+
var import_node_readline = require("readline");
|
|
543
|
+
var import_chalk3 = __toESM(require("chalk"), 1);
|
|
544
|
+
function printPlan(renames, stubs) {
|
|
545
|
+
if (renames.length > 0) {
|
|
546
|
+
console.log(import_chalk3.default.bold("\nFile renames:"));
|
|
547
|
+
for (const r of renames) {
|
|
548
|
+
console.log(` ${import_chalk3.default.red(r.oldPath)} \u2192 ${import_chalk3.default.green(r.newPath)}`);
|
|
354
549
|
}
|
|
355
|
-
return count;
|
|
356
|
-
} catch {
|
|
357
|
-
return null;
|
|
358
550
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
if (!SOURCE_EXTS.has(ext)) return void 0;
|
|
364
|
-
if (filename.startsWith("index.") || filename.includes(".config.") || filename.includes(".test.") || filename.includes(".spec.") || filename.startsWith(".")) {
|
|
365
|
-
return void 0;
|
|
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.`;
|
|
373
|
-
}
|
|
374
|
-
function checkMissingTests(projectRoot, config, severity) {
|
|
375
|
-
const violations = [];
|
|
376
|
-
const { testPattern } = config.structure;
|
|
377
|
-
if (!testPattern) return violations;
|
|
378
|
-
const srcDir = config.structure.srcDir;
|
|
379
|
-
if (!srcDir) return violations;
|
|
380
|
-
const srcPath = path4.join(projectRoot, srcDir);
|
|
381
|
-
if (!fs4.existsSync(srcPath)) return violations;
|
|
382
|
-
const testSuffix = testPattern.replace("*", "");
|
|
383
|
-
const sourceFiles = collectSourceFiles(srcPath, projectRoot);
|
|
384
|
-
for (const relFile of sourceFiles) {
|
|
385
|
-
const basename2 = path4.basename(relFile);
|
|
386
|
-
if (basename2.includes(".test.") || basename2.includes(".spec.") || basename2.startsWith("index.") || basename2.endsWith(".d.ts")) {
|
|
387
|
-
continue;
|
|
388
|
-
}
|
|
389
|
-
const ext = path4.extname(basename2);
|
|
390
|
-
if (!SOURCE_EXTS.has(ext)) continue;
|
|
391
|
-
const stem = basename2.slice(0, basename2.indexOf("."));
|
|
392
|
-
const expectedTestFile = `${stem}${testSuffix}`;
|
|
393
|
-
const dir = path4.dirname(path4.join(projectRoot, relFile));
|
|
394
|
-
const colocatedTest = path4.join(dir, expectedTestFile);
|
|
395
|
-
const testsDir = config.structure.tests;
|
|
396
|
-
const dedicatedTest = testsDir ? path4.join(projectRoot, testsDir, expectedTestFile) : null;
|
|
397
|
-
const hasTest = fs4.existsSync(colocatedTest) || dedicatedTest !== null && fs4.existsSync(dedicatedTest);
|
|
398
|
-
if (!hasTest) {
|
|
399
|
-
violations.push({
|
|
400
|
-
file: relFile,
|
|
401
|
-
rule: "missing-test",
|
|
402
|
-
message: `No test file found. Expected \`${expectedTestFile}\`.`,
|
|
403
|
-
severity
|
|
404
|
-
});
|
|
551
|
+
if (stubs.length > 0) {
|
|
552
|
+
console.log(import_chalk3.default.bold("\nTest stubs to create:"));
|
|
553
|
+
for (const s of stubs) {
|
|
554
|
+
console.log(` ${import_chalk3.default.green("+")} ${s.path}`);
|
|
405
555
|
}
|
|
406
556
|
}
|
|
407
|
-
return violations;
|
|
408
557
|
}
|
|
409
|
-
function
|
|
558
|
+
function checkGitDirty(projectRoot) {
|
|
410
559
|
try {
|
|
411
|
-
const output = (0,
|
|
560
|
+
const output = (0, import_node_child_process2.execSync)("git status --porcelain", {
|
|
412
561
|
cwd: projectRoot,
|
|
413
562
|
encoding: "utf-8"
|
|
414
563
|
});
|
|
415
|
-
return output.trim().
|
|
564
|
+
return output.trim().length > 0;
|
|
416
565
|
} catch {
|
|
417
|
-
return
|
|
566
|
+
return false;
|
|
418
567
|
}
|
|
419
568
|
}
|
|
420
|
-
function
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
569
|
+
function getConventionValue(convention) {
|
|
570
|
+
if (typeof convention === "string") return convention;
|
|
571
|
+
if (convention && typeof convention === "object" && "value" in convention) {
|
|
572
|
+
return convention.value;
|
|
573
|
+
}
|
|
574
|
+
return void 0;
|
|
575
|
+
}
|
|
576
|
+
function promptConfirm(question) {
|
|
577
|
+
const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
|
|
578
|
+
return new Promise((resolve4) => {
|
|
579
|
+
rl.question(`${question} (y/N) `, (answer) => {
|
|
580
|
+
rl.close();
|
|
581
|
+
resolve4(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// src/commands/fix-imports.ts
|
|
587
|
+
var path7 = __toESM(require("path"), 1);
|
|
588
|
+
function stripExtension(filePath) {
|
|
589
|
+
return filePath.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
|
|
590
|
+
}
|
|
591
|
+
function computeNewSpecifier(oldSpecifier, newBare) {
|
|
592
|
+
const hasJsExt = oldSpecifier.endsWith(".js");
|
|
593
|
+
const base = hasJsExt ? oldSpecifier.slice(0, -3) : oldSpecifier;
|
|
594
|
+
const dir = base.lastIndexOf("/");
|
|
595
|
+
const prefix = dir >= 0 ? base.slice(0, dir + 1) : "";
|
|
596
|
+
const newSpec = prefix + newBare;
|
|
597
|
+
return hasJsExt ? `${newSpec}.js` : newSpec;
|
|
598
|
+
}
|
|
599
|
+
async function updateImportsAfterRenames(renames, projectRoot) {
|
|
600
|
+
if (renames.length === 0) return [];
|
|
601
|
+
const { Project, SyntaxKind } = await import("ts-morph");
|
|
602
|
+
const renameMap = /* @__PURE__ */ new Map();
|
|
603
|
+
for (const r of renames) {
|
|
604
|
+
const oldStripped = stripExtension(r.oldAbsPath);
|
|
605
|
+
const newFilename = path7.basename(r.newPath);
|
|
606
|
+
const newName = newFilename.slice(0, newFilename.indexOf("."));
|
|
607
|
+
renameMap.set(oldStripped, { newBare: newName });
|
|
608
|
+
}
|
|
609
|
+
const project = new Project({
|
|
610
|
+
tsConfigFilePath: void 0,
|
|
611
|
+
skipAddingFilesFromTsConfig: true
|
|
612
|
+
});
|
|
613
|
+
project.addSourceFilesAtPaths(path7.join(projectRoot, "**/*.{ts,tsx,js,jsx,mjs,cjs}"));
|
|
614
|
+
const updates = [];
|
|
615
|
+
const extensions = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
|
|
616
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
617
|
+
const filePath = sourceFile.getFilePath();
|
|
618
|
+
if (filePath.includes("/node_modules/") || filePath.includes("/dist/")) continue;
|
|
619
|
+
const fileDir = path7.dirname(filePath);
|
|
620
|
+
for (const decl of sourceFile.getImportDeclarations()) {
|
|
621
|
+
const specifier = decl.getModuleSpecifierValue();
|
|
622
|
+
if (!specifier.startsWith(".")) continue;
|
|
623
|
+
const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
|
|
624
|
+
if (!match) continue;
|
|
625
|
+
const newSpec = computeNewSpecifier(specifier, match.newBare);
|
|
626
|
+
updates.push({
|
|
627
|
+
file: filePath,
|
|
628
|
+
oldSpecifier: specifier,
|
|
629
|
+
newSpecifier: newSpec,
|
|
630
|
+
line: decl.getStartLineNumber()
|
|
631
|
+
});
|
|
632
|
+
decl.setModuleSpecifier(newSpec);
|
|
428
633
|
}
|
|
429
|
-
for (const
|
|
430
|
-
const
|
|
431
|
-
if (
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
634
|
+
for (const decl of sourceFile.getExportDeclarations()) {
|
|
635
|
+
const specifier = decl.getModuleSpecifierValue();
|
|
636
|
+
if (!specifier || !specifier.startsWith(".")) continue;
|
|
637
|
+
const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
|
|
638
|
+
if (!match) continue;
|
|
639
|
+
const newSpec = computeNewSpecifier(specifier, match.newBare);
|
|
640
|
+
updates.push({
|
|
641
|
+
file: filePath,
|
|
642
|
+
oldSpecifier: specifier,
|
|
643
|
+
newSpecifier: newSpec,
|
|
644
|
+
line: decl.getStartLineNumber()
|
|
645
|
+
});
|
|
646
|
+
decl.setModuleSpecifier(newSpec);
|
|
647
|
+
}
|
|
648
|
+
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
649
|
+
if (call.getExpression().getKind() !== SyntaxKind.ImportKeyword) continue;
|
|
650
|
+
const args = call.getArguments();
|
|
651
|
+
if (args.length === 0) continue;
|
|
652
|
+
const arg = args[0];
|
|
653
|
+
if (arg.getKind() !== SyntaxKind.StringLiteral) continue;
|
|
654
|
+
const specifier = arg.getText().slice(1, -1);
|
|
655
|
+
if (!specifier.startsWith(".")) continue;
|
|
656
|
+
const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
|
|
657
|
+
if (!match) continue;
|
|
658
|
+
const newSpec = computeNewSpecifier(specifier, match.newBare);
|
|
659
|
+
updates.push({
|
|
660
|
+
file: filePath,
|
|
661
|
+
oldSpecifier: specifier,
|
|
662
|
+
newSpecifier: newSpec,
|
|
663
|
+
line: call.getStartLineNumber()
|
|
664
|
+
});
|
|
665
|
+
const quote = arg.getText()[0];
|
|
666
|
+
arg.replaceWithText(`${quote}${newSpec}${quote}`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
if (updates.length > 0) {
|
|
670
|
+
await project.save();
|
|
671
|
+
}
|
|
672
|
+
return updates;
|
|
673
|
+
}
|
|
674
|
+
function resolveToRenamedFile(specifier, fromDir, renameMap, extensions) {
|
|
675
|
+
const cleanSpec = specifier.endsWith(".js") ? specifier.slice(0, -3) : specifier;
|
|
676
|
+
const resolved = path7.resolve(fromDir, cleanSpec);
|
|
677
|
+
for (const ext of extensions) {
|
|
678
|
+
const candidate = resolved + ext;
|
|
679
|
+
const stripped = stripExtension(candidate);
|
|
680
|
+
const match = renameMap.get(stripped);
|
|
681
|
+
if (match) return match;
|
|
682
|
+
}
|
|
683
|
+
return void 0;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// src/commands/fix-naming.ts
|
|
687
|
+
var fs7 = __toESM(require("fs"), 1);
|
|
688
|
+
var path8 = __toESM(require("path"), 1);
|
|
689
|
+
|
|
690
|
+
// src/commands/convert-name.ts
|
|
691
|
+
function splitIntoWords(name) {
|
|
692
|
+
const parts = name.split(/[-_]/);
|
|
693
|
+
const words = [];
|
|
694
|
+
for (const part of parts) {
|
|
695
|
+
if (part === "") continue;
|
|
696
|
+
let current = "";
|
|
697
|
+
for (let i = 0; i < part.length; i++) {
|
|
698
|
+
const ch = part[i];
|
|
699
|
+
const isUpper = ch >= "A" && ch <= "Z";
|
|
700
|
+
if (isUpper && current.length > 0) {
|
|
701
|
+
const prevIsUpper = current[current.length - 1] >= "A" && current[current.length - 1] <= "Z";
|
|
702
|
+
const nextIsLower = i + 1 < part.length && part[i + 1] >= "a" && part[i + 1] <= "z";
|
|
703
|
+
if (!prevIsUpper || nextIsLower) {
|
|
704
|
+
words.push(current.toLowerCase());
|
|
705
|
+
current = "";
|
|
441
706
|
}
|
|
442
707
|
}
|
|
708
|
+
current += ch;
|
|
443
709
|
}
|
|
710
|
+
if (current) words.push(current.toLowerCase());
|
|
711
|
+
}
|
|
712
|
+
return words;
|
|
713
|
+
}
|
|
714
|
+
function convertName(bare, target) {
|
|
715
|
+
const words = splitIntoWords(bare);
|
|
716
|
+
if (words.length === 0) return bare;
|
|
717
|
+
switch (target) {
|
|
718
|
+
case "kebab-case":
|
|
719
|
+
return words.join("-");
|
|
720
|
+
case "camelCase":
|
|
721
|
+
return words[0] + words.slice(1).map(capitalize).join("");
|
|
722
|
+
case "PascalCase":
|
|
723
|
+
return words.map(capitalize).join("");
|
|
724
|
+
case "snake_case":
|
|
725
|
+
return words.join("_");
|
|
726
|
+
default:
|
|
727
|
+
return bare;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
function capitalize(word) {
|
|
731
|
+
if (word.length === 0) return word;
|
|
732
|
+
return word[0].toUpperCase() + word.slice(1);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// src/commands/fix-naming.ts
|
|
736
|
+
function computeRename(relPath, targetConvention, projectRoot) {
|
|
737
|
+
const filename = path8.basename(relPath);
|
|
738
|
+
const dir = path8.dirname(relPath);
|
|
739
|
+
const dotIndex = filename.indexOf(".");
|
|
740
|
+
if (dotIndex === -1) return null;
|
|
741
|
+
const bare = filename.slice(0, dotIndex);
|
|
742
|
+
const suffix = filename.slice(dotIndex);
|
|
743
|
+
const newBare = convertName(bare, targetConvention);
|
|
744
|
+
if (newBare === bare) return null;
|
|
745
|
+
const newFilename = newBare + suffix;
|
|
746
|
+
const newRelPath = path8.join(dir, newFilename);
|
|
747
|
+
const oldAbsPath = path8.join(projectRoot, relPath);
|
|
748
|
+
const newAbsPath = path8.join(projectRoot, newRelPath);
|
|
749
|
+
if (fs7.existsSync(newAbsPath)) return null;
|
|
750
|
+
return { oldPath: relPath, newPath: newRelPath, oldAbsPath, newAbsPath };
|
|
751
|
+
}
|
|
752
|
+
function executeRename(rename) {
|
|
753
|
+
if (fs7.existsSync(rename.newAbsPath)) return false;
|
|
754
|
+
fs7.renameSync(rename.oldAbsPath, rename.newAbsPath);
|
|
755
|
+
return true;
|
|
756
|
+
}
|
|
757
|
+
function deduplicateRenames(renames) {
|
|
758
|
+
const seen = /* @__PURE__ */ new Set();
|
|
759
|
+
const result = [];
|
|
760
|
+
for (const r of renames) {
|
|
761
|
+
if (seen.has(r.newAbsPath)) continue;
|
|
762
|
+
seen.add(r.newAbsPath);
|
|
763
|
+
result.push(r);
|
|
764
|
+
}
|
|
765
|
+
return result;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// src/commands/fix-tests.ts
|
|
769
|
+
var fs8 = __toESM(require("fs"), 1);
|
|
770
|
+
var path9 = __toESM(require("path"), 1);
|
|
771
|
+
function generateTestStub(sourceRelPath, config, projectRoot) {
|
|
772
|
+
const { testPattern } = config.structure;
|
|
773
|
+
if (!testPattern) return null;
|
|
774
|
+
const basename6 = path9.basename(sourceRelPath);
|
|
775
|
+
const stem = basename6.slice(0, basename6.indexOf("."));
|
|
776
|
+
const testSuffix = testPattern.replace("*", "");
|
|
777
|
+
const testFilename = `${stem}${testSuffix}`;
|
|
778
|
+
const dir = path9.dirname(path9.join(projectRoot, sourceRelPath));
|
|
779
|
+
const testAbsPath = path9.join(dir, testFilename);
|
|
780
|
+
if (fs8.existsSync(testAbsPath)) return null;
|
|
781
|
+
return {
|
|
782
|
+
path: path9.relative(projectRoot, testAbsPath),
|
|
783
|
+
absPath: testAbsPath,
|
|
784
|
+
moduleName: stem
|
|
444
785
|
};
|
|
445
|
-
walk(projectRoot);
|
|
446
|
-
return files;
|
|
447
786
|
}
|
|
448
|
-
function
|
|
449
|
-
const
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
787
|
+
function writeTestStub(stub, config) {
|
|
788
|
+
const runner = config.stack.testRunner === "jest" ? "jest" : "vitest";
|
|
789
|
+
const importLine = runner === "jest" ? "" : "import { describe, it, expect } from 'vitest';\n\n";
|
|
790
|
+
const content = `${importLine}describe('${stub.moduleName}', () => {
|
|
791
|
+
it.todo('add tests');
|
|
792
|
+
});
|
|
793
|
+
`;
|
|
794
|
+
fs8.mkdirSync(path9.dirname(stub.absPath), { recursive: true });
|
|
795
|
+
fs8.writeFileSync(stub.absPath, content);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// src/commands/fix.ts
|
|
799
|
+
var CONFIG_FILE3 = "viberails.config.json";
|
|
800
|
+
async function fixCommand(options, cwd) {
|
|
801
|
+
const startDir = cwd ?? process.cwd();
|
|
802
|
+
const projectRoot = findProjectRoot(startDir);
|
|
803
|
+
if (!projectRoot) {
|
|
804
|
+
console.error(`${import_chalk4.default.red("Error:")} No package.json found. Are you in a JS/TS project?`);
|
|
805
|
+
return 1;
|
|
806
|
+
}
|
|
807
|
+
const configPath = path10.join(projectRoot, CONFIG_FILE3);
|
|
808
|
+
if (!fs9.existsSync(configPath)) {
|
|
809
|
+
console.error(
|
|
810
|
+
`${import_chalk4.default.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
|
|
811
|
+
);
|
|
812
|
+
return 1;
|
|
813
|
+
}
|
|
814
|
+
const config = await (0, import_config3.loadConfig)(configPath);
|
|
815
|
+
if (!options.yes && !options.dryRun) {
|
|
816
|
+
const isDirty = checkGitDirty(projectRoot);
|
|
817
|
+
if (isDirty) {
|
|
818
|
+
console.log(
|
|
819
|
+
import_chalk4.default.yellow("Warning: You have uncommitted changes. Consider committing first.")
|
|
820
|
+
);
|
|
456
821
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
822
|
+
}
|
|
823
|
+
const shouldFixNaming = !options.rule || options.rule.includes("file-naming");
|
|
824
|
+
const shouldFixTests = !options.rule || options.rule.includes("missing-test");
|
|
825
|
+
const allFiles = getAllSourceFiles(projectRoot, config);
|
|
826
|
+
const renames = [];
|
|
827
|
+
if (shouldFixNaming) {
|
|
828
|
+
for (const file of allFiles) {
|
|
829
|
+
const resolved = resolveConfigForFile(file, config);
|
|
830
|
+
if (!resolved.rules.enforceNaming || !resolved.conventions.fileNaming) continue;
|
|
831
|
+
const violation = checkNaming(file, resolved.conventions);
|
|
832
|
+
if (!violation) continue;
|
|
833
|
+
const convention = getConventionValue(resolved.conventions.fileNaming);
|
|
834
|
+
if (!convention) continue;
|
|
835
|
+
const rename = computeRename(file, convention, projectRoot);
|
|
836
|
+
if (rename) renames.push(rename);
|
|
464
837
|
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
if (relPath.startsWith(`${prefix}/`) || relPath === prefix) return true;
|
|
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;
|
|
838
|
+
}
|
|
839
|
+
const dedupedRenames = deduplicateRenames(renames);
|
|
840
|
+
const testStubs = [];
|
|
841
|
+
if (shouldFixTests && config.rules.requireTests) {
|
|
842
|
+
const testViolations = checkMissingTests(projectRoot, config, "warn");
|
|
843
|
+
for (const v of testViolations) {
|
|
844
|
+
const stub = generateTestStub(v.file, config, projectRoot);
|
|
845
|
+
if (stub) testStubs.push(stub);
|
|
479
846
|
}
|
|
480
847
|
}
|
|
481
|
-
|
|
848
|
+
if (dedupedRenames.length === 0 && testStubs.length === 0) {
|
|
849
|
+
console.log(`${import_chalk4.default.green("\u2713")} No fixable violations found.`);
|
|
850
|
+
return 0;
|
|
851
|
+
}
|
|
852
|
+
printPlan(dedupedRenames, testStubs);
|
|
853
|
+
if (options.dryRun) {
|
|
854
|
+
console.log(import_chalk4.default.dim("\nDry run \u2014 no changes applied."));
|
|
855
|
+
return 0;
|
|
856
|
+
}
|
|
857
|
+
if (!options.yes) {
|
|
858
|
+
const confirmed = await promptConfirm("Apply these fixes?");
|
|
859
|
+
if (!confirmed) {
|
|
860
|
+
console.log("Aborted.");
|
|
861
|
+
return 0;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
let renameCount = 0;
|
|
865
|
+
for (const rename of dedupedRenames) {
|
|
866
|
+
if (executeRename(rename)) {
|
|
867
|
+
renameCount++;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
let importUpdateCount = 0;
|
|
871
|
+
if (renameCount > 0) {
|
|
872
|
+
const appliedRenames = dedupedRenames.filter((r) => fs9.existsSync(r.newAbsPath));
|
|
873
|
+
const updates = await updateImportsAfterRenames(appliedRenames, projectRoot);
|
|
874
|
+
importUpdateCount = updates.length;
|
|
875
|
+
}
|
|
876
|
+
let stubCount = 0;
|
|
877
|
+
for (const stub of testStubs) {
|
|
878
|
+
if (!fs9.existsSync(stub.absPath)) {
|
|
879
|
+
writeTestStub(stub, config);
|
|
880
|
+
stubCount++;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
console.log("");
|
|
884
|
+
if (renameCount > 0) {
|
|
885
|
+
console.log(`${import_chalk4.default.green("\u2713")} Renamed ${renameCount} file${renameCount > 1 ? "s" : ""}`);
|
|
886
|
+
}
|
|
887
|
+
if (importUpdateCount > 0) {
|
|
888
|
+
console.log(
|
|
889
|
+
`${import_chalk4.default.green("\u2713")} Updated ${importUpdateCount} import${importUpdateCount > 1 ? "s" : ""}`
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
if (stubCount > 0) {
|
|
893
|
+
console.log(`${import_chalk4.default.green("\u2713")} Generated ${stubCount} test stub${stubCount > 1 ? "s" : ""}`);
|
|
894
|
+
}
|
|
895
|
+
return 0;
|
|
482
896
|
}
|
|
483
897
|
|
|
484
898
|
// src/commands/init.ts
|
|
485
|
-
var
|
|
486
|
-
var
|
|
487
|
-
var
|
|
899
|
+
var fs12 = __toESM(require("fs"), 1);
|
|
900
|
+
var path13 = __toESM(require("path"), 1);
|
|
901
|
+
var import_config4 = require("@viberails/config");
|
|
488
902
|
var import_scanner = require("@viberails/scanner");
|
|
489
|
-
var
|
|
903
|
+
var import_chalk8 = __toESM(require("chalk"), 1);
|
|
490
904
|
|
|
491
905
|
// src/display.ts
|
|
906
|
+
var import_types3 = require("@viberails/types");
|
|
907
|
+
var import_chalk6 = __toESM(require("chalk"), 1);
|
|
908
|
+
|
|
909
|
+
// src/display-helpers.ts
|
|
492
910
|
var import_types = require("@viberails/types");
|
|
493
|
-
|
|
911
|
+
function groupByRole(directories) {
|
|
912
|
+
const map = /* @__PURE__ */ new Map();
|
|
913
|
+
for (const dir of directories) {
|
|
914
|
+
if (dir.role === "unknown") continue;
|
|
915
|
+
const existing = map.get(dir.role);
|
|
916
|
+
if (existing) {
|
|
917
|
+
existing.dirs.push(dir);
|
|
918
|
+
} else {
|
|
919
|
+
map.set(dir.role, { dirs: [dir] });
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
const groups = [];
|
|
923
|
+
for (const [role, { dirs }] of map) {
|
|
924
|
+
const label = import_types.ROLE_DESCRIPTIONS[role] ?? role;
|
|
925
|
+
const totalFiles = dirs.reduce((sum, d) => sum + d.fileCount, 0);
|
|
926
|
+
groups.push({
|
|
927
|
+
role,
|
|
928
|
+
label,
|
|
929
|
+
dirCount: dirs.length,
|
|
930
|
+
totalFiles,
|
|
931
|
+
singlePath: dirs.length === 1 ? dirs[0].path : void 0
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
return groups;
|
|
935
|
+
}
|
|
936
|
+
function formatSummary(stats, packageCount) {
|
|
937
|
+
const parts = [];
|
|
938
|
+
if (packageCount && packageCount > 1) {
|
|
939
|
+
parts.push(`${packageCount} packages`);
|
|
940
|
+
}
|
|
941
|
+
parts.push(`${stats.totalFiles.toLocaleString()} source files`);
|
|
942
|
+
parts.push(`${stats.totalLines.toLocaleString()} lines`);
|
|
943
|
+
parts.push(`avg ${Math.round(stats.averageFileLines)} lines/file`);
|
|
944
|
+
return parts.join(" \xB7 ");
|
|
945
|
+
}
|
|
946
|
+
function formatExtensions(filesByExtension, maxEntries = 4) {
|
|
947
|
+
return Object.entries(filesByExtension).sort(([, a], [, b]) => b - a).slice(0, maxEntries).map(([ext, count]) => `${ext} ${count}`).join(" \xB7 ");
|
|
948
|
+
}
|
|
949
|
+
function formatRoleGroup(group) {
|
|
950
|
+
const files = group.totalFiles === 1 ? "1 file" : `${group.totalFiles} files`;
|
|
951
|
+
if (group.singlePath) {
|
|
952
|
+
return `${group.label} \u2014 ${group.singlePath} (${files})`;
|
|
953
|
+
}
|
|
954
|
+
const dirs = group.dirCount === 1 ? "1 dir" : `${group.dirCount} dirs`;
|
|
955
|
+
return `${group.label} \u2014 ${dirs} (${files})`;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// src/display-monorepo.ts
|
|
959
|
+
var import_types2 = require("@viberails/types");
|
|
960
|
+
var import_chalk5 = __toESM(require("chalk"), 1);
|
|
961
|
+
function formatPackageSummary(pkg) {
|
|
962
|
+
const parts = [];
|
|
963
|
+
if (pkg.stack.framework) {
|
|
964
|
+
parts.push(formatItem(pkg.stack.framework, import_types2.FRAMEWORK_NAMES));
|
|
965
|
+
}
|
|
966
|
+
if (pkg.stack.styling) {
|
|
967
|
+
parts.push(formatItem(pkg.stack.styling, import_types2.STYLING_NAMES));
|
|
968
|
+
}
|
|
969
|
+
const files = `${pkg.statistics.totalFiles} files`;
|
|
970
|
+
const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
|
|
971
|
+
return ` ${pkg.relativePath} \u2014 ${detail}`;
|
|
972
|
+
}
|
|
973
|
+
function displayMonorepoResults(scanResult) {
|
|
974
|
+
const { stack, packages } = scanResult;
|
|
975
|
+
console.log(`
|
|
976
|
+
${import_chalk5.default.bold(`Detected: (monorepo, ${packages.length} packages)`)}`);
|
|
977
|
+
console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.language)}`);
|
|
978
|
+
if (stack.packageManager) {
|
|
979
|
+
console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.packageManager)}`);
|
|
980
|
+
}
|
|
981
|
+
if (stack.linter) {
|
|
982
|
+
console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.linter)}`);
|
|
983
|
+
}
|
|
984
|
+
if (stack.formatter) {
|
|
985
|
+
console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.formatter)}`);
|
|
986
|
+
}
|
|
987
|
+
if (stack.testRunner) {
|
|
988
|
+
console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
|
|
989
|
+
}
|
|
990
|
+
console.log("");
|
|
991
|
+
for (const pkg of packages) {
|
|
992
|
+
console.log(formatPackageSummary(pkg));
|
|
993
|
+
}
|
|
994
|
+
const packagesWithDirs = packages.filter(
|
|
995
|
+
(pkg) => pkg.structure.directories.some((d) => d.role !== "unknown")
|
|
996
|
+
);
|
|
997
|
+
if (packagesWithDirs.length > 0) {
|
|
998
|
+
console.log(`
|
|
999
|
+
${import_chalk5.default.bold("Structure:")}`);
|
|
1000
|
+
for (const pkg of packagesWithDirs) {
|
|
1001
|
+
const groups = groupByRole(pkg.structure.directories);
|
|
1002
|
+
if (groups.length === 0) continue;
|
|
1003
|
+
console.log(` ${pkg.relativePath}:`);
|
|
1004
|
+
for (const group of groups) {
|
|
1005
|
+
console.log(` ${import_chalk5.default.green("\u2713")} ${formatRoleGroup(group)}`);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
displayConventions(scanResult);
|
|
1010
|
+
displaySummarySection(scanResult);
|
|
1011
|
+
console.log("");
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// src/display.ts
|
|
494
1015
|
var CONVENTION_LABELS = {
|
|
495
1016
|
fileNaming: "File naming",
|
|
496
1017
|
componentNaming: "Component naming",
|
|
@@ -508,82 +1029,194 @@ function confidenceLabel(convention) {
|
|
|
508
1029
|
}
|
|
509
1030
|
return `${pct}% \u2014 medium confidence, suggested only`;
|
|
510
1031
|
}
|
|
1032
|
+
function displayConventions(scanResult) {
|
|
1033
|
+
const conventionEntries = Object.entries(scanResult.conventions);
|
|
1034
|
+
if (conventionEntries.length === 0) return;
|
|
1035
|
+
console.log(`
|
|
1036
|
+
${import_chalk6.default.bold("Conventions:")}`);
|
|
1037
|
+
for (const [key, convention] of conventionEntries) {
|
|
1038
|
+
if (convention.confidence === "low") continue;
|
|
1039
|
+
const label = CONVENTION_LABELS[key] ?? key;
|
|
1040
|
+
if (scanResult.packages.length > 1) {
|
|
1041
|
+
const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
|
|
1042
|
+
const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
|
|
1043
|
+
if (allSame || pkgValues.length <= 1) {
|
|
1044
|
+
const ind = convention.confidence === "high" ? import_chalk6.default.green("\u2713") : import_chalk6.default.yellow("~");
|
|
1045
|
+
const detail = import_chalk6.default.dim(`(${confidenceLabel(convention)})`);
|
|
1046
|
+
console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
|
|
1047
|
+
} else {
|
|
1048
|
+
console.log(` ${import_chalk6.default.yellow("~")} ${label}: varies by package`);
|
|
1049
|
+
for (const pv of pkgValues) {
|
|
1050
|
+
const pct = Math.round(pv.convention.consistency);
|
|
1051
|
+
console.log(` ${pv.relativePath}: ${pv.convention.value} (${pct}%)`);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
} else {
|
|
1055
|
+
const ind = convention.confidence === "high" ? import_chalk6.default.green("\u2713") : import_chalk6.default.yellow("~");
|
|
1056
|
+
const detail = import_chalk6.default.dim(`(${confidenceLabel(convention)})`);
|
|
1057
|
+
console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
function displaySummarySection(scanResult) {
|
|
1062
|
+
const pkgCount = scanResult.packages.length > 1 ? scanResult.packages.length : void 0;
|
|
1063
|
+
console.log(`
|
|
1064
|
+
${import_chalk6.default.bold("Summary:")}`);
|
|
1065
|
+
console.log(` ${formatSummary(scanResult.statistics, pkgCount)}`);
|
|
1066
|
+
const ext = formatExtensions(scanResult.statistics.filesByExtension);
|
|
1067
|
+
if (ext) {
|
|
1068
|
+
console.log(` ${ext}`);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
511
1071
|
function displayScanResults(scanResult) {
|
|
512
|
-
|
|
1072
|
+
if (scanResult.packages.length > 1) {
|
|
1073
|
+
displayMonorepoResults(scanResult);
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
const { stack } = scanResult;
|
|
513
1077
|
console.log(`
|
|
514
|
-
${
|
|
1078
|
+
${import_chalk6.default.bold("Detected:")}`);
|
|
515
1079
|
if (stack.framework) {
|
|
516
|
-
console.log(` ${
|
|
1080
|
+
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.framework, import_types3.FRAMEWORK_NAMES)}`);
|
|
517
1081
|
}
|
|
518
|
-
console.log(` ${
|
|
1082
|
+
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.language)}`);
|
|
519
1083
|
if (stack.styling) {
|
|
520
|
-
console.log(` ${
|
|
1084
|
+
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.styling, import_types3.STYLING_NAMES)}`);
|
|
521
1085
|
}
|
|
522
1086
|
if (stack.backend) {
|
|
523
|
-
console.log(` ${
|
|
1087
|
+
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.backend, import_types3.FRAMEWORK_NAMES)}`);
|
|
524
1088
|
}
|
|
525
1089
|
if (stack.linter) {
|
|
526
|
-
console.log(` ${
|
|
1090
|
+
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.linter)}`);
|
|
1091
|
+
}
|
|
1092
|
+
if (stack.formatter) {
|
|
1093
|
+
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.formatter)}`);
|
|
527
1094
|
}
|
|
528
1095
|
if (stack.testRunner) {
|
|
529
|
-
console.log(` ${
|
|
1096
|
+
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
|
|
530
1097
|
}
|
|
531
1098
|
if (stack.packageManager) {
|
|
532
|
-
console.log(` ${
|
|
1099
|
+
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.packageManager)}`);
|
|
533
1100
|
}
|
|
534
1101
|
if (stack.libraries.length > 0) {
|
|
535
1102
|
for (const lib of stack.libraries) {
|
|
536
|
-
console.log(` ${
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
const meaningfulDirs = scanResult.structure.directories.filter((d) => d.role !== "unknown");
|
|
540
|
-
if (meaningfulDirs.length > 0) {
|
|
541
|
-
console.log(`
|
|
542
|
-
${import_chalk3.default.bold("Structure:")}`);
|
|
543
|
-
for (const dir of meaningfulDirs) {
|
|
544
|
-
const label = import_types.ROLE_DESCRIPTIONS[dir.role] ?? dir.role;
|
|
545
|
-
const files = dir.fileCount === 1 ? "1 file" : `${dir.fileCount} files`;
|
|
546
|
-
console.log(` ${import_chalk3.default.green("\u2713")} ${dir.path} \u2014 ${label} (${files})`);
|
|
1103
|
+
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(lib, import_types3.LIBRARY_NAMES)}`);
|
|
547
1104
|
}
|
|
548
1105
|
}
|
|
549
|
-
const
|
|
550
|
-
if (
|
|
1106
|
+
const groups = groupByRole(scanResult.structure.directories);
|
|
1107
|
+
if (groups.length > 0) {
|
|
551
1108
|
console.log(`
|
|
552
|
-
${
|
|
553
|
-
for (const
|
|
554
|
-
|
|
555
|
-
const label = CONVENTION_LABELS[key] ?? key;
|
|
556
|
-
const ind = convention.confidence === "high" ? import_chalk3.default.green("\u2713") : import_chalk3.default.yellow("~");
|
|
557
|
-
const detail = import_chalk3.default.dim(`(${confidenceLabel(convention)})`);
|
|
558
|
-
console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
|
|
1109
|
+
${import_chalk6.default.bold("Structure:")}`);
|
|
1110
|
+
for (const group of groups) {
|
|
1111
|
+
console.log(` ${import_chalk6.default.green("\u2713")} ${formatRoleGroup(group)}`);
|
|
559
1112
|
}
|
|
560
1113
|
}
|
|
1114
|
+
displayConventions(scanResult);
|
|
1115
|
+
displaySummarySection(scanResult);
|
|
561
1116
|
console.log("");
|
|
562
1117
|
}
|
|
563
1118
|
|
|
564
1119
|
// src/utils/write-generated-files.ts
|
|
565
|
-
var
|
|
566
|
-
var
|
|
1120
|
+
var fs10 = __toESM(require("fs"), 1);
|
|
1121
|
+
var path11 = __toESM(require("path"), 1);
|
|
567
1122
|
var import_context = require("@viberails/context");
|
|
568
1123
|
var CONTEXT_DIR = ".viberails";
|
|
569
1124
|
var CONTEXT_FILE = "context.md";
|
|
570
1125
|
var SCAN_RESULT_FILE = "scan-result.json";
|
|
571
1126
|
function writeGeneratedFiles(projectRoot, config, scanResult) {
|
|
572
|
-
const contextDir =
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
1127
|
+
const contextDir = path11.join(projectRoot, CONTEXT_DIR);
|
|
1128
|
+
try {
|
|
1129
|
+
if (!fs10.existsSync(contextDir)) {
|
|
1130
|
+
fs10.mkdirSync(contextDir, { recursive: true });
|
|
1131
|
+
}
|
|
1132
|
+
const context = (0, import_context.generateContext)(config);
|
|
1133
|
+
fs10.writeFileSync(path11.join(contextDir, CONTEXT_FILE), context);
|
|
1134
|
+
fs10.writeFileSync(
|
|
1135
|
+
path11.join(contextDir, SCAN_RESULT_FILE),
|
|
1136
|
+
`${JSON.stringify(scanResult, null, 2)}
|
|
581
1137
|
`
|
|
582
|
-
|
|
1138
|
+
);
|
|
1139
|
+
} catch (err) {
|
|
1140
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1141
|
+
throw new Error(`Failed to write generated files to ${contextDir}: ${message}`);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// src/commands/init-hooks.ts
|
|
1146
|
+
var fs11 = __toESM(require("fs"), 1);
|
|
1147
|
+
var path12 = __toESM(require("path"), 1);
|
|
1148
|
+
var import_chalk7 = __toESM(require("chalk"), 1);
|
|
1149
|
+
function setupPreCommitHook(projectRoot) {
|
|
1150
|
+
const lefthookPath = path12.join(projectRoot, "lefthook.yml");
|
|
1151
|
+
if (fs11.existsSync(lefthookPath)) {
|
|
1152
|
+
addLefthookPreCommit(lefthookPath);
|
|
1153
|
+
console.log(` ${import_chalk7.default.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
const huskyDir = path12.join(projectRoot, ".husky");
|
|
1157
|
+
if (fs11.existsSync(huskyDir)) {
|
|
1158
|
+
writeHuskyPreCommit(huskyDir);
|
|
1159
|
+
console.log(` ${import_chalk7.default.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
const gitDir = path12.join(projectRoot, ".git");
|
|
1163
|
+
if (fs11.existsSync(gitDir)) {
|
|
1164
|
+
const hooksDir = path12.join(gitDir, "hooks");
|
|
1165
|
+
if (!fs11.existsSync(hooksDir)) {
|
|
1166
|
+
fs11.mkdirSync(hooksDir, { recursive: true });
|
|
1167
|
+
}
|
|
1168
|
+
writeGitHookPreCommit(hooksDir);
|
|
1169
|
+
console.log(` ${import_chalk7.default.green("\u2713")} .git/hooks/pre-commit`);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
function writeGitHookPreCommit(hooksDir) {
|
|
1173
|
+
const hookPath = path12.join(hooksDir, "pre-commit");
|
|
1174
|
+
if (fs11.existsSync(hookPath)) {
|
|
1175
|
+
const existing = fs11.readFileSync(hookPath, "utf-8");
|
|
1176
|
+
if (existing.includes("viberails")) return;
|
|
1177
|
+
fs11.writeFileSync(
|
|
1178
|
+
hookPath,
|
|
1179
|
+
`${existing.trimEnd()}
|
|
1180
|
+
|
|
1181
|
+
# viberails check
|
|
1182
|
+
npx viberails check --staged
|
|
1183
|
+
`
|
|
1184
|
+
);
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
const script = [
|
|
1188
|
+
"#!/bin/sh",
|
|
1189
|
+
"# Generated by viberails \u2014 https://viberails.sh",
|
|
1190
|
+
"",
|
|
1191
|
+
"npx viberails check --staged",
|
|
1192
|
+
""
|
|
1193
|
+
].join("\n");
|
|
1194
|
+
fs11.writeFileSync(hookPath, script, { mode: 493 });
|
|
1195
|
+
}
|
|
1196
|
+
function addLefthookPreCommit(lefthookPath) {
|
|
1197
|
+
const content = fs11.readFileSync(lefthookPath, "utf-8");
|
|
1198
|
+
if (content.includes("viberails")) return;
|
|
1199
|
+
const addition = ["", " viberails:", " run: npx viberails check --staged"].join("\n");
|
|
1200
|
+
fs11.writeFileSync(lefthookPath, `${content.trimEnd()}
|
|
1201
|
+
${addition}
|
|
1202
|
+
`);
|
|
1203
|
+
}
|
|
1204
|
+
function writeHuskyPreCommit(huskyDir) {
|
|
1205
|
+
const hookPath = path12.join(huskyDir, "pre-commit");
|
|
1206
|
+
if (fs11.existsSync(hookPath)) {
|
|
1207
|
+
const existing = fs11.readFileSync(hookPath, "utf-8");
|
|
1208
|
+
if (!existing.includes("viberails")) {
|
|
1209
|
+
fs11.writeFileSync(hookPath, `${existing.trimEnd()}
|
|
1210
|
+
npx viberails check --staged
|
|
1211
|
+
`);
|
|
1212
|
+
}
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
fs11.writeFileSync(hookPath, "#!/bin/sh\nnpx viberails check --staged\n", { mode: 493 });
|
|
583
1216
|
}
|
|
584
1217
|
|
|
585
1218
|
// src/commands/init.ts
|
|
586
|
-
var
|
|
1219
|
+
var CONFIG_FILE4 = "viberails.config.json";
|
|
587
1220
|
function filterHighConfidence(conventions) {
|
|
588
1221
|
const filtered = {};
|
|
589
1222
|
for (const [key, value] of Object.entries(conventions)) {
|
|
@@ -604,19 +1237,19 @@ async function initCommand(options, cwd) {
|
|
|
604
1237
|
"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
1238
|
);
|
|
606
1239
|
}
|
|
607
|
-
const configPath =
|
|
608
|
-
if (
|
|
1240
|
+
const configPath = path13.join(projectRoot, CONFIG_FILE4);
|
|
1241
|
+
if (fs12.existsSync(configPath)) {
|
|
609
1242
|
console.log(
|
|
610
|
-
|
|
1243
|
+
import_chalk8.default.yellow("!") + " viberails is already initialized in this project.\n Run " + import_chalk8.default.cyan("viberails sync") + " to update the generated files."
|
|
611
1244
|
);
|
|
612
1245
|
return;
|
|
613
1246
|
}
|
|
614
|
-
console.log(
|
|
1247
|
+
console.log(import_chalk8.default.dim("Scanning project..."));
|
|
615
1248
|
const scanResult = await (0, import_scanner.scan)(projectRoot);
|
|
616
1249
|
displayScanResults(scanResult);
|
|
617
1250
|
if (scanResult.statistics.totalFiles === 0) {
|
|
618
1251
|
console.log(
|
|
619
|
-
|
|
1252
|
+
import_chalk8.default.yellow("!") + " No source files detected. viberails will generate context with minimal content.\n Run " + import_chalk8.default.cyan("viberails sync") + " after adding source files.\n"
|
|
620
1253
|
);
|
|
621
1254
|
}
|
|
622
1255
|
if (!options.yes) {
|
|
@@ -626,7 +1259,7 @@ async function initCommand(options, cwd) {
|
|
|
626
1259
|
return;
|
|
627
1260
|
}
|
|
628
1261
|
}
|
|
629
|
-
const config = (0,
|
|
1262
|
+
const config = (0, import_config4.generateConfig)(scanResult);
|
|
630
1263
|
if (options.yes) {
|
|
631
1264
|
config.conventions = filterHighConfidence(config.conventions);
|
|
632
1265
|
}
|
|
@@ -636,7 +1269,7 @@ async function initCommand(options, cwd) {
|
|
|
636
1269
|
shouldInfer = await confirm("Infer boundary rules from import patterns?");
|
|
637
1270
|
}
|
|
638
1271
|
if (shouldInfer) {
|
|
639
|
-
console.log(
|
|
1272
|
+
console.log(import_chalk8.default.dim("Building import graph..."));
|
|
640
1273
|
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
641
1274
|
const packages = resolveWorkspacePackages(projectRoot, config.workspace);
|
|
642
1275
|
const graph = await buildImportGraph(projectRoot, { packages, ignore: config.ignore });
|
|
@@ -644,116 +1277,48 @@ async function initCommand(options, cwd) {
|
|
|
644
1277
|
if (inferred.length > 0) {
|
|
645
1278
|
config.boundaries = inferred;
|
|
646
1279
|
config.rules.enforceBoundaries = true;
|
|
647
|
-
console.log(` ${
|
|
1280
|
+
console.log(` ${import_chalk8.default.green("\u2713")} Inferred ${inferred.length} boundary rules`);
|
|
648
1281
|
}
|
|
649
1282
|
}
|
|
650
1283
|
}
|
|
651
|
-
|
|
1284
|
+
fs12.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
652
1285
|
`);
|
|
653
1286
|
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
654
1287
|
updateGitignore(projectRoot);
|
|
655
1288
|
setupPreCommitHook(projectRoot);
|
|
656
1289
|
console.log(`
|
|
657
|
-
${
|
|
658
|
-
console.log(` ${
|
|
659
|
-
console.log(` ${
|
|
660
|
-
console.log(` ${
|
|
1290
|
+
${import_chalk8.default.bold("Created:")}`);
|
|
1291
|
+
console.log(` ${import_chalk8.default.green("\u2713")} ${CONFIG_FILE4}`);
|
|
1292
|
+
console.log(` ${import_chalk8.default.green("\u2713")} .viberails/context.md`);
|
|
1293
|
+
console.log(` ${import_chalk8.default.green("\u2713")} .viberails/scan-result.json`);
|
|
661
1294
|
console.log(`
|
|
662
|
-
${
|
|
663
|
-
console.log(` 1. Review ${
|
|
1295
|
+
${import_chalk8.default.bold("Next steps:")}`);
|
|
1296
|
+
console.log(` 1. Review ${import_chalk8.default.cyan("viberails.config.json")} and adjust rules`);
|
|
664
1297
|
console.log(
|
|
665
|
-
` 2. Commit ${
|
|
1298
|
+
` 2. Commit ${import_chalk8.default.cyan("viberails.config.json")} and ${import_chalk8.default.cyan(".viberails/context.md")}`
|
|
666
1299
|
);
|
|
667
|
-
console.log(` 3. Run ${
|
|
1300
|
+
console.log(` 3. Run ${import_chalk8.default.cyan("viberails check")} to verify your project passes`);
|
|
668
1301
|
}
|
|
669
1302
|
function updateGitignore(projectRoot) {
|
|
670
|
-
const gitignorePath =
|
|
1303
|
+
const gitignorePath = path13.join(projectRoot, ".gitignore");
|
|
671
1304
|
let content = "";
|
|
672
|
-
if (
|
|
673
|
-
content =
|
|
1305
|
+
if (fs12.existsSync(gitignorePath)) {
|
|
1306
|
+
content = fs12.readFileSync(gitignorePath, "utf-8");
|
|
674
1307
|
}
|
|
675
1308
|
if (!content.includes(".viberails/scan-result.json")) {
|
|
676
1309
|
const block = "\n# viberails\n.viberails/scan-result.json\n";
|
|
677
|
-
|
|
1310
|
+
fs12.writeFileSync(gitignorePath, `${content.trimEnd()}
|
|
678
1311
|
${block}`);
|
|
679
1312
|
}
|
|
680
1313
|
}
|
|
681
|
-
function setupPreCommitHook(projectRoot) {
|
|
682
|
-
const lefthookPath = path6.join(projectRoot, "lefthook.yml");
|
|
683
|
-
if (fs6.existsSync(lefthookPath)) {
|
|
684
|
-
addLefthookPreCommit(lefthookPath);
|
|
685
|
-
console.log(` ${import_chalk4.default.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
|
|
686
|
-
return;
|
|
687
|
-
}
|
|
688
|
-
const huskyDir = path6.join(projectRoot, ".husky");
|
|
689
|
-
if (fs6.existsSync(huskyDir)) {
|
|
690
|
-
writeHuskyPreCommit(huskyDir);
|
|
691
|
-
console.log(` ${import_chalk4.default.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
|
|
692
|
-
return;
|
|
693
|
-
}
|
|
694
|
-
const gitDir = path6.join(projectRoot, ".git");
|
|
695
|
-
if (fs6.existsSync(gitDir)) {
|
|
696
|
-
const hooksDir = path6.join(gitDir, "hooks");
|
|
697
|
-
if (!fs6.existsSync(hooksDir)) {
|
|
698
|
-
fs6.mkdirSync(hooksDir, { recursive: true });
|
|
699
|
-
}
|
|
700
|
-
writeGitHookPreCommit(hooksDir);
|
|
701
|
-
console.log(` ${import_chalk4.default.green("\u2713")} .git/hooks/pre-commit`);
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
function writeGitHookPreCommit(hooksDir) {
|
|
705
|
-
const hookPath = path6.join(hooksDir, "pre-commit");
|
|
706
|
-
if (fs6.existsSync(hookPath)) {
|
|
707
|
-
const existing = fs6.readFileSync(hookPath, "utf-8");
|
|
708
|
-
if (existing.includes("viberails")) return;
|
|
709
|
-
fs6.writeFileSync(
|
|
710
|
-
hookPath,
|
|
711
|
-
`${existing.trimEnd()}
|
|
712
|
-
|
|
713
|
-
# viberails check
|
|
714
|
-
npx viberails check --staged
|
|
715
|
-
`
|
|
716
|
-
);
|
|
717
|
-
return;
|
|
718
|
-
}
|
|
719
|
-
const script = [
|
|
720
|
-
"#!/bin/sh",
|
|
721
|
-
"# Generated by viberails \u2014 https://viberails.sh",
|
|
722
|
-
"",
|
|
723
|
-
"npx viberails check --staged",
|
|
724
|
-
""
|
|
725
|
-
].join("\n");
|
|
726
|
-
fs6.writeFileSync(hookPath, script, { mode: 493 });
|
|
727
|
-
}
|
|
728
|
-
function addLefthookPreCommit(lefthookPath) {
|
|
729
|
-
const content = fs6.readFileSync(lefthookPath, "utf-8");
|
|
730
|
-
if (content.includes("viberails")) return;
|
|
731
|
-
const addition = ["", " viberails:", " run: npx viberails check --staged"].join("\n");
|
|
732
|
-
fs6.writeFileSync(lefthookPath, `${content.trimEnd()}
|
|
733
|
-
${addition}
|
|
734
|
-
`);
|
|
735
|
-
}
|
|
736
|
-
function writeHuskyPreCommit(huskyDir) {
|
|
737
|
-
const hookPath = path6.join(huskyDir, "pre-commit");
|
|
738
|
-
if (fs6.existsSync(hookPath)) {
|
|
739
|
-
const existing = fs6.readFileSync(hookPath, "utf-8");
|
|
740
|
-
if (!existing.includes("viberails")) {
|
|
741
|
-
fs6.writeFileSync(hookPath, `${existing.trimEnd()}
|
|
742
|
-
npx viberails check --staged
|
|
743
|
-
`);
|
|
744
|
-
}
|
|
745
|
-
return;
|
|
746
|
-
}
|
|
747
|
-
fs6.writeFileSync(hookPath, "#!/bin/sh\nnpx viberails check --staged\n", { mode: 493 });
|
|
748
|
-
}
|
|
749
1314
|
|
|
750
1315
|
// src/commands/sync.ts
|
|
751
|
-
var
|
|
752
|
-
var
|
|
753
|
-
var
|
|
1316
|
+
var fs13 = __toESM(require("fs"), 1);
|
|
1317
|
+
var path14 = __toESM(require("path"), 1);
|
|
1318
|
+
var import_config5 = require("@viberails/config");
|
|
754
1319
|
var import_scanner2 = require("@viberails/scanner");
|
|
755
|
-
var
|
|
756
|
-
var
|
|
1320
|
+
var import_chalk9 = __toESM(require("chalk"), 1);
|
|
1321
|
+
var CONFIG_FILE5 = "viberails.config.json";
|
|
757
1322
|
async function syncCommand(cwd) {
|
|
758
1323
|
const startDir = cwd ?? process.cwd();
|
|
759
1324
|
const projectRoot = findProjectRoot(startDir);
|
|
@@ -762,23 +1327,23 @@ async function syncCommand(cwd) {
|
|
|
762
1327
|
"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
1328
|
);
|
|
764
1329
|
}
|
|
765
|
-
const configPath =
|
|
766
|
-
const existing = await (0,
|
|
767
|
-
console.log(
|
|
1330
|
+
const configPath = path14.join(projectRoot, CONFIG_FILE5);
|
|
1331
|
+
const existing = await (0, import_config5.loadConfig)(configPath);
|
|
1332
|
+
console.log(import_chalk9.default.dim("Scanning project..."));
|
|
768
1333
|
const scanResult = await (0, import_scanner2.scan)(projectRoot);
|
|
769
|
-
const merged = (0,
|
|
770
|
-
|
|
1334
|
+
const merged = (0, import_config5.mergeConfig)(existing, scanResult);
|
|
1335
|
+
fs13.writeFileSync(configPath, `${JSON.stringify(merged, null, 2)}
|
|
771
1336
|
`);
|
|
772
1337
|
writeGeneratedFiles(projectRoot, merged, scanResult);
|
|
773
1338
|
console.log(`
|
|
774
|
-
${
|
|
775
|
-
console.log(` ${
|
|
776
|
-
console.log(` ${
|
|
777
|
-
console.log(` ${
|
|
1339
|
+
${import_chalk9.default.bold("Synced:")}`);
|
|
1340
|
+
console.log(` ${import_chalk9.default.green("\u2713")} ${CONFIG_FILE5} \u2014 updated`);
|
|
1341
|
+
console.log(` ${import_chalk9.default.green("\u2713")} .viberails/context.md \u2014 regenerated`);
|
|
1342
|
+
console.log(` ${import_chalk9.default.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
|
|
778
1343
|
}
|
|
779
1344
|
|
|
780
1345
|
// src/index.ts
|
|
781
|
-
var VERSION = "0.1
|
|
1346
|
+
var VERSION = "0.2.1";
|
|
782
1347
|
var program = new import_commander.Command();
|
|
783
1348
|
program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
|
|
784
1349
|
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 +1351,7 @@ program.command("init", { isDefault: true }).description("Scan your project and
|
|
|
786
1351
|
await initCommand(options);
|
|
787
1352
|
} catch (err) {
|
|
788
1353
|
const message = err instanceof Error ? err.message : String(err);
|
|
789
|
-
console.error(`${
|
|
1354
|
+
console.error(`${import_chalk10.default.red("Error:")} ${message}`);
|
|
790
1355
|
process.exit(1);
|
|
791
1356
|
}
|
|
792
1357
|
});
|
|
@@ -795,7 +1360,7 @@ program.command("sync").description("Re-scan and update generated files").action
|
|
|
795
1360
|
await syncCommand();
|
|
796
1361
|
} catch (err) {
|
|
797
1362
|
const message = err instanceof Error ? err.message : String(err);
|
|
798
|
-
console.error(`${
|
|
1363
|
+
console.error(`${import_chalk10.default.red("Error:")} ${message}`);
|
|
799
1364
|
process.exit(1);
|
|
800
1365
|
}
|
|
801
1366
|
});
|
|
@@ -808,7 +1373,17 @@ program.command("check").description("Check files against enforced rules").optio
|
|
|
808
1373
|
process.exit(exitCode);
|
|
809
1374
|
} catch (err) {
|
|
810
1375
|
const message = err instanceof Error ? err.message : String(err);
|
|
811
|
-
console.error(`${
|
|
1376
|
+
console.error(`${import_chalk10.default.red("Error:")} ${message}`);
|
|
1377
|
+
process.exit(1);
|
|
1378
|
+
}
|
|
1379
|
+
});
|
|
1380
|
+
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) => {
|
|
1381
|
+
try {
|
|
1382
|
+
const exitCode = await fixCommand(options);
|
|
1383
|
+
process.exit(exitCode);
|
|
1384
|
+
} catch (err) {
|
|
1385
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1386
|
+
console.error(`${import_chalk10.default.red("Error:")} ${message}`);
|
|
812
1387
|
process.exit(1);
|
|
813
1388
|
}
|
|
814
1389
|
});
|
|
@@ -817,7 +1392,7 @@ program.command("boundaries").description("Display, infer, or inspect import bou
|
|
|
817
1392
|
await boundariesCommand(options);
|
|
818
1393
|
} catch (err) {
|
|
819
1394
|
const message = err instanceof Error ? err.message : String(err);
|
|
820
|
-
console.error(`${
|
|
1395
|
+
console.error(`${import_chalk10.default.red("Error:")} ${message}`);
|
|
821
1396
|
process.exit(1);
|
|
822
1397
|
}
|
|
823
1398
|
});
|