@vibecheckai/cli 3.7.0 → 3.9.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/README.md +135 -63
- package/bin/_deprecations.js +447 -19
- package/bin/_router.js +1 -1
- package/bin/registry.js +347 -280
- package/bin/runners/context/generators/cursor-enhanced.js +2439 -0
- package/bin/runners/lib/agent-firewall/enforcement/gateway.js +1059 -0
- package/bin/runners/lib/agent-firewall/enforcement/index.js +98 -0
- package/bin/runners/lib/agent-firewall/enforcement/mode.js +318 -0
- package/bin/runners/lib/agent-firewall/enforcement/orchestrator.js +484 -0
- package/bin/runners/lib/agent-firewall/enforcement/proof-artifact.js +418 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/change-event.schema.json +173 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/intent.schema.json +181 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/verdict.schema.json +222 -0
- package/bin/runners/lib/agent-firewall/enforcement/verdict-v2.js +333 -0
- package/bin/runners/lib/agent-firewall/index.js +200 -0
- package/bin/runners/lib/agent-firewall/integration/index.js +20 -0
- package/bin/runners/lib/agent-firewall/integration/ship-gate.js +437 -0
- package/bin/runners/lib/agent-firewall/intent/alignment-engine.js +634 -0
- package/bin/runners/lib/agent-firewall/intent/auto-detect.js +426 -0
- package/bin/runners/lib/agent-firewall/intent/index.js +102 -0
- package/bin/runners/lib/agent-firewall/intent/schema.js +352 -0
- package/bin/runners/lib/agent-firewall/intent/store.js +283 -0
- package/bin/runners/lib/agent-firewall/interception/fs-interceptor.js +502 -0
- package/bin/runners/lib/agent-firewall/interception/index.js +23 -0
- package/bin/runners/lib/agent-firewall/interceptor/base.js +7 -3
- package/bin/runners/lib/agent-firewall/session/collector.js +451 -0
- package/bin/runners/lib/agent-firewall/session/index.js +26 -0
- package/bin/runners/lib/artifact-envelope.js +540 -0
- package/bin/runners/lib/auth-shared.js +977 -0
- package/bin/runners/lib/checkpoint.js +941 -0
- package/bin/runners/lib/cleanup/engine.js +571 -0
- package/bin/runners/lib/cleanup/index.js +53 -0
- package/bin/runners/lib/cleanup/output.js +375 -0
- package/bin/runners/lib/cleanup/rules.js +1060 -0
- package/bin/runners/lib/doctor/diagnosis-receipt.js +454 -0
- package/bin/runners/lib/doctor/failure-signatures.js +526 -0
- package/bin/runners/lib/doctor/fix-script.js +336 -0
- package/bin/runners/lib/doctor/modules/build-tools.js +453 -0
- package/bin/runners/lib/doctor/modules/index.js +62 -3
- package/bin/runners/lib/doctor/modules/os-quirks.js +706 -0
- package/bin/runners/lib/doctor/modules/repo-integrity.js +485 -0
- package/bin/runners/lib/doctor/safe-repair.js +384 -0
- package/bin/runners/lib/engine/ast-cache.js +210 -210
- package/bin/runners/lib/engine/auth-extractor.js +211 -211
- package/bin/runners/lib/engine/billing-extractor.js +112 -112
- package/bin/runners/lib/engine/enforcement-extractor.js +100 -100
- package/bin/runners/lib/engine/env-extractor.js +207 -207
- package/bin/runners/lib/engine/express-extractor.js +208 -208
- package/bin/runners/lib/engine/extractors.js +849 -849
- package/bin/runners/lib/engine/index.js +207 -207
- package/bin/runners/lib/engine/repo-index.js +514 -514
- package/bin/runners/lib/engine/types.js +124 -124
- package/bin/runners/lib/engines/attack-detector.js +1192 -0
- package/bin/runners/lib/entitlements-v2.js +2 -2
- package/bin/runners/lib/missions/briefing.js +427 -0
- package/bin/runners/lib/missions/checkpoint.js +753 -0
- package/bin/runners/lib/missions/hardening.js +851 -0
- package/bin/runners/lib/missions/plan.js +421 -32
- package/bin/runners/lib/missions/safety-gates.js +645 -0
- package/bin/runners/lib/missions/schema.js +478 -0
- package/bin/runners/lib/packs/bundle.js +675 -0
- package/bin/runners/lib/packs/evidence-pack.js +671 -0
- package/bin/runners/lib/packs/pack-factory.js +837 -0
- package/bin/runners/lib/packs/permissions-pack.js +686 -0
- package/bin/runners/lib/packs/proof-graph-pack.js +779 -0
- package/bin/runners/lib/safelist/index.js +96 -0
- package/bin/runners/lib/safelist/integration.js +334 -0
- package/bin/runners/lib/safelist/matcher.js +696 -0
- package/bin/runners/lib/safelist/schema.js +948 -0
- package/bin/runners/lib/safelist/store.js +438 -0
- package/bin/runners/lib/schemas/ship-manifest.schema.json +251 -0
- package/bin/runners/lib/ship-gate.js +832 -0
- package/bin/runners/lib/ship-manifest.js +1153 -0
- package/bin/runners/lib/ship-output.js +1 -1
- package/bin/runners/lib/unified-cli-output.js +710 -383
- package/bin/runners/lib/upsell.js +3 -3
- package/bin/runners/lib/why-tree.js +650 -0
- package/bin/runners/runAllowlist.js +33 -4
- package/bin/runners/runApprove.js +240 -1122
- package/bin/runners/runAudit.js +692 -0
- package/bin/runners/runAuth.js +325 -29
- package/bin/runners/runCheckpoint.js +442 -494
- package/bin/runners/runCleanup.js +343 -0
- package/bin/runners/runDoctor.js +269 -19
- package/bin/runners/runFix.js +411 -32
- package/bin/runners/runForge.js +411 -0
- package/bin/runners/runIntent.js +906 -0
- package/bin/runners/runKickoff.js +878 -0
- package/bin/runners/runLaunch.js +2000 -0
- package/bin/runners/runLink.js +785 -0
- package/bin/runners/runMcp.js +1741 -837
- package/bin/runners/runPacks.js +2089 -0
- package/bin/runners/runPolish.js +41 -0
- package/bin/runners/runReality.js +178 -1
- package/bin/runners/runSafelist.js +1190 -0
- package/bin/runners/runScan.js +21 -9
- package/bin/runners/runShield.js +1282 -0
- package/bin/runners/runShip.js +395 -16
- package/bin/vibecheck.js +34 -6
- package/mcp-server/README.md +117 -158
- package/mcp-server/handlers/index.ts +2 -2
- package/mcp-server/handlers/tool-handler.ts +50 -11
- package/mcp-server/index.js +16 -0
- package/mcp-server/intent-firewall-interceptor.js +529 -0
- package/mcp-server/lib/executor.ts +5 -5
- package/mcp-server/lib/index.ts +14 -4
- package/mcp-server/lib/sandbox.test.ts +4 -4
- package/mcp-server/lib/sandbox.ts +2 -2
- package/mcp-server/manifest.json +473 -0
- package/mcp-server/package.json +1 -1
- package/mcp-server/registry/tool-registry.js +315 -523
- package/mcp-server/registry/tools.json +442 -428
- package/mcp-server/registry.test.ts +18 -12
- package/mcp-server/tier-auth.js +68 -11
- package/mcp-server/tools-v3.js +70 -16
- package/mcp-server/tsconfig.json +1 -0
- package/package.json +2 -1
- package/bin/runners/runProof.zip +0 -0
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vibecheck link - Instant Project Binding
|
|
3
|
+
*
|
|
4
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
+
* ZERO QUESTIONS. MAXIMUM DETECTION. UNDER 10 SECONDS.
|
|
6
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
7
|
+
*
|
|
8
|
+
* Goal: "Project Bound" receipt in <10 seconds with smart next step suggestion.
|
|
9
|
+
*
|
|
10
|
+
* @version 1.0.0
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
"use strict";
|
|
14
|
+
|
|
15
|
+
const fs = require("fs");
|
|
16
|
+
const path = require("path");
|
|
17
|
+
const crypto = require("crypto");
|
|
18
|
+
const { parseGlobalFlags, shouldShowBanner } = require("./lib/global-flags");
|
|
19
|
+
const { EXIT } = require("./lib/exit-codes");
|
|
20
|
+
|
|
21
|
+
// Reuse existing detection modules
|
|
22
|
+
const { detectPackageManager } = require("./lib/detect");
|
|
23
|
+
const { detectMonorepo } = require("./context/monorepo");
|
|
24
|
+
|
|
25
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
26
|
+
// CONSTANTS
|
|
27
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
28
|
+
|
|
29
|
+
const VERSION = "1.0.0";
|
|
30
|
+
const CLI_VERSION = require("../../package.json").version || "4.0.0";
|
|
31
|
+
const SCHEMA = "https://vibecheck.dev/schemas/project.v1.json";
|
|
32
|
+
|
|
33
|
+
// ANSI colors (minimal, fast)
|
|
34
|
+
const c = {
|
|
35
|
+
reset: "\x1b[0m",
|
|
36
|
+
bold: "\x1b[1m",
|
|
37
|
+
dim: "\x1b[2m",
|
|
38
|
+
green: "\x1b[32m",
|
|
39
|
+
yellow: "\x1b[33m",
|
|
40
|
+
cyan: "\x1b[36m",
|
|
41
|
+
red: "\x1b[31m",
|
|
42
|
+
gray: "\x1b[90m",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const sym = {
|
|
46
|
+
success: "✓",
|
|
47
|
+
warning: "⚠",
|
|
48
|
+
error: "✗",
|
|
49
|
+
arrow: "→",
|
|
50
|
+
dot: "•",
|
|
51
|
+
link: "🔗",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Link-specific exit codes
|
|
55
|
+
const LINK_EXIT = {
|
|
56
|
+
SUCCESS: 0,
|
|
57
|
+
PARTIAL: 1,
|
|
58
|
+
USER_ERROR: 3,
|
|
59
|
+
NOT_FOUND: 4,
|
|
60
|
+
INTERNAL_ERROR: 10,
|
|
61
|
+
PERMISSION_DENIED: 11,
|
|
62
|
+
ALREADY_LINKED: 12,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
66
|
+
// PROJECT ROOT DETECTION
|
|
67
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
68
|
+
|
|
69
|
+
function findProjectRoot(startDir) {
|
|
70
|
+
let current = path.resolve(startDir);
|
|
71
|
+
const root = path.parse(current).root;
|
|
72
|
+
let levels = 0;
|
|
73
|
+
const maxLevels = 10;
|
|
74
|
+
|
|
75
|
+
while (current !== root && levels < maxLevels) {
|
|
76
|
+
// package.json is the definitive marker
|
|
77
|
+
if (fs.existsSync(path.join(current, "package.json"))) {
|
|
78
|
+
return current;
|
|
79
|
+
}
|
|
80
|
+
// .git is acceptable fallback (monorepo root)
|
|
81
|
+
if (fs.existsSync(path.join(current, ".git"))) {
|
|
82
|
+
return current;
|
|
83
|
+
}
|
|
84
|
+
current = path.dirname(current);
|
|
85
|
+
levels++;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
92
|
+
// RUNTIME DETECTION
|
|
93
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
94
|
+
|
|
95
|
+
function detectRuntime(projectRoot) {
|
|
96
|
+
const sources = [
|
|
97
|
+
{ file: ".nvmrc", parse: (content) => content.trim().replace(/^v/, "") },
|
|
98
|
+
{ file: ".node-version", parse: (content) => content.trim().replace(/^v/, "") },
|
|
99
|
+
{
|
|
100
|
+
file: ".tool-versions",
|
|
101
|
+
parse: (content) => {
|
|
102
|
+
const match = content.match(/nodejs\s+(\S+)/);
|
|
103
|
+
return match ? match[1] : null;
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
for (const { file, parse } of sources) {
|
|
109
|
+
const filePath = path.join(projectRoot, file);
|
|
110
|
+
if (fs.existsSync(filePath)) {
|
|
111
|
+
try {
|
|
112
|
+
const version = parse(fs.readFileSync(filePath, "utf-8"));
|
|
113
|
+
if (version) return { version, source: file };
|
|
114
|
+
} catch {
|
|
115
|
+
// Continue to next source
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Fallback to current process version
|
|
121
|
+
return {
|
|
122
|
+
version: process.version.replace(/^v/, ""),
|
|
123
|
+
source: "process",
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
128
|
+
// FRAMEWORK DETECTION (Fast, dependency-based)
|
|
129
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
130
|
+
|
|
131
|
+
function detectFramework(projectRoot) {
|
|
132
|
+
const pkgPath = path.join(projectRoot, "package.json");
|
|
133
|
+
if (!fs.existsSync(pkgPath)) {
|
|
134
|
+
return { name: "node", confidence: "low", version: null };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
139
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
140
|
+
|
|
141
|
+
// Priority order (most specific first)
|
|
142
|
+
const frameworks = [
|
|
143
|
+
{ key: "next", name: "Next.js", configs: ["next.config.js", "next.config.mjs", "next.config.ts"] },
|
|
144
|
+
{ key: "@remix-run/react", name: "Remix", configs: ["remix.config.js"] },
|
|
145
|
+
{ key: "@sveltejs/kit", name: "SvelteKit", configs: ["svelte.config.js"] },
|
|
146
|
+
{ key: "nuxt", name: "Nuxt", configs: ["nuxt.config.ts", "nuxt.config.js"] },
|
|
147
|
+
{ key: "@nestjs/core", name: "NestJS", configs: [] },
|
|
148
|
+
{ key: "fastify", name: "Fastify", configs: [] },
|
|
149
|
+
{ key: "express", name: "Express", configs: [] },
|
|
150
|
+
{ key: "hono", name: "Hono", configs: [] },
|
|
151
|
+
{ key: "react", name: "React", configs: ["vite.config.ts", "vite.config.js"] },
|
|
152
|
+
{ key: "vue", name: "Vue", configs: ["vite.config.ts", "vite.config.js"] },
|
|
153
|
+
{ key: "svelte", name: "Svelte", configs: ["vite.config.ts", "vite.config.js"] },
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
for (const fw of frameworks) {
|
|
157
|
+
if (deps[fw.key]) {
|
|
158
|
+
const version = deps[fw.key].replace(/[\^~]/, "");
|
|
159
|
+
const hasConfig = fw.configs.some((cfg) => fs.existsSync(path.join(projectRoot, cfg)));
|
|
160
|
+
return {
|
|
161
|
+
name: fw.name,
|
|
162
|
+
key: fw.key.replace(/^@/, "").replace(/\//g, "-"),
|
|
163
|
+
version,
|
|
164
|
+
confidence: hasConfig ? "high" : "medium",
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Check for TypeScript project
|
|
170
|
+
if (deps.typescript) {
|
|
171
|
+
return { name: "Node.js", key: "node", version: null, confidence: "medium", typescript: true };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return { name: "Node.js", key: "node", version: null, confidence: "low" };
|
|
175
|
+
} catch {
|
|
176
|
+
return { name: "Node.js", key: "node", version: null, confidence: "low" };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
181
|
+
// CI/CD DETECTION
|
|
182
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
183
|
+
|
|
184
|
+
function detectCI(projectRoot) {
|
|
185
|
+
const ciSystems = [
|
|
186
|
+
{ path: ".github/workflows", name: "GitHub Actions", key: "github" },
|
|
187
|
+
{ path: ".gitlab-ci.yml", name: "GitLab CI", key: "gitlab" },
|
|
188
|
+
{ path: ".circleci/config.yml", name: "CircleCI", key: "circleci" },
|
|
189
|
+
{ path: "Jenkinsfile", name: "Jenkins", key: "jenkins" },
|
|
190
|
+
{ path: ".travis.yml", name: "Travis CI", key: "travis" },
|
|
191
|
+
{ path: "azure-pipelines.yml", name: "Azure Pipelines", key: "azure" },
|
|
192
|
+
{ path: "bitbucket-pipelines.yml", name: "Bitbucket Pipelines", key: "bitbucket" },
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
const detected = [];
|
|
196
|
+
for (const ci of ciSystems) {
|
|
197
|
+
const fullPath = path.join(projectRoot, ci.path);
|
|
198
|
+
if (fs.existsSync(fullPath)) {
|
|
199
|
+
detected.push({ name: ci.name, key: ci.key });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Check if vibecheck is already in CI
|
|
204
|
+
let hasVibecheckCI = false;
|
|
205
|
+
const ghWorkflowsDir = path.join(projectRoot, ".github", "workflows");
|
|
206
|
+
if (fs.existsSync(ghWorkflowsDir)) {
|
|
207
|
+
try {
|
|
208
|
+
const workflows = fs.readdirSync(ghWorkflowsDir);
|
|
209
|
+
for (const wf of workflows) {
|
|
210
|
+
if (wf.endsWith(".yml") || wf.endsWith(".yaml")) {
|
|
211
|
+
const content = fs.readFileSync(path.join(ghWorkflowsDir, wf), "utf-8");
|
|
212
|
+
if (content.includes("vibecheck")) {
|
|
213
|
+
hasVibecheckCI = true;
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} catch {
|
|
219
|
+
// Ignore read errors
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
detected: detected.length > 0,
|
|
225
|
+
systems: detected,
|
|
226
|
+
hasVibecheckCI,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
231
|
+
// PERMISSION CHECK
|
|
232
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
233
|
+
|
|
234
|
+
function checkPermissions(targetDir) {
|
|
235
|
+
const vibecheckDir = path.join(targetDir, ".vibecheck");
|
|
236
|
+
const testFile = path.join(vibecheckDir, ".write-test");
|
|
237
|
+
const warnings = [];
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
// Ensure directory exists
|
|
241
|
+
if (!fs.existsSync(vibecheckDir)) {
|
|
242
|
+
fs.mkdirSync(vibecheckDir, { recursive: true });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Test write
|
|
246
|
+
fs.writeFileSync(testFile, "vibecheck-permission-test");
|
|
247
|
+
fs.unlinkSync(testFile);
|
|
248
|
+
|
|
249
|
+
return { ok: true, warnings };
|
|
250
|
+
} catch (e) {
|
|
251
|
+
const isWindows = process.platform === "win32";
|
|
252
|
+
const fixes = isWindows
|
|
253
|
+
? [
|
|
254
|
+
`icacls "${vibecheckDir}" /grant "%USERNAME%":F`,
|
|
255
|
+
`Remove-Item "${vibecheckDir}" -Recurse -Force; vibecheck link`,
|
|
256
|
+
]
|
|
257
|
+
: [
|
|
258
|
+
`chmod 755 "${targetDir}"`,
|
|
259
|
+
`sudo chown -R $USER "${vibecheckDir}"`,
|
|
260
|
+
`rm -rf "${vibecheckDir}" && vibecheck link`,
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
ok: false,
|
|
265
|
+
error: e.code === "EACCES" || e.code === "EPERM" ? "PERMISSION_DENIED" : e.code,
|
|
266
|
+
message: e.message,
|
|
267
|
+
warnings,
|
|
268
|
+
fixes,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
274
|
+
// MANIFEST HASH GENERATION
|
|
275
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
276
|
+
|
|
277
|
+
function generateManifestHash(projectRoot, detection) {
|
|
278
|
+
const hash = crypto.createHash("sha256");
|
|
279
|
+
const inputs = {};
|
|
280
|
+
|
|
281
|
+
// Hash package.json
|
|
282
|
+
const pkgPath = path.join(projectRoot, "package.json");
|
|
283
|
+
if (fs.existsSync(pkgPath)) {
|
|
284
|
+
const pkgContent = fs.readFileSync(pkgPath);
|
|
285
|
+
hash.update(pkgContent);
|
|
286
|
+
inputs.packageJson = "sha256:" + crypto.createHash("sha256").update(pkgContent).digest("hex").slice(0, 12);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Hash lockfile
|
|
290
|
+
const lockfiles = ["pnpm-lock.yaml", "yarn.lock", "package-lock.json", "bun.lockb"];
|
|
291
|
+
for (const lf of lockfiles) {
|
|
292
|
+
const lfPath = path.join(projectRoot, lf);
|
|
293
|
+
if (fs.existsSync(lfPath)) {
|
|
294
|
+
const lfContent = fs.readFileSync(lfPath);
|
|
295
|
+
hash.update(lfContent);
|
|
296
|
+
inputs.lockfile = "sha256:" + crypto.createHash("sha256").update(lfContent).digest("hex").slice(0, 12);
|
|
297
|
+
inputs.lockfileName = lf;
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Hash detection result
|
|
303
|
+
const detectionStr = JSON.stringify(detection);
|
|
304
|
+
hash.update(detectionStr);
|
|
305
|
+
inputs.detection = "sha256:" + crypto.createHash("sha256").update(detectionStr).digest("hex").slice(0, 12);
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
hash: "sha256:" + hash.digest("hex").slice(0, 16),
|
|
309
|
+
inputs,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
314
|
+
// SMART NEXT STEP
|
|
315
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
316
|
+
|
|
317
|
+
function getNextCommand(detection, state) {
|
|
318
|
+
// 1. Permission issues → doctor
|
|
319
|
+
if (state.permissionWarnings && state.permissionWarnings.length > 0) {
|
|
320
|
+
return { cmd: "vibecheck doctor --fix", reason: "fix permission issues" };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// 2. Missing lockfile → doctor
|
|
324
|
+
if (!detection.manifest?.inputs?.lockfile) {
|
|
325
|
+
return { cmd: "vibecheck doctor", reason: "no lockfile detected" };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// 3. No CI detected → suggest CI setup
|
|
329
|
+
if (!detection.ci?.detected) {
|
|
330
|
+
return { cmd: "vibecheck packs ci", reason: "add CI/CD workflow" };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// 4. CI exists but no vibecheck in it → suggest adding
|
|
334
|
+
if (detection.ci?.detected && !detection.ci?.hasVibecheckCI) {
|
|
335
|
+
return { cmd: "vibecheck packs ci", reason: "add vibecheck to CI" };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// 5. Has legacy config → suggest doctor
|
|
339
|
+
if (state.hasLegacyConfig) {
|
|
340
|
+
return { cmd: "vibecheck doctor --migrate", reason: "migrate legacy config" };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// 6. Default → audit (happy path)
|
|
344
|
+
return { cmd: "vibecheck audit", reason: "run your first scan" };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
348
|
+
// FILE WRITING
|
|
349
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
350
|
+
|
|
351
|
+
function writeProjectFiles(projectRoot, projectName, detection, manifest) {
|
|
352
|
+
const created = [];
|
|
353
|
+
const vibecheckDir = path.join(projectRoot, ".vibecheck");
|
|
354
|
+
|
|
355
|
+
// Ensure directory exists
|
|
356
|
+
if (!fs.existsSync(vibecheckDir)) {
|
|
357
|
+
fs.mkdirSync(vibecheckDir, { recursive: true });
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// 1. Write project.json (the receipt)
|
|
361
|
+
const projectJson = {
|
|
362
|
+
$schema: SCHEMA,
|
|
363
|
+
version: VERSION,
|
|
364
|
+
linkedAt: new Date().toISOString(),
|
|
365
|
+
linkedBy: `vibecheck@${CLI_VERSION}`,
|
|
366
|
+
project: {
|
|
367
|
+
name: projectName,
|
|
368
|
+
root: projectRoot,
|
|
369
|
+
},
|
|
370
|
+
detection: {
|
|
371
|
+
packageManager: detection.packageManager,
|
|
372
|
+
framework: detection.framework?.name || "Node.js",
|
|
373
|
+
frameworkKey: detection.framework?.key || "node",
|
|
374
|
+
frameworkVersion: detection.framework?.version || null,
|
|
375
|
+
runtime: "node",
|
|
376
|
+
runtimeVersion: detection.runtime?.version || process.version.replace(/^v/, ""),
|
|
377
|
+
monorepo: detection.monorepo?.isMonorepo
|
|
378
|
+
? {
|
|
379
|
+
type: detection.monorepo.type,
|
|
380
|
+
workspaces: detection.monorepo.workspaces?.map((w) => w.path) || [],
|
|
381
|
+
packages: detection.monorepo.workspaces?.length || 0,
|
|
382
|
+
tools: detection.monorepo.tools || [],
|
|
383
|
+
}
|
|
384
|
+
: null,
|
|
385
|
+
ci: detection.ci?.detected
|
|
386
|
+
? {
|
|
387
|
+
systems: detection.ci.systems?.map((s) => s.key) || [],
|
|
388
|
+
hasVibecheckCI: detection.ci.hasVibecheckCI || false,
|
|
389
|
+
}
|
|
390
|
+
: null,
|
|
391
|
+
},
|
|
392
|
+
manifestHash: manifest.hash,
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
fs.writeFileSync(path.join(vibecheckDir, "project.json"), JSON.stringify(projectJson, null, 2));
|
|
396
|
+
created.push(".vibecheck/project.json");
|
|
397
|
+
|
|
398
|
+
// 2. Write manifest.json (fingerprint)
|
|
399
|
+
const manifestJson = {
|
|
400
|
+
hash: manifest.hash,
|
|
401
|
+
computed: new Date().toISOString(),
|
|
402
|
+
inputs: manifest.inputs,
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
fs.writeFileSync(path.join(vibecheckDir, "manifest.json"), JSON.stringify(manifestJson, null, 2));
|
|
406
|
+
created.push(".vibecheck/manifest.json");
|
|
407
|
+
|
|
408
|
+
// 3. Write .gitignore for vibecheck outputs
|
|
409
|
+
const gitignorePath = path.join(vibecheckDir, ".gitignore");
|
|
410
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
411
|
+
const gitignoreContent = `# VibeCheck generated outputs (safe to ignore)
|
|
412
|
+
results/
|
|
413
|
+
runs/
|
|
414
|
+
checkpoints/
|
|
415
|
+
*.log
|
|
416
|
+
.write-test
|
|
417
|
+
`;
|
|
418
|
+
fs.writeFileSync(gitignorePath, gitignoreContent);
|
|
419
|
+
created.push(".vibecheck/.gitignore");
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return created;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
426
|
+
// OUTPUT FUNCTIONS
|
|
427
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
428
|
+
|
|
429
|
+
function formatDetectionSummary(detection) {
|
|
430
|
+
const parts = [];
|
|
431
|
+
|
|
432
|
+
// Framework
|
|
433
|
+
if (detection.framework?.name && detection.framework.name !== "Node.js") {
|
|
434
|
+
parts.push(detection.framework.name);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Package manager
|
|
438
|
+
if (detection.packageManager && detection.packageManager !== "npm") {
|
|
439
|
+
parts.push(detection.packageManager);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Runtime
|
|
443
|
+
if (detection.runtime?.version) {
|
|
444
|
+
const majorVersion = detection.runtime.version.split(".")[0];
|
|
445
|
+
parts.push(`Node ${majorVersion}`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Monorepo
|
|
449
|
+
if (detection.monorepo?.isMonorepo) {
|
|
450
|
+
const count = detection.monorepo.workspaces?.length || 0;
|
|
451
|
+
parts.push(`${detection.monorepo.type} monorepo (${count} packages)`);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return parts.length > 0 ? parts.join(" + ") : "Node.js";
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function outputSuccess(projectName, detection, next, elapsed, opts) {
|
|
458
|
+
const summary = formatDetectionSummary(detection);
|
|
459
|
+
|
|
460
|
+
if (!opts.quiet) {
|
|
461
|
+
console.log();
|
|
462
|
+
console.log(`${c.green}${sym.success}${c.reset} ${c.bold}Project bound:${c.reset} ${projectName} ${c.dim}(${summary})${c.reset}`);
|
|
463
|
+
console.log(` ${c.cyan}${sym.arrow}${c.reset} Next: ${c.cyan}${next.cmd}${c.reset}`);
|
|
464
|
+
|
|
465
|
+
if (opts.verbose) {
|
|
466
|
+
console.log();
|
|
467
|
+
console.log(`${c.dim} Detection: ${elapsed}ms${c.reset}`);
|
|
468
|
+
console.log(`${c.dim} Manifest: ${detection.manifest?.hash || "unknown"}${c.reset}`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
console.log();
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return LINK_EXIT.SUCCESS;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function outputWarning(projectName, detection, warnings, next, elapsed, opts) {
|
|
478
|
+
const summary = formatDetectionSummary(detection);
|
|
479
|
+
|
|
480
|
+
if (!opts.quiet) {
|
|
481
|
+
console.log();
|
|
482
|
+
console.log(`${c.green}${sym.success}${c.reset} ${c.bold}Project bound:${c.reset} ${projectName} ${c.dim}(${summary})${c.reset}`);
|
|
483
|
+
|
|
484
|
+
for (const warn of warnings) {
|
|
485
|
+
console.log(` ${c.yellow}${sym.warning}${c.reset} ${warn}`);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
console.log(` ${c.cyan}${sym.arrow}${c.reset} Next: ${c.cyan}${next.cmd}${c.reset}`);
|
|
489
|
+
console.log();
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return LINK_EXIT.PARTIAL;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function outputJson(result) {
|
|
496
|
+
console.log(JSON.stringify(result, null, 2));
|
|
497
|
+
return result.success ? LINK_EXIT.SUCCESS : LINK_EXIT.PARTIAL;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function errorNotAProject(opts) {
|
|
501
|
+
if (opts.json) {
|
|
502
|
+
return outputJson({
|
|
503
|
+
success: false,
|
|
504
|
+
error: "NOT_A_PROJECT",
|
|
505
|
+
message: "No package.json found",
|
|
506
|
+
fixes: ["cd /path/to/your/project && vibecheck link", "npm init -y && vibecheck link"],
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
console.log();
|
|
511
|
+
console.log(`${c.red}${sym.error}${c.reset} ${c.bold}No package.json found${c.reset}`);
|
|
512
|
+
console.log();
|
|
513
|
+
console.log(` This doesn't look like a JavaScript/TypeScript project.`);
|
|
514
|
+
console.log();
|
|
515
|
+
console.log(` ${c.bold}Fix:${c.reset}`);
|
|
516
|
+
console.log(` ${c.cyan}cd /path/to/your/project && vibecheck link${c.reset}`);
|
|
517
|
+
console.log(` ${c.cyan}npm init -y && vibecheck link${c.reset} ${c.dim}# Create new project${c.reset}`);
|
|
518
|
+
console.log();
|
|
519
|
+
|
|
520
|
+
return LINK_EXIT.NOT_FOUND;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function errorAlreadyLinked(projectJsonPath, opts) {
|
|
524
|
+
let linkedAt = "unknown";
|
|
525
|
+
try {
|
|
526
|
+
const content = JSON.parse(fs.readFileSync(projectJsonPath, "utf-8"));
|
|
527
|
+
if (content.linkedAt) {
|
|
528
|
+
const date = new Date(content.linkedAt);
|
|
529
|
+
const now = new Date();
|
|
530
|
+
const diffMs = now - date;
|
|
531
|
+
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
532
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
533
|
+
|
|
534
|
+
if (diffDays > 0) {
|
|
535
|
+
linkedAt = `${diffDays} day${diffDays > 1 ? "s" : ""} ago`;
|
|
536
|
+
} else if (diffHours > 0) {
|
|
537
|
+
linkedAt = `${diffHours} hour${diffHours > 1 ? "s" : ""} ago`;
|
|
538
|
+
} else {
|
|
539
|
+
const diffMins = Math.floor(diffMs / (1000 * 60));
|
|
540
|
+
linkedAt = `${diffMins} minute${diffMins !== 1 ? "s" : ""} ago`;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
} catch {
|
|
544
|
+
// Ignore parse errors
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (opts.json) {
|
|
548
|
+
return outputJson({
|
|
549
|
+
success: false,
|
|
550
|
+
error: "ALREADY_LINKED",
|
|
551
|
+
message: `Project already linked (${linkedAt})`,
|
|
552
|
+
linkedAt,
|
|
553
|
+
fixes: ["vibecheck link --force", "vibecheck doctor"],
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
console.log();
|
|
558
|
+
console.log(`${c.yellow}${sym.warning}${c.reset} ${c.bold}Project already linked${c.reset} ${c.dim}(${linkedAt})${c.reset}`);
|
|
559
|
+
console.log();
|
|
560
|
+
console.log(` To re-link: ${c.cyan}vibecheck link --force${c.reset}`);
|
|
561
|
+
console.log(` To check: ${c.cyan}vibecheck doctor${c.reset}`);
|
|
562
|
+
console.log();
|
|
563
|
+
|
|
564
|
+
return LINK_EXIT.ALREADY_LINKED;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function errorPermissionDenied(permCheck, opts) {
|
|
568
|
+
if (opts.json) {
|
|
569
|
+
return outputJson({
|
|
570
|
+
success: false,
|
|
571
|
+
error: "PERMISSION_DENIED",
|
|
572
|
+
message: permCheck.message,
|
|
573
|
+
fixes: permCheck.fixes,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
console.log();
|
|
578
|
+
console.log(`${c.red}${sym.error}${c.reset} ${c.bold}Cannot write to .vibecheck/${c.reset}`);
|
|
579
|
+
console.log();
|
|
580
|
+
console.log(` ${c.bold}Fix:${c.reset} Run one of these commands:`);
|
|
581
|
+
for (const fix of permCheck.fixes) {
|
|
582
|
+
console.log(` ${c.cyan}${fix}${c.reset}`);
|
|
583
|
+
}
|
|
584
|
+
console.log();
|
|
585
|
+
|
|
586
|
+
return LINK_EXIT.PERMISSION_DENIED;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
590
|
+
// ARGS PARSER
|
|
591
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
592
|
+
|
|
593
|
+
function parseArgs(args) {
|
|
594
|
+
const { flags: globalFlags, cleanArgs } = parseGlobalFlags(args);
|
|
595
|
+
|
|
596
|
+
const opts = {
|
|
597
|
+
path: globalFlags.path || ".",
|
|
598
|
+
force: false,
|
|
599
|
+
dryRun: false,
|
|
600
|
+
json: globalFlags.json || false,
|
|
601
|
+
verbose: globalFlags.verbose || false,
|
|
602
|
+
quiet: globalFlags.quiet || false,
|
|
603
|
+
noBanner: globalFlags.noBanner || false,
|
|
604
|
+
help: globalFlags.help || false,
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
for (let i = 0; i < cleanArgs.length; i++) {
|
|
608
|
+
const arg = cleanArgs[i];
|
|
609
|
+
if (arg === "--force" || arg === "-f") opts.force = true;
|
|
610
|
+
if (arg === "--dry-run" || arg === "--dryrun") opts.dryRun = true;
|
|
611
|
+
if (arg.startsWith("--path=")) opts.path = arg.split("=")[1];
|
|
612
|
+
if ((arg === "--path" || arg === "-p") && cleanArgs[i + 1]) opts.path = cleanArgs[++i];
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return opts;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
619
|
+
// HELP
|
|
620
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
621
|
+
|
|
622
|
+
function printHelp() {
|
|
623
|
+
console.log(`
|
|
624
|
+
${c.bold}vibecheck link${c.reset} - Instant project binding
|
|
625
|
+
|
|
626
|
+
${c.bold}Usage:${c.reset}
|
|
627
|
+
vibecheck link [options]
|
|
628
|
+
|
|
629
|
+
${c.bold}Options:${c.reset}
|
|
630
|
+
${c.cyan}--path, -p <dir>${c.reset} Project path ${c.dim}(default: current directory)${c.reset}
|
|
631
|
+
${c.cyan}--force, -f${c.reset} Re-link even if already linked
|
|
632
|
+
${c.cyan}--dry-run${c.reset} Preview without writing files
|
|
633
|
+
${c.cyan}--json${c.reset} Output as JSON (CI-friendly)
|
|
634
|
+
${c.cyan}--verbose, -v${c.reset} Show detection details
|
|
635
|
+
${c.cyan}--quiet, -q${c.reset} Suppress all output except errors
|
|
636
|
+
${c.cyan}--help, -h${c.reset} Show this help
|
|
637
|
+
|
|
638
|
+
${c.bold}What it does:${c.reset}
|
|
639
|
+
${c.dim}1.${c.reset} Finds project root (package.json or .git)
|
|
640
|
+
${c.dim}2.${c.reset} Detects: package manager, framework, runtime, monorepo, CI
|
|
641
|
+
${c.dim}3.${c.reset} Creates .vibecheck/project.json (the "bound" receipt)
|
|
642
|
+
${c.dim}4.${c.reset} Generates manifest hash for change detection
|
|
643
|
+
${c.dim}5.${c.reset} Suggests smart next command based on findings
|
|
644
|
+
|
|
645
|
+
${c.bold}Examples:${c.reset}
|
|
646
|
+
${c.cyan}vibecheck link${c.reset} # Bind current project
|
|
647
|
+
${c.cyan}vibecheck link -p ~/my-app${c.reset} # Bind specific project
|
|
648
|
+
${c.cyan}vibecheck link --force${c.reset} # Re-bind existing project
|
|
649
|
+
${c.cyan}vibecheck link --json${c.reset} # JSON output for CI
|
|
650
|
+
|
|
651
|
+
${c.bold}Exit Codes:${c.reset}
|
|
652
|
+
${c.green}0${c.reset} Success - project bound
|
|
653
|
+
${c.yellow}1${c.reset} Partial - bound with warnings
|
|
654
|
+
${c.red}3${c.reset} User error - invalid arguments
|
|
655
|
+
${c.red}4${c.reset} Not found - no package.json
|
|
656
|
+
${c.red}11${c.reset} Permission denied - can't write to .vibecheck/
|
|
657
|
+
${c.red}12${c.reset} Already linked - use --force to re-link
|
|
658
|
+
`);
|
|
659
|
+
return 0;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
663
|
+
// MAIN
|
|
664
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
665
|
+
|
|
666
|
+
async function runLink(args) {
|
|
667
|
+
const opts = parseArgs(args);
|
|
668
|
+
|
|
669
|
+
if (opts.help) {
|
|
670
|
+
return printHelp();
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const startTime = Date.now();
|
|
674
|
+
const warnings = [];
|
|
675
|
+
|
|
676
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
677
|
+
// 1. Find project root
|
|
678
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
679
|
+
const projectRoot = findProjectRoot(opts.path);
|
|
680
|
+
if (!projectRoot) {
|
|
681
|
+
return errorNotAProject(opts);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const projectName = path.basename(projectRoot);
|
|
685
|
+
|
|
686
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
687
|
+
// 2. Check if already linked
|
|
688
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
689
|
+
const projectJsonPath = path.join(projectRoot, ".vibecheck", "project.json");
|
|
690
|
+
if (fs.existsSync(projectJsonPath) && !opts.force) {
|
|
691
|
+
return errorAlreadyLinked(projectJsonPath, opts);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
695
|
+
// 3. Check permissions
|
|
696
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
697
|
+
const permCheck = checkPermissions(projectRoot);
|
|
698
|
+
if (!permCheck.ok) {
|
|
699
|
+
return errorPermissionDenied(permCheck, opts);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
703
|
+
// 4. Run detection pipeline (the fast part)
|
|
704
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
705
|
+
const detection = {
|
|
706
|
+
packageManager: detectPackageManager(projectRoot),
|
|
707
|
+
framework: detectFramework(projectRoot),
|
|
708
|
+
runtime: detectRuntime(projectRoot),
|
|
709
|
+
monorepo: detectMonorepo(projectRoot),
|
|
710
|
+
ci: detectCI(projectRoot),
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
// Framework detection warnings
|
|
714
|
+
if (detection.framework?.confidence === "low") {
|
|
715
|
+
warnings.push("Could not detect framework (using Node.js defaults)");
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
719
|
+
// 5. Generate manifest hash
|
|
720
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
721
|
+
const manifest = generateManifestHash(projectRoot, detection);
|
|
722
|
+
detection.manifest = manifest;
|
|
723
|
+
|
|
724
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
725
|
+
// 6. Write files (unless dry-run)
|
|
726
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
727
|
+
let created = [];
|
|
728
|
+
if (!opts.dryRun) {
|
|
729
|
+
created = writeProjectFiles(projectRoot, projectName, detection, manifest);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
733
|
+
// 7. Determine next command
|
|
734
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
735
|
+
const state = {
|
|
736
|
+
permissionWarnings: permCheck.warnings,
|
|
737
|
+
hasLegacyConfig:
|
|
738
|
+
fs.existsSync(path.join(projectRoot, ".vibecheckrc")) ||
|
|
739
|
+
fs.existsSync(path.join(projectRoot, ".vibecheckrc.json")),
|
|
740
|
+
};
|
|
741
|
+
const next = getNextCommand(detection, state);
|
|
742
|
+
|
|
743
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
744
|
+
// 8. Output
|
|
745
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
746
|
+
const elapsed = Date.now() - startTime;
|
|
747
|
+
|
|
748
|
+
if (opts.json) {
|
|
749
|
+
return outputJson({
|
|
750
|
+
success: true,
|
|
751
|
+
elapsed,
|
|
752
|
+
project: {
|
|
753
|
+
name: projectName,
|
|
754
|
+
root: projectRoot,
|
|
755
|
+
hash: manifest.hash,
|
|
756
|
+
},
|
|
757
|
+
detection: {
|
|
758
|
+
packageManager: detection.packageManager,
|
|
759
|
+
framework: detection.framework?.name || "Node.js",
|
|
760
|
+
frameworkVersion: detection.framework?.version || null,
|
|
761
|
+
runtime: `node@${detection.runtime?.version || "unknown"}`,
|
|
762
|
+
monorepo: detection.monorepo?.isMonorepo
|
|
763
|
+
? { type: detection.monorepo.type, packages: detection.monorepo.workspaces?.length || 0 }
|
|
764
|
+
: null,
|
|
765
|
+
ci: detection.ci?.detected ? { systems: detection.ci.systems?.map((s) => s.key) || [] } : null,
|
|
766
|
+
},
|
|
767
|
+
created,
|
|
768
|
+
warnings,
|
|
769
|
+
nextCommand: next.cmd,
|
|
770
|
+
dryRun: opts.dryRun,
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if (warnings.length > 0) {
|
|
775
|
+
return outputWarning(projectName, detection, warnings, next, elapsed, opts);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
return outputSuccess(projectName, detection, next, elapsed, opts);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
782
|
+
// EXPORTS
|
|
783
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
784
|
+
|
|
785
|
+
module.exports = { runLink };
|