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