ripp-cli 1.0.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.
@@ -0,0 +1,183 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const yaml = require('js-yaml');
4
+ const readline = require('readline');
5
+
6
+ /**
7
+ * RIPP Intent Confirmation
8
+ * Human confirmation workflow for candidate intent
9
+ */
10
+
11
+ /**
12
+ * Confirm candidates interactively
13
+ */
14
+ async function confirmIntent(cwd, options = {}) {
15
+ const candidatesPath = path.join(cwd, '.ripp', 'intent.candidates.yaml');
16
+
17
+ if (!fs.existsSync(candidatesPath)) {
18
+ throw new Error('No candidate intent found. Run "ripp discover" first.');
19
+ }
20
+
21
+ const candidatesContent = fs.readFileSync(candidatesPath, 'utf8');
22
+ const candidates = yaml.load(candidatesContent);
23
+
24
+ if (!candidates.candidates || candidates.candidates.length === 0) {
25
+ throw new Error('No candidates found in intent.candidates.yaml');
26
+ }
27
+
28
+ // Interactive mode
29
+ if (options.interactive !== false) {
30
+ return await interactiveConfirm(cwd, candidates);
31
+ } else {
32
+ // Generate markdown checklist mode
33
+ return await generateChecklistConfirm(cwd, candidates);
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Interactive confirmation via terminal
39
+ */
40
+ async function interactiveConfirm(cwd, candidates) {
41
+ const rl = readline.createInterface({
42
+ input: process.stdin,
43
+ output: process.stdout
44
+ });
45
+
46
+ const confirmed = [];
47
+ const rejected = [];
48
+
49
+ console.log('\n=== RIPP Intent Confirmation ===');
50
+ console.log(`Found ${candidates.candidates.length} candidate(s)\n`);
51
+
52
+ for (let i = 0; i < candidates.candidates.length; i++) {
53
+ const candidate = candidates.candidates[i];
54
+
55
+ console.log(`\n--- Candidate ${i + 1}/${candidates.candidates.length} ---`);
56
+ console.log(`Section: ${candidate.section || 'unknown'}`);
57
+ console.log(`Confidence: ${(candidate.confidence * 100).toFixed(1)}%`);
58
+ console.log(`Evidence: ${candidate.evidence.length} reference(s)`);
59
+ console.log('\nContent:');
60
+ console.log(yaml.dump(candidate.content, { indent: 2 }));
61
+
62
+ const answer = await question(rl, '\nAccept this candidate? (y/n/e to edit/s to skip): ');
63
+
64
+ if (answer.toLowerCase() === 'y') {
65
+ confirmed.push({
66
+ section: candidate.section,
67
+ source: 'confirmed',
68
+ confirmed_at: new Date().toISOString(),
69
+ confirmed_by: options.user || 'unknown',
70
+ original_confidence: candidate.confidence,
71
+ evidence: candidate.evidence,
72
+ content: candidate.content
73
+ });
74
+ console.log('✓ Accepted');
75
+ } else if (answer.toLowerCase() === 'e') {
76
+ console.log('Manual editing not yet supported. Skipping...');
77
+ // TODO: Open editor for manual changes
78
+ } else if (answer.toLowerCase() === 's') {
79
+ console.log('⊘ Skipped');
80
+ } else {
81
+ rejected.push({
82
+ section: candidate.section,
83
+ original_content: candidate.content,
84
+ original_confidence: candidate.confidence,
85
+ rejected_at: new Date().toISOString(),
86
+ rejected_by: options.user || 'unknown'
87
+ });
88
+ console.log('✗ Rejected');
89
+ }
90
+ }
91
+
92
+ rl.close();
93
+
94
+ // Save confirmed intent
95
+ const confirmedData = {
96
+ version: '1.0',
97
+ confirmed
98
+ };
99
+
100
+ const confirmedPath = path.join(cwd, '.ripp', 'intent.confirmed.yaml');
101
+ fs.writeFileSync(confirmedPath, yaml.dump(confirmedData, { indent: 2 }), 'utf8');
102
+
103
+ // Save rejected intent (optional)
104
+ if (rejected.length > 0) {
105
+ const rejectedData = {
106
+ version: '1.0',
107
+ rejected
108
+ };
109
+
110
+ const rejectedPath = path.join(cwd, '.ripp', 'intent.rejected.yaml');
111
+ fs.writeFileSync(rejectedPath, yaml.dump(rejectedData, { indent: 2 }), 'utf8');
112
+ }
113
+
114
+ return {
115
+ confirmedPath,
116
+ confirmedCount: confirmed.length,
117
+ rejectedCount: rejected.length
118
+ };
119
+ }
120
+
121
+ /**
122
+ * Generate markdown checklist for manual confirmation
123
+ */
124
+ async function generateChecklistConfirm(cwd, candidates) {
125
+ const checklistPath = path.join(cwd, '.ripp', 'intent.checklist.md');
126
+
127
+ let markdown = '# RIPP Intent Confirmation Checklist\n\n';
128
+ markdown += `Generated: ${new Date().toISOString()}\n\n`;
129
+ markdown += `Total Candidates: ${candidates.candidates.length}\n\n`;
130
+ markdown += '## Instructions\n\n';
131
+ markdown += '1. Review each candidate below\n';
132
+ markdown += '2. Check the box [ ] if you accept the candidate (change to [x])\n';
133
+ markdown += '3. Edit the content as needed\n';
134
+ markdown +=
135
+ '4. Save this file and run `ripp build --from-checklist` to compile confirmed intent\n\n';
136
+ markdown += '---\n\n';
137
+
138
+ candidates.candidates.forEach((candidate, index) => {
139
+ markdown += `## Candidate ${index + 1}: ${candidate.section || 'unknown'}\n\n`;
140
+ markdown += `- **Confidence**: ${(candidate.confidence * 100).toFixed(1)}%\n`;
141
+ markdown += `- **Evidence**: ${candidate.evidence.length} reference(s)\n\n`;
142
+
143
+ markdown += '### Accept?\n\n';
144
+ markdown += '- [ ] Accept this candidate\n\n';
145
+
146
+ markdown += '### Content\n\n';
147
+ markdown += '```yaml\n';
148
+ markdown += yaml.dump(candidate.content, { indent: 2 });
149
+ markdown += '```\n\n';
150
+
151
+ markdown += '### Evidence References\n\n';
152
+ candidate.evidence.forEach(ev => {
153
+ markdown += `- \`${ev.file}:${ev.line}\`\n`;
154
+ if (ev.snippet) {
155
+ markdown += ` \`\`\`\n ${ev.snippet}\n \`\`\`\n`;
156
+ }
157
+ });
158
+
159
+ markdown += '\n---\n\n';
160
+ });
161
+
162
+ fs.writeFileSync(checklistPath, markdown, 'utf8');
163
+
164
+ return {
165
+ checklistPath,
166
+ totalCandidates: candidates.candidates.length
167
+ };
168
+ }
169
+
170
+ /**
171
+ * Helper to ask questions in readline
172
+ */
173
+ function question(rl, prompt) {
174
+ return new Promise(resolve => {
175
+ rl.question(prompt, resolve);
176
+ });
177
+ }
178
+
179
+ module.exports = {
180
+ confirmIntent,
181
+ interactiveConfirm,
182
+ generateChecklistConfirm
183
+ };
@@ -0,0 +1,119 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const yaml = require('js-yaml');
4
+ const { createProvider } = require('./ai-provider');
5
+ const { loadConfig, checkAiEnabled } = require('./config');
6
+
7
+ /**
8
+ * RIPP Intent Discovery
9
+ * AI-assisted candidate intent inference from evidence packs
10
+ */
11
+
12
+ /**
13
+ * Discover candidate intent from evidence pack
14
+ */
15
+ async function discoverIntent(cwd, options = {}) {
16
+ // Load configuration
17
+ const config = loadConfig(cwd);
18
+
19
+ // Check if AI is enabled
20
+ const aiCheck = checkAiEnabled(config);
21
+ if (!aiCheck.enabled) {
22
+ throw new Error(`AI is not enabled: ${aiCheck.reason}`);
23
+ }
24
+
25
+ // Load evidence pack
26
+ const evidencePath = path.join(cwd, '.ripp', 'evidence', 'evidence.index.json');
27
+ if (!fs.existsSync(evidencePath)) {
28
+ throw new Error('Evidence pack not found. Run "ripp evidence build" first.');
29
+ }
30
+
31
+ const evidencePack = JSON.parse(fs.readFileSync(evidencePath, 'utf8'));
32
+
33
+ // Create AI provider
34
+ const provider = createProvider(config.ai);
35
+
36
+ if (!provider.isConfigured()) {
37
+ throw new Error('AI provider is not properly configured. Check environment variables.');
38
+ }
39
+
40
+ // Infer intent
41
+ const targetLevel = options.targetLevel || 1;
42
+ const candidates = await provider.inferIntent(evidencePack, {
43
+ targetLevel,
44
+ minConfidence: config.discovery.minConfidence
45
+ });
46
+
47
+ // Filter by minimum confidence if configured
48
+ if (config.discovery.minConfidence > 0) {
49
+ candidates.candidates = candidates.candidates.filter(
50
+ c => c.confidence >= config.discovery.minConfidence
51
+ );
52
+ }
53
+
54
+ // Write candidates to file
55
+ const candidatesPath = path.join(cwd, '.ripp', 'intent.candidates.yaml');
56
+ const yamlContent = yaml.dump(candidates, { indent: 2, lineWidth: 100 });
57
+ fs.writeFileSync(candidatesPath, yamlContent, 'utf8');
58
+
59
+ return {
60
+ candidates,
61
+ candidatesPath,
62
+ totalCandidates: candidates.candidates.length
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Validate candidate intent structure
68
+ */
69
+ function validateCandidates(candidates) {
70
+ const errors = [];
71
+
72
+ if (!candidates.version || candidates.version !== '1.0') {
73
+ errors.push('Missing or invalid version field');
74
+ }
75
+
76
+ if (!candidates.created) {
77
+ errors.push('Missing created timestamp');
78
+ }
79
+
80
+ if (!Array.isArray(candidates.candidates)) {
81
+ errors.push('candidates must be an array');
82
+ return errors; // Can't continue validation
83
+ }
84
+
85
+ candidates.candidates.forEach((candidate, index) => {
86
+ const prefix = `Candidate ${index + 1}`;
87
+
88
+ if (candidate.source !== 'inferred') {
89
+ errors.push(`${prefix}: source must be "inferred"`);
90
+ }
91
+
92
+ if (
93
+ typeof candidate.confidence !== 'number' ||
94
+ candidate.confidence < 0 ||
95
+ candidate.confidence > 1
96
+ ) {
97
+ errors.push(`${prefix}: confidence must be between 0.0 and 1.0`);
98
+ }
99
+
100
+ if (!Array.isArray(candidate.evidence) || candidate.evidence.length === 0) {
101
+ errors.push(`${prefix}: must have at least one evidence reference`);
102
+ }
103
+
104
+ if (candidate.requires_human_confirmation !== true) {
105
+ errors.push(`${prefix}: requires_human_confirmation must be true`);
106
+ }
107
+
108
+ if (!candidate.content) {
109
+ errors.push(`${prefix}: missing content field`);
110
+ }
111
+ });
112
+
113
+ return errors;
114
+ }
115
+
116
+ module.exports = {
117
+ discoverIntent,
118
+ validateCandidates
119
+ };
@@ -0,0 +1,368 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const crypto = require('crypto');
4
+ const { glob } = require('glob');
5
+ const yaml = require('js-yaml');
6
+
7
+ /**
8
+ * RIPP Evidence Pack Builder
9
+ * Scans repository and extracts high-signal facts for intent discovery
10
+ */
11
+
12
+ // Secret patterns for redaction (best-effort)
13
+ // More conservative patterns to avoid false positives
14
+ const SECRET_PATTERNS = [
15
+ /api[_-]?key[s]?\s*[:=]\s*['"]([^'"]+)['"]/gi,
16
+ /secret[_-]?key[s]?\s*[:=]\s*['"]([^'"]+)['"]/gi,
17
+ /password[s]?\s*[:=]\s*['"]([^'"]+)['"]/gi,
18
+ /token[s]?\s*[:=]\s*['"]([^'"]+)['"]/gi,
19
+ /bearer\s+([a-zA-Z0-9_\-\.]{20,})/gi, // Bearer tokens
20
+ /sk-[a-zA-Z0-9]{32,}/gi, // OpenAI-style keys
21
+ /ghp_[a-zA-Z0-9]{36,}/gi // GitHub tokens
22
+ ];
23
+
24
+ /**
25
+ * Build evidence pack from repository
26
+ */
27
+ async function buildEvidencePack(cwd, config) {
28
+ const evidenceDir = path.join(cwd, '.ripp', 'evidence');
29
+
30
+ // Create evidence directory
31
+ if (!fs.existsSync(evidenceDir)) {
32
+ fs.mkdirSync(evidenceDir, { recursive: true });
33
+ }
34
+
35
+ // Scan files
36
+ const { files, excludedCount } = await scanFiles(cwd, config.evidencePack);
37
+
38
+ // Extract evidence from files
39
+ const evidence = await extractEvidence(files, cwd);
40
+
41
+ // Build index
42
+ const index = {
43
+ version: '1.0',
44
+ created: new Date().toISOString(),
45
+ stats: {
46
+ totalFiles: files.length + excludedCount,
47
+ totalSize: files.reduce((sum, f) => sum + f.size, 0),
48
+ includedFiles: files.length,
49
+ excludedFiles: excludedCount
50
+ },
51
+ includePatterns: config.evidencePack.includeGlobs,
52
+ excludePatterns: config.evidencePack.excludeGlobs,
53
+ files: files.map(f => ({
54
+ path: f.path,
55
+ hash: f.hash,
56
+ size: f.size,
57
+ type: f.type
58
+ })),
59
+ evidence
60
+ };
61
+
62
+ // Write index
63
+ const indexPath = path.join(evidenceDir, 'evidence.index.json');
64
+ fs.writeFileSync(indexPath, JSON.stringify(index, null, 2), 'utf8');
65
+
66
+ // Copy relevant files to evidence directory (optional, for provenance)
67
+ // For now, we just keep the index with hashes
68
+
69
+ return { index, evidenceDir, indexPath };
70
+ }
71
+
72
+ /**
73
+ * Scan files matching include/exclude patterns
74
+ */
75
+ async function scanFiles(cwd, evidenceConfig) {
76
+ const files = [];
77
+ const { includeGlobs, excludeGlobs, maxFileSize } = evidenceConfig;
78
+
79
+ // Build combined pattern
80
+ const patterns = includeGlobs.map(pattern => path.join(cwd, pattern));
81
+
82
+ for (const pattern of patterns) {
83
+ const matches = await glob(pattern, {
84
+ ignore: excludeGlobs.map(ex => path.join(cwd, ex)),
85
+ nodir: true,
86
+ absolute: true
87
+ });
88
+
89
+ for (const filePath of matches) {
90
+ try {
91
+ const stats = fs.statSync(filePath);
92
+
93
+ // Skip files that are too large
94
+ if (stats.size > maxFileSize) {
95
+ continue;
96
+ }
97
+
98
+ const content = fs.readFileSync(filePath, 'utf8');
99
+ const hash = crypto.createHash('sha256').update(content).digest('hex');
100
+ const relativePath = path.relative(cwd, filePath);
101
+
102
+ files.push({
103
+ path: relativePath,
104
+ absolutePath: filePath,
105
+ hash,
106
+ size: stats.size,
107
+ type: detectFileType(relativePath),
108
+ content
109
+ });
110
+ } catch (error) {
111
+ // Skip files we can't read
112
+ continue;
113
+ }
114
+ }
115
+ }
116
+
117
+ return files;
118
+ }
119
+
120
+ /**
121
+ * Detect file type based on path and extension
122
+ */
123
+ function detectFileType(filePath) {
124
+ const ext = path.extname(filePath).toLowerCase();
125
+
126
+ if (filePath.includes('.github/workflows/')) {
127
+ return 'workflow';
128
+ }
129
+
130
+ if (ext === '.json' && (filePath.includes('package.json') || filePath.includes('schema'))) {
131
+ return 'config';
132
+ }
133
+
134
+ if (ext === '.sql' || filePath.includes('migration') || filePath.includes('schema')) {
135
+ return 'schema';
136
+ }
137
+
138
+ if (['.js', '.ts', '.jsx', '.tsx', '.py', '.rb', '.go', '.java', '.cs'].includes(ext)) {
139
+ return 'source';
140
+ }
141
+
142
+ return 'other';
143
+ }
144
+
145
+ /**
146
+ * Extract high-signal evidence from files
147
+ */
148
+ async function extractEvidence(files, cwd) {
149
+ const evidence = {
150
+ dependencies: [],
151
+ routes: [],
152
+ schemas: [],
153
+ auth: [],
154
+ workflows: []
155
+ };
156
+
157
+ for (const file of files) {
158
+ try {
159
+ // Extract dependencies
160
+ if (file.path.includes('package.json')) {
161
+ const deps = extractDependencies(file);
162
+ evidence.dependencies.push(...deps);
163
+ }
164
+
165
+ // Extract routes (best-effort pattern matching)
166
+ if (file.type === 'source') {
167
+ const routes = extractRoutes(file);
168
+ evidence.routes.push(...routes);
169
+
170
+ // Extract auth signals
171
+ const auth = extractAuthSignals(file);
172
+ evidence.auth.push(...auth);
173
+ }
174
+
175
+ // Extract schemas
176
+ if (file.type === 'schema' || file.path.includes('model')) {
177
+ const schemas = extractSchemas(file);
178
+ evidence.schemas.push(...schemas);
179
+ }
180
+
181
+ // Extract workflows
182
+ if (file.type === 'workflow') {
183
+ const workflow = extractWorkflow(file);
184
+ if (workflow) {
185
+ evidence.workflows.push(workflow);
186
+ }
187
+ }
188
+ } catch (error) {
189
+ // Skip files with extraction errors
190
+ continue;
191
+ }
192
+ }
193
+
194
+ return evidence;
195
+ }
196
+
197
+ /**
198
+ * Extract dependencies from package.json
199
+ */
200
+ function extractDependencies(file) {
201
+ const deps = [];
202
+
203
+ try {
204
+ const pkg = JSON.parse(file.content);
205
+
206
+ if (pkg.dependencies) {
207
+ for (const [name, version] of Object.entries(pkg.dependencies)) {
208
+ deps.push({
209
+ name,
210
+ version,
211
+ type: 'runtime',
212
+ source: file.path
213
+ });
214
+ }
215
+ }
216
+
217
+ if (pkg.devDependencies) {
218
+ for (const [name, version] of Object.entries(pkg.devDependencies)) {
219
+ deps.push({
220
+ name,
221
+ version,
222
+ type: 'dev',
223
+ source: file.path
224
+ });
225
+ }
226
+ }
227
+ } catch (error) {
228
+ // Invalid JSON, skip
229
+ }
230
+
231
+ return deps;
232
+ }
233
+
234
+ /**
235
+ * Extract API routes (best-effort pattern matching)
236
+ */
237
+ function extractRoutes(file) {
238
+ const routes = [];
239
+ const lines = file.content.split('\n');
240
+
241
+ // Common patterns for Express, FastAPI, etc.
242
+ const routePatterns = [
243
+ /\.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
244
+ /@(Get|Post|Put|Delete|Patch)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
245
+ /router\.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
246
+ /app\.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)['"`]/gi
247
+ ];
248
+
249
+ lines.forEach((line, index) => {
250
+ for (const pattern of routePatterns) {
251
+ pattern.lastIndex = 0; // Reset regex
252
+ const match = pattern.exec(line);
253
+ if (match) {
254
+ const method = match[1].toUpperCase();
255
+ const routePath = match[2];
256
+
257
+ routes.push({
258
+ method,
259
+ path: routePath,
260
+ source: `${file.path}:${index + 1}`,
261
+ snippet: redactSecrets(line.trim())
262
+ });
263
+ }
264
+ }
265
+ });
266
+
267
+ return routes;
268
+ }
269
+
270
+ /**
271
+ * Extract auth/permission signals
272
+ */
273
+ function extractAuthSignals(file) {
274
+ const signals = [];
275
+ const lines = file.content.split('\n');
276
+
277
+ const authPatterns = [
278
+ { pattern: /(authenticate|auth|requireAuth|isAuthenticated)/i, type: 'middleware' },
279
+ { pattern: /(authorize|checkPermission|requireRole|hasRole)/i, type: 'guard' },
280
+ { pattern: /(jwt|bearer|oauth|session)/i, type: 'config' }
281
+ ];
282
+
283
+ lines.forEach((line, index) => {
284
+ for (const { pattern, type } of authPatterns) {
285
+ if (pattern.test(line)) {
286
+ signals.push({
287
+ type,
288
+ source: `${file.path}:${index + 1}`,
289
+ snippet: redactSecrets(line.trim())
290
+ });
291
+ break; // Only one signal per line
292
+ }
293
+ }
294
+ });
295
+
296
+ return signals;
297
+ }
298
+
299
+ /**
300
+ * Extract schema definitions (best-effort)
301
+ */
302
+ function extractSchemas(file) {
303
+ const schemas = [];
304
+
305
+ // Try to detect schema type
306
+ if (file.path.includes('migration')) {
307
+ schemas.push({
308
+ name: path.basename(file.path, path.extname(file.path)),
309
+ type: 'migration',
310
+ source: file.path,
311
+ snippet: redactSecrets(file.content.substring(0, 500))
312
+ });
313
+ } else if (file.path.includes('model')) {
314
+ schemas.push({
315
+ name: path.basename(file.path, path.extname(file.path)),
316
+ type: 'model',
317
+ source: file.path,
318
+ snippet: redactSecrets(file.content.substring(0, 500))
319
+ });
320
+ }
321
+
322
+ return schemas;
323
+ }
324
+
325
+ /**
326
+ * Extract workflow configuration
327
+ */
328
+ function extractWorkflow(file) {
329
+ try {
330
+ const workflow = yaml.load(file.content);
331
+
332
+ if (workflow && workflow.name) {
333
+ return {
334
+ name: workflow.name,
335
+ triggers: Object.keys(workflow.on || {}),
336
+ source: file.path
337
+ };
338
+ }
339
+ } catch (error) {
340
+ // Invalid YAML, skip
341
+ }
342
+
343
+ return null;
344
+ }
345
+
346
+ /**
347
+ * Redact potential secrets from text (best-effort)
348
+ */
349
+ function redactSecrets(text) {
350
+ let redacted = text;
351
+
352
+ for (const pattern of SECRET_PATTERNS) {
353
+ pattern.lastIndex = 0; // Reset regex
354
+ redacted = redacted.replace(pattern, (match, p1) => {
355
+ if (p1 && p1.length > 8) {
356
+ return match.replace(p1, '[REDACTED]');
357
+ }
358
+ return match;
359
+ });
360
+ }
361
+
362
+ return redacted;
363
+ }
364
+
365
+ module.exports = {
366
+ buildEvidencePack,
367
+ redactSecrets
368
+ };