@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.
Files changed (118) hide show
  1. package/README.md +135 -63
  2. package/bin/_deprecations.js +447 -19
  3. package/bin/_router.js +1 -1
  4. package/bin/registry.js +347 -280
  5. package/bin/runners/context/generators/cursor-enhanced.js +2439 -0
  6. package/bin/runners/lib/agent-firewall/enforcement/gateway.js +1059 -0
  7. package/bin/runners/lib/agent-firewall/enforcement/index.js +98 -0
  8. package/bin/runners/lib/agent-firewall/enforcement/mode.js +318 -0
  9. package/bin/runners/lib/agent-firewall/enforcement/orchestrator.js +484 -0
  10. package/bin/runners/lib/agent-firewall/enforcement/proof-artifact.js +418 -0
  11. package/bin/runners/lib/agent-firewall/enforcement/schemas/change-event.schema.json +173 -0
  12. package/bin/runners/lib/agent-firewall/enforcement/schemas/intent.schema.json +181 -0
  13. package/bin/runners/lib/agent-firewall/enforcement/schemas/verdict.schema.json +222 -0
  14. package/bin/runners/lib/agent-firewall/enforcement/verdict-v2.js +333 -0
  15. package/bin/runners/lib/agent-firewall/index.js +200 -0
  16. package/bin/runners/lib/agent-firewall/integration/index.js +20 -0
  17. package/bin/runners/lib/agent-firewall/integration/ship-gate.js +437 -0
  18. package/bin/runners/lib/agent-firewall/intent/alignment-engine.js +634 -0
  19. package/bin/runners/lib/agent-firewall/intent/auto-detect.js +426 -0
  20. package/bin/runners/lib/agent-firewall/intent/index.js +102 -0
  21. package/bin/runners/lib/agent-firewall/intent/schema.js +352 -0
  22. package/bin/runners/lib/agent-firewall/intent/store.js +283 -0
  23. package/bin/runners/lib/agent-firewall/interception/fs-interceptor.js +502 -0
  24. package/bin/runners/lib/agent-firewall/interception/index.js +23 -0
  25. package/bin/runners/lib/agent-firewall/interceptor/base.js +7 -3
  26. package/bin/runners/lib/agent-firewall/session/collector.js +451 -0
  27. package/bin/runners/lib/agent-firewall/session/index.js +26 -0
  28. package/bin/runners/lib/artifact-envelope.js +540 -0
  29. package/bin/runners/lib/auth-shared.js +977 -0
  30. package/bin/runners/lib/checkpoint.js +941 -0
  31. package/bin/runners/lib/cleanup/engine.js +571 -0
  32. package/bin/runners/lib/cleanup/index.js +53 -0
  33. package/bin/runners/lib/cleanup/output.js +375 -0
  34. package/bin/runners/lib/cleanup/rules.js +1060 -0
  35. package/bin/runners/lib/doctor/diagnosis-receipt.js +454 -0
  36. package/bin/runners/lib/doctor/failure-signatures.js +526 -0
  37. package/bin/runners/lib/doctor/fix-script.js +336 -0
  38. package/bin/runners/lib/doctor/modules/build-tools.js +453 -0
  39. package/bin/runners/lib/doctor/modules/index.js +62 -3
  40. package/bin/runners/lib/doctor/modules/os-quirks.js +706 -0
  41. package/bin/runners/lib/doctor/modules/repo-integrity.js +485 -0
  42. package/bin/runners/lib/doctor/safe-repair.js +384 -0
  43. package/bin/runners/lib/engine/ast-cache.js +210 -210
  44. package/bin/runners/lib/engine/auth-extractor.js +211 -211
  45. package/bin/runners/lib/engine/billing-extractor.js +112 -112
  46. package/bin/runners/lib/engine/enforcement-extractor.js +100 -100
  47. package/bin/runners/lib/engine/env-extractor.js +207 -207
  48. package/bin/runners/lib/engine/express-extractor.js +208 -208
  49. package/bin/runners/lib/engine/extractors.js +849 -849
  50. package/bin/runners/lib/engine/index.js +207 -207
  51. package/bin/runners/lib/engine/repo-index.js +514 -514
  52. package/bin/runners/lib/engine/types.js +124 -124
  53. package/bin/runners/lib/engines/attack-detector.js +1192 -0
  54. package/bin/runners/lib/entitlements-v2.js +2 -2
  55. package/bin/runners/lib/missions/briefing.js +427 -0
  56. package/bin/runners/lib/missions/checkpoint.js +753 -0
  57. package/bin/runners/lib/missions/hardening.js +851 -0
  58. package/bin/runners/lib/missions/plan.js +421 -32
  59. package/bin/runners/lib/missions/safety-gates.js +645 -0
  60. package/bin/runners/lib/missions/schema.js +478 -0
  61. package/bin/runners/lib/packs/bundle.js +675 -0
  62. package/bin/runners/lib/packs/evidence-pack.js +671 -0
  63. package/bin/runners/lib/packs/pack-factory.js +837 -0
  64. package/bin/runners/lib/packs/permissions-pack.js +686 -0
  65. package/bin/runners/lib/packs/proof-graph-pack.js +779 -0
  66. package/bin/runners/lib/safelist/index.js +96 -0
  67. package/bin/runners/lib/safelist/integration.js +334 -0
  68. package/bin/runners/lib/safelist/matcher.js +696 -0
  69. package/bin/runners/lib/safelist/schema.js +948 -0
  70. package/bin/runners/lib/safelist/store.js +438 -0
  71. package/bin/runners/lib/schemas/ship-manifest.schema.json +251 -0
  72. package/bin/runners/lib/ship-gate.js +832 -0
  73. package/bin/runners/lib/ship-manifest.js +1153 -0
  74. package/bin/runners/lib/ship-output.js +1 -1
  75. package/bin/runners/lib/unified-cli-output.js +710 -383
  76. package/bin/runners/lib/upsell.js +3 -3
  77. package/bin/runners/lib/why-tree.js +650 -0
  78. package/bin/runners/runAllowlist.js +33 -4
  79. package/bin/runners/runApprove.js +240 -1122
  80. package/bin/runners/runAudit.js +692 -0
  81. package/bin/runners/runAuth.js +325 -29
  82. package/bin/runners/runCheckpoint.js +442 -494
  83. package/bin/runners/runCleanup.js +343 -0
  84. package/bin/runners/runDoctor.js +269 -19
  85. package/bin/runners/runFix.js +411 -32
  86. package/bin/runners/runForge.js +411 -0
  87. package/bin/runners/runIntent.js +906 -0
  88. package/bin/runners/runKickoff.js +878 -0
  89. package/bin/runners/runLaunch.js +2000 -0
  90. package/bin/runners/runLink.js +785 -0
  91. package/bin/runners/runMcp.js +1741 -837
  92. package/bin/runners/runPacks.js +2089 -0
  93. package/bin/runners/runPolish.js +41 -0
  94. package/bin/runners/runReality.js +178 -1
  95. package/bin/runners/runSafelist.js +1190 -0
  96. package/bin/runners/runScan.js +21 -9
  97. package/bin/runners/runShield.js +1282 -0
  98. package/bin/runners/runShip.js +395 -16
  99. package/bin/vibecheck.js +34 -6
  100. package/mcp-server/README.md +117 -158
  101. package/mcp-server/handlers/index.ts +2 -2
  102. package/mcp-server/handlers/tool-handler.ts +50 -11
  103. package/mcp-server/index.js +16 -0
  104. package/mcp-server/intent-firewall-interceptor.js +529 -0
  105. package/mcp-server/lib/executor.ts +5 -5
  106. package/mcp-server/lib/index.ts +14 -4
  107. package/mcp-server/lib/sandbox.test.ts +4 -4
  108. package/mcp-server/lib/sandbox.ts +2 -2
  109. package/mcp-server/manifest.json +473 -0
  110. package/mcp-server/package.json +1 -1
  111. package/mcp-server/registry/tool-registry.js +315 -523
  112. package/mcp-server/registry/tools.json +442 -428
  113. package/mcp-server/registry.test.ts +18 -12
  114. package/mcp-server/tier-auth.js +68 -11
  115. package/mcp-server/tools-v3.js +70 -16
  116. package/mcp-server/tsconfig.json +1 -0
  117. package/package.json +2 -1
  118. 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 };