viberails 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,829 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/index.ts
32
+ var index_exports = {};
33
+ __export(index_exports, {
34
+ VERSION: () => VERSION
35
+ });
36
+ module.exports = __toCommonJS(index_exports);
37
+ var import_chalk6 = __toESM(require("chalk"), 1);
38
+ var import_commander = require("commander");
39
+
40
+ // src/commands/boundaries.ts
41
+ var fs3 = __toESM(require("fs"), 1);
42
+ var path3 = __toESM(require("path"), 1);
43
+ var import_config = require("@viberails/config");
44
+ var import_chalk = __toESM(require("chalk"), 1);
45
+
46
+ // src/utils/find-project-root.ts
47
+ var fs = __toESM(require("fs"), 1);
48
+ var path = __toESM(require("path"), 1);
49
+ function findProjectRoot(startDir) {
50
+ let dir = path.resolve(startDir);
51
+ while (true) {
52
+ if (fs.existsSync(path.join(dir, "package.json"))) {
53
+ return dir;
54
+ }
55
+ const parent = path.dirname(dir);
56
+ if (parent === dir) {
57
+ return null;
58
+ }
59
+ dir = parent;
60
+ }
61
+ }
62
+
63
+ // src/utils/prompt.ts
64
+ var readline = __toESM(require("readline"), 1);
65
+ async function confirm(message) {
66
+ const rl = readline.createInterface({
67
+ input: process.stdin,
68
+ output: process.stdout
69
+ });
70
+ return new Promise((resolve3) => {
71
+ rl.question(`${message} (Y/n) `, (answer) => {
72
+ rl.close();
73
+ const trimmed = answer.trim().toLowerCase();
74
+ resolve3(trimmed === "" || trimmed === "y" || trimmed === "yes");
75
+ });
76
+ });
77
+ }
78
+
79
+ // src/utils/resolve-workspace-packages.ts
80
+ var fs2 = __toESM(require("fs"), 1);
81
+ var path2 = __toESM(require("path"), 1);
82
+ function resolveWorkspacePackages(projectRoot, workspace) {
83
+ const packages = [];
84
+ for (const relativePath of workspace.packages) {
85
+ const absPath = path2.join(projectRoot, relativePath);
86
+ const pkgJsonPath = path2.join(absPath, "package.json");
87
+ if (!fs2.existsSync(pkgJsonPath)) continue;
88
+ let pkg;
89
+ try {
90
+ pkg = JSON.parse(fs2.readFileSync(pkgJsonPath, "utf-8"));
91
+ } catch {
92
+ continue;
93
+ }
94
+ const name = pkg.name;
95
+ if (!name) continue;
96
+ const allDeps = [
97
+ ...Object.keys(pkg.dependencies ?? {}),
98
+ ...Object.keys(pkg.devDependencies ?? {})
99
+ ];
100
+ packages.push({ name, path: absPath, relativePath, internalDeps: allDeps });
101
+ }
102
+ const packageNames = new Set(packages.map((p) => p.name));
103
+ for (const pkg of packages) {
104
+ pkg.internalDeps = pkg.internalDeps.filter((dep) => packageNames.has(dep));
105
+ }
106
+ return packages;
107
+ }
108
+
109
+ // src/commands/boundaries.ts
110
+ var CONFIG_FILE = "viberails.config.json";
111
+ async function boundariesCommand(options, cwd) {
112
+ const startDir = cwd ?? process.cwd();
113
+ const projectRoot = findProjectRoot(startDir);
114
+ if (!projectRoot) {
115
+ throw new Error("No package.json found. Are you in a JS/TS project?");
116
+ }
117
+ const configPath = path3.join(projectRoot, CONFIG_FILE);
118
+ if (!fs3.existsSync(configPath)) {
119
+ throw new Error("No viberails.config.json found. Run `viberails init` first.");
120
+ }
121
+ const config = await (0, import_config.loadConfig)(configPath);
122
+ if (options.graph) {
123
+ await showGraph(projectRoot, config);
124
+ return;
125
+ }
126
+ if (options.infer) {
127
+ await inferAndDisplay(projectRoot, config, configPath);
128
+ return;
129
+ }
130
+ displayRules(config);
131
+ }
132
+ function displayRules(config) {
133
+ if (!config.boundaries || config.boundaries.length === 0) {
134
+ console.log(import_chalk.default.yellow("No boundary rules configured."));
135
+ console.log(`Run ${import_chalk.default.cyan("viberails boundaries --infer")} to generate rules.`);
136
+ return;
137
+ }
138
+ const allowRules = config.boundaries.filter((r) => r.allow);
139
+ const denyRules = config.boundaries.filter((r) => !r.allow);
140
+ console.log(`
141
+ ${import_chalk.default.bold(`Boundary rules (${config.boundaries.length} rules):`)}
142
+ `);
143
+ for (const r of allowRules) {
144
+ console.log(` ${import_chalk.default.green("\u2713")} ${r.from} \u2192 ${r.to}`);
145
+ }
146
+ for (const r of denyRules) {
147
+ const reason = r.reason ? import_chalk.default.dim(` (${r.reason})`) : "";
148
+ console.log(` ${import_chalk.default.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
149
+ }
150
+ console.log(
151
+ `
152
+ Enforcement: ${config.rules.enforceBoundaries ? import_chalk.default.green("on") : import_chalk.default.yellow("off")}`
153
+ );
154
+ }
155
+ async function inferAndDisplay(projectRoot, config, configPath) {
156
+ console.log(import_chalk.default.dim("Analyzing imports..."));
157
+ const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
158
+ const packages = config.workspace ? resolveWorkspacePackages(projectRoot, config.workspace) : void 0;
159
+ const graph = await buildImportGraph(projectRoot, {
160
+ packages,
161
+ ignore: config.ignore
162
+ });
163
+ console.log(import_chalk.default.dim(`${graph.nodes.length} files, ${graph.edges.length} edges`));
164
+ const inferred = inferBoundaries(graph);
165
+ if (inferred.length === 0) {
166
+ console.log(import_chalk.default.yellow("No boundary rules could be inferred."));
167
+ return;
168
+ }
169
+ const allow = inferred.filter((r) => r.allow);
170
+ const deny = inferred.filter((r) => !r.allow);
171
+ console.log(`
172
+ ${import_chalk.default.bold("Inferred boundary rules:")}
173
+ `);
174
+ for (const r of allow) {
175
+ console.log(` ${import_chalk.default.green("\u2713")} ${r.from} \u2192 ${r.to}`);
176
+ }
177
+ for (const r of deny) {
178
+ const reason = r.reason ? import_chalk.default.dim(` (${r.reason})`) : "";
179
+ console.log(` ${import_chalk.default.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
180
+ }
181
+ console.log(`
182
+ ${allow.length} allowed, ${deny.length} denied`);
183
+ const shouldSave = await confirm("\nSave to viberails.config.json?");
184
+ if (shouldSave) {
185
+ config.boundaries = inferred;
186
+ config.rules.enforceBoundaries = true;
187
+ fs3.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
188
+ `);
189
+ console.log(`${import_chalk.default.green("\u2713")} Saved ${inferred.length} rules`);
190
+ }
191
+ }
192
+ async function showGraph(projectRoot, config) {
193
+ console.log(import_chalk.default.dim("Building import graph..."));
194
+ const { buildImportGraph } = await import("@viberails/graph");
195
+ const packages = config.workspace ? resolveWorkspacePackages(projectRoot, config.workspace) : void 0;
196
+ const graph = await buildImportGraph(projectRoot, {
197
+ packages,
198
+ ignore: config.ignore
199
+ });
200
+ console.log(`
201
+ ${import_chalk.default.bold("Import dependency graph:")}
202
+ `);
203
+ console.log(` ${graph.nodes.length} files, ${graph.edges.length} imports
204
+ `);
205
+ if (graph.packages.length > 0) {
206
+ for (const pkg of graph.packages) {
207
+ const deps = pkg.internalDeps.length > 0 ? `
208
+ ${pkg.internalDeps.map((d) => ` \u2192 ${d}`).join("\n")}` : import_chalk.default.dim(" (no internal deps)");
209
+ console.log(` ${pkg.name}${deps}`);
210
+ }
211
+ }
212
+ if (graph.cycles.length > 0) {
213
+ console.log(`
214
+ ${import_chalk.default.yellow("Cycles detected:")}`);
215
+ for (const cycle of graph.cycles) {
216
+ const paths = cycle.map((f) => path3.relative(projectRoot, f));
217
+ console.log(` ${paths.join(" \u2192 ")}`);
218
+ }
219
+ }
220
+ }
221
+
222
+ // src/commands/check.ts
223
+ var import_node_child_process = require("child_process");
224
+ var fs4 = __toESM(require("fs"), 1);
225
+ 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
+ var SOURCE_EXTS = /* @__PURE__ */ new Set([
230
+ ".ts",
231
+ ".tsx",
232
+ ".js",
233
+ ".jsx",
234
+ ".mjs",
235
+ ".cjs",
236
+ ".vue",
237
+ ".svelte",
238
+ ".astro"
239
+ ]);
240
+ var NAMING_PATTERNS = {
241
+ "kebab-case": /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/,
242
+ camelCase: /^[a-z][a-zA-Z0-9]*$/,
243
+ PascalCase: /^[A-Z][a-zA-Z0-9]*$/,
244
+ snake_case: /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/
245
+ };
246
+ async function checkCommand(options, cwd) {
247
+ const startDir = cwd ?? process.cwd();
248
+ const projectRoot = findProjectRoot(startDir);
249
+ if (!projectRoot) {
250
+ console.error(`${import_chalk2.default.red("Error:")} No package.json found. Are you in a JS/TS project?`);
251
+ return 1;
252
+ }
253
+ const configPath = path4.join(projectRoot, CONFIG_FILE2);
254
+ if (!fs4.existsSync(configPath)) {
255
+ console.error(
256
+ `${import_chalk2.default.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
257
+ );
258
+ return 1;
259
+ }
260
+ const config = await (0, import_config2.loadConfig)(configPath);
261
+ let filesToCheck;
262
+ if (options.staged) {
263
+ filesToCheck = getStagedFiles(projectRoot);
264
+ } else if (options.files && options.files.length > 0) {
265
+ filesToCheck = options.files;
266
+ } else {
267
+ filesToCheck = getAllSourceFiles(projectRoot, config);
268
+ }
269
+ if (filesToCheck.length === 0) {
270
+ console.log(`${import_chalk2.default.green("\u2713")} No files to check.`);
271
+ return 0;
272
+ }
273
+ const violations = [];
274
+ const severity = config.enforcement === "enforce" ? "error" : "warn";
275
+ for (const file of filesToCheck) {
276
+ const absPath = path4.isAbsolute(file) ? file : path4.join(projectRoot, file);
277
+ const relPath = path4.relative(projectRoot, absPath);
278
+ if (isIgnored(relPath, config.ignore)) continue;
279
+ if (!fs4.existsSync(absPath)) continue;
280
+ if (config.rules.maxFileLines > 0) {
281
+ const lines = countFileLines(absPath);
282
+ if (lines !== null && lines > config.rules.maxFileLines) {
283
+ violations.push({
284
+ file: relPath,
285
+ rule: "file-size",
286
+ message: `${lines} lines (max ${config.rules.maxFileLines}). Split into focused modules.`,
287
+ severity
288
+ });
289
+ }
290
+ }
291
+ if (config.rules.enforceNaming && config.conventions.fileNaming) {
292
+ const namingViolation = checkNaming(relPath, config);
293
+ if (namingViolation) {
294
+ violations.push({
295
+ file: relPath,
296
+ rule: "file-naming",
297
+ message: namingViolation,
298
+ severity
299
+ });
300
+ }
301
+ }
302
+ }
303
+ if (config.rules.requireTests && !options.staged && !options.files) {
304
+ const testViolations = checkMissingTests(projectRoot, config, severity);
305
+ violations.push(...testViolations);
306
+ }
307
+ if (config.rules.enforceBoundaries && config.boundaries && config.boundaries.length > 0 && !options.noBoundaries) {
308
+ const startTime = Date.now();
309
+ const { buildImportGraph, checkBoundaries } = await import("@viberails/graph");
310
+ const packages = config.workspace ? resolveWorkspacePackages(projectRoot, config.workspace) : void 0;
311
+ const graph = await buildImportGraph(projectRoot, {
312
+ packages,
313
+ ignore: config.ignore
314
+ });
315
+ const boundaryViolations = checkBoundaries(graph, config.boundaries);
316
+ const filterSet = options.staged || options.files ? new Set(filesToCheck.map((f) => path4.resolve(projectRoot, f))) : null;
317
+ for (const bv of boundaryViolations) {
318
+ if (filterSet && !filterSet.has(bv.file)) continue;
319
+ const relFile = path4.relative(projectRoot, bv.file);
320
+ violations.push({
321
+ file: relFile,
322
+ rule: "boundary-violation",
323
+ message: `Imports "${bv.specifier}" violating boundary: ${bv.rule.from} \u2192 ${bv.rule.to}${bv.rule.reason ? ` (${bv.rule.reason})` : ""}`,
324
+ severity
325
+ });
326
+ }
327
+ const elapsed = Date.now() - startTime;
328
+ console.log(import_chalk2.default.dim(` Boundary check: ${graph.nodes.length} files in ${elapsed}ms`));
329
+ }
330
+ if (violations.length === 0) {
331
+ console.log(`${import_chalk2.default.green("\u2713")} ${filesToCheck.length} files checked \u2014 no violations`);
332
+ return 0;
333
+ }
334
+ for (const v of violations) {
335
+ const icon = v.severity === "error" ? import_chalk2.default.red("\u2717") : import_chalk2.default.yellow("!");
336
+ console.log(`${icon} ${import_chalk2.default.dim(v.rule)} ${v.file}: ${v.message}`);
337
+ }
338
+ const word = violations.length === 1 ? "violation" : "violations";
339
+ console.log(`
340
+ ${violations.length} ${word} found.`);
341
+ if (config.enforcement === "enforce") {
342
+ console.log(import_chalk2.default.red("Fix violations before committing."));
343
+ return 1;
344
+ }
345
+ return 0;
346
+ }
347
+ function countFileLines(filePath) {
348
+ try {
349
+ const content = fs4.readFileSync(filePath, "utf-8");
350
+ if (content.length === 0) return 0;
351
+ let count = 1;
352
+ for (let i = 0; i < content.length; i++) {
353
+ if (content.charCodeAt(i) === 10) count++;
354
+ }
355
+ return count;
356
+ } catch {
357
+ return null;
358
+ }
359
+ }
360
+ function checkNaming(relPath, config) {
361
+ const filename = path4.basename(relPath);
362
+ const ext = path4.extname(filename);
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
+ });
405
+ }
406
+ }
407
+ return violations;
408
+ }
409
+ function getStagedFiles(projectRoot) {
410
+ try {
411
+ const output = (0, import_node_child_process.execSync)("git diff --cached --name-only --diff-filter=ACM", {
412
+ cwd: projectRoot,
413
+ encoding: "utf-8"
414
+ });
415
+ return output.trim().split("\n").filter(Boolean);
416
+ } catch {
417
+ return [];
418
+ }
419
+ }
420
+ function getAllSourceFiles(projectRoot, config) {
421
+ const files = [];
422
+ const walk = (dir) => {
423
+ let entries;
424
+ try {
425
+ entries = fs4.readdirSync(dir, { withFileTypes: true });
426
+ } catch {
427
+ return;
428
+ }
429
+ for (const entry of entries) {
430
+ const rel = path4.relative(projectRoot, path4.join(dir, entry.name));
431
+ if (entry.isDirectory()) {
432
+ if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist") {
433
+ continue;
434
+ }
435
+ if (isIgnored(rel, config.ignore)) continue;
436
+ walk(path4.join(dir, entry.name));
437
+ } else if (entry.isFile()) {
438
+ const ext = path4.extname(entry.name);
439
+ if (SOURCE_EXTS.has(ext) && !isIgnored(rel, config.ignore)) {
440
+ files.push(rel);
441
+ }
442
+ }
443
+ }
444
+ };
445
+ walk(projectRoot);
446
+ return files;
447
+ }
448
+ function collectSourceFiles(dir, projectRoot) {
449
+ const files = [];
450
+ const walk = (d) => {
451
+ let entries;
452
+ try {
453
+ entries = fs4.readdirSync(d, { withFileTypes: true });
454
+ } catch {
455
+ return;
456
+ }
457
+ for (const entry of entries) {
458
+ if (entry.isDirectory()) {
459
+ if (entry.name === "node_modules") continue;
460
+ walk(path4.join(d, entry.name));
461
+ } else if (entry.isFile()) {
462
+ files.push(path4.relative(projectRoot, path4.join(d, entry.name)));
463
+ }
464
+ }
465
+ };
466
+ walk(dir);
467
+ return files;
468
+ }
469
+ function isIgnored(relPath, ignorePatterns) {
470
+ for (const pattern of ignorePatterns) {
471
+ if (pattern.endsWith("/**")) {
472
+ const prefix = pattern.slice(0, -3);
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;
479
+ }
480
+ }
481
+ return false;
482
+ }
483
+
484
+ // src/commands/init.ts
485
+ var fs6 = __toESM(require("fs"), 1);
486
+ var path6 = __toESM(require("path"), 1);
487
+ var import_config3 = require("@viberails/config");
488
+ var import_scanner = require("@viberails/scanner");
489
+ var import_chalk4 = __toESM(require("chalk"), 1);
490
+
491
+ // src/display.ts
492
+ var import_types = require("@viberails/types");
493
+ var import_chalk3 = __toESM(require("chalk"), 1);
494
+ var CONVENTION_LABELS = {
495
+ fileNaming: "File naming",
496
+ componentNaming: "Component naming",
497
+ hookNaming: "Hook naming",
498
+ importAlias: "Import alias"
499
+ };
500
+ function formatItem(item, nameMap) {
501
+ const name = nameMap?.[item.name] ?? item.name;
502
+ return item.version ? `${name} ${item.version}` : name;
503
+ }
504
+ function confidenceLabel(convention) {
505
+ const pct = Math.round(convention.consistency);
506
+ if (convention.confidence === "high") {
507
+ return `${pct}% \u2014 high confidence, will enforce`;
508
+ }
509
+ return `${pct}% \u2014 medium confidence, suggested only`;
510
+ }
511
+ function displayScanResults(scanResult) {
512
+ const { stack, conventions } = scanResult;
513
+ console.log(`
514
+ ${import_chalk3.default.bold("Detected:")}`);
515
+ if (stack.framework) {
516
+ console.log(` ${import_chalk3.default.green("\u2713")} ${formatItem(stack.framework, import_types.FRAMEWORK_NAMES)}`);
517
+ }
518
+ console.log(` ${import_chalk3.default.green("\u2713")} ${formatItem(stack.language)}`);
519
+ if (stack.styling) {
520
+ console.log(` ${import_chalk3.default.green("\u2713")} ${formatItem(stack.styling, import_types.STYLING_NAMES)}`);
521
+ }
522
+ if (stack.backend) {
523
+ console.log(` ${import_chalk3.default.green("\u2713")} ${formatItem(stack.backend, import_types.FRAMEWORK_NAMES)}`);
524
+ }
525
+ if (stack.linter) {
526
+ console.log(` ${import_chalk3.default.green("\u2713")} ${formatItem(stack.linter)}`);
527
+ }
528
+ if (stack.testRunner) {
529
+ console.log(` ${import_chalk3.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
530
+ }
531
+ if (stack.packageManager) {
532
+ console.log(` ${import_chalk3.default.green("\u2713")} ${formatItem(stack.packageManager)}`);
533
+ }
534
+ if (stack.libraries.length > 0) {
535
+ for (const lib of stack.libraries) {
536
+ console.log(` ${import_chalk3.default.green("\u2713")} ${formatItem(lib, import_types.LIBRARY_NAMES)}`);
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})`);
547
+ }
548
+ }
549
+ const conventionEntries = Object.entries(conventions);
550
+ if (conventionEntries.length > 0) {
551
+ console.log(`
552
+ ${import_chalk3.default.bold("Conventions:")}`);
553
+ for (const [key, convention] of conventionEntries) {
554
+ if (convention.confidence === "low") continue;
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}`);
559
+ }
560
+ }
561
+ console.log("");
562
+ }
563
+
564
+ // src/utils/write-generated-files.ts
565
+ var fs5 = __toESM(require("fs"), 1);
566
+ var path5 = __toESM(require("path"), 1);
567
+ var import_context = require("@viberails/context");
568
+ var CONTEXT_DIR = ".viberails";
569
+ var CONTEXT_FILE = "context.md";
570
+ var SCAN_RESULT_FILE = "scan-result.json";
571
+ function writeGeneratedFiles(projectRoot, config, scanResult) {
572
+ const contextDir = path5.join(projectRoot, CONTEXT_DIR);
573
+ if (!fs5.existsSync(contextDir)) {
574
+ fs5.mkdirSync(contextDir, { recursive: true });
575
+ }
576
+ const context = (0, import_context.generateContext)(config);
577
+ fs5.writeFileSync(path5.join(contextDir, CONTEXT_FILE), context);
578
+ fs5.writeFileSync(
579
+ path5.join(contextDir, SCAN_RESULT_FILE),
580
+ `${JSON.stringify(scanResult, null, 2)}
581
+ `
582
+ );
583
+ }
584
+
585
+ // src/commands/init.ts
586
+ var CONFIG_FILE3 = "viberails.config.json";
587
+ function filterHighConfidence(conventions) {
588
+ const filtered = {};
589
+ for (const [key, value] of Object.entries(conventions)) {
590
+ if (value === void 0) continue;
591
+ if (typeof value === "string") {
592
+ filtered[key] = value;
593
+ } else if (value._confidence === "high") {
594
+ filtered[key] = value;
595
+ }
596
+ }
597
+ return filtered;
598
+ }
599
+ async function initCommand(options, cwd) {
600
+ const startDir = cwd ?? process.cwd();
601
+ const projectRoot = findProjectRoot(startDir);
602
+ if (!projectRoot) {
603
+ throw new Error(
604
+ "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
+ );
606
+ }
607
+ const configPath = path6.join(projectRoot, CONFIG_FILE3);
608
+ if (fs6.existsSync(configPath)) {
609
+ console.log(
610
+ import_chalk4.default.yellow("!") + " viberails is already initialized in this project.\n Run " + import_chalk4.default.cyan("viberails sync") + " to update the generated files."
611
+ );
612
+ return;
613
+ }
614
+ console.log(import_chalk4.default.dim("Scanning project..."));
615
+ const scanResult = await (0, import_scanner.scan)(projectRoot);
616
+ displayScanResults(scanResult);
617
+ if (scanResult.statistics.totalFiles === 0) {
618
+ console.log(
619
+ import_chalk4.default.yellow("!") + " No source files detected. viberails will generate context with minimal content.\n Run " + import_chalk4.default.cyan("viberails sync") + " after adding source files.\n"
620
+ );
621
+ }
622
+ if (!options.yes) {
623
+ const accepted = await confirm("Does this look right?");
624
+ if (!accepted) {
625
+ console.log("Aborted.");
626
+ return;
627
+ }
628
+ }
629
+ const config = (0, import_config3.generateConfig)(scanResult);
630
+ if (options.yes) {
631
+ config.conventions = filterHighConfidence(config.conventions);
632
+ }
633
+ if (config.workspace && config.workspace.packages.length > 0) {
634
+ let shouldInfer = options.yes;
635
+ if (!options.yes) {
636
+ shouldInfer = await confirm("Infer boundary rules from import patterns?");
637
+ }
638
+ if (shouldInfer) {
639
+ console.log(import_chalk4.default.dim("Building import graph..."));
640
+ const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
641
+ const packages = resolveWorkspacePackages(projectRoot, config.workspace);
642
+ const graph = await buildImportGraph(projectRoot, { packages, ignore: config.ignore });
643
+ const inferred = inferBoundaries(graph);
644
+ if (inferred.length > 0) {
645
+ config.boundaries = inferred;
646
+ config.rules.enforceBoundaries = true;
647
+ console.log(` ${import_chalk4.default.green("\u2713")} Inferred ${inferred.length} boundary rules`);
648
+ }
649
+ }
650
+ }
651
+ fs6.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
652
+ `);
653
+ writeGeneratedFiles(projectRoot, config, scanResult);
654
+ updateGitignore(projectRoot);
655
+ setupPreCommitHook(projectRoot);
656
+ console.log(`
657
+ ${import_chalk4.default.bold("Created:")}`);
658
+ console.log(` ${import_chalk4.default.green("\u2713")} ${CONFIG_FILE3}`);
659
+ console.log(` ${import_chalk4.default.green("\u2713")} .viberails/context.md`);
660
+ console.log(` ${import_chalk4.default.green("\u2713")} .viberails/scan-result.json`);
661
+ console.log(`
662
+ ${import_chalk4.default.bold("Next steps:")}`);
663
+ console.log(` 1. Review ${import_chalk4.default.cyan("viberails.config.json")} and adjust rules`);
664
+ console.log(
665
+ ` 2. Commit ${import_chalk4.default.cyan("viberails.config.json")} and ${import_chalk4.default.cyan(".viberails/context.md")}`
666
+ );
667
+ console.log(` 3. Run ${import_chalk4.default.cyan("viberails check")} to verify your project passes`);
668
+ }
669
+ function updateGitignore(projectRoot) {
670
+ const gitignorePath = path6.join(projectRoot, ".gitignore");
671
+ let content = "";
672
+ if (fs6.existsSync(gitignorePath)) {
673
+ content = fs6.readFileSync(gitignorePath, "utf-8");
674
+ }
675
+ if (!content.includes(".viberails/scan-result.json")) {
676
+ const block = "\n# viberails\n.viberails/scan-result.json\n";
677
+ fs6.writeFileSync(gitignorePath, `${content.trimEnd()}
678
+ ${block}`);
679
+ }
680
+ }
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
+
750
+ // src/commands/sync.ts
751
+ var fs7 = __toESM(require("fs"), 1);
752
+ var path7 = __toESM(require("path"), 1);
753
+ var import_config4 = require("@viberails/config");
754
+ var import_scanner2 = require("@viberails/scanner");
755
+ var import_chalk5 = __toESM(require("chalk"), 1);
756
+ var CONFIG_FILE4 = "viberails.config.json";
757
+ async function syncCommand(cwd) {
758
+ const startDir = cwd ?? process.cwd();
759
+ const projectRoot = findProjectRoot(startDir);
760
+ if (!projectRoot) {
761
+ throw new Error(
762
+ "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
+ );
764
+ }
765
+ const configPath = path7.join(projectRoot, CONFIG_FILE4);
766
+ const existing = await (0, import_config4.loadConfig)(configPath);
767
+ console.log(import_chalk5.default.dim("Scanning project..."));
768
+ const scanResult = await (0, import_scanner2.scan)(projectRoot);
769
+ const merged = (0, import_config4.mergeConfig)(existing, scanResult);
770
+ fs7.writeFileSync(configPath, `${JSON.stringify(merged, null, 2)}
771
+ `);
772
+ writeGeneratedFiles(projectRoot, merged, scanResult);
773
+ console.log(`
774
+ ${import_chalk5.default.bold("Synced:")}`);
775
+ console.log(` ${import_chalk5.default.green("\u2713")} ${CONFIG_FILE4} \u2014 updated`);
776
+ console.log(` ${import_chalk5.default.green("\u2713")} .viberails/context.md \u2014 regenerated`);
777
+ console.log(` ${import_chalk5.default.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
778
+ }
779
+
780
+ // src/index.ts
781
+ var VERSION = "0.1.0";
782
+ var program = new import_commander.Command();
783
+ program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
784
+ 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) => {
785
+ try {
786
+ await initCommand(options);
787
+ } catch (err) {
788
+ const message = err instanceof Error ? err.message : String(err);
789
+ console.error(`${import_chalk6.default.red("Error:")} ${message}`);
790
+ process.exit(1);
791
+ }
792
+ });
793
+ program.command("sync").description("Re-scan and update generated files").action(async () => {
794
+ try {
795
+ await syncCommand();
796
+ } catch (err) {
797
+ const message = err instanceof Error ? err.message : String(err);
798
+ console.error(`${import_chalk6.default.red("Error:")} ${message}`);
799
+ process.exit(1);
800
+ }
801
+ });
802
+ program.command("check").description("Check files against enforced rules").option("--staged", "Check only staged files (for pre-commit hooks)").option("--files <files...>", "Check specific files").option("--no-boundaries", "Skip boundary checking").action(async (options) => {
803
+ try {
804
+ const exitCode = await checkCommand({
805
+ ...options,
806
+ noBoundaries: options.boundaries === false
807
+ });
808
+ process.exit(exitCode);
809
+ } catch (err) {
810
+ const message = err instanceof Error ? err.message : String(err);
811
+ console.error(`${import_chalk6.default.red("Error:")} ${message}`);
812
+ process.exit(1);
813
+ }
814
+ });
815
+ program.command("boundaries").description("Display, infer, or inspect import boundary rules").option("--infer", "Infer boundary rules from current import patterns").option("--graph", "Display import graph summary").action(async (options) => {
816
+ try {
817
+ await boundariesCommand(options);
818
+ } catch (err) {
819
+ const message = err instanceof Error ? err.message : String(err);
820
+ console.error(`${import_chalk6.default.red("Error:")} ${message}`);
821
+ process.exit(1);
822
+ }
823
+ });
824
+ program.parse();
825
+ // Annotate the CommonJS export names for ESM import in node:
826
+ 0 && (module.exports = {
827
+ VERSION
828
+ });
829
+ //# sourceMappingURL=index.cjs.map