ripp-cli 1.0.1 → 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.
@@ -53,23 +53,69 @@ async function interactiveConfirm(cwd, candidates) {
53
53
  const candidate = candidates.candidates[i];
54
54
 
55
55
  console.log(`\n--- Candidate ${i + 1}/${candidates.candidates.length} ---`);
56
- console.log(`Section: ${candidate.section || 'unknown'}`);
56
+ const sectionName = candidate.purpose?.problem ? 'purpose' : 'full-packet';
57
+ console.log(`Section: ${sectionName}`);
57
58
  console.log(`Confidence: ${(candidate.confidence * 100).toFixed(1)}%`);
58
59
  console.log(`Evidence: ${candidate.evidence.length} reference(s)`);
59
60
  console.log('\nContent:');
60
- console.log(yaml.dump(candidate.content, { indent: 2 }));
61
+ // Build content object from candidate fields
62
+ const content = {};
63
+ const contentFields = [
64
+ 'purpose',
65
+ 'ux_flow',
66
+ 'data_contracts',
67
+ 'api_contracts',
68
+ 'permissions',
69
+ 'failure_modes',
70
+ 'audit_events',
71
+ 'nfrs',
72
+ 'acceptance_tests',
73
+ 'design_philosophy',
74
+ 'design_decisions',
75
+ 'constraints',
76
+ 'success_criteria'
77
+ ];
78
+ contentFields.forEach(field => {
79
+ if (candidate[field]) {
80
+ content[field] = candidate[field];
81
+ }
82
+ });
83
+ console.log(yaml.dump(content, { indent: 2 }));
61
84
 
62
85
  const answer = await question(rl, '\nAccept this candidate? (y/n/e to edit/s to skip): ');
63
86
 
64
87
  if (answer.toLowerCase() === 'y') {
88
+ // Build content object from candidate fields
89
+ const content = {};
90
+ const contentFields = [
91
+ 'purpose',
92
+ 'ux_flow',
93
+ 'data_contracts',
94
+ 'api_contracts',
95
+ 'permissions',
96
+ 'failure_modes',
97
+ 'audit_events',
98
+ 'nfrs',
99
+ 'acceptance_tests',
100
+ 'design_philosophy',
101
+ 'design_decisions',
102
+ 'constraints',
103
+ 'success_criteria'
104
+ ];
105
+ contentFields.forEach(field => {
106
+ if (candidate[field]) {
107
+ content[field] = candidate[field];
108
+ }
109
+ });
110
+
65
111
  confirmed.push({
66
- section: candidate.section,
112
+ section: candidate.purpose?.problem ? 'purpose' : 'full-packet',
67
113
  source: 'confirmed',
68
114
  confirmed_at: new Date().toISOString(),
69
115
  confirmed_by: options.user || 'unknown',
70
116
  original_confidence: candidate.confidence,
71
117
  evidence: candidate.evidence,
72
- content: candidate.content
118
+ content: content
73
119
  });
74
120
  console.log('✓ Accepted');
75
121
  } else if (answer.toLowerCase() === 'e') {
@@ -136,7 +182,10 @@ async function generateChecklistConfirm(cwd, candidates) {
136
182
  markdown += '---\n\n';
137
183
 
138
184
  candidates.candidates.forEach((candidate, index) => {
139
- markdown += `## Candidate ${index + 1}: ${candidate.section || 'unknown'}\n\n`;
185
+ // Extract section name from purpose or use generic identifier
186
+ const sectionName = candidate.purpose?.problem ? 'purpose' : 'full-packet';
187
+
188
+ markdown += `## Candidate ${index + 1}: ${sectionName}\n\n`;
140
189
  markdown += `- **Confidence**: ${(candidate.confidence * 100).toFixed(1)}%\n`;
141
190
  markdown += `- **Evidence**: ${candidate.evidence.length} reference(s)\n\n`;
142
191
 
@@ -145,7 +194,29 @@ async function generateChecklistConfirm(cwd, candidates) {
145
194
 
146
195
  markdown += '### Content\n\n';
147
196
  markdown += '```yaml\n';
148
- markdown += yaml.dump(candidate.content, { indent: 2 });
197
+ // Build content object from candidate fields (purpose, ux_flow, data_contracts, etc.)
198
+ const content = {};
199
+ const contentFields = [
200
+ 'purpose',
201
+ 'ux_flow',
202
+ 'data_contracts',
203
+ 'api_contracts',
204
+ 'permissions',
205
+ 'failure_modes',
206
+ 'audit_events',
207
+ 'nfrs',
208
+ 'acceptance_tests',
209
+ 'design_philosophy',
210
+ 'design_decisions',
211
+ 'constraints',
212
+ 'success_criteria'
213
+ ];
214
+ contentFields.forEach(field => {
215
+ if (candidate[field]) {
216
+ content[field] = candidate[field];
217
+ }
218
+ });
219
+ markdown += yaml.dump(content, { indent: 2 });
149
220
  markdown += '```\n\n';
150
221
 
151
222
  markdown += '### Evidence References\n\n';
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
+ };