ripp-cli 1.0.0 → 1.2.1

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/lib/doctor.js ADDED
@@ -0,0 +1,370 @@
1
+ /**
2
+ * RIPP Doctor - Health Check and Diagnostics
3
+ *
4
+ * Checks repository health and provides actionable fix-it commands
5
+ * for common RIPP setup issues.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const { execSync } = require('child_process');
11
+
12
+ /**
13
+ * Run all health checks
14
+ * @param {string} cwd - Current working directory
15
+ * @returns {Object} Health check results
16
+ */
17
+ function runHealthChecks(cwd = process.cwd()) {
18
+ const checks = {
19
+ nodeVersion: checkNodeVersion(),
20
+ gitRepository: checkGitRepository(cwd),
21
+ rippDirectory: checkRippDirectory(cwd),
22
+ configFile: checkConfigFile(cwd),
23
+ evidencePack: checkEvidencePack(cwd),
24
+ candidates: checkCandidates(cwd),
25
+ confirmedIntent: checkConfirmedIntent(cwd),
26
+ schema: checkSchema(),
27
+ cliVersion: checkCliVersion()
28
+ };
29
+
30
+ // Calculate overall health
31
+ const total = Object.keys(checks).length;
32
+ const passed = Object.values(checks).filter(c => c.status === 'pass').length;
33
+ const warnings = Object.values(checks).filter(c => c.status === 'warning').length;
34
+
35
+ return {
36
+ checks,
37
+ summary: {
38
+ total,
39
+ passed,
40
+ warnings,
41
+ failed: total - passed - warnings,
42
+ healthy: passed === total
43
+ }
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Check Node.js version (>= 20.0.0)
49
+ */
50
+ function checkNodeVersion() {
51
+ const version = process.version;
52
+ const major = parseInt(version.slice(1).split('.')[0]);
53
+
54
+ if (major >= 20) {
55
+ return {
56
+ status: 'pass',
57
+ message: `Node.js ${version}`,
58
+ fix: null
59
+ };
60
+ } else {
61
+ return {
62
+ status: 'fail',
63
+ message: `Node.js ${version} is too old`,
64
+ fix: 'Install Node.js 20 or later: https://nodejs.org/',
65
+ docs: 'https://dylan-natter.github.io/ripp-protocol/getting-started.html#prerequisites'
66
+ };
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Check if current directory is a Git repository
72
+ */
73
+ function checkGitRepository(cwd) {
74
+ try {
75
+ const gitDir = path.join(cwd, '.git');
76
+ if (fs.existsSync(gitDir)) {
77
+ return {
78
+ status: 'pass',
79
+ message: 'Git repository detected',
80
+ fix: null
81
+ };
82
+ } else {
83
+ return {
84
+ status: 'fail',
85
+ message: 'Not a Git repository',
86
+ fix: 'Initialize Git: git init',
87
+ docs: 'https://git-scm.com/docs/git-init'
88
+ };
89
+ }
90
+ } catch (error) {
91
+ return {
92
+ status: 'fail',
93
+ message: 'Unable to check Git repository',
94
+ fix: 'Ensure you are in a valid directory',
95
+ docs: null
96
+ };
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Check if .ripp directory exists
102
+ */
103
+ function checkRippDirectory(cwd) {
104
+ const rippDir = path.join(cwd, '.ripp');
105
+ if (fs.existsSync(rippDir) && fs.statSync(rippDir).isDirectory()) {
106
+ return {
107
+ status: 'pass',
108
+ message: '.ripp directory exists',
109
+ fix: null
110
+ };
111
+ } else {
112
+ return {
113
+ status: 'fail',
114
+ message: '.ripp directory not found',
115
+ fix: 'Initialize RIPP: ripp init',
116
+ docs: 'https://dylan-natter.github.io/ripp-protocol/getting-started.html'
117
+ };
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Check if config.yaml exists
123
+ */
124
+ function checkConfigFile(cwd) {
125
+ const configPath = path.join(cwd, '.ripp', 'config.yaml');
126
+ if (fs.existsSync(configPath)) {
127
+ return {
128
+ status: 'pass',
129
+ message: 'config.yaml present',
130
+ fix: null
131
+ };
132
+ } else {
133
+ return {
134
+ status: 'warning',
135
+ message: 'config.yaml not found (using defaults)',
136
+ fix: 'Initialize RIPP: ripp init',
137
+ docs: 'https://dylan-natter.github.io/ripp-protocol/getting-started.html'
138
+ };
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Check if evidence pack exists
144
+ */
145
+ function checkEvidencePack(cwd) {
146
+ const evidenceIndex = path.join(cwd, '.ripp', 'evidence', 'index.yaml');
147
+ if (fs.existsSync(evidenceIndex)) {
148
+ return {
149
+ status: 'pass',
150
+ message: 'Evidence pack built',
151
+ fix: null
152
+ };
153
+ } else {
154
+ return {
155
+ status: 'warning',
156
+ message: 'Evidence pack not built',
157
+ fix: 'Build evidence: ripp evidence build',
158
+ docs: 'https://dylan-natter.github.io/ripp-protocol/getting-started.html#step-2-build-evidence'
159
+ };
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Check if candidates exist (discovery has run)
165
+ */
166
+ function checkCandidates(cwd) {
167
+ const candidatesDir = path.join(cwd, '.ripp', 'candidates');
168
+ if (fs.existsSync(candidatesDir)) {
169
+ const files = fs.readdirSync(candidatesDir);
170
+ const yamlFiles = files.filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
171
+
172
+ if (yamlFiles.length > 0) {
173
+ return {
174
+ status: 'pass',
175
+ message: `${yamlFiles.length} candidate(s) found`,
176
+ fix: null
177
+ };
178
+ } else {
179
+ return {
180
+ status: 'warning',
181
+ message: 'No candidate files in candidates directory',
182
+ fix: 'Run discovery: ripp discover (requires AI enabled)',
183
+ docs: 'https://dylan-natter.github.io/ripp-protocol/getting-started.html#step-3-discover-intent'
184
+ };
185
+ }
186
+ } else {
187
+ return {
188
+ status: 'warning',
189
+ message: 'Discovery not run',
190
+ fix: 'Run discovery: ripp discover (requires AI enabled)',
191
+ docs: 'https://dylan-natter.github.io/ripp-protocol/getting-started.html#step-3-discover-intent'
192
+ };
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Check if confirmed intent exists
198
+ */
199
+ function checkConfirmedIntent(cwd) {
200
+ const intentPath = path.join(cwd, '.ripp', 'intent.confirmed.yaml');
201
+ if (fs.existsSync(intentPath)) {
202
+ return {
203
+ status: 'pass',
204
+ message: 'Intent confirmed',
205
+ fix: null
206
+ };
207
+ } else {
208
+ return {
209
+ status: 'warning',
210
+ message: 'Intent not confirmed',
211
+ fix: 'Confirm intent: ripp confirm --checklist (then ripp build --from-checklist)',
212
+ docs: 'https://dylan-natter.github.io/ripp-protocol/getting-started.html#step-4-confirm-intent'
213
+ };
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Check if RIPP schema is accessible
219
+ */
220
+ function checkSchema() {
221
+ try {
222
+ // First check bundled schema (always available when CLI is installed)
223
+ const bundledSchemaPath = path.join(__dirname, '../schema', 'ripp-1.0.schema.json');
224
+ if (fs.existsSync(bundledSchemaPath)) {
225
+ return {
226
+ status: 'pass',
227
+ message: 'RIPP schema accessible',
228
+ fix: null
229
+ };
230
+ }
231
+
232
+ // Fallback: check if we're in project root (for development)
233
+ const projectRoot = path.resolve(__dirname, '../../..');
234
+ const schemaPath = path.join(projectRoot, 'schema', 'ripp-1.0.schema.json');
235
+ if (fs.existsSync(schemaPath)) {
236
+ return {
237
+ status: 'pass',
238
+ message: 'RIPP schema accessible',
239
+ fix: null
240
+ };
241
+ } else {
242
+ return {
243
+ status: 'warning',
244
+ message: 'RIPP schema not found locally',
245
+ fix: 'Schema will be loaded from repository when needed',
246
+ docs: 'https://dylan-natter.github.io/ripp-protocol/schema/ripp-1.0.schema.json'
247
+ };
248
+ }
249
+ } catch (error) {
250
+ return {
251
+ status: 'warning',
252
+ message: 'Unable to check schema',
253
+ fix: null,
254
+ docs: null
255
+ };
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Check CLI version
261
+ */
262
+ function checkCliVersion() {
263
+ try {
264
+ const pkg = require('../package.json');
265
+ return {
266
+ status: 'pass',
267
+ message: `ripp-cli v${pkg.version}`,
268
+ fix: null
269
+ };
270
+ } catch (error) {
271
+ return {
272
+ status: 'warning',
273
+ message: 'Unable to determine CLI version',
274
+ fix: null,
275
+ docs: null
276
+ };
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Format health check results as text
282
+ */
283
+ function formatHealthCheckText(results) {
284
+ const { checks, summary } = results;
285
+
286
+ let output = '\n';
287
+ output += '🔍 RIPP Health Check\n';
288
+ output += '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n';
289
+
290
+ // Overall summary
291
+ if (summary.healthy) {
292
+ output += '✅ All checks passed!\n\n';
293
+ } else {
294
+ output += `📊 Summary: ${summary.passed}/${summary.total} checks passed`;
295
+ if (summary.warnings > 0) {
296
+ output += `, ${summary.warnings} warnings`;
297
+ }
298
+ if (summary.failed > 0) {
299
+ output += `, ${summary.failed} failed`;
300
+ }
301
+ output += '\n\n';
302
+ }
303
+
304
+ // Individual checks
305
+ for (const [name, check] of Object.entries(checks)) {
306
+ const icon = check.status === 'pass' ? '✓' : check.status === 'warning' ? '⚠' : '✗';
307
+ const statusColor = check.status === 'pass' ? '' : check.status === 'warning' ? '⚠ ' : '✗ ';
308
+
309
+ output += `${icon} ${formatCheckName(name)}: ${check.message}\n`;
310
+
311
+ if (check.fix) {
312
+ output += ` → Fix: ${check.fix}\n`;
313
+ }
314
+
315
+ if (check.docs) {
316
+ output += ` → Docs: ${check.docs}\n`;
317
+ }
318
+
319
+ output += '\n';
320
+ }
321
+
322
+ // Next steps
323
+ if (!summary.healthy) {
324
+ output += '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
325
+ output += '💡 Next Steps:\n\n';
326
+
327
+ const failedChecks = Object.entries(checks)
328
+ .filter(([_, check]) => check.status === 'fail')
329
+ .map(([_, check]) => check.fix)
330
+ .filter(fix => fix !== null);
331
+
332
+ if (failedChecks.length > 0) {
333
+ failedChecks.forEach((fix, idx) => {
334
+ output += ` ${idx + 1}. ${fix}\n`;
335
+ });
336
+ } else {
337
+ output += ' All critical checks passed. Address warnings to improve workflow.\n';
338
+ }
339
+
340
+ output += '\n';
341
+ }
342
+
343
+ output += '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
344
+ output += 'For more help: https://dylan-natter.github.io/ripp-protocol/getting-started.html\n';
345
+
346
+ return output;
347
+ }
348
+
349
+ /**
350
+ * Format check name for display
351
+ */
352
+ function formatCheckName(name) {
353
+ const names = {
354
+ nodeVersion: 'Node.js Version',
355
+ gitRepository: 'Git Repository',
356
+ rippDirectory: 'RIPP Directory',
357
+ configFile: 'Configuration',
358
+ evidencePack: 'Evidence Pack',
359
+ candidates: 'Intent Candidates',
360
+ confirmedIntent: 'Confirmed Intent',
361
+ schema: 'RIPP Schema',
362
+ cliVersion: 'CLI Version'
363
+ };
364
+ return names[name] || name;
365
+ }
366
+
367
+ module.exports = {
368
+ runHealthChecks,
369
+ formatHealthCheckText
370
+ };
package/lib/evidence.js CHANGED
@@ -74,14 +74,15 @@ async function buildEvidencePack(cwd, config) {
74
74
  */
75
75
  async function scanFiles(cwd, evidenceConfig) {
76
76
  const files = [];
77
+ let excludedCount = 0;
77
78
  const { includeGlobs, excludeGlobs, maxFileSize } = evidenceConfig;
78
79
 
79
- // Build combined pattern
80
- const patterns = includeGlobs.map(pattern => path.join(cwd, pattern));
81
-
82
- for (const pattern of patterns) {
80
+ // Use glob with cwd option instead of joining paths
81
+ // This ensures patterns work correctly on all platforms
82
+ for (const pattern of includeGlobs) {
83
83
  const matches = await glob(pattern, {
84
- ignore: excludeGlobs.map(ex => path.join(cwd, ex)),
84
+ cwd: cwd,
85
+ ignore: excludeGlobs,
85
86
  nodir: true,
86
87
  absolute: true
87
88
  });
@@ -92,6 +93,7 @@ async function scanFiles(cwd, evidenceConfig) {
92
93
 
93
94
  // Skip files that are too large
94
95
  if (stats.size > maxFileSize) {
96
+ excludedCount++;
95
97
  continue;
96
98
  }
97
99
 
@@ -109,12 +111,13 @@ async function scanFiles(cwd, evidenceConfig) {
109
111
  });
110
112
  } catch (error) {
111
113
  // Skip files we can't read
114
+ excludedCount++;
112
115
  continue;
113
116
  }
114
117
  }
115
118
  }
116
119
 
117
- return files;
120
+ return { files, excludedCount };
118
121
  }
119
122
 
120
123
  /**
@@ -151,7 +154,10 @@ async function extractEvidence(files, cwd) {
151
154
  routes: [],
152
155
  schemas: [],
153
156
  auth: [],
154
- workflows: []
157
+ workflows: [],
158
+ projectType: detectProjectType(files),
159
+ keyInsights: extractKeyInsights(files, cwd),
160
+ codeSnippets: extractKeyCodeSnippets(files)
155
161
  };
156
162
 
157
163
  for (const file of files) {
@@ -362,6 +368,203 @@ function redactSecrets(text) {
362
368
  return redacted;
363
369
  }
364
370
 
371
+ /**
372
+ * Detect project type from evidence
373
+ */
374
+ function detectProjectType(files) {
375
+ const indicators = {
376
+ cli: 0,
377
+ webApp: 0,
378
+ api: 0,
379
+ library: 0,
380
+ protocol: 0
381
+ };
382
+
383
+ for (const file of files) {
384
+ const content = file.content ? file.content.toLowerCase() : '';
385
+ const path = file.path.toLowerCase();
386
+
387
+ // CLI tool indicators
388
+ if (path.includes('/bin/') || path.includes('cli') || path.includes('command'))
389
+ indicators.cli += 3;
390
+ if (
391
+ content.includes('commander') ||
392
+ content.includes('yargs') ||
393
+ content.includes('process.argv')
394
+ )
395
+ indicators.cli += 2;
396
+ if (path === 'package.json' && content.includes('"bin"')) indicators.cli += 4;
397
+
398
+ // Web app indicators
399
+ if (path.includes('app/') || path.includes('pages/') || path.includes('components/'))
400
+ indicators.webApp += 3;
401
+ if (content.includes('react') || content.includes('vue') || content.includes('angular'))
402
+ indicators.webApp += 2;
403
+ if (path.includes('index.html') || path.includes('app.tsx')) indicators.webApp += 3;
404
+
405
+ // API indicators
406
+ if (path.includes('api/') || path.includes('routes/') || path.includes('controllers/'))
407
+ indicators.api += 3;
408
+ if (content.includes('express') || content.includes('fastify') || content.includes('koa'))
409
+ indicators.api += 2;
410
+ if (content.includes('@app.route') || content.includes('@route') || content.includes('router.'))
411
+ indicators.api += 2;
412
+
413
+ // Library indicators
414
+ if (path === 'package.json' && !content.includes('"bin"') && !content.includes('"scripts"'))
415
+ indicators.library += 2;
416
+ if (path.includes('lib/') || path.includes('src/index')) indicators.library += 1;
417
+
418
+ // Protocol/spec indicators
419
+ if (path.includes('spec.md') || path.includes('protocol') || path.includes('rfc'))
420
+ indicators.protocol += 4;
421
+ if (path.includes('schema/') && path.includes('.json')) indicators.protocol += 2;
422
+ }
423
+
424
+ // Return type with highest score
425
+ const sorted = Object.entries(indicators).sort((a, b) => b[1] - a[1]);
426
+ return {
427
+ primary: sorted[0][0],
428
+ secondary: sorted[1][1] > 0 ? sorted[1][0] : null,
429
+ confidence: sorted[0][1] > 5 ? 'high' : sorted[0][1] > 2 ? 'medium' : 'low',
430
+ scores: indicators
431
+ };
432
+ }
433
+
434
+ /**
435
+ * Extract key insights from README, package.json, and main files
436
+ */
437
+ function extractKeyInsights(files, cwd) {
438
+ const insights = {
439
+ purpose: null,
440
+ description: null,
441
+ mainFeatures: [],
442
+ architecture: null
443
+ };
444
+
445
+ for (const file of files) {
446
+ const path = file.path.toLowerCase();
447
+ const content = file.content || '';
448
+
449
+ // Extract from README
450
+ if (path.includes('readme.md') || path.includes('readme.txt')) {
451
+ // Extract first paragraph as description
452
+ const lines = content.split('\n');
453
+ let desc = '';
454
+ for (const line of lines) {
455
+ if (line.trim() && !line.startsWith('#') && !line.startsWith('[')) {
456
+ desc += line.trim() + ' ';
457
+ if (desc.length > 200) break;
458
+ }
459
+ }
460
+ if (desc) insights.description = desc.slice(0, 300);
461
+
462
+ // Extract features (look for bullet points or numbered lists)
463
+ const featureMatch = content.match(
464
+ /(?:features|capabilities|includes)[\s\S]{0,50}?\n((?:[-*]\s.+\n)+)/i
465
+ );
466
+ if (featureMatch) {
467
+ insights.mainFeatures = featureMatch[1]
468
+ .split('\n')
469
+ .map(f => f.replace(/^[-*]\s+/, '').trim())
470
+ .filter(f => f.length > 0)
471
+ .slice(0, 5);
472
+ }
473
+ }
474
+
475
+ // Extract from package.json
476
+ if (path === 'package.json') {
477
+ try {
478
+ const pkg = JSON.parse(content);
479
+ if (pkg.description && !insights.description) {
480
+ insights.description = pkg.description;
481
+ }
482
+ if (pkg.name && !insights.purpose) {
483
+ insights.purpose = `${pkg.name}: ${pkg.description || 'No description'}`;
484
+ }
485
+ } catch (e) {
486
+ // Ignore JSON parse errors
487
+ }
488
+ }
489
+
490
+ // Extract from SPEC files
491
+ if (path.includes('spec.md') || path.includes('architecture.md')) {
492
+ const purposeMatch = content.match(/(?:purpose|goal|objective)[\s:]+([^\n]{50,300})/i);
493
+ if (purposeMatch && !insights.purpose) {
494
+ insights.purpose = purposeMatch[1].trim();
495
+ }
496
+ }
497
+ }
498
+
499
+ return insights;
500
+ }
501
+
502
+ /**
503
+ * Extract key code snippets (functions, classes, exports)
504
+ */
505
+ function extractKeyCodeSnippets(files) {
506
+ const snippets = [];
507
+ let count = 0;
508
+ const maxSnippets = 15;
509
+
510
+ for (const file of files) {
511
+ if (count >= maxSnippets) break;
512
+ if (!file.content || file.type !== 'source') continue;
513
+
514
+ const content = file.content;
515
+ const path = file.path;
516
+
517
+ // Extract function definitions (JavaScript/TypeScript)
518
+ const funcMatches = content.matchAll(
519
+ /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*{([^}]{0,200})/g
520
+ );
521
+ for (const match of funcMatches) {
522
+ if (count >= maxSnippets) break;
523
+ snippets.push({
524
+ file: path,
525
+ type: 'function',
526
+ name: match[1],
527
+ snippet: match[0].substring(0, 150)
528
+ });
529
+ count++;
530
+ }
531
+
532
+ // Extract class definitions
533
+ const classMatches = content.matchAll(
534
+ /(?:export\s+)?class\s+(\w+)(?:\s+extends\s+\w+)?\s*{([^}]{0,150})/g
535
+ );
536
+ for (const match of classMatches) {
537
+ if (count >= maxSnippets) break;
538
+ snippets.push({
539
+ file: path,
540
+ type: 'class',
541
+ name: match[1],
542
+ snippet: match[0].substring(0, 150)
543
+ });
544
+ count++;
545
+ }
546
+
547
+ // Extract key comments (JSDoc, purpose statements)
548
+ const commentMatches = content.matchAll(/\/\*\*\s*\n\s*\*\s*([^\n]{30,200})/g);
549
+ for (const match of commentMatches) {
550
+ if (count >= maxSnippets) break;
551
+ if (
552
+ match[1].toLowerCase().includes('purpose') ||
553
+ match[1].toLowerCase().includes('description')
554
+ ) {
555
+ snippets.push({
556
+ file: path,
557
+ type: 'comment',
558
+ snippet: match[1].trim()
559
+ });
560
+ count++;
561
+ }
562
+ }
563
+ }
564
+
565
+ return snippets.slice(0, maxSnippets);
566
+ }
567
+
365
568
  module.exports = {
366
569
  buildEvidencePack,
367
570
  redactSecrets