vibe-me 2.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/vibes.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // vibes — Semantic repository layers + standardized docs for humans and AI
3
+ // vibes — The complete semantic layer for any project
4
+ // Repository memory + Product memory + Business memory + AI instructions
4
5
  // Zero dependencies. Single file. Works with npx.
5
6
 
6
7
  const fs = require('fs');
@@ -14,12 +15,24 @@ const VIBE_DIR = '.vibe';
14
15
  const DOCS_DIR = 'docs';
15
16
  const TEMPLATES_DIR = path.join(__dirname, '..', 'templates');
16
17
  const SPEC_DIR = path.join(__dirname, '..', 'spec');
18
+ const HUB_CONFIG = path.join(process.env.USERPROFILE || process.env.HOME || '', '.vibes-hub');
17
19
 
20
+ // All .vibe/ files — the full suite, always installed
18
21
  const VIBE_FILES = [
22
+ // Core — repository memory
19
23
  'purpose.md', 'architecture.md', 'flows.md',
20
- 'entities.md', 'decisions.md', 'state.json'
24
+ 'entities.md', 'decisions.md', 'state.json',
25
+ // Living context
26
+ 'context.md',
27
+ // AI agent guide
28
+ 'ai.md',
29
+ // Product memory
30
+ 'product.md', 'users.md', 'metrics.md', 'experiments.md',
31
+ // Business memory
32
+ 'business.md', 'market.md', 'risks.md',
21
33
  ];
22
34
 
35
+ // All docs/ files
23
36
  const DOCS_FILES = [
24
37
  'README.md', 'topology.md', 'architecture.md', 'api.md',
25
38
  'issues.md', 'resolved.md', 'roadmap.md', 'developer_guide.md',
@@ -31,7 +44,7 @@ const DOCS_EXTRA = ['decisions/0001-template.md'];
31
44
  const SPEC_FILE = 'VIBE_GUIDE.md';
32
45
 
33
46
  // ─────────────────────────────────────────────
34
- // Colors (no dependencies raw ANSI)
47
+ // Colors (raw ANSI, zero dependencies)
35
48
  // ─────────────────────────────────────────────
36
49
 
37
50
  const bold = (s) => `\x1b[1m${s}\x1b[0m`;
@@ -40,7 +53,6 @@ const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
40
53
  const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
41
54
  const dim = (s) => `\x1b[2m${s}\x1b[0m`;
42
55
  const red = (s) => `\x1b[31m${s}\x1b[0m`;
43
- const magenta = (s) => `\x1b[35m${s}\x1b[0m`;
44
56
 
45
57
  // ─────────────────────────────────────────────
46
58
  // Helpers
@@ -60,6 +72,8 @@ function getProjectName(dir) {
60
72
  ['Cargo.toml', /^name\s*=\s*"(.+)"/m],
61
73
  ['go.mod', /^module\s+(.+)/m],
62
74
  ['pyproject.toml', /^name\s*=\s*"(.+)"/m],
75
+ ['setup.py', /name\s*=\s*['"](.+?)['"]/m],
76
+ ['CMakeLists.txt', /project\s*\(\s*(\S+)/m],
63
77
  ]) {
64
78
  const p = path.join(dir, file);
65
79
  if (!exists(p)) continue;
@@ -95,13 +109,24 @@ function cmdInit(targetDir) {
95
109
 
96
110
  console.log('');
97
111
  console.log(bold(' ⚡ vibes init'));
98
- console.log(dim(` Creating .vibe/ semantic layer for ${cyan(name)}`));
112
+ console.log(dim(` Creating .vibe/ for ${cyan(name)} — full suite`));
99
113
  console.log('');
100
114
 
101
115
  fs.mkdirSync(vibeDir, { recursive: true });
102
116
 
117
+ // Group labels for visual output
118
+ const groups = {
119
+ 'purpose.md': '── Core (repository memory) ──',
120
+ 'context.md': '── Living Context ──',
121
+ 'ai.md': '── AI Agent Guide ──',
122
+ 'product.md': '── Product Memory ──',
123
+ 'business.md': '── Business Memory ──',
124
+ };
125
+
103
126
  let count = 0;
104
127
  for (const file of VIBE_FILES) {
128
+ if (groups[file]) console.log(dim(` ${groups[file]}`));
129
+
105
130
  const src = path.join(TEMPLATES_DIR, file);
106
131
  const dest = path.join(vibeDir, file);
107
132
  if (file === 'state.json') {
@@ -117,6 +142,7 @@ function cmdInit(targetDir) {
117
142
 
118
143
  // Copy guide
119
144
  copy(path.join(SPEC_DIR, SPEC_FILE), path.join(vibeDir, SPEC_FILE));
145
+ console.log('');
120
146
  console.log(green(' ✔ ') + dim('.vibe/') + SPEC_FILE + dim(' (agent instructions)'));
121
147
  count++;
122
148
 
@@ -130,29 +156,24 @@ function cmdDocs(targetDir) {
130
156
  const name = getProjectName(targetDir);
131
157
 
132
158
  if (exists(docsDir)) {
133
- // Check if it has content already
134
159
  const contents = fs.readdirSync(docsDir);
135
160
  if (contents.length > 0) {
136
161
  console.log(yellow('\n ⚠ docs/ already exists with ' + contents.length + ' items.'));
137
- console.log(dim(' Skipping files that already exist. Adding missing ones only.\n'));
162
+ console.log(dim(' Adding missing files only.\n'));
138
163
  }
139
164
  }
140
165
 
141
166
  console.log('');
142
167
  console.log(bold(' 📄 vibes docs'));
143
- console.log(dim(` Creating docs/ structure for ${cyan(name)}`));
168
+ console.log(dim(` Creating docs/ for ${cyan(name)}`));
144
169
  console.log('');
145
170
 
146
- // Create docs dir and subdirs
147
171
  fs.mkdirSync(docsDir, { recursive: true });
148
172
  for (const sub of DOCS_DIRS) {
149
173
  fs.mkdirSync(path.join(docsDir, sub), { recursive: true });
150
174
  }
151
175
 
152
- let created = 0;
153
- let skipped = 0;
154
-
155
- // Copy doc files (skip if they already exist)
176
+ let created = 0, skipped = 0;
156
177
  for (const file of [...DOCS_FILES, ...DOCS_EXTRA]) {
157
178
  const src = path.join(TEMPLATES_DIR, 'docs', file);
158
179
  const dest = path.join(docsDir, file);
@@ -167,19 +188,8 @@ function cmdDocs(targetDir) {
167
188
  }
168
189
 
169
190
  console.log('');
170
- if (skipped > 0) {
171
- console.log(green(` ✔ Created ${created} files`) + dim(`, skipped ${skipped} existing`));
172
- } else {
173
- console.log(green(` ✔ Created ${created} files in docs/`));
174
- }
175
- console.log('');
176
- console.log(bold(' Next steps:'));
177
- console.log('');
178
- console.log(' Tell your AI agent:');
179
- console.log('');
180
- console.log(cyan(' "Read .vibe/VIBE_GUIDE.md for context, then analyze'));
181
- console.log(cyan(' the codebase and fill out all skeleton files in docs/.'));
182
- console.log(cyan(' Ask me any questions you can\'t answer from the code."'));
191
+ if (skipped > 0) console.log(green(` ✔ Created ${created} files`) + dim(`, skipped ${skipped} existing`));
192
+ else console.log(green(` ✔ Created ${created} files in docs/`));
183
193
  console.log('');
184
194
  }
185
195
 
@@ -197,12 +207,129 @@ function cmdCheck(targetDir) {
197
207
  console.log('');
198
208
  console.log(bold(' 🔍 vibes check'));
199
209
 
200
- let passed = 0, failed = 0, warnings = 0;
210
+ let passed = 0, failed = 0, warnings = 0, na = 0;
211
+
212
+ // ─── Universal quality checks ───
213
+ function checkQuality(content, file) {
214
+ const issues = [];
215
+
216
+ // FAIL: Still has template instruction comments
217
+ if (content.includes('INSTRUCTIONS FOR AI AGENT')) {
218
+ issues.push({ level: 'fail', msg: 'still contains template instructions (unfilled)' });
219
+ return issues;
220
+ }
221
+
222
+ // FAIL: Too short to be real content
223
+ const lines = content.split('\n').length;
224
+ if (lines < 8) {
225
+ issues.push({ level: 'fail', msg: `only ${lines} lines — needs real content` });
226
+ return issues;
227
+ }
228
+
229
+ // WARN: Has unfilled placeholder brackets like [Component Name]
230
+ const placeholders = content.match(/\[(?:Component|Persona|Experiment|Decision|Error|Deliverable|Feature|Issue)\s*\w*\]/gi);
231
+ if (placeholders && placeholders.length > 1) {
232
+ issues.push({ level: 'warn', msg: `${placeholders.length} unfilled placeholders (${placeholders[0]}, ...)` });
233
+ }
234
+
235
+ // WARN: Empty bullet points (lines that are just "- " or "- \n")
236
+ const emptyBullets = (content.match(/^- ?\s*$/gm) || []).length;
237
+ if (emptyBullets > 2) {
238
+ issues.push({ level: 'warn', msg: `${emptyBullets} empty bullet points` });
239
+ }
201
240
 
202
- // Check .vibe/
241
+ // WARN: Empty table rows (| | | |)
242
+ const emptyRows = (content.match(/^\|\s*\|\s*\|\s*\|/gm) || []).length;
243
+ if (emptyRows > 1) {
244
+ issues.push({ level: 'warn', msg: `${emptyRows} empty table rows` });
245
+ }
246
+
247
+ return issues;
248
+ }
249
+
250
+ // ─── File-specific structure checks ───
251
+ function checkStructure(content, file) {
252
+ const issues = [];
253
+
254
+ switch (file) {
255
+ case 'purpose.md':
256
+ if (!content.toLowerCase().includes('not do') && !content.toLowerCase().includes('does not'))
257
+ issues.push({ level: 'warn', msg: 'missing "NOT do" / scope boundary section' });
258
+ if (!content.toLowerCase().includes('who'))
259
+ issues.push({ level: 'warn', msg: 'doesn\'t mention who uses it' });
260
+ break;
261
+
262
+ case 'architecture.md':
263
+ if (!content.includes('→') && !content.includes('->') && !content.includes('connects') && !content.includes('talks to'))
264
+ issues.push({ level: 'warn', msg: 'no data flow described (missing → or connection language)' });
265
+ break;
266
+
267
+ case 'flows.md':
268
+ const flowHeaders = (content.match(/^##\s+/gm) || []).length;
269
+ if (flowHeaders < 2) issues.push({ level: 'warn', msg: `only ${flowHeaders} flow(s) — most projects have 3+` });
270
+ break;
271
+
272
+ case 'entities.md':
273
+ if (!content.toLowerCase().includes('depends') && !content.toLowerCase().includes('relationship'))
274
+ issues.push({ level: 'warn', msg: 'no dependency/relationship info for entities' });
275
+ break;
276
+
277
+ case 'decisions.md':
278
+ const decisionHeaders = (content.match(/^##\s+/gm) || []).length;
279
+ if (decisionHeaders < 2) issues.push({ level: 'warn', msg: `only ${decisionHeaders} decision(s) — aim for 3+` });
280
+ if (!content.includes('Depends On') && !content.includes('Threatened By') && !content.includes('depends on'))
281
+ issues.push({ level: 'warn', msg: 'no dependency graph fields (Depends On, Threatened By)' });
282
+ break;
283
+
284
+ case 'state.json':
285
+ try {
286
+ const s = JSON.parse(content);
287
+ if (!s.vibe_updated) issues.push({ level: 'warn', msg: 'missing vibe_updated timestamp' });
288
+ else {
289
+ const days = (Date.now() - new Date(s.vibe_updated).getTime()) / 86400000;
290
+ if (days > 30) issues.push({ level: 'warn', msg: `last updated ${Math.floor(days)} days ago — stale?` });
291
+ }
292
+ if (!s.project) issues.push({ level: 'warn', msg: 'missing project name' });
293
+ } catch {
294
+ issues.push({ level: 'fail', msg: 'invalid JSON' });
295
+ }
296
+ break;
297
+
298
+ case 'context.md':
299
+ if (!content.includes('## Current Focus'))
300
+ issues.push({ level: 'warn', msg: 'missing "Current Focus" section' });
301
+ else {
302
+ const focusIdx = content.indexOf('## Current Focus');
303
+ const nextSection = content.indexOf('##', focusIdx + 16);
304
+ const focusContent = content.substring(focusIdx, nextSection > 0 ? nextSection : undefined);
305
+ const focusBullets = (focusContent.match(/^- .{3,}/gm) || []).length;
306
+ if (focusBullets === 0) issues.push({ level: 'warn', msg: 'Current Focus has no entries' });
307
+ }
308
+ break;
309
+
310
+ case 'ai.md':
311
+ if (!content.toLowerCase().includes('never') && !content.toLowerCase().includes('don\'t') && !content.toLowerCase().includes('do not'))
312
+ issues.push({ level: 'warn', msg: 'no constraints defined — what should agents NOT touch?' });
313
+ break;
314
+
315
+ case 'product.md':
316
+ if (!content.toLowerCase().includes('retention') && !content.toLowerCase().includes('why') && !content.toLowerCase().includes('value'))
317
+ issues.push({ level: 'warn', msg: 'missing value prop or retention drivers' });
318
+ break;
319
+
320
+ case 'users.md':
321
+ if (!content.toLowerCase().includes('frustrat') && !content.toLowerCase().includes('fear') && !content.toLowerCase().includes('pain') && !content.toLowerCase().includes('goal'))
322
+ issues.push({ level: 'warn', msg: 'missing emotional reality (frustrations, fears, goals)' });
323
+ break;
324
+ }
325
+
326
+ return issues;
327
+ }
328
+
329
+ // ─── Run checks on .vibe/ ───
203
330
  if (hasVibe) {
204
331
  console.log('');
205
- console.log(dim(' ── .vibe/ (semantic layer) ──'));
332
+ console.log(dim(' ── .vibe/ ──'));
206
333
  console.log('');
207
334
 
208
335
  for (const file of VIBE_FILES) {
@@ -212,34 +339,35 @@ function cmdCheck(targetDir) {
212
339
  const content = fs.readFileSync(fp, 'utf-8').trim();
213
340
  const lines = content.split('\n').length;
214
341
 
215
- if (lines < 5) { console.log(yellow(' ⚠ ') + file + ` — looks empty (${lines} lines)`); warnings++; continue; }
216
-
217
- // File-specific quality checks
218
- let warn = null;
219
- if (file === 'purpose.md' && !content.toLowerCase().includes('not do')) warn = 'missing "NOT do" section';
220
- if (file === 'decisions.md' && (content.match(/^## Why /gm) || []).length < 2) warn = 'fewer than 2 decisions';
221
- if (file === 'entities.md' && !content.includes('What depends on it')) warn = 'missing "What depends on it?" fields';
222
- if (file === 'flows.md' && (content.match(/^## \d+\./gm) || []).length < 2) warn = 'fewer than 2 flows';
223
- if (file === 'state.json') {
224
- try {
225
- const s = JSON.parse(content);
226
- if (!s.vibe_updated) warn = 'missing vibe_updated timestamp';
227
- else {
228
- const days = (Date.now() - new Date(s.vibe_updated).getTime()) / 86400000;
229
- if (days > 30) warn = `last updated ${Math.floor(days)} days ago`;
230
- }
231
- } catch { console.log(red(' ✖ ') + file + ' — invalid JSON'); failed++; continue; }
342
+ // N/A check
343
+ if (content.match(/^N\/A/m)) {
344
+ console.log(dim(' ⊘ ') + file + dim(' — N/A (not applicable)'));
345
+ na++;
346
+ continue;
232
347
  }
233
348
 
234
- if (warn) { console.log(yellow(' ⚠ ') + file + ` — ${warn}`); warnings++; }
235
- else { console.log(green(' ✔ ') + file + ` — ${lines} lines`); passed++; }
349
+ // Run quality + structure checks
350
+ const allIssues = [...checkQuality(content, file), ...checkStructure(content, file)];
351
+ const fails = allIssues.filter(i => i.level === 'fail');
352
+ const warns = allIssues.filter(i => i.level === 'warn');
353
+
354
+ if (fails.length > 0) {
355
+ console.log(red(' ✖ ') + file + ' — ' + fails[0].msg);
356
+ failed++;
357
+ } else if (warns.length > 0) {
358
+ console.log(yellow(' ⚠ ') + file + ` — ${lines} lines` + yellow(` (${warns.map(w => w.msg).join('; ')})`));
359
+ warnings++;
360
+ } else {
361
+ console.log(green(' ✔ ') + file + ` — ${lines} lines`);
362
+ passed++;
363
+ }
236
364
  }
237
365
  }
238
366
 
239
- // Check docs/
367
+ // ─── Run checks on docs/ ───
240
368
  if (hasDocs) {
241
369
  console.log('');
242
- console.log(dim(' ── docs/ (operational docs) ──'));
370
+ console.log(dim(' ── docs/ ──'));
243
371
  console.log('');
244
372
 
245
373
  for (const file of DOCS_FILES) {
@@ -249,15 +377,31 @@ function cmdCheck(targetDir) {
249
377
  const content = fs.readFileSync(fp, 'utf-8').trim();
250
378
  const lines = content.split('\n').length;
251
379
 
252
- if (lines < 5) { console.log(yellow(' ⚠ ') + file + ` — looks empty (${lines} lines)`); warnings++; }
253
- else { console.log(green(' ✔ ') + file + ` — ${lines} lines`); passed++; }
380
+ const allIssues = checkQuality(content, file);
381
+ const fails = allIssues.filter(i => i.level === 'fail');
382
+ const warns = allIssues.filter(i => i.level === 'warn');
383
+
384
+ if (fails.length > 0) {
385
+ console.log(red(' ✖ ') + file + ' — ' + fails[0].msg);
386
+ failed++;
387
+ } else if (warns.length > 0) {
388
+ console.log(yellow(' ⚠ ') + file + ` — ${lines} lines` + yellow(` (${warns.map(w => w.msg).join('; ')})`));
389
+ warnings++;
390
+ } else {
391
+ console.log(green(' ✔ ') + file + ` — ${lines} lines`);
392
+ passed++;
393
+ }
254
394
  }
255
395
  }
256
396
 
397
+ // ─── Summary ───
257
398
  console.log('');
258
- if (failed > 0) console.log(red(` Result: ${failed} failed, ${warnings} warnings, ${passed} passed`));
259
- else if (warnings > 0) console.log(yellow(` Result: ${warnings} warnings, ${passed} passed`));
260
- else console.log(green(` Result: All ${passed} files pass ✔`));
399
+ let result = '';
400
+ if (failed > 0) result = red(`${failed} failed`);
401
+ if (warnings > 0) result += (result ? ', ' : '') + yellow(`${warnings} warnings`);
402
+ if (na > 0) result += (result ? ', ' : '') + dim(`${na} N/A`);
403
+ result += (result ? ', ' : '') + green(`${passed} passed`);
404
+ console.log(` Result: ${result}`);
261
405
  console.log('');
262
406
  }
263
407
 
@@ -270,23 +414,25 @@ function cmdStatus(targetDir) {
270
414
  console.log(bold(` 📊 ${name}`));
271
415
  console.log('');
272
416
 
273
- // .vibe status
274
417
  if (exists(vibeDir)) {
275
- const filled = VIBE_FILES.filter(f => {
418
+ // Count filled vs N/A vs empty
419
+ let filled = 0, naCount = 0, empty = 0;
420
+ for (const f of VIBE_FILES) {
276
421
  const fp = path.join(vibeDir, f);
277
- if (!exists(fp)) return false;
278
- return fs.readFileSync(fp, 'utf-8').trim().split('\n').length >= 10;
279
- }).length;
280
- const icon = filled === VIBE_FILES.length ? green('') : filled > 0 ? yellow('◐') : red('✖');
281
- console.log(` ${icon} .vibe/ ${filled}/${VIBE_FILES.length} files filled`);
422
+ if (!exists(fp)) { empty++; continue; }
423
+ const content = fs.readFileSync(fp, 'utf-8').trim();
424
+ if (content.match(/^N\/A/m)) naCount++;
425
+ else if (content.split('\n').length >= 10) filled++;
426
+ else empty++;
427
+ }
428
+ const icon = empty === 0 ? green('✔') : filled > 0 ? yellow('◐') : red('✖');
429
+ console.log(` ${icon} .vibe/ ${filled} filled, ${naCount} N/A, ${empty} empty (${VIBE_FILES.length} total)`);
282
430
 
283
- // Show state.json health if available
284
431
  const statePath = path.join(vibeDir, 'state.json');
285
432
  if (exists(statePath)) {
286
433
  try {
287
434
  const s = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
288
435
  if (s.health) console.log(dim(` health: ${s.health}`));
289
- if (s.version) console.log(dim(` version: ${s.version}`));
290
436
  if (s.vibe_updated) {
291
437
  const days = Math.floor((Date.now() - new Date(s.vibe_updated).getTime()) / 86400000);
292
438
  console.log(dim(` updated: ${days === 0 ? 'today' : days + ' days ago'}`));
@@ -297,7 +443,6 @@ function cmdStatus(targetDir) {
297
443
  console.log(red(' ✖') + ' .vibe/ not initialized');
298
444
  }
299
445
 
300
- // docs status
301
446
  if (exists(docsDir)) {
302
447
  const filled = DOCS_FILES.filter(f => {
303
448
  const fp = path.join(docsDir, f);
@@ -321,17 +466,10 @@ function cmdAll(targetDir) {
321
466
  const vibeDir = path.join(targetDir, VIBE_DIR);
322
467
  const docsDir = path.join(targetDir, DOCS_DIR);
323
468
 
324
- if (!exists(vibeDir)) {
325
- cmdInit(targetDir);
326
- } else {
327
- console.log(yellow('\n ⊘ .vibe/ already exists, skipping init'));
328
- }
469
+ if (!exists(vibeDir)) cmdInit(targetDir);
470
+ else console.log(yellow('\n ⊘ .vibe/ already exists, skipping init'));
329
471
 
330
- if (!exists(docsDir) || fs.readdirSync(docsDir).length === 0) {
331
- cmdDocs(targetDir);
332
- } else {
333
- cmdDocs(targetDir); // docs command already handles existing files gracefully
334
- }
472
+ cmdDocs(targetDir);
335
473
  }
336
474
 
337
475
  function cmdReset(targetDir) {
@@ -345,56 +483,439 @@ function cmdReset(targetDir) {
345
483
  cmdAll(targetDir);
346
484
  }
347
485
 
486
+ function copyDirRecursive(src, dest) {
487
+ if (!exists(src)) return 0;
488
+ fs.mkdirSync(dest, { recursive: true });
489
+ let count = 0;
490
+ for (const item of fs.readdirSync(src)) {
491
+ const srcPath = path.join(src, item);
492
+ const destPath = path.join(dest, item);
493
+ const stat = fs.statSync(srcPath);
494
+ if (stat.isDirectory()) {
495
+ count += copyDirRecursive(srcPath, destPath);
496
+ } else {
497
+ if (item === 'VIBE_GUIDE.md') continue;
498
+ fs.writeFileSync(destPath, fs.readFileSync(srcPath));
499
+ count++;
500
+ }
501
+ }
502
+ return count;
503
+ }
504
+
505
+ function cmdExport(targetDir, hubPathArg) {
506
+ targetDir = path.resolve(targetDir);
507
+ let hubPath = hubPathArg;
508
+ if (!hubPath && exists(HUB_CONFIG)) {
509
+ hubPath = fs.readFileSync(HUB_CONFIG, 'utf-8').trim();
510
+ }
511
+ if (!hubPath) {
512
+ console.log(red('\n ✖ No hub path specified.'));
513
+ console.log('');
514
+ console.log(' First time? Set your hub path:');
515
+ console.log(cyan(' vibes hub C:\\Users\\You\\Documents\\VibeHub'));
516
+ console.log('');
517
+ console.log(' Then run:');
518
+ console.log(cyan(' vibes export'));
519
+ console.log('');
520
+ process.exit(1);
521
+ }
522
+
523
+ hubPath = path.resolve(hubPath);
524
+ const name = getProjectName(targetDir);
525
+ const projectHub = path.join(hubPath, name);
526
+
527
+ console.log('');
528
+ console.log(bold(' 📤 vibes export'));
529
+ console.log(dim(` Exporting ${cyan(name)} → ${hubPath}`));
530
+ console.log('');
531
+
532
+ let total = 0;
533
+
534
+ const vibeDir = path.join(targetDir, VIBE_DIR);
535
+ if (exists(vibeDir)) {
536
+ const count = copyDirRecursive(vibeDir, path.join(projectHub, VIBE_DIR));
537
+ console.log(green(' ✔ ') + `.vibe/ — ${count} files`);
538
+ total += count;
539
+ } else {
540
+ console.log(yellow(' ⚠ ') + '.vibe/ not found, skipping');
541
+ }
542
+
543
+ const docsDir = path.join(targetDir, DOCS_DIR);
544
+ if (exists(docsDir)) {
545
+ const count = copyDirRecursive(docsDir, path.join(projectHub, DOCS_DIR));
546
+ console.log(green(' ✔ ') + `docs/ — ${count} files`);
547
+ total += count;
548
+ } else {
549
+ console.log(yellow(' ⚠ ') + 'docs/ not found, skipping');
550
+ }
551
+
552
+ const meta = { project: name, exported_at: now(), source: targetDir };
553
+ fs.writeFileSync(path.join(projectHub, 'export.json'), JSON.stringify(meta, null, 2), 'utf-8');
554
+ total++;
555
+
556
+ console.log('');
557
+ console.log(green(` ✔ Exported ${total} files to ${projectHub}`));
558
+
559
+ generateHubIndex(hubPath);
560
+
561
+ console.log('');
562
+ console.log(dim(' Commit and push your hub repo to sync across machines:'));
563
+ console.log(cyan(` cd "${hubPath}" && git add -A && git commit -m "export ${name}" && git push`));
564
+ console.log('');
565
+ }
566
+
567
+ function generateHubIndex(hubPath) {
568
+ const projects = [];
569
+ for (const dir of fs.readdirSync(hubPath)) {
570
+ const dirPath = path.join(hubPath, dir);
571
+ if (!fs.statSync(dirPath).isDirectory()) continue;
572
+ if (dir.startsWith('.') || dir.startsWith('_')) continue;
573
+
574
+ const exportFile = path.join(dirPath, 'export.json');
575
+ const vibeDir = path.join(dirPath, VIBE_DIR);
576
+ const docsDir = path.join(dirPath, DOCS_DIR);
577
+
578
+ let exportedAt = 'unknown';
579
+ if (exists(exportFile)) {
580
+ try {
581
+ const meta = JSON.parse(fs.readFileSync(exportFile, 'utf-8'));
582
+ exportedAt = meta.exported_at ? meta.exported_at.split('T')[0] : 'unknown';
583
+ } catch {}
584
+ }
585
+
586
+ let vibeFilled = 0, vibeNA = 0, vibeTotal = 0;
587
+ if (exists(vibeDir)) {
588
+ for (const f of VIBE_FILES) {
589
+ const fp = path.join(vibeDir, f);
590
+ if (!exists(fp)) continue;
591
+ vibeTotal++;
592
+ const content = fs.readFileSync(fp, 'utf-8').trim();
593
+ if (content.match(/^N\/A/m)) vibeNA++;
594
+ else if (content.split('\n').length >= 10) vibeFilled++;
595
+ }
596
+ }
597
+
598
+ let docsFilled = 0;
599
+ if (exists(docsDir)) {
600
+ for (const f of DOCS_FILES) {
601
+ const fp = path.join(docsDir, f);
602
+ if (!exists(fp)) continue;
603
+ if (fs.readFileSync(fp, 'utf-8').trim().split('\n').length >= 10) docsFilled++;
604
+ }
605
+ }
606
+
607
+ let purpose = '';
608
+ const purposeFile = path.join(vibeDir, 'purpose.md');
609
+ if (exists(purposeFile)) {
610
+ for (const line of fs.readFileSync(purposeFile, 'utf-8').split('\n')) {
611
+ const t = line.trim();
612
+ if (t && !t.startsWith('#') && !t.startsWith('<!--') && !t.startsWith('-->') && !t.startsWith('[')) {
613
+ purpose = t.substring(0, 80);
614
+ break;
615
+ }
616
+ }
617
+ }
618
+
619
+ projects.push({ name: dir, exportedAt, vibeFilled, vibeNA, vibeTotal, docsFilled, purpose });
620
+ }
621
+
622
+ if (projects.length === 0) return;
623
+
624
+ let md = `# Vibes Hub\n\n`;
625
+ md += `> Auto-generated index. Last updated: ${now().split('T')[0]}\n\n`;
626
+ md += `| Project | .vibe/ | docs/ | Last Export | Purpose |\n`;
627
+ md += `|---|---|---|---|---|\n`;
628
+ for (const p of projects.sort((a, b) => a.name.localeCompare(b.name))) {
629
+ const vs = p.vibeTotal > 0 ? `${p.vibeFilled}/${p.vibeTotal}` + (p.vibeNA > 0 ? ` (${p.vibeNA} N/A)` : '') : '—';
630
+ const ds = p.docsFilled > 0 ? `${p.docsFilled}/${DOCS_FILES.length}` : '—';
631
+ md += `| [${p.name}](./${p.name}/) | ${vs} | ${ds} | ${p.exportedAt} | ${p.purpose} |\n`;
632
+ }
633
+ md += `\n---\n\n**${projects.length} projects** tracked.\n`;
634
+
635
+ fs.writeFileSync(path.join(hubPath, 'index.md'), md, 'utf-8');
636
+ console.log(green(' ✔ ') + 'Updated hub index.md');
637
+ }
638
+
639
+ function cmdHub(hubPathArg) {
640
+ if (!hubPathArg) {
641
+ if (exists(HUB_CONFIG)) {
642
+ const saved = fs.readFileSync(HUB_CONFIG, 'utf-8').trim();
643
+ console.log(`\n Hub path: ${cyan(saved)}\n`);
644
+ } else {
645
+ console.log(yellow('\n No hub configured.'));
646
+ console.log(dim(' Set one with: vibes hub <path>\n'));
647
+ }
648
+ return;
649
+ }
650
+
651
+ const resolved = path.resolve(hubPathArg);
652
+ const isNew = !exists(resolved) || fs.readdirSync(resolved).length === 0;
653
+
654
+ // Create directory
655
+ fs.mkdirSync(resolved, { recursive: true });
656
+
657
+ console.log('');
658
+ console.log(bold(' 🗂️ vibes hub setup'));
659
+ console.log('');
660
+
661
+ if (isNew) {
662
+ // Initialize git
663
+ const { execSync } = require('child_process');
664
+ try {
665
+ execSync('git init', { cwd: resolved, stdio: 'ignore' });
666
+ console.log(green(' ✔ ') + 'Created directory & initialized git');
667
+ } catch {
668
+ console.log(green(' ✔ ') + 'Created directory');
669
+ console.log(yellow(' ⚠ ') + 'Could not git init (git not found?)');
670
+ }
671
+
672
+ // Create README
673
+ const readme = [
674
+ '# Vibes Hub',
675
+ '',
676
+ 'Central collection of `.vibe/` and `docs/` files from all my projects.',
677
+ '',
678
+ '## How this works',
679
+ '',
680
+ '1. Run `vibes export` from any project directory',
681
+ '2. Commit and push: `git add -A && git commit -m "update" && git push`',
682
+ '3. Pull on another machine: `git pull`',
683
+ '',
684
+ '## Projects',
685
+ '',
686
+ 'See [index.md](index.md) for auto-generated dashboard.',
687
+ '',
688
+ '## Cross-Project Intelligence',
689
+ '',
690
+ 'The `_insights/` folder is where AI analyzes patterns across all your projects.',
691
+ '',
692
+ '- **[ANALYZE.md](_insights/ANALYZE.md)** — Copy-paste prompt for your AI agent',
693
+ '- **[patterns.md](_insights/patterns.md)** — Architecture, security, and tech patterns',
694
+ '- **[standards.md](_insights/standards.md)** — Universal rules for all projects',
695
+ '- **[opportunities.md](_insights/opportunities.md)** — What one project can learn from another',
696
+ '',
697
+ 'Export 3+ projects, then run the analysis prompt to find cross-project improvements.',
698
+ '',
699
+ ].join('\n');
700
+ fs.writeFileSync(path.join(resolved, 'README.md'), readme, 'utf-8');
701
+ console.log(green(' ✔ ') + 'Created README.md');
702
+
703
+ // Create .gitignore
704
+ fs.writeFileSync(path.join(resolved, '.gitignore'), 'node_modules/\n.DS_Store\nThumbs.db\n', 'utf-8');
705
+ console.log(green(' ✔ ') + 'Created .gitignore');
706
+
707
+ // Create _insights/ intelligence layer
708
+ const insightsDir = path.join(resolved, '_insights');
709
+ fs.mkdirSync(insightsDir, { recursive: true });
710
+
711
+ // Analysis prompt — the AI reads this to know what to do
712
+ const analyze = [
713
+ '# Hub Analysis Prompt',
714
+ '',
715
+ '> Copy-paste this to your AI agent after exporting 3+ projects.',
716
+ '',
717
+ '---',
718
+ '',
719
+ '```',
720
+ 'I have a Vibes Hub — a central collection of .vibe/ and docs/ files',
721
+ 'from all my projects. Read every project folder in this hub and:',
722
+ '',
723
+ '1. CROSS-PROJECT PATTERNS',
724
+ ' - What architectural patterns appear across multiple projects?',
725
+ ' - What security approaches are used? Are they consistent?',
726
+ ' - What tech stack choices are shared? Where do they diverge?',
727
+ ' - What naming conventions or coding standards are consistent?',
728
+ '',
729
+ '2. BEST PRACTICES TO REPLICATE',
730
+ ' - Which project has the best documentation quality?',
731
+ ' - Which project solved a problem that others still have?',
732
+ ' - What did one project do exceptionally well that could set',
733
+ ' the standard for all projects?',
734
+ ' - Are there security, testing, or deployment practices from',
735
+ ' one project that others are missing?',
736
+ '',
737
+ '3. TRANSFERABLE IMPROVEMENTS',
738
+ ' - For each project, list specific improvements it could adopt',
739
+ ' from another project. Be concrete: "Project A could adopt',
740
+ ' Project B\'s approach to X because Y."',
741
+ ' - Identify shared problems that could have a shared solution.',
742
+ '',
743
+ '4. RISKS AND GAPS',
744
+ ' - Which projects have risks that could affect other projects?',
745
+ ' - Are there shared dependencies that create portfolio risk?',
746
+ ' - Which projects have missing or weak documentation areas?',
747
+ '',
748
+ '5. UNIVERSAL STANDARDS',
749
+ ' - Based on what works best across projects, propose standards',
750
+ ' that should apply to ALL projects going forward.',
751
+ ' - These could be file size limits, security practices, naming',
752
+ ' conventions, documentation requirements, etc.',
753
+ '',
754
+ 'Write your findings into the _insights/ folder:',
755
+ '- patterns.md — Cross-project patterns',
756
+ '- standards.md — Proposed universal standards',
757
+ '- opportunities.md — Specific transferable improvements',
758
+ '',
759
+ 'Be specific. Reference actual project names and files.',
760
+ '```',
761
+ '',
762
+ ].join('\n');
763
+ fs.writeFileSync(path.join(insightsDir, 'ANALYZE.md'), analyze, 'utf-8');
764
+
765
+ // Patterns skeleton
766
+ const patterns = [
767
+ '# Cross-Project Patterns',
768
+ '',
769
+ '> Auto-populated by AI analysis. Run the prompt in ANALYZE.md.',
770
+ '',
771
+ '## Architecture Patterns',
772
+ '',
773
+ '<!-- What architectural approaches appear across multiple projects? -->',
774
+ '',
775
+ '## Security Patterns',
776
+ '',
777
+ '<!-- How do projects handle auth, encryption, secrets? -->',
778
+ '',
779
+ '## Shared Tech Stack',
780
+ '',
781
+ '<!-- Languages, frameworks, databases used across projects -->',
782
+ '',
783
+ '| Technology | Used In | Notes |',
784
+ '|---|---|---|',
785
+ '| | | |',
786
+ '',
787
+ '## Common Anti-Patterns',
788
+ '',
789
+ '<!-- Mistakes or bad patterns that appear in multiple projects -->',
790
+ '',
791
+ ].join('\n');
792
+ fs.writeFileSync(path.join(insightsDir, 'patterns.md'), patterns, 'utf-8');
793
+
794
+ // Standards skeleton
795
+ const standards = [
796
+ '# Universal Standards',
797
+ '',
798
+ '> Standards that should apply to ALL projects, based on what works best.',
799
+ '',
800
+ '## Code Standards',
801
+ '',
802
+ '<!-- File size limits, naming conventions, banned patterns -->',
803
+ '',
804
+ '## Documentation Standards',
805
+ '',
806
+ '<!-- What every project must document, minimum quality bar -->',
807
+ '',
808
+ '## Security Standards',
809
+ '',
810
+ '<!-- Auth patterns, encryption requirements, secrets management -->',
811
+ '',
812
+ '## AI Agent Standards',
813
+ '',
814
+ '<!-- Rules every AI agent must follow across all projects -->',
815
+ '',
816
+ ].join('\n');
817
+ fs.writeFileSync(path.join(insightsDir, 'standards.md'), standards, 'utf-8');
818
+
819
+ // Opportunities skeleton
820
+ const opportunities = [
821
+ '# Transferable Improvements',
822
+ '',
823
+ '> Specific things one project does well that others should adopt.',
824
+ '',
825
+ '## [Source Project] → [Target Project]',
826
+ '',
827
+ '**What:** [Specific practice or solution]',
828
+ '',
829
+ '**Why:** [Why the target project would benefit]',
830
+ '',
831
+ '**How:** [What the target project needs to change]',
832
+ '',
833
+ '---',
834
+ '',
835
+ '<!-- Copy the template above for each transferable improvement. -->',
836
+ '',
837
+ ].join('\n');
838
+ fs.writeFileSync(path.join(insightsDir, 'opportunities.md'), opportunities, 'utf-8');
839
+
840
+ console.log(green(' ✔ ') + 'Created _insights/ (analysis prompt + templates)');
841
+ } else {
842
+ console.log(green(' ✔ ') + 'Directory already exists');
843
+ }
844
+
845
+ // Save config
846
+ fs.writeFileSync(HUB_CONFIG, resolved, 'utf-8');
847
+ console.log(green(' ✔ ') + `Hub path saved: ${cyan(resolved)}`);
848
+
849
+ console.log('');
850
+ console.log(bold(' Next — connect to GitHub:'));
851
+ console.log('');
852
+ console.log(dim(' 1. Go to ') + cyan('github.com/new'));
853
+ console.log(dim(' Name: ') + bold('VibeHub') + dim(' (or whatever you want)'));
854
+ console.log(dim(' Visibility: ') + bold('Private'));
855
+ console.log(dim(' Click "Create repository"'));
856
+ console.log('');
857
+ console.log(dim(' 2. Run these commands:'));
858
+ console.log('');
859
+ console.log(cyan(` cd "${resolved}"`));
860
+ console.log(cyan(' git remote add origin git@github.com:YOUR_USERNAME/VibeHub.git'));
861
+ console.log(cyan(' git add -A && git commit -m "init hub" && git push -u origin main'));
862
+ console.log('');
863
+ console.log(dim(' After that, exporting from any project is just:'));
864
+ console.log('');
865
+ console.log(cyan(' vibes export'));
866
+ console.log(cyan(` cd "${resolved}" && git add -A && git commit -m "update" && git push`));
867
+ console.log('');
868
+ console.log(dim(' On another computer, clone the repo and run:'));
869
+ console.log(cyan(' vibes hub <path-to-cloned-repo>'));
870
+ console.log('');
871
+ }
872
+
348
873
  function printNextSteps() {
349
874
  console.log('');
350
- console.log(bold(' Next steps:'));
875
+ console.log(bold(' What to do next:'));
351
876
  console.log('');
352
- console.log(' 1. Point your AI coding agent at this project');
353
- console.log(' 2. Tell it:');
877
+ console.log(' Tell your AI coding agent:');
354
878
  console.log('');
355
879
  console.log(cyan(' "Read .vibe/VIBE_GUIDE.md, then analyze this codebase'));
356
- console.log(cyan(' and fill out all the skeleton files in .vibe/'));
880
+ console.log(cyan(' and fill out all the skeleton files in .vibe/ and docs/.'));
881
+ console.log(cyan(' For files that aren\'t relevant (like business.md for a'));
882
+ console.log(cyan(' library), write N/A with a brief explanation.'));
357
883
  console.log(cyan(' Ask me any questions you can\'t answer from the code."'));
358
884
  console.log('');
359
- console.log(' 3. Run ' + cyan('vibes docs') + ' to scaffold operational docs too');
360
- console.log(' 4. Review the output, commit to version control');
361
- console.log('');
362
885
  }
363
886
 
364
887
  function printHelp() {
365
888
  console.log('');
366
- console.log(bold(' vibes') + ' — Semantic layers + standardized docs for humans and AI');
889
+ console.log(bold(' vibes') + ' — The complete semantic layer for any project');
367
890
  console.log('');
368
891
  console.log(' ' + bold('Usage:'));
369
892
  console.log('');
370
- console.log(' vibes init Create .vibe/ semantic layer (6 files)');
371
- console.log(' vibes docs Create docs/ operational documentation (11 files)');
372
- console.log(' vibes all Create both .vibe/ and docs/ at once');
373
- console.log(' vibes check Validate all files for completeness');
374
- console.log(' vibes status Quick health overview');
375
- console.log(' vibes reset Delete and recreate everything');
376
- console.log(' vibes help Show this help message');
377
- console.log('');
378
- console.log(' ' + bold('.vibe/') + dim(' — Semantic layer (intent, decisions, flows)'));
379
- console.log(' ' + dim('├── purpose.md — What is this? Who is it for?'));
380
- console.log(' ' + dim('├── architecture.md — Systems and how they connect'));
381
- console.log(' ' + dim('├── flows.md — User journeys, step by step'));
382
- console.log(' ' + dim('├── entities.md — Important nouns and relationships'));
383
- console.log(' ' + dim('├── decisions.md — Why things exist the way they do'));
384
- console.log(' ' + dim('└── state.json — Machine-readable project health'));
385
- console.log('');
386
- console.log(' ' + bold('docs/') + dim(' — Operational docs (topology, API, issues, guides)'));
387
- console.log(' ' + dim('├── README.md — Executive summary'));
388
- console.log(' ' + dim('├── topology.md — File/folder map'));
389
- console.log(' ' + dim('├── architecture.md — Component-level details'));
390
- console.log(' ' + dim('├── api.md API endpoints'));
391
- console.log(' ' + dim('├── issues.md — Open bugs and blockers'));
392
- console.log(' ' + dim('├── resolved.md — Closed issues archive'));
393
- console.log(' ' + dim('├── roadmap.md — Milestones and priorities'));
394
- console.log(' ' + dim('├── developer_guide.md Setup, build, test, deploy'));
395
- console.log(' ' + dim('├── troubleshooting.md Common errors and fixes'));
396
- console.log(' ' + dim('├── glossary.md — Project-specific terms'));
397
- console.log(' ' + dim('└── decisions/ — Architecture decision records'));
893
+ console.log(' vibes init Create .vibe/ with the full suite (15 files + guide)');
894
+ console.log(' vibes docs Create docs/ operational documentation (11 files)');
895
+ console.log(' vibes all Create both .vibe/ and docs/ at once');
896
+ console.log(' vibes check Validate all files for completeness');
897
+ console.log(' vibes status Quick health dashboard');
898
+ console.log(' vibes reset Delete and recreate everything');
899
+ console.log('');
900
+ console.log(' ' + bold('Hub (cross-project sync):'));
901
+ console.log('');
902
+ console.log(' vibes hub <path> Set your central hub directory (saved globally)');
903
+ console.log(' vibes export Export .vibe/ + docs/ to your hub');
904
+ console.log(' vibes export <path> Export to a specific hub path');
905
+ console.log('');
906
+ console.log(' ' + bold('.vibe/') + dim(' Project memory'));
907
+ console.log(dim(' Core: purpose · architecture · flows · entities · decisions · state'));
908
+ console.log(dim(' Context: context (living state, updated weekly)'));
909
+ console.log(dim(' AI: ai (agent constraints, safe zones, rules)'));
910
+ console.log(dim(' Product: product · users · metrics · experiments'));
911
+ console.log(dim(' Business: business · market · risks'));
912
+ console.log('');
913
+ console.log(' ' + bold('docs/') + dim(' — Operational documentation'));
914
+ console.log(dim(' README · topology · architecture · api · issues · resolved'));
915
+ console.log(dim(' roadmap · developer_guide · troubleshooting · glossary · decisions/'));
916
+ console.log('');
917
+ console.log(dim(' Files that aren\'t relevant to your project? Mark them N/A.'));
918
+ console.log(dim(' Your AI agent will figure out which ones apply.'));
398
919
  console.log('');
399
920
  }
400
921
 
@@ -413,6 +934,8 @@ switch (command) {
413
934
  case 'check': cmdCheck(targetDir); break;
414
935
  case 'status': cmdStatus(targetDir); break;
415
936
  case 'reset': cmdReset(targetDir); break;
937
+ case 'export': cmdExport('.', args[1]); break;
938
+ case 'hub': cmdHub(args[1]); break;
416
939
  case 'help': case '--help': case '-h': printHelp(); break;
417
940
  default:
418
941
  console.log(red(`\n Unknown command: ${command}`));