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