viberails 0.6.9 → 0.6.11
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 +484 -289
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +484 -289
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import
|
|
4
|
+
import chalk20 from "chalk";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/commands/boundaries.ts
|
|
@@ -129,12 +129,12 @@ async function promptNamingMenu(state) {
|
|
|
129
129
|
},
|
|
130
130
|
{
|
|
131
131
|
value: "componentNaming",
|
|
132
|
-
label: `${state.componentNaming ? ok : unset} Component
|
|
132
|
+
label: `${state.componentNaming ? ok : unset} Component exports`,
|
|
133
133
|
hint: state.componentNaming ?? HINT_NOT_SET
|
|
134
134
|
},
|
|
135
135
|
{
|
|
136
136
|
value: "hookNaming",
|
|
137
|
-
label: `${state.hookNaming ? ok : unset} Hook
|
|
137
|
+
label: `${state.hookNaming ? ok : unset} Hook exports`,
|
|
138
138
|
hint: state.hookNaming ?? HINT_NOT_SET
|
|
139
139
|
},
|
|
140
140
|
{
|
|
@@ -171,7 +171,7 @@ async function promptNamingMenu(state) {
|
|
|
171
171
|
}
|
|
172
172
|
if (choice === "componentNaming") {
|
|
173
173
|
const selected = await clack.select({
|
|
174
|
-
message: "Component naming
|
|
174
|
+
message: "Component export naming (e.g. UserProfile)",
|
|
175
175
|
options: [
|
|
176
176
|
...COMPONENT_NAMING_OPTIONS,
|
|
177
177
|
{ value: SENTINEL_CLEAR, label: "Clear (no convention)" }
|
|
@@ -183,7 +183,7 @@ async function promptNamingMenu(state) {
|
|
|
183
183
|
}
|
|
184
184
|
if (choice === "hookNaming") {
|
|
185
185
|
const selected = await clack.select({
|
|
186
|
-
message: "Hook naming
|
|
186
|
+
message: "Hook export naming (e.g. useAuth)",
|
|
187
187
|
options: [
|
|
188
188
|
...HOOK_NAMING_OPTIONS,
|
|
189
189
|
{ value: SENTINEL_CLEAR, label: "Clear (no convention)" }
|
|
@@ -1419,9 +1419,9 @@ async function checkCommand(options, cwd) {
|
|
|
1419
1419
|
}
|
|
1420
1420
|
const violations = [];
|
|
1421
1421
|
const severity = options.enforce ? "error" : "warn";
|
|
1422
|
-
const
|
|
1422
|
+
const log10 = options.format !== "json" && !options.hook && !options.quiet ? (msg) => process.stderr.write(chalk5.dim(msg)) : () => {
|
|
1423
1423
|
};
|
|
1424
|
-
|
|
1424
|
+
log10(" Checking files...");
|
|
1425
1425
|
for (const file of filesToCheck) {
|
|
1426
1426
|
const absPath = path7.isAbsolute(file) ? file : path7.join(projectRoot, file);
|
|
1427
1427
|
const relPath = path7.relative(projectRoot, absPath);
|
|
@@ -1454,9 +1454,9 @@ async function checkCommand(options, cwd) {
|
|
|
1454
1454
|
}
|
|
1455
1455
|
}
|
|
1456
1456
|
}
|
|
1457
|
-
|
|
1457
|
+
log10(" done\n");
|
|
1458
1458
|
if (!options.files) {
|
|
1459
|
-
|
|
1459
|
+
log10(" Checking missing tests...");
|
|
1460
1460
|
const testViolations = checkMissingTests(projectRoot, config, severity);
|
|
1461
1461
|
if (options.staged) {
|
|
1462
1462
|
const stagedSet = new Set(filesToCheck);
|
|
@@ -1469,14 +1469,14 @@ async function checkCommand(options, cwd) {
|
|
|
1469
1469
|
} else {
|
|
1470
1470
|
violations.push(...testViolations);
|
|
1471
1471
|
}
|
|
1472
|
-
|
|
1472
|
+
log10(" done\n");
|
|
1473
1473
|
}
|
|
1474
1474
|
if (!options.files && !options.staged && !options.diffBase) {
|
|
1475
|
-
|
|
1475
|
+
log10(" Running test coverage...\n");
|
|
1476
1476
|
const coverageViolations = checkCoverage(projectRoot, config, filesToCheck, {
|
|
1477
1477
|
staged: options.staged,
|
|
1478
1478
|
enforce: options.enforce,
|
|
1479
|
-
onProgress: (pkg) =>
|
|
1479
|
+
onProgress: (pkg) => log10(` Coverage: ${pkg}...
|
|
1480
1480
|
`)
|
|
1481
1481
|
});
|
|
1482
1482
|
violations.push(...coverageViolations);
|
|
@@ -1501,7 +1501,7 @@ async function checkCommand(options, cwd) {
|
|
|
1501
1501
|
severity
|
|
1502
1502
|
});
|
|
1503
1503
|
}
|
|
1504
|
-
|
|
1504
|
+
log10(` Boundary check: ${graph.nodes.length} files in ${Date.now() - startTime}ms
|
|
1505
1505
|
`);
|
|
1506
1506
|
}
|
|
1507
1507
|
if (options.format === "json") {
|
|
@@ -2795,10 +2795,10 @@ ${chalk10.yellow("!")} No safe fixes to apply. Resolve aliased imports first.`);
|
|
|
2795
2795
|
// src/commands/init.ts
|
|
2796
2796
|
import * as fs21 from "fs";
|
|
2797
2797
|
import * as path21 from "path";
|
|
2798
|
-
import * as
|
|
2798
|
+
import * as clack14 from "@clack/prompts";
|
|
2799
2799
|
import { compactConfig as compactConfig4, generateConfig as generateConfig2 } from "@viberails/config";
|
|
2800
2800
|
import { scan as scan3 } from "@viberails/scanner";
|
|
2801
|
-
import
|
|
2801
|
+
import chalk18 from "chalk";
|
|
2802
2802
|
|
|
2803
2803
|
// src/utils/check-prerequisites.ts
|
|
2804
2804
|
import * as fs14 from "fs";
|
|
@@ -2905,15 +2905,7 @@ async function executeDeferredInstalls(projectRoot, installs) {
|
|
|
2905
2905
|
return successCount;
|
|
2906
2906
|
}
|
|
2907
2907
|
|
|
2908
|
-
// src/utils/prompt-main-menu.ts
|
|
2909
|
-
import * as clack11 from "@clack/prompts";
|
|
2910
|
-
|
|
2911
|
-
// src/utils/prompt-main-menu-handlers.ts
|
|
2912
|
-
import * as clack10 from "@clack/prompts";
|
|
2913
|
-
|
|
2914
2908
|
// src/utils/prompt-integrations.ts
|
|
2915
|
-
import * as fs15 from "fs";
|
|
2916
|
-
import * as path15 from "path";
|
|
2917
2909
|
import * as clack9 from "@clack/prompts";
|
|
2918
2910
|
function buildLefthookInstallCommand(pm, isWorkspace) {
|
|
2919
2911
|
if (pm === "yarn") return "yarn add -D lefthook";
|
|
@@ -2921,26 +2913,24 @@ function buildLefthookInstallCommand(pm, isWorkspace) {
|
|
|
2921
2913
|
if (pm === "npm") return "npm install -D lefthook";
|
|
2922
2914
|
return `${pm} add -D lefthook`;
|
|
2923
2915
|
}
|
|
2924
|
-
async function promptIntegrationsDeferred(hookManager, tools
|
|
2916
|
+
async function promptIntegrationsDeferred(hookManager, tools) {
|
|
2917
|
+
const hasHookManager = !!hookManager;
|
|
2925
2918
|
const options = [];
|
|
2926
|
-
const
|
|
2927
|
-
|
|
2928
|
-
const pm = packageManager ?? "npm";
|
|
2929
|
-
options.push({
|
|
2930
|
-
value: "installLefthook",
|
|
2931
|
-
label: "Install Lefthook",
|
|
2932
|
-
hint: `after final confirmation \u2014 ${buildLefthookInstallCommand(pm, isWorkspace)}`
|
|
2933
|
-
});
|
|
2934
|
-
}
|
|
2935
|
-
const hookLabel = hookManager ? `Pre-commit hook (${hookManager})` : "Pre-commit hook";
|
|
2936
|
-
const hookHint = needsLefthook ? "uses Lefthook if installed above, otherwise local git hook" : "runs viberails checks when you commit";
|
|
2919
|
+
const hookLabel = hasHookManager ? `Pre-commit hook (${hookManager})` : "Pre-commit hook";
|
|
2920
|
+
const hookHint = hasHookManager ? "runs viberails checks when you commit" : "local hook only \u2014 use lefthook or husky to commit hooks to repo";
|
|
2937
2921
|
options.push({ value: "preCommit", label: hookLabel, hint: hookHint });
|
|
2938
|
-
if (tools?.
|
|
2922
|
+
if (tools?.typecheckLabel) {
|
|
2939
2923
|
options.push({
|
|
2940
2924
|
value: "typecheck",
|
|
2941
|
-
label:
|
|
2925
|
+
label: `Typecheck (${tools.typecheckLabel})`,
|
|
2942
2926
|
hint: "pre-commit hook + CI check"
|
|
2943
2927
|
});
|
|
2928
|
+
} else if (tools?.isTypeScript) {
|
|
2929
|
+
options.push({
|
|
2930
|
+
value: "typecheck",
|
|
2931
|
+
label: "Typecheck",
|
|
2932
|
+
hint: "needs root tsconfig.json, typecheck script, or turbo task"
|
|
2933
|
+
});
|
|
2944
2934
|
}
|
|
2945
2935
|
if (tools?.linter) {
|
|
2946
2936
|
const linterName = tools.linter === "biome" ? "Biome" : "ESLint";
|
|
@@ -2967,7 +2957,12 @@ async function promptIntegrationsDeferred(hookManager, tools, packageManager, is
|
|
|
2967
2957
|
hint: "blocks PRs that fail viberails check"
|
|
2968
2958
|
}
|
|
2969
2959
|
);
|
|
2970
|
-
const
|
|
2960
|
+
const hasTypecheck = !!tools?.typecheckLabel;
|
|
2961
|
+
const initialValues = options.filter((o) => {
|
|
2962
|
+
if (o.value === "preCommit" && !hasHookManager) return false;
|
|
2963
|
+
if (o.value === "typecheck" && !hasTypecheck) return false;
|
|
2964
|
+
return true;
|
|
2965
|
+
}).map((o) => o.value);
|
|
2971
2966
|
const result = await clack9.multiselect({
|
|
2972
2967
|
message: "Integrations",
|
|
2973
2968
|
options,
|
|
@@ -2975,20 +2970,6 @@ async function promptIntegrationsDeferred(hookManager, tools, packageManager, is
|
|
|
2975
2970
|
required: false
|
|
2976
2971
|
});
|
|
2977
2972
|
assertNotCancelled(result);
|
|
2978
|
-
let lefthookInstall;
|
|
2979
|
-
if (needsLefthook && result.includes("installLefthook")) {
|
|
2980
|
-
const pm = packageManager ?? "npm";
|
|
2981
|
-
lefthookInstall = {
|
|
2982
|
-
label: "Lefthook",
|
|
2983
|
-
command: buildLefthookInstallCommand(pm, isWorkspace),
|
|
2984
|
-
onSuccess: projectRoot ? () => {
|
|
2985
|
-
const ymlPath = path15.join(projectRoot, "lefthook.yml");
|
|
2986
|
-
if (!fs15.existsSync(ymlPath)) {
|
|
2987
|
-
fs15.writeFileSync(ymlPath, "# Generated by viberails\n");
|
|
2988
|
-
}
|
|
2989
|
-
} : void 0
|
|
2990
|
-
};
|
|
2991
|
-
}
|
|
2992
2973
|
return {
|
|
2993
2974
|
choice: {
|
|
2994
2975
|
preCommitHook: result.includes("preCommit"),
|
|
@@ -2997,39 +2978,16 @@ async function promptIntegrationsDeferred(hookManager, tools, packageManager, is
|
|
|
2997
2978
|
githubAction: result.includes("githubAction"),
|
|
2998
2979
|
typecheckHook: result.includes("typecheck"),
|
|
2999
2980
|
lintHook: result.includes("lint")
|
|
3000
|
-
}
|
|
3001
|
-
lefthookInstall
|
|
2981
|
+
}
|
|
3002
2982
|
};
|
|
3003
2983
|
}
|
|
3004
2984
|
|
|
2985
|
+
// src/utils/prompt-main-menu.ts
|
|
2986
|
+
import * as clack11 from "@clack/prompts";
|
|
2987
|
+
|
|
3005
2988
|
// src/utils/prompt-main-menu-handlers.ts
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
const state = {
|
|
3009
|
-
maxFileLines: config.rules.maxFileLines,
|
|
3010
|
-
maxTestFileLines: config.rules.maxTestFileLines,
|
|
3011
|
-
testCoverage: config.rules.testCoverage,
|
|
3012
|
-
enforceMissingTests: config.rules.enforceMissingTests,
|
|
3013
|
-
enforceNaming: config.rules.enforceNaming,
|
|
3014
|
-
fileNamingValue: rootPkg.conventions?.fileNaming,
|
|
3015
|
-
componentNaming: rootPkg.conventions?.componentNaming,
|
|
3016
|
-
hookNaming: rootPkg.conventions?.hookNaming,
|
|
3017
|
-
importAlias: rootPkg.conventions?.importAlias,
|
|
3018
|
-
coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
|
|
3019
|
-
coverageCommand: config.defaults?.coverage?.command
|
|
3020
|
-
};
|
|
3021
|
-
await promptNamingMenu(state);
|
|
3022
|
-
rootPkg.conventions = rootPkg.conventions ?? {};
|
|
3023
|
-
config.rules.enforceNaming = state.enforceNaming;
|
|
3024
|
-
if (state.fileNamingValue) {
|
|
3025
|
-
rootPkg.conventions.fileNaming = state.fileNamingValue;
|
|
3026
|
-
} else {
|
|
3027
|
-
delete rootPkg.conventions.fileNaming;
|
|
3028
|
-
}
|
|
3029
|
-
rootPkg.conventions.componentNaming = state.componentNaming || void 0;
|
|
3030
|
-
rootPkg.conventions.hookNaming = state.hookNaming || void 0;
|
|
3031
|
-
rootPkg.conventions.importAlias = state.importAlias || void 0;
|
|
3032
|
-
}
|
|
2989
|
+
import * as clack10 from "@clack/prompts";
|
|
2990
|
+
import chalk12 from "chalk";
|
|
3033
2991
|
async function handleFileNaming(config, scanResult) {
|
|
3034
2992
|
const isMonorepo = config.packages.length > 1;
|
|
3035
2993
|
if (isMonorepo) {
|
|
@@ -3169,24 +3127,95 @@ async function handleBoundaries(config, state, opts) {
|
|
|
3169
3127
|
clack10.log.warn(`Boundary inference failed: ${err instanceof Error ? err.message : err}`);
|
|
3170
3128
|
}
|
|
3171
3129
|
}
|
|
3172
|
-
async function
|
|
3173
|
-
const
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3130
|
+
async function handleAiContext(config) {
|
|
3131
|
+
const rootPkg = getRootPackage(config.packages);
|
|
3132
|
+
rootPkg.conventions = rootPkg.conventions ?? {};
|
|
3133
|
+
while (true) {
|
|
3134
|
+
const ok = chalk12.green("\u2713");
|
|
3135
|
+
const unset = chalk12.dim("-");
|
|
3136
|
+
const options = [
|
|
3137
|
+
{
|
|
3138
|
+
value: "componentNaming",
|
|
3139
|
+
label: `${rootPkg.conventions.componentNaming ? ok : unset} Component exports`,
|
|
3140
|
+
hint: rootPkg.conventions.componentNaming ?? "not set"
|
|
3141
|
+
},
|
|
3142
|
+
{
|
|
3143
|
+
value: "hookNaming",
|
|
3144
|
+
label: `${rootPkg.conventions.hookNaming ? ok : unset} Hook exports`,
|
|
3145
|
+
hint: rootPkg.conventions.hookNaming ?? "not set"
|
|
3146
|
+
},
|
|
3147
|
+
{
|
|
3148
|
+
value: "importAlias",
|
|
3149
|
+
label: `${rootPkg.conventions.importAlias ? ok : unset} Import alias`,
|
|
3150
|
+
hint: rootPkg.conventions.importAlias ?? "not set"
|
|
3151
|
+
},
|
|
3152
|
+
{ value: "back", label: " Back" }
|
|
3153
|
+
];
|
|
3154
|
+
const choice = await clack10.select({
|
|
3155
|
+
message: "AI context \u2014 conventions written to context.md for AI tools",
|
|
3156
|
+
options
|
|
3157
|
+
});
|
|
3158
|
+
if (isCancelled(choice) || choice === "back") return;
|
|
3159
|
+
if (choice === "componentNaming") {
|
|
3160
|
+
const selected = await clack10.select({
|
|
3161
|
+
message: "Component export naming (e.g. UserProfile)",
|
|
3162
|
+
options: [
|
|
3163
|
+
...COMPONENT_NAMING_OPTIONS,
|
|
3164
|
+
{ value: SENTINEL_CLEAR, label: "Clear (no convention)" }
|
|
3165
|
+
],
|
|
3166
|
+
initialValue: rootPkg.conventions.componentNaming ?? SENTINEL_CLEAR
|
|
3167
|
+
});
|
|
3168
|
+
if (isCancelled(selected)) continue;
|
|
3169
|
+
rootPkg.conventions.componentNaming = selected === SENTINEL_CLEAR ? void 0 : selected;
|
|
3170
|
+
}
|
|
3171
|
+
if (choice === "hookNaming") {
|
|
3172
|
+
const selected = await clack10.select({
|
|
3173
|
+
message: "Hook export naming (e.g. useAuth)",
|
|
3174
|
+
options: [
|
|
3175
|
+
...HOOK_NAMING_OPTIONS,
|
|
3176
|
+
{ value: SENTINEL_CLEAR, label: "Clear (no convention)" }
|
|
3177
|
+
],
|
|
3178
|
+
initialValue: rootPkg.conventions.hookNaming ?? SENTINEL_CLEAR
|
|
3179
|
+
});
|
|
3180
|
+
if (isCancelled(selected)) continue;
|
|
3181
|
+
rootPkg.conventions.hookNaming = selected === SENTINEL_CLEAR ? void 0 : selected;
|
|
3182
|
+
}
|
|
3183
|
+
if (choice === "importAlias") {
|
|
3184
|
+
const selected = await clack10.select({
|
|
3185
|
+
message: "Import alias pattern",
|
|
3186
|
+
options: [
|
|
3187
|
+
{ value: "@/*", label: "@/*", hint: "import { x } from '@/utils'" },
|
|
3188
|
+
{ value: "~/*", label: "~/*", hint: "import { x } from '~/utils'" },
|
|
3189
|
+
{ value: SENTINEL_CUSTOM, label: "Custom..." },
|
|
3190
|
+
{ value: SENTINEL_CLEAR, label: "Clear (no alias)" }
|
|
3191
|
+
],
|
|
3192
|
+
initialValue: rootPkg.conventions.importAlias ?? SENTINEL_CLEAR
|
|
3193
|
+
});
|
|
3194
|
+
if (isCancelled(selected)) continue;
|
|
3195
|
+
if (selected === SENTINEL_CLEAR) {
|
|
3196
|
+
rootPkg.conventions.importAlias = void 0;
|
|
3197
|
+
} else if (selected === SENTINEL_CUSTOM) {
|
|
3198
|
+
const result = await clack10.text({
|
|
3199
|
+
message: "Custom import alias (e.g. #/*)?",
|
|
3200
|
+
initialValue: rootPkg.conventions.importAlias ?? "",
|
|
3201
|
+
placeholder: "e.g. #/*",
|
|
3202
|
+
validate: (v) => {
|
|
3203
|
+
if (typeof v !== "string" || !v.trim()) return "Alias cannot be empty";
|
|
3204
|
+
if (!/^[a-zA-Z@~#$][a-zA-Z0-9@~#$_-]*\/\*$/.test(v.trim()))
|
|
3205
|
+
return "Must match pattern like @/*, ~/*, or #src/*";
|
|
3206
|
+
}
|
|
3207
|
+
});
|
|
3208
|
+
if (isCancelled(result)) continue;
|
|
3209
|
+
rootPkg.conventions.importAlias = result.trim();
|
|
3210
|
+
} else {
|
|
3211
|
+
rootPkg.conventions.importAlias = selected;
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3185
3214
|
}
|
|
3186
3215
|
}
|
|
3187
3216
|
|
|
3188
3217
|
// src/utils/prompt-main-menu-hints.ts
|
|
3189
|
-
import
|
|
3218
|
+
import chalk13 from "chalk";
|
|
3190
3219
|
function fileLimitsHint(config) {
|
|
3191
3220
|
const max = config.rules.maxFileLines;
|
|
3192
3221
|
const test = config.rules.maxTestFileLines;
|
|
@@ -3202,7 +3231,7 @@ function fileNamingHint(config, scanResult) {
|
|
|
3202
3231
|
);
|
|
3203
3232
|
return detected ? `${naming} (detected)` : naming;
|
|
3204
3233
|
}
|
|
3205
|
-
return "
|
|
3234
|
+
return "not set \u2014 select to configure";
|
|
3206
3235
|
}
|
|
3207
3236
|
function fileNamingStatus(config) {
|
|
3208
3237
|
if (!config.rules.enforceNaming) return "unconfigured";
|
|
@@ -3229,30 +3258,27 @@ function coverageHint(config, hasTestRunner) {
|
|
|
3229
3258
|
}
|
|
3230
3259
|
return `${config.rules.testCoverage}%`;
|
|
3231
3260
|
}
|
|
3232
|
-
function
|
|
3261
|
+
function aiContextHint(config) {
|
|
3233
3262
|
const rootPkg = getRootPackage(config.packages);
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
if (
|
|
3251
|
-
if (
|
|
3252
|
-
|
|
3253
|
-
if (state.integrations.claudeMdRef) items.push(chalk12.green("CLAUDE.md"));
|
|
3254
|
-
if (state.integrations.githubAction) items.push(chalk12.green("CI"));
|
|
3255
|
-
return items.length > 0 ? items.join(chalk12.dim(" \xB7 ")) : "none selected";
|
|
3263
|
+
const count = [
|
|
3264
|
+
rootPkg.conventions?.componentNaming,
|
|
3265
|
+
rootPkg.conventions?.hookNaming,
|
|
3266
|
+
rootPkg.conventions?.importAlias
|
|
3267
|
+
].filter(Boolean).length;
|
|
3268
|
+
if (count === 3) return "all set";
|
|
3269
|
+
if (count > 0) return `${count} of 3 conventions`;
|
|
3270
|
+
return "none set \u2014 optional AI guidelines";
|
|
3271
|
+
}
|
|
3272
|
+
function aiContextStatus(config) {
|
|
3273
|
+
const rootPkg = getRootPackage(config.packages);
|
|
3274
|
+
const count = [
|
|
3275
|
+
rootPkg.conventions?.componentNaming,
|
|
3276
|
+
rootPkg.conventions?.hookNaming,
|
|
3277
|
+
rootPkg.conventions?.importAlias
|
|
3278
|
+
].filter(Boolean).length;
|
|
3279
|
+
if (count === 3) return "ok";
|
|
3280
|
+
if (count > 0) return "partial";
|
|
3281
|
+
return "unconfigured";
|
|
3256
3282
|
}
|
|
3257
3283
|
function packageOverridesHint(config) {
|
|
3258
3284
|
const rootNaming = getRootPackage(config.packages).conventions?.fileNaming;
|
|
@@ -3270,17 +3296,6 @@ function boundariesHint(config, state) {
|
|
|
3270
3296
|
const pkgCount = Object.keys(deny).length;
|
|
3271
3297
|
return `${ruleCount} rules across ${pkgCount} packages`;
|
|
3272
3298
|
}
|
|
3273
|
-
function advancedNamingStatus(config) {
|
|
3274
|
-
if (!config.rules.enforceNaming) return "unconfigured";
|
|
3275
|
-
const rootPkg = getRootPackage(config.packages);
|
|
3276
|
-
const hasFile = !!rootPkg.conventions?.fileNaming;
|
|
3277
|
-
const hasComp = !!rootPkg.conventions?.componentNaming;
|
|
3278
|
-
const hasHook = !!rootPkg.conventions?.hookNaming;
|
|
3279
|
-
const hasAlias = !!rootPkg.conventions?.importAlias;
|
|
3280
|
-
if (hasFile && hasComp && hasHook && hasAlias) return "ok";
|
|
3281
|
-
if (hasFile || hasComp || hasHook || hasAlias) return "partial";
|
|
3282
|
-
return "unconfigured";
|
|
3283
|
-
}
|
|
3284
3299
|
function packageOverridesStatus(config) {
|
|
3285
3300
|
const rootNaming = getRootPackage(config.packages).conventions?.fileNaming;
|
|
3286
3301
|
const editable = config.packages.filter((p) => p.path !== ".");
|
|
@@ -3290,10 +3305,10 @@ function packageOverridesStatus(config) {
|
|
|
3290
3305
|
return customized ? "ok" : "unconfigured";
|
|
3291
3306
|
}
|
|
3292
3307
|
function statusIcon(status) {
|
|
3293
|
-
if (status === "ok") return
|
|
3294
|
-
if (status === "needs-input") return
|
|
3295
|
-
if (status === "unconfigured") return
|
|
3296
|
-
return
|
|
3308
|
+
if (status === "ok") return chalk13.green("\u2713");
|
|
3309
|
+
if (status === "needs-input") return chalk13.yellow("?");
|
|
3310
|
+
if (status === "unconfigured") return chalk13.dim("-");
|
|
3311
|
+
return chalk13.yellow("~");
|
|
3297
3312
|
}
|
|
3298
3313
|
function buildMainMenuOptions(config, scanResult, state) {
|
|
3299
3314
|
const namingStatus = fileNamingStatus(config);
|
|
@@ -3307,7 +3322,7 @@ function buildMainMenuOptions(config, scanResult, state) {
|
|
|
3307
3322
|
},
|
|
3308
3323
|
{
|
|
3309
3324
|
value: "fileNaming",
|
|
3310
|
-
label: `${statusIcon(namingStatus)}
|
|
3325
|
+
label: `${statusIcon(namingStatus)} File naming`,
|
|
3311
3326
|
hint: fileNamingHint(config, scanResult)
|
|
3312
3327
|
},
|
|
3313
3328
|
{
|
|
@@ -3321,9 +3336,9 @@ function buildMainMenuOptions(config, scanResult, state) {
|
|
|
3321
3336
|
hint: coverageHint(config, state.hasTestRunner)
|
|
3322
3337
|
},
|
|
3323
3338
|
{
|
|
3324
|
-
value: "
|
|
3325
|
-
label: `${statusIcon(
|
|
3326
|
-
hint:
|
|
3339
|
+
value: "aiContext",
|
|
3340
|
+
label: `${statusIcon(aiContextStatus(config))} AI context`,
|
|
3341
|
+
hint: aiContextHint(config)
|
|
3327
3342
|
}
|
|
3328
3343
|
];
|
|
3329
3344
|
if (config.packages.length > 1) {
|
|
@@ -3340,9 +3355,7 @@ function buildMainMenuOptions(config, scanResult, state) {
|
|
|
3340
3355
|
{ value: "boundaries", label: `${bIcon} Boundaries`, hint: boundariesHint(config, state) }
|
|
3341
3356
|
);
|
|
3342
3357
|
}
|
|
3343
|
-
const iIcon = state.visited.integrations ? statusIcon("ok") : statusIcon("unconfigured");
|
|
3344
3358
|
options.push(
|
|
3345
|
-
{ value: "integrations", label: `${iIcon} Integrations`, hint: integrationsHint(state) },
|
|
3346
3359
|
{ value: "reset", label: " Reset all to defaults" },
|
|
3347
3360
|
{ value: "review", label: " Review scan details", hint: "detected stack & conventions" },
|
|
3348
3361
|
{ value: "done", label: " Done \u2014 write config" }
|
|
@@ -3354,7 +3367,7 @@ function buildMainMenuOptions(config, scanResult, state) {
|
|
|
3354
3367
|
async function promptMainMenu(config, scanResult, opts) {
|
|
3355
3368
|
const originalConfig = structuredClone(config);
|
|
3356
3369
|
const state = {
|
|
3357
|
-
visited: {
|
|
3370
|
+
visited: { boundaries: false },
|
|
3358
3371
|
deferredInstalls: [],
|
|
3359
3372
|
hasTestRunner: opts.hasTestRunner,
|
|
3360
3373
|
hookManager: opts.hookManager
|
|
@@ -3381,10 +3394,9 @@ async function promptMainMenu(config, scanResult, opts) {
|
|
|
3381
3394
|
if (choice === "fileNaming") await handleFileNaming(config, scanResult);
|
|
3382
3395
|
if (choice === "missingTests") await handleMissingTests(config);
|
|
3383
3396
|
if (choice === "coverage") await handleCoverage(config, state, opts);
|
|
3384
|
-
if (choice === "
|
|
3397
|
+
if (choice === "aiContext") await handleAiContext(config);
|
|
3385
3398
|
if (choice === "packageOverrides") await handlePackageOverrides(config);
|
|
3386
3399
|
if (choice === "boundaries") await handleBoundaries(config, state, opts);
|
|
3387
|
-
if (choice === "integrations") await handleIntegrations(state, opts);
|
|
3388
3400
|
if (choice === "review") clack11.note(formatScanResultsText(scanResult), "Scan details");
|
|
3389
3401
|
if (choice === "reset") {
|
|
3390
3402
|
const confirmed = await clack11.confirm({
|
|
@@ -3395,8 +3407,7 @@ async function promptMainMenu(config, scanResult, opts) {
|
|
|
3395
3407
|
if (confirmed) {
|
|
3396
3408
|
Object.assign(config, structuredClone(originalConfig));
|
|
3397
3409
|
state.deferredInstalls = [];
|
|
3398
|
-
state.visited = {
|
|
3399
|
-
state.integrations = void 0;
|
|
3410
|
+
state.visited = { boundaries: false };
|
|
3400
3411
|
clack11.log.info("Reset all settings to scan-detected defaults.");
|
|
3401
3412
|
}
|
|
3402
3413
|
}
|
|
@@ -3404,37 +3415,26 @@ async function promptMainMenu(config, scanResult, opts) {
|
|
|
3404
3415
|
return state;
|
|
3405
3416
|
}
|
|
3406
3417
|
|
|
3407
|
-
// src/utils/
|
|
3408
|
-
import * as
|
|
3409
|
-
import * as
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
let content = "";
|
|
3413
|
-
if (fs16.existsSync(gitignorePath)) {
|
|
3414
|
-
content = fs16.readFileSync(gitignorePath, "utf-8");
|
|
3415
|
-
}
|
|
3416
|
-
if (!content.includes(".viberails/scan-result.json")) {
|
|
3417
|
-
const block = "\n# viberails\n.viberails/scan-result.json\n";
|
|
3418
|
-
const prefix = content.length === 0 ? "" : `${content.trimEnd()}
|
|
3419
|
-
`;
|
|
3420
|
-
fs16.writeFileSync(gitignorePath, `${prefix}${block}`);
|
|
3421
|
-
}
|
|
3422
|
-
}
|
|
3418
|
+
// src/utils/prompt-prereqs.ts
|
|
3419
|
+
import * as fs17 from "fs";
|
|
3420
|
+
import * as path17 from "path";
|
|
3421
|
+
import * as clack12 from "@clack/prompts";
|
|
3422
|
+
import chalk15 from "chalk";
|
|
3423
3423
|
|
|
3424
3424
|
// src/commands/init-hooks.ts
|
|
3425
|
-
import * as
|
|
3426
|
-
import * as
|
|
3427
|
-
import
|
|
3425
|
+
import * as fs16 from "fs";
|
|
3426
|
+
import * as path16 from "path";
|
|
3427
|
+
import chalk14 from "chalk";
|
|
3428
3428
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
3429
3429
|
|
|
3430
3430
|
// src/commands/resolve-typecheck.ts
|
|
3431
|
-
import * as
|
|
3432
|
-
import * as
|
|
3431
|
+
import * as fs15 from "fs";
|
|
3432
|
+
import * as path15 from "path";
|
|
3433
3433
|
function hasTurboTask(projectRoot, taskName) {
|
|
3434
|
-
const turboPath =
|
|
3435
|
-
if (!
|
|
3434
|
+
const turboPath = path15.join(projectRoot, "turbo.json");
|
|
3435
|
+
if (!fs15.existsSync(turboPath)) return false;
|
|
3436
3436
|
try {
|
|
3437
|
-
const turbo = JSON.parse(
|
|
3437
|
+
const turbo = JSON.parse(fs15.readFileSync(turboPath, "utf-8"));
|
|
3438
3438
|
const tasks = turbo.tasks ?? turbo.pipeline ?? {};
|
|
3439
3439
|
return taskName in tasks;
|
|
3440
3440
|
} catch {
|
|
@@ -3445,10 +3445,10 @@ function resolveTypecheckCommand(projectRoot, packageManager) {
|
|
|
3445
3445
|
if (hasTurboTask(projectRoot, "typecheck")) {
|
|
3446
3446
|
return { command: "npx turbo typecheck", label: "turbo typecheck" };
|
|
3447
3447
|
}
|
|
3448
|
-
const pkgJsonPath =
|
|
3449
|
-
if (
|
|
3448
|
+
const pkgJsonPath = path15.join(projectRoot, "package.json");
|
|
3449
|
+
if (fs15.existsSync(pkgJsonPath)) {
|
|
3450
3450
|
try {
|
|
3451
|
-
const pkg = JSON.parse(
|
|
3451
|
+
const pkg = JSON.parse(fs15.readFileSync(pkgJsonPath, "utf-8"));
|
|
3452
3452
|
if (pkg.scripts?.typecheck) {
|
|
3453
3453
|
const pm = packageManager ?? "npm";
|
|
3454
3454
|
return { command: `${pm} run typecheck`, label: `${pm} run typecheck` };
|
|
@@ -3456,7 +3456,7 @@ function resolveTypecheckCommand(projectRoot, packageManager) {
|
|
|
3456
3456
|
} catch {
|
|
3457
3457
|
}
|
|
3458
3458
|
}
|
|
3459
|
-
if (
|
|
3459
|
+
if (fs15.existsSync(path15.join(projectRoot, "tsconfig.json"))) {
|
|
3460
3460
|
return { command: "npx tsc --noEmit", label: "tsc --noEmit" };
|
|
3461
3461
|
}
|
|
3462
3462
|
return {
|
|
@@ -3466,36 +3466,36 @@ function resolveTypecheckCommand(projectRoot, packageManager) {
|
|
|
3466
3466
|
|
|
3467
3467
|
// src/commands/init-hooks.ts
|
|
3468
3468
|
function setupPreCommitHook(projectRoot) {
|
|
3469
|
-
const lefthookPath =
|
|
3470
|
-
if (
|
|
3469
|
+
const lefthookPath = path16.join(projectRoot, "lefthook.yml");
|
|
3470
|
+
if (fs16.existsSync(lefthookPath)) {
|
|
3471
3471
|
addLefthookPreCommit(lefthookPath);
|
|
3472
|
-
console.log(` ${
|
|
3472
|
+
console.log(` ${chalk14.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
|
|
3473
3473
|
return "lefthook.yml";
|
|
3474
3474
|
}
|
|
3475
|
-
const huskyDir =
|
|
3476
|
-
if (
|
|
3475
|
+
const huskyDir = path16.join(projectRoot, ".husky");
|
|
3476
|
+
if (fs16.existsSync(huskyDir)) {
|
|
3477
3477
|
writeHuskyPreCommit(huskyDir);
|
|
3478
|
-
console.log(` ${
|
|
3478
|
+
console.log(` ${chalk14.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
|
|
3479
3479
|
return ".husky/pre-commit";
|
|
3480
3480
|
}
|
|
3481
|
-
const gitDir =
|
|
3482
|
-
if (
|
|
3483
|
-
const hooksDir =
|
|
3484
|
-
if (!
|
|
3485
|
-
|
|
3481
|
+
const gitDir = path16.join(projectRoot, ".git");
|
|
3482
|
+
if (fs16.existsSync(gitDir)) {
|
|
3483
|
+
const hooksDir = path16.join(gitDir, "hooks");
|
|
3484
|
+
if (!fs16.existsSync(hooksDir)) {
|
|
3485
|
+
fs16.mkdirSync(hooksDir, { recursive: true });
|
|
3486
3486
|
}
|
|
3487
3487
|
writeGitHookPreCommit(hooksDir);
|
|
3488
|
-
console.log(` ${
|
|
3488
|
+
console.log(` ${chalk14.green("\u2713")} .git/hooks/pre-commit`);
|
|
3489
3489
|
return ".git/hooks/pre-commit";
|
|
3490
3490
|
}
|
|
3491
3491
|
return void 0;
|
|
3492
3492
|
}
|
|
3493
3493
|
function writeGitHookPreCommit(hooksDir) {
|
|
3494
|
-
const hookPath =
|
|
3495
|
-
if (
|
|
3496
|
-
const existing =
|
|
3494
|
+
const hookPath = path16.join(hooksDir, "pre-commit");
|
|
3495
|
+
if (fs16.existsSync(hookPath)) {
|
|
3496
|
+
const existing = fs16.readFileSync(hookPath, "utf-8");
|
|
3497
3497
|
if (existing.includes("viberails")) return;
|
|
3498
|
-
|
|
3498
|
+
fs16.writeFileSync(
|
|
3499
3499
|
hookPath,
|
|
3500
3500
|
`${existing.trimEnd()}
|
|
3501
3501
|
|
|
@@ -3512,10 +3512,10 @@ if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails chec
|
|
|
3512
3512
|
"if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --staged; else npx viberails check --staged; fi",
|
|
3513
3513
|
""
|
|
3514
3514
|
].join("\n");
|
|
3515
|
-
|
|
3515
|
+
fs16.writeFileSync(hookPath, script, { mode: 493 });
|
|
3516
3516
|
}
|
|
3517
3517
|
function addLefthookPreCommit(lefthookPath) {
|
|
3518
|
-
const content =
|
|
3518
|
+
const content = fs16.readFileSync(lefthookPath, "utf-8");
|
|
3519
3519
|
if (content.includes("viberails")) return;
|
|
3520
3520
|
const doc = parseYaml(content) ?? {};
|
|
3521
3521
|
if (!doc["pre-commit"]) {
|
|
@@ -3527,28 +3527,28 @@ function addLefthookPreCommit(lefthookPath) {
|
|
|
3527
3527
|
doc["pre-commit"].commands.viberails = {
|
|
3528
3528
|
run: "if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --staged; else npx viberails check --staged; fi"
|
|
3529
3529
|
};
|
|
3530
|
-
|
|
3530
|
+
fs16.writeFileSync(lefthookPath, stringifyYaml(doc));
|
|
3531
3531
|
}
|
|
3532
3532
|
function detectHookManager(projectRoot) {
|
|
3533
|
-
if (
|
|
3534
|
-
if (
|
|
3533
|
+
if (fs16.existsSync(path16.join(projectRoot, "lefthook.yml"))) return "Lefthook";
|
|
3534
|
+
if (fs16.existsSync(path16.join(projectRoot, ".husky"))) return "Husky";
|
|
3535
3535
|
return void 0;
|
|
3536
3536
|
}
|
|
3537
3537
|
function setupClaudeCodeHook(projectRoot) {
|
|
3538
|
-
const claudeDir =
|
|
3539
|
-
if (!
|
|
3540
|
-
|
|
3538
|
+
const claudeDir = path16.join(projectRoot, ".claude");
|
|
3539
|
+
if (!fs16.existsSync(claudeDir)) {
|
|
3540
|
+
fs16.mkdirSync(claudeDir, { recursive: true });
|
|
3541
3541
|
}
|
|
3542
|
-
const settingsPath =
|
|
3542
|
+
const settingsPath = path16.join(claudeDir, "settings.json");
|
|
3543
3543
|
let settings = {};
|
|
3544
|
-
if (
|
|
3544
|
+
if (fs16.existsSync(settingsPath)) {
|
|
3545
3545
|
try {
|
|
3546
|
-
settings = JSON.parse(
|
|
3546
|
+
settings = JSON.parse(fs16.readFileSync(settingsPath, "utf-8"));
|
|
3547
3547
|
} catch {
|
|
3548
3548
|
console.warn(
|
|
3549
|
-
` ${
|
|
3549
|
+
` ${chalk14.yellow("!")} .claude/settings.json contains invalid JSON \u2014 skipping hook setup`
|
|
3550
3550
|
);
|
|
3551
|
-
console.warn(` Fix the JSON manually, then re-run ${
|
|
3551
|
+
console.warn(` Fix the JSON manually, then re-run ${chalk14.cyan("viberails init --force")}`);
|
|
3552
3552
|
return;
|
|
3553
3553
|
}
|
|
3554
3554
|
}
|
|
@@ -3569,30 +3569,30 @@ function setupClaudeCodeHook(projectRoot) {
|
|
|
3569
3569
|
}
|
|
3570
3570
|
];
|
|
3571
3571
|
settings.hooks = hooks;
|
|
3572
|
-
|
|
3572
|
+
fs16.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
3573
3573
|
`);
|
|
3574
|
-
console.log(` ${
|
|
3574
|
+
console.log(` ${chalk14.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
|
|
3575
3575
|
}
|
|
3576
3576
|
function setupClaudeMdReference(projectRoot) {
|
|
3577
|
-
const claudeMdPath =
|
|
3577
|
+
const claudeMdPath = path16.join(projectRoot, "CLAUDE.md");
|
|
3578
3578
|
let content = "";
|
|
3579
|
-
if (
|
|
3580
|
-
content =
|
|
3579
|
+
if (fs16.existsSync(claudeMdPath)) {
|
|
3580
|
+
content = fs16.readFileSync(claudeMdPath, "utf-8");
|
|
3581
3581
|
}
|
|
3582
3582
|
if (content.includes("@.viberails/context.md")) return;
|
|
3583
3583
|
const ref = "\n@.viberails/context.md\n";
|
|
3584
3584
|
const prefix = content.length === 0 ? "" : content.trimEnd();
|
|
3585
|
-
|
|
3586
|
-
console.log(` ${
|
|
3585
|
+
fs16.writeFileSync(claudeMdPath, prefix + ref);
|
|
3586
|
+
console.log(` ${chalk14.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
|
|
3587
3587
|
}
|
|
3588
3588
|
function setupGithubAction(projectRoot, packageManager, options) {
|
|
3589
|
-
const workflowDir =
|
|
3590
|
-
const workflowPath =
|
|
3591
|
-
if (
|
|
3592
|
-
const existing =
|
|
3589
|
+
const workflowDir = path16.join(projectRoot, ".github", "workflows");
|
|
3590
|
+
const workflowPath = path16.join(workflowDir, "viberails.yml");
|
|
3591
|
+
if (fs16.existsSync(workflowPath)) {
|
|
3592
|
+
const existing = fs16.readFileSync(workflowPath, "utf-8");
|
|
3593
3593
|
if (existing.includes("viberails")) return void 0;
|
|
3594
3594
|
}
|
|
3595
|
-
|
|
3595
|
+
fs16.mkdirSync(workflowDir, { recursive: true });
|
|
3596
3596
|
const pm = packageManager || "npm";
|
|
3597
3597
|
const installCmd = pm === "yarn" ? "yarn install --frozen-lockfile" : pm === "pnpm" ? "pnpm install --frozen-lockfile" : "npm ci";
|
|
3598
3598
|
const runPrefix = pm === "npm" ? "npx" : `${pm} exec`;
|
|
@@ -3646,30 +3646,212 @@ function setupGithubAction(projectRoot, packageManager, options) {
|
|
|
3646
3646
|
""
|
|
3647
3647
|
);
|
|
3648
3648
|
const content = lines.filter((l) => l !== void 0).join("\n");
|
|
3649
|
-
|
|
3649
|
+
fs16.writeFileSync(workflowPath, content);
|
|
3650
3650
|
return ".github/workflows/viberails.yml";
|
|
3651
3651
|
}
|
|
3652
3652
|
function writeHuskyPreCommit(huskyDir) {
|
|
3653
|
-
const hookPath =
|
|
3653
|
+
const hookPath = path16.join(huskyDir, "pre-commit");
|
|
3654
3654
|
const cmd = "if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --staged; else npx viberails check --staged; fi";
|
|
3655
|
-
if (
|
|
3656
|
-
const existing =
|
|
3655
|
+
if (fs16.existsSync(hookPath)) {
|
|
3656
|
+
const existing = fs16.readFileSync(hookPath, "utf-8");
|
|
3657
3657
|
if (!existing.includes("viberails")) {
|
|
3658
|
-
|
|
3658
|
+
fs16.writeFileSync(hookPath, `${existing.trimEnd()}
|
|
3659
3659
|
${cmd}
|
|
3660
3660
|
`);
|
|
3661
3661
|
}
|
|
3662
3662
|
return;
|
|
3663
3663
|
}
|
|
3664
|
-
|
|
3664
|
+
fs16.writeFileSync(hookPath, `#!/bin/sh
|
|
3665
3665
|
${cmd}
|
|
3666
3666
|
`, { mode: 493 });
|
|
3667
3667
|
}
|
|
3668
3668
|
|
|
3669
|
+
// src/utils/prompt-prereqs.ts
|
|
3670
|
+
function buildVitestInstallCommand(pm, isWorkspace) {
|
|
3671
|
+
if (pm === "yarn") return "yarn add -D vitest";
|
|
3672
|
+
if (pm === "npm") return "npm install -D vitest";
|
|
3673
|
+
return isWorkspace ? "pnpm add -D -w vitest" : "pnpm add -D vitest";
|
|
3674
|
+
}
|
|
3675
|
+
function statusIcon2(status) {
|
|
3676
|
+
if (status === "ok") return chalk15.green("\u2713");
|
|
3677
|
+
if (status === "missing") return chalk15.yellow("!");
|
|
3678
|
+
if (status === "skipped") return chalk15.dim("\u2717");
|
|
3679
|
+
return chalk15.dim("-");
|
|
3680
|
+
}
|
|
3681
|
+
function buildReadinessNote(state) {
|
|
3682
|
+
const lines = [];
|
|
3683
|
+
const tr = state.testRunner;
|
|
3684
|
+
lines.push(
|
|
3685
|
+
`${statusIcon2(tr.status)} Test runner ${tr.label ?? (tr.status === "skipped" ? "skipped" : "not detected")}`
|
|
3686
|
+
);
|
|
3687
|
+
const hm = state.hookManager;
|
|
3688
|
+
lines.push(
|
|
3689
|
+
`${statusIcon2(hm.status)} Hook manager ${hm.label ?? (hm.status === "skipped" ? "skipped" : "not detected")}`
|
|
3690
|
+
);
|
|
3691
|
+
const li = state.linter;
|
|
3692
|
+
lines.push(`${statusIcon2(li.status)} Linter ${li.label ?? "none"}`);
|
|
3693
|
+
const tc = state.typecheck;
|
|
3694
|
+
if (tc.status === "ok") {
|
|
3695
|
+
lines.push(`${statusIcon2("ok")} Typecheck ${tc.label}`);
|
|
3696
|
+
} else if (tc.status === "skipped") {
|
|
3697
|
+
lines.push(`${statusIcon2("skipped")} Typecheck skipped`);
|
|
3698
|
+
} else {
|
|
3699
|
+
lines.push(
|
|
3700
|
+
`${statusIcon2("missing")} Typecheck needs root tsconfig.json, typecheck script, or turbo task`
|
|
3701
|
+
);
|
|
3702
|
+
}
|
|
3703
|
+
return lines.join("\n");
|
|
3704
|
+
}
|
|
3705
|
+
function hasMissing(state) {
|
|
3706
|
+
return state.testRunner.status === "missing" || state.hookManager.status === "missing" || state.typecheck.status === "missing";
|
|
3707
|
+
}
|
|
3708
|
+
async function promptPrereqs(projectRoot, scanResult, hookManager, packageManager, isWorkspace) {
|
|
3709
|
+
let hasTestRunner = !!scanResult.stack.testRunner;
|
|
3710
|
+
let currentHookManager = hookManager;
|
|
3711
|
+
let skipCoverage = false;
|
|
3712
|
+
let skipHooks = false;
|
|
3713
|
+
const linterName = scanResult.stack.linter?.name;
|
|
3714
|
+
const linterLabel = linterName === "biome" ? "Biome" : linterName === "eslint" ? "ESLint" : linterName;
|
|
3715
|
+
const typecheckResolved = resolveTypecheckCommand(projectRoot, packageManager);
|
|
3716
|
+
const state = {
|
|
3717
|
+
testRunner: hasTestRunner ? { status: "ok", label: scanResult.stack.testRunner?.name } : { status: "missing" },
|
|
3718
|
+
hookManager: currentHookManager ? { status: "ok", label: currentHookManager } : { status: "missing" },
|
|
3719
|
+
linter: linterName ? { status: "ok", label: linterLabel } : { status: "none" },
|
|
3720
|
+
typecheck: typecheckResolved.label ? { status: "ok", label: typecheckResolved.label } : { status: "missing", reason: typecheckResolved.reason }
|
|
3721
|
+
};
|
|
3722
|
+
if (!hasMissing(state)) {
|
|
3723
|
+
return {
|
|
3724
|
+
hasTestRunner,
|
|
3725
|
+
hookManager: currentHookManager,
|
|
3726
|
+
skipCoverage,
|
|
3727
|
+
skipHooks,
|
|
3728
|
+
typecheckLabel: typecheckResolved.label
|
|
3729
|
+
};
|
|
3730
|
+
}
|
|
3731
|
+
if (state.testRunner.status === "missing") {
|
|
3732
|
+
clack12.note(buildReadinessNote(state), "Project readiness");
|
|
3733
|
+
const cmd = buildVitestInstallCommand(packageManager, isWorkspace);
|
|
3734
|
+
const choice = await clack12.select({
|
|
3735
|
+
message: "No test runner detected. Coverage checks require one.",
|
|
3736
|
+
options: [
|
|
3737
|
+
{ value: "install", label: "Install vitest", hint: cmd },
|
|
3738
|
+
{ value: "skip", label: "Skip \u2014 disable coverage checks" },
|
|
3739
|
+
{ value: "exit", label: "Exit" }
|
|
3740
|
+
]
|
|
3741
|
+
});
|
|
3742
|
+
assertNotCancelled(choice);
|
|
3743
|
+
if (choice === "install") {
|
|
3744
|
+
const s = clack12.spinner();
|
|
3745
|
+
s.start("Installing vitest...");
|
|
3746
|
+
const result = await spawnAsync(cmd, projectRoot);
|
|
3747
|
+
if (result.status === 0) {
|
|
3748
|
+
s.stop("Installed vitest");
|
|
3749
|
+
hasTestRunner = true;
|
|
3750
|
+
state.testRunner = { status: "ok", label: "vitest" };
|
|
3751
|
+
} else {
|
|
3752
|
+
s.stop("Failed to install vitest");
|
|
3753
|
+
clack12.log.warn(`Install manually: ${cmd}`);
|
|
3754
|
+
skipCoverage = true;
|
|
3755
|
+
state.testRunner = { status: "skipped" };
|
|
3756
|
+
}
|
|
3757
|
+
} else if (choice === "skip") {
|
|
3758
|
+
skipCoverage = true;
|
|
3759
|
+
state.testRunner = { status: "skipped" };
|
|
3760
|
+
} else {
|
|
3761
|
+
clack12.outro("Aborted.");
|
|
3762
|
+
process.exit(0);
|
|
3763
|
+
}
|
|
3764
|
+
}
|
|
3765
|
+
if (state.hookManager.status === "missing") {
|
|
3766
|
+
clack12.note(buildReadinessNote(state), "Project readiness");
|
|
3767
|
+
const cmd = buildLefthookInstallCommand(packageManager, isWorkspace);
|
|
3768
|
+
const choice = await clack12.select({
|
|
3769
|
+
message: "No git hook manager detected. Pre-commit integration requires one.",
|
|
3770
|
+
options: [
|
|
3771
|
+
{ value: "install", label: "Install lefthook", hint: cmd },
|
|
3772
|
+
{ value: "skip", label: "Skip \u2014 no pre-commit integration" },
|
|
3773
|
+
{ value: "exit", label: "Exit" }
|
|
3774
|
+
]
|
|
3775
|
+
});
|
|
3776
|
+
assertNotCancelled(choice);
|
|
3777
|
+
if (choice === "install") {
|
|
3778
|
+
const s = clack12.spinner();
|
|
3779
|
+
s.start("Installing lefthook...");
|
|
3780
|
+
const result = await spawnAsync(cmd, projectRoot);
|
|
3781
|
+
if (result.status === 0) {
|
|
3782
|
+
s.stop("Installed lefthook");
|
|
3783
|
+
const ymlPath = path17.join(projectRoot, "lefthook.yml");
|
|
3784
|
+
if (!fs17.existsSync(ymlPath)) {
|
|
3785
|
+
fs17.writeFileSync(ymlPath, "# Managed by viberails\npre-commit:\n commands: {}\n");
|
|
3786
|
+
}
|
|
3787
|
+
currentHookManager = detectHookManager(projectRoot);
|
|
3788
|
+
state.hookManager = { status: "ok", label: currentHookManager ?? "lefthook" };
|
|
3789
|
+
} else {
|
|
3790
|
+
s.stop("Failed to install lefthook");
|
|
3791
|
+
clack12.log.warn(`Install manually: ${cmd}`);
|
|
3792
|
+
skipHooks = true;
|
|
3793
|
+
state.hookManager = { status: "skipped" };
|
|
3794
|
+
}
|
|
3795
|
+
} else if (choice === "skip") {
|
|
3796
|
+
skipHooks = true;
|
|
3797
|
+
state.hookManager = { status: "skipped" };
|
|
3798
|
+
} else {
|
|
3799
|
+
clack12.outro("Aborted.");
|
|
3800
|
+
process.exit(0);
|
|
3801
|
+
}
|
|
3802
|
+
}
|
|
3803
|
+
if (state.typecheck.status === "missing") {
|
|
3804
|
+
clack12.note(buildReadinessNote(state), "Project readiness");
|
|
3805
|
+
const choice = await clack12.select({
|
|
3806
|
+
message: "No typecheck command found. Without this, pre-commit and CI typecheck hooks will be unavailable.",
|
|
3807
|
+
options: [
|
|
3808
|
+
{
|
|
3809
|
+
value: "continue",
|
|
3810
|
+
label: "Continue without typecheck",
|
|
3811
|
+
hint: "add a root tsconfig.json or typecheck script later, then re-run viberails"
|
|
3812
|
+
},
|
|
3813
|
+
{ value: "exit", label: "Exit \u2014 fix this first" }
|
|
3814
|
+
]
|
|
3815
|
+
});
|
|
3816
|
+
assertNotCancelled(choice);
|
|
3817
|
+
if (choice === "exit") {
|
|
3818
|
+
clack12.outro(
|
|
3819
|
+
"Add a root tsconfig.json, a typecheck script, or a turbo typecheck task, then re-run viberails."
|
|
3820
|
+
);
|
|
3821
|
+
process.exit(0);
|
|
3822
|
+
}
|
|
3823
|
+
state.typecheck = { status: "skipped" };
|
|
3824
|
+
}
|
|
3825
|
+
return {
|
|
3826
|
+
hasTestRunner,
|
|
3827
|
+
hookManager: currentHookManager,
|
|
3828
|
+
skipCoverage,
|
|
3829
|
+
skipHooks,
|
|
3830
|
+
typecheckLabel: typecheckResolved.label
|
|
3831
|
+
};
|
|
3832
|
+
}
|
|
3833
|
+
|
|
3834
|
+
// src/utils/update-gitignore.ts
|
|
3835
|
+
import * as fs18 from "fs";
|
|
3836
|
+
import * as path18 from "path";
|
|
3837
|
+
function updateGitignore(projectRoot) {
|
|
3838
|
+
const gitignorePath = path18.join(projectRoot, ".gitignore");
|
|
3839
|
+
let content = "";
|
|
3840
|
+
if (fs18.existsSync(gitignorePath)) {
|
|
3841
|
+
content = fs18.readFileSync(gitignorePath, "utf-8");
|
|
3842
|
+
}
|
|
3843
|
+
if (!content.includes(".viberails/scan-result.json")) {
|
|
3844
|
+
const block = "\n# viberails\n.viberails/scan-result.json\n";
|
|
3845
|
+
const prefix = content.length === 0 ? "" : `${content.trimEnd()}
|
|
3846
|
+
`;
|
|
3847
|
+
fs18.writeFileSync(gitignorePath, `${prefix}${block}`);
|
|
3848
|
+
}
|
|
3849
|
+
}
|
|
3850
|
+
|
|
3669
3851
|
// src/commands/init-hooks-extra.ts
|
|
3670
3852
|
import * as fs19 from "fs";
|
|
3671
3853
|
import * as path19 from "path";
|
|
3672
|
-
import
|
|
3854
|
+
import chalk16 from "chalk";
|
|
3673
3855
|
import { parse as parseYaml2, stringify as stringifyYaml2 } from "yaml";
|
|
3674
3856
|
function addPreCommitStep(projectRoot, name, command, marker, lefthookExtra) {
|
|
3675
3857
|
const lefthookPath = path19.join(projectRoot, "lefthook.yml");
|
|
@@ -3729,12 +3911,12 @@ ${command}
|
|
|
3729
3911
|
function setupTypecheckHook(projectRoot, packageManager) {
|
|
3730
3912
|
const resolved = resolveTypecheckCommand(projectRoot, packageManager);
|
|
3731
3913
|
if (!resolved.command) {
|
|
3732
|
-
console.log(` ${
|
|
3914
|
+
console.log(` ${chalk16.yellow("!")} Skipped typecheck hook: ${resolved.reason}`);
|
|
3733
3915
|
return void 0;
|
|
3734
3916
|
}
|
|
3735
3917
|
const target = addPreCommitStep(projectRoot, "typecheck", resolved.command, "typecheck");
|
|
3736
3918
|
if (target) {
|
|
3737
|
-
console.log(` ${
|
|
3919
|
+
console.log(` ${chalk16.green("\u2713")} ${target} \u2014 added typecheck (${resolved.label})`);
|
|
3738
3920
|
}
|
|
3739
3921
|
return target;
|
|
3740
3922
|
}
|
|
@@ -3744,18 +3926,19 @@ function setupLintHook(projectRoot, linter) {
|
|
|
3744
3926
|
let command;
|
|
3745
3927
|
let lefthookExtra;
|
|
3746
3928
|
if (isLefthook) {
|
|
3747
|
-
command = linter === "biome" ? "npx biome check {staged_files}" : "npx eslint {staged_files}";
|
|
3929
|
+
command = linter === "biome" ? "npx biome check --write {staged_files}" : "npx eslint --fix {staged_files}";
|
|
3748
3930
|
lefthookExtra = {
|
|
3749
|
-
glob: linter === "biome" ? "*.{js,ts,jsx,tsx,json,css}" : "*.{js,ts,jsx,tsx}"
|
|
3931
|
+
glob: linter === "biome" ? "*.{js,ts,jsx,tsx,json,css}" : "*.{js,ts,jsx,tsx}",
|
|
3932
|
+
stage_fixed: "true"
|
|
3750
3933
|
};
|
|
3751
3934
|
} else {
|
|
3752
3935
|
const exts = linter === "biome" ? "'*.js' '*.ts' '*.jsx' '*.tsx' '*.json' '*.css'" : "'*.js' '*.ts' '*.jsx' '*.tsx'";
|
|
3753
|
-
const lintCmd = linter === "biome" ? "biome check" : "eslint";
|
|
3936
|
+
const lintCmd = linter === "biome" ? "biome check --write" : "eslint --fix";
|
|
3754
3937
|
command = `git diff --cached --name-only --diff-filter=ACMR -- ${exts} | xargs npx ${lintCmd}`;
|
|
3755
3938
|
}
|
|
3756
3939
|
const target = addPreCommitStep(projectRoot, "lint", command, linter, lefthookExtra);
|
|
3757
3940
|
if (target) {
|
|
3758
|
-
console.log(` ${
|
|
3941
|
+
console.log(` ${chalk16.green("\u2713")} ${target} \u2014 added ${linterName} lint check`);
|
|
3759
3942
|
}
|
|
3760
3943
|
return target;
|
|
3761
3944
|
}
|
|
@@ -3764,7 +3947,7 @@ function setupSelectedIntegrations(projectRoot, integrations, opts) {
|
|
|
3764
3947
|
if (integrations.preCommitHook) {
|
|
3765
3948
|
const t = setupPreCommitHook(projectRoot);
|
|
3766
3949
|
if (t && opts.lefthookExpected && !t.includes("lefthook")) {
|
|
3767
|
-
console.log(` ${
|
|
3950
|
+
console.log(` ${chalk16.yellow("!")} Lefthook install failed \u2014 fell back to ${t}`);
|
|
3768
3951
|
}
|
|
3769
3952
|
created.push(t ? `${t} \u2014 added viberails pre-commit` : "pre-commit hook skipped");
|
|
3770
3953
|
}
|
|
@@ -3797,10 +3980,10 @@ function setupSelectedIntegrations(projectRoot, integrations, opts) {
|
|
|
3797
3980
|
// src/commands/init-non-interactive.ts
|
|
3798
3981
|
import * as fs20 from "fs";
|
|
3799
3982
|
import * as path20 from "path";
|
|
3800
|
-
import * as
|
|
3983
|
+
import * as clack13 from "@clack/prompts";
|
|
3801
3984
|
import { compactConfig as compactConfig3, generateConfig } from "@viberails/config";
|
|
3802
3985
|
import { scan as scan2 } from "@viberails/scanner";
|
|
3803
|
-
import
|
|
3986
|
+
import chalk17 from "chalk";
|
|
3804
3987
|
|
|
3805
3988
|
// src/utils/filter-confidence.ts
|
|
3806
3989
|
function filterHighConfidence(conventions, meta) {
|
|
@@ -3821,7 +4004,7 @@ function getExemptedPackages(config) {
|
|
|
3821
4004
|
return config.packages.filter((pkg) => pkg.rules?.testCoverage === 0 && pkg.path !== ".").map((pkg) => pkg.path);
|
|
3822
4005
|
}
|
|
3823
4006
|
async function initNonInteractive(projectRoot, configPath) {
|
|
3824
|
-
const s =
|
|
4007
|
+
const s = clack13.spinner();
|
|
3825
4008
|
s.start("Scanning project...");
|
|
3826
4009
|
const scanResult = await scan2(projectRoot);
|
|
3827
4010
|
const config = generateConfig(scanResult);
|
|
@@ -3836,11 +4019,11 @@ async function initNonInteractive(projectRoot, configPath) {
|
|
|
3836
4019
|
const exempted = getExemptedPackages(config);
|
|
3837
4020
|
if (exempted.length > 0) {
|
|
3838
4021
|
console.log(
|
|
3839
|
-
` ${
|
|
4022
|
+
` ${chalk17.dim("Auto-exempted from coverage:")} ${exempted.join(", ")} ${chalk17.dim("(types-only)")}`
|
|
3840
4023
|
);
|
|
3841
4024
|
}
|
|
3842
4025
|
if (config.packages.length > 1) {
|
|
3843
|
-
const bs =
|
|
4026
|
+
const bs = clack13.spinner();
|
|
3844
4027
|
bs.start("Building import graph...");
|
|
3845
4028
|
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
3846
4029
|
const packages = resolveWorkspacePackages(projectRoot, config.packages);
|
|
@@ -3873,14 +4056,14 @@ async function initNonInteractive(projectRoot, configPath) {
|
|
|
3873
4056
|
const hookManager = detectHookManager(projectRoot);
|
|
3874
4057
|
const hasHookManager = hookManager === "Lefthook" || hookManager === "Husky";
|
|
3875
4058
|
const preCommitTarget = hasHookManager ? setupPreCommitHook(projectRoot) : void 0;
|
|
3876
|
-
const ok =
|
|
4059
|
+
const ok = chalk17.green("\u2713");
|
|
3877
4060
|
const created = [
|
|
3878
4061
|
`${ok} ${path20.basename(configPath)}`,
|
|
3879
4062
|
`${ok} .viberails/context.md`,
|
|
3880
4063
|
`${ok} .viberails/scan-result.json`,
|
|
3881
4064
|
`${ok} .claude/settings.json \u2014 added viberails hook`,
|
|
3882
4065
|
`${ok} CLAUDE.md \u2014 added @.viberails/context.md reference`,
|
|
3883
|
-
preCommitTarget ? `${ok} ${preCommitTarget}` : `${
|
|
4066
|
+
preCommitTarget ? `${ok} ${preCommitTarget}` : `${chalk17.yellow("!")} pre-commit hook skipped (install lefthook or husky)`,
|
|
3884
4067
|
actionTarget ? `${ok} ${actionTarget} \u2014 blocks PRs on violations` : ""
|
|
3885
4068
|
].filter(Boolean);
|
|
3886
4069
|
if (hasHookManager && isTypeScript) setupTypecheckHook(projectRoot, rootPkgPm);
|
|
@@ -3905,8 +4088,8 @@ async function initCommand(options, cwd) {
|
|
|
3905
4088
|
return initInteractive(projectRoot, configPath, options);
|
|
3906
4089
|
}
|
|
3907
4090
|
console.log(
|
|
3908
|
-
`${
|
|
3909
|
-
Run ${
|
|
4091
|
+
`${chalk18.yellow("!")} viberails is already initialized.
|
|
4092
|
+
Run ${chalk18.cyan("viberails")} to review or edit the existing setup, ${chalk18.cyan("viberails sync")} to update generated files, or ${chalk18.cyan("viberails init --force")} to replace it.`
|
|
3910
4093
|
);
|
|
3911
4094
|
return;
|
|
3912
4095
|
}
|
|
@@ -3914,11 +4097,11 @@ async function initCommand(options, cwd) {
|
|
|
3914
4097
|
await initInteractive(projectRoot, configPath, options);
|
|
3915
4098
|
}
|
|
3916
4099
|
async function initInteractive(projectRoot, configPath, options) {
|
|
3917
|
-
|
|
4100
|
+
clack14.intro("viberails");
|
|
3918
4101
|
if (fs21.existsSync(configPath) && !options.force) {
|
|
3919
4102
|
const action = await promptExistingConfigAction(path21.basename(configPath));
|
|
3920
4103
|
if (action === "cancel") {
|
|
3921
|
-
|
|
4104
|
+
clack14.outro("Aborted. No files were written.");
|
|
3922
4105
|
return;
|
|
3923
4106
|
}
|
|
3924
4107
|
if (action === "edit") {
|
|
@@ -3932,45 +4115,60 @@ async function initInteractive(projectRoot, configPath, options) {
|
|
|
3932
4115
|
`${path21.basename(configPath)} already exists and will be replaced. Continue?`
|
|
3933
4116
|
);
|
|
3934
4117
|
if (!replace) {
|
|
3935
|
-
|
|
4118
|
+
clack14.outro("Aborted. No files were written.");
|
|
3936
4119
|
return;
|
|
3937
4120
|
}
|
|
3938
4121
|
}
|
|
3939
|
-
const s =
|
|
4122
|
+
const s = clack14.spinner();
|
|
3940
4123
|
s.start("Scanning project...");
|
|
3941
4124
|
const scanResult = await scan3(projectRoot);
|
|
3942
4125
|
const config = generateConfig2(scanResult);
|
|
3943
4126
|
s.stop("Scan complete");
|
|
3944
4127
|
if (scanResult.statistics.totalFiles === 0) {
|
|
3945
|
-
|
|
4128
|
+
clack14.log.warn(
|
|
3946
4129
|
"No source files detected. Try running from the project root,\nor check that source files exist. Run viberails sync after adding files."
|
|
3947
4130
|
);
|
|
3948
4131
|
}
|
|
3949
|
-
const hasTestRunner = !!scanResult.stack.testRunner;
|
|
3950
|
-
const hookManager = detectHookManager(projectRoot);
|
|
3951
|
-
const coveragePrereqs = checkCoveragePrereqs(projectRoot, scanResult);
|
|
3952
4132
|
const rootPkgStack = (config.packages.find((p) => p.path === ".") ?? config.packages[0])?.stack;
|
|
4133
|
+
const packageManager = rootPkgStack?.packageManager?.split("@")[0] ?? "npm";
|
|
4134
|
+
const isWorkspace = config.packages.length > 1;
|
|
4135
|
+
const prereqs = await promptPrereqs(
|
|
4136
|
+
projectRoot,
|
|
4137
|
+
scanResult,
|
|
4138
|
+
detectHookManager(projectRoot),
|
|
4139
|
+
packageManager,
|
|
4140
|
+
isWorkspace
|
|
4141
|
+
);
|
|
4142
|
+
if (prereqs.skipCoverage) config.rules.testCoverage = 0;
|
|
4143
|
+
const coveragePrereqs = prereqs.hasTestRunner ? checkCoveragePrereqs(projectRoot, scanResult) : [];
|
|
3953
4144
|
const state = await promptMainMenu(config, scanResult, {
|
|
3954
|
-
hasTestRunner,
|
|
3955
|
-
hookManager,
|
|
4145
|
+
hasTestRunner: prereqs.hasTestRunner,
|
|
4146
|
+
hookManager: prereqs.hookManager,
|
|
3956
4147
|
coveragePrereqs,
|
|
3957
4148
|
projectRoot,
|
|
3958
4149
|
tools: {
|
|
3959
4150
|
isTypeScript: rootPkgStack?.language?.split("@")[0] === "typescript",
|
|
3960
4151
|
linter: rootPkgStack?.linter?.split("@")[0],
|
|
3961
|
-
packageManager
|
|
3962
|
-
isWorkspace
|
|
4152
|
+
packageManager,
|
|
4153
|
+
isWorkspace
|
|
3963
4154
|
}
|
|
3964
4155
|
});
|
|
3965
4156
|
const shouldWrite = await confirm3("Apply this setup?");
|
|
3966
4157
|
if (!shouldWrite) {
|
|
3967
|
-
|
|
4158
|
+
clack14.outro("Aborted. No files were written.");
|
|
3968
4159
|
return;
|
|
3969
4160
|
}
|
|
4161
|
+
const integrations = await promptIntegrationsDeferred(prereqs.hookManager, {
|
|
4162
|
+
isTypeScript: rootPkgStack?.language?.split("@")[0] === "typescript",
|
|
4163
|
+
typecheckLabel: prereqs.typecheckLabel,
|
|
4164
|
+
linter: rootPkgStack?.linter?.split("@")[0],
|
|
4165
|
+
packageManager,
|
|
4166
|
+
isWorkspace
|
|
4167
|
+
});
|
|
3970
4168
|
if (state.deferredInstalls.length > 0) {
|
|
3971
4169
|
await executeDeferredInstalls(projectRoot, state.deferredInstalls);
|
|
3972
4170
|
}
|
|
3973
|
-
const ws =
|
|
4171
|
+
const ws = clack14.spinner();
|
|
3974
4172
|
ws.start("Writing configuration...");
|
|
3975
4173
|
const compacted = compactConfig4(config);
|
|
3976
4174
|
fs21.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
|
|
@@ -3978,31 +4176,28 @@ async function initInteractive(projectRoot, configPath, options) {
|
|
|
3978
4176
|
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
3979
4177
|
updateGitignore(projectRoot);
|
|
3980
4178
|
ws.stop("Configuration written");
|
|
3981
|
-
const ok =
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
});
|
|
3992
|
-
}
|
|
3993
|
-
clack13.outro(
|
|
4179
|
+
const ok = chalk18.green("\u2713");
|
|
4180
|
+
clack14.log.step(`${ok} ${path21.basename(configPath)}`);
|
|
4181
|
+
clack14.log.step(`${ok} .viberails/context.md`);
|
|
4182
|
+
clack14.log.step(`${ok} .viberails/scan-result.json`);
|
|
4183
|
+
setupSelectedIntegrations(projectRoot, integrations.choice, {
|
|
4184
|
+
linter: rootPkgStack?.linter?.split("@")[0],
|
|
4185
|
+
packageManager,
|
|
4186
|
+
lefthookExpected: prereqs.hookManager === "lefthook"
|
|
4187
|
+
});
|
|
4188
|
+
clack14.outro(
|
|
3994
4189
|
`Done! Next: review viberails.config.json, then run viberails check
|
|
3995
|
-
${
|
|
4190
|
+
${chalk18.dim("Tip: use")} ${chalk18.cyan("viberails check --enforce")} ${chalk18.dim("in CI to block PRs on violations.")}`
|
|
3996
4191
|
);
|
|
3997
4192
|
}
|
|
3998
4193
|
|
|
3999
4194
|
// src/commands/sync.ts
|
|
4000
4195
|
import * as fs22 from "fs";
|
|
4001
4196
|
import * as path22 from "path";
|
|
4002
|
-
import * as
|
|
4197
|
+
import * as clack15 from "@clack/prompts";
|
|
4003
4198
|
import { compactConfig as compactConfig5, loadConfig as loadConfig5, mergeConfig as mergeConfig2 } from "@viberails/config";
|
|
4004
4199
|
import { scan as scan4 } from "@viberails/scanner";
|
|
4005
|
-
import
|
|
4200
|
+
import chalk19 from "chalk";
|
|
4006
4201
|
var CONFIG_FILE6 = "viberails.config.json";
|
|
4007
4202
|
var SCAN_RESULT_FILE2 = ".viberails/scan-result.json";
|
|
4008
4203
|
function loadPreviousStats(projectRoot) {
|
|
@@ -4028,7 +4223,7 @@ async function syncCommand(options, cwd) {
|
|
|
4028
4223
|
const configPath = path22.join(projectRoot, CONFIG_FILE6);
|
|
4029
4224
|
const existing = await loadConfig5(configPath);
|
|
4030
4225
|
const previousStats = loadPreviousStats(projectRoot);
|
|
4031
|
-
const s =
|
|
4226
|
+
const s = clack15.spinner();
|
|
4032
4227
|
s.start("Scanning project...");
|
|
4033
4228
|
const scanResult = await scan4(projectRoot);
|
|
4034
4229
|
s.stop("Scan complete");
|
|
@@ -4043,19 +4238,19 @@ async function syncCommand(options, cwd) {
|
|
|
4043
4238
|
const statsDelta = previousStats ? formatStatsDelta(previousStats, scanResult.statistics) : void 0;
|
|
4044
4239
|
if (changes.length > 0 || statsDelta) {
|
|
4045
4240
|
console.log(`
|
|
4046
|
-
${
|
|
4241
|
+
${chalk19.bold("Changes:")}`);
|
|
4047
4242
|
for (const change of changes) {
|
|
4048
|
-
const icon = change.type === "removed" ?
|
|
4243
|
+
const icon = change.type === "removed" ? chalk19.red("-") : chalk19.green("+");
|
|
4049
4244
|
console.log(` ${icon} ${change.description}`);
|
|
4050
4245
|
}
|
|
4051
4246
|
if (statsDelta) {
|
|
4052
|
-
console.log(` ${
|
|
4247
|
+
console.log(` ${chalk19.dim(statsDelta)}`);
|
|
4053
4248
|
}
|
|
4054
4249
|
}
|
|
4055
4250
|
if (options?.interactive) {
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
const decision = await
|
|
4251
|
+
clack15.intro("viberails sync (interactive)");
|
|
4252
|
+
clack15.note(formatRulesText(merged).join("\n"), "Rules after sync");
|
|
4253
|
+
const decision = await clack15.select({
|
|
4059
4254
|
message: "How would you like to proceed?",
|
|
4060
4255
|
options: [
|
|
4061
4256
|
{ value: "accept", label: "Accept changes" },
|
|
@@ -4065,7 +4260,7 @@ ${chalk17.bold("Changes:")}`);
|
|
|
4065
4260
|
});
|
|
4066
4261
|
assertNotCancelled(decision);
|
|
4067
4262
|
if (decision === "cancel") {
|
|
4068
|
-
|
|
4263
|
+
clack15.outro("Sync cancelled. No files were written.");
|
|
4069
4264
|
return;
|
|
4070
4265
|
}
|
|
4071
4266
|
if (decision === "customize") {
|
|
@@ -4089,8 +4284,8 @@ ${chalk17.bold("Changes:")}`);
|
|
|
4089
4284
|
fs22.writeFileSync(configPath, `${JSON.stringify(recompacted, null, 2)}
|
|
4090
4285
|
`);
|
|
4091
4286
|
writeGeneratedFiles(projectRoot, merged, scanResult);
|
|
4092
|
-
|
|
4093
|
-
|
|
4287
|
+
clack15.log.success("Updated config with your customizations.");
|
|
4288
|
+
clack15.outro("Done! Run viberails check to verify.");
|
|
4094
4289
|
return;
|
|
4095
4290
|
}
|
|
4096
4291
|
}
|
|
@@ -4098,18 +4293,18 @@ ${chalk17.bold("Changes:")}`);
|
|
|
4098
4293
|
`);
|
|
4099
4294
|
writeGeneratedFiles(projectRoot, merged, scanResult);
|
|
4100
4295
|
console.log(`
|
|
4101
|
-
${
|
|
4296
|
+
${chalk19.bold("Synced:")}`);
|
|
4102
4297
|
if (configChanged) {
|
|
4103
|
-
console.log(` ${
|
|
4298
|
+
console.log(` ${chalk19.yellow("!")} ${CONFIG_FILE6} \u2014 updated (review changes)`);
|
|
4104
4299
|
} else {
|
|
4105
|
-
console.log(` ${
|
|
4300
|
+
console.log(` ${chalk19.green("\u2713")} ${CONFIG_FILE6} \u2014 unchanged`);
|
|
4106
4301
|
}
|
|
4107
|
-
console.log(` ${
|
|
4108
|
-
console.log(` ${
|
|
4302
|
+
console.log(` ${chalk19.green("\u2713")} .viberails/context.md \u2014 regenerated`);
|
|
4303
|
+
console.log(` ${chalk19.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
|
|
4109
4304
|
}
|
|
4110
4305
|
|
|
4111
4306
|
// src/index.ts
|
|
4112
|
-
var VERSION = "0.6.
|
|
4307
|
+
var VERSION = "0.6.11";
|
|
4113
4308
|
var program = new Command();
|
|
4114
4309
|
program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
|
|
4115
4310
|
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)").option("-f, --force", "Re-initialize, replacing existing config").action(async (options) => {
|
|
@@ -4117,7 +4312,7 @@ program.command("init", { isDefault: true }).description("Scan your project and
|
|
|
4117
4312
|
await initCommand(options);
|
|
4118
4313
|
} catch (err) {
|
|
4119
4314
|
const message = err instanceof Error ? err.message : String(err);
|
|
4120
|
-
console.error(`${
|
|
4315
|
+
console.error(`${chalk20.red("Error:")} ${message}`);
|
|
4121
4316
|
process.exit(1);
|
|
4122
4317
|
}
|
|
4123
4318
|
});
|
|
@@ -4126,7 +4321,7 @@ program.command("sync").description("Re-scan and update generated files").option
|
|
|
4126
4321
|
await syncCommand(options);
|
|
4127
4322
|
} catch (err) {
|
|
4128
4323
|
const message = err instanceof Error ? err.message : String(err);
|
|
4129
|
-
console.error(`${
|
|
4324
|
+
console.error(`${chalk20.red("Error:")} ${message}`);
|
|
4130
4325
|
process.exit(1);
|
|
4131
4326
|
}
|
|
4132
4327
|
});
|
|
@@ -4135,7 +4330,7 @@ program.command("config").description("Interactively edit existing config rules"
|
|
|
4135
4330
|
await configCommand(options);
|
|
4136
4331
|
} catch (err) {
|
|
4137
4332
|
const message = err instanceof Error ? err.message : String(err);
|
|
4138
|
-
console.error(`${
|
|
4333
|
+
console.error(`${chalk20.red("Error:")} ${message}`);
|
|
4139
4334
|
process.exit(1);
|
|
4140
4335
|
}
|
|
4141
4336
|
});
|
|
@@ -4156,7 +4351,7 @@ program.command("check").description("Check files against enforced rules").optio
|
|
|
4156
4351
|
process.exit(exitCode);
|
|
4157
4352
|
} catch (err) {
|
|
4158
4353
|
const message = err instanceof Error ? err.message : String(err);
|
|
4159
|
-
console.error(`${
|
|
4354
|
+
console.error(`${chalk20.red("Error:")} ${message}`);
|
|
4160
4355
|
process.exit(1);
|
|
4161
4356
|
}
|
|
4162
4357
|
}
|
|
@@ -4167,7 +4362,7 @@ program.command("fix").description("Auto-fix file naming violations and generate
|
|
|
4167
4362
|
process.exit(exitCode);
|
|
4168
4363
|
} catch (err) {
|
|
4169
4364
|
const message = err instanceof Error ? err.message : String(err);
|
|
4170
|
-
console.error(`${
|
|
4365
|
+
console.error(`${chalk20.red("Error:")} ${message}`);
|
|
4171
4366
|
process.exit(1);
|
|
4172
4367
|
}
|
|
4173
4368
|
});
|
|
@@ -4176,7 +4371,7 @@ program.command("boundaries").description("Display, infer, or inspect import bou
|
|
|
4176
4371
|
await boundariesCommand(options);
|
|
4177
4372
|
} catch (err) {
|
|
4178
4373
|
const message = err instanceof Error ? err.message : String(err);
|
|
4179
|
-
console.error(`${
|
|
4374
|
+
console.error(`${chalk20.red("Error:")} ${message}`);
|
|
4180
4375
|
process.exit(1);
|
|
4181
4376
|
}
|
|
4182
4377
|
});
|