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.
package/index.js ADDED
@@ -0,0 +1,1350 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const yaml = require('js-yaml');
6
+ const Ajv = require('ajv');
7
+ const addFormats = require('ajv-formats');
8
+ const { glob } = require('glob');
9
+ const { execSync } = require('child_process');
10
+ const { lintPacket, generateJsonReport, generateMarkdownReport } = require('./lib/linter');
11
+ const { packagePacket, formatAsJson, formatAsYaml, formatAsMarkdown } = require('./lib/packager');
12
+ const { analyzeInput } = require('./lib/analyzer');
13
+ const { initRepository } = require('./lib/init');
14
+ const { loadConfig, checkAiEnabled } = require('./lib/config');
15
+ const { buildEvidencePack } = require('./lib/evidence');
16
+ const { discoverIntent } = require('./lib/discovery');
17
+ const { confirmIntent } = require('./lib/confirmation');
18
+ const { buildCanonicalArtifacts } = require('./lib/build');
19
+ const { migrateDirectoryStructure } = require('./lib/migrate');
20
+
21
+ // ANSI color codes
22
+ const colors = {
23
+ reset: '\x1b[0m',
24
+ green: '\x1b[32m',
25
+ red: '\x1b[31m',
26
+ yellow: '\x1b[33m',
27
+ blue: '\x1b[34m',
28
+ gray: '\x1b[90m'
29
+ };
30
+
31
+ // Configuration constants
32
+ const MIN_FILES_FOR_TABLE = 4;
33
+ const MAX_FILENAME_WIDTH = 40;
34
+ const TRUNCATED_FILENAME_PREFIX_LENGTH = 3;
35
+
36
+ function log(color, symbol, message) {
37
+ console.log(`${color}${symbol}${colors.reset} ${message}`);
38
+ }
39
+
40
+ function loadSchema() {
41
+ const schemaPath = path.join(__dirname, '../../schema/ripp-1.0.schema.json');
42
+ try {
43
+ return JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
44
+ } catch (error) {
45
+ console.error(`${colors.red}Error: Could not load schema from ${schemaPath}${colors.reset}`);
46
+ console.error(error.message);
47
+ process.exit(1);
48
+ }
49
+ }
50
+
51
+ function loadPacket(filePath) {
52
+ const ext = path.extname(filePath);
53
+ const content = fs.readFileSync(filePath, 'utf8');
54
+
55
+ try {
56
+ if (ext === '.yaml' || ext === '.yml') {
57
+ return yaml.load(content);
58
+ } else if (ext === '.json') {
59
+ return JSON.parse(content);
60
+ } else {
61
+ throw new Error(`Unsupported file extension: ${ext}`);
62
+ }
63
+ } catch (error) {
64
+ throw new Error(`Failed to parse ${filePath}: ${error.message}`);
65
+ }
66
+ }
67
+
68
+ function validatePacket(packet, schema, filePath, options = {}) {
69
+ const ajv = new Ajv({ allErrors: true, strict: false });
70
+ addFormats(ajv);
71
+
72
+ const validate = ajv.compile(schema);
73
+ const valid = validate(packet);
74
+
75
+ const errors = [];
76
+ const warnings = [];
77
+ const levelRequirements = {
78
+ 2: ['api_contracts', 'permissions', 'failure_modes'],
79
+ 3: ['api_contracts', 'permissions', 'failure_modes', 'audit_events', 'nfrs', 'acceptance_tests']
80
+ };
81
+
82
+ // Schema validation errors with enhanced messaging
83
+ if (!valid) {
84
+ validate.errors.forEach(error => {
85
+ const field = error.instancePath || 'root';
86
+ let message = error.message;
87
+
88
+ // Enhanced error messages for level-based requirements
89
+ if (error.keyword === 'required' && packet.level >= 2) {
90
+ const missingProp = error.params.missingProperty;
91
+ const level = packet.level;
92
+ const isLevelRequirement = levelRequirements[level]?.includes(missingProp);
93
+
94
+ if (isLevelRequirement) {
95
+ message = `Level ${level} requires '${missingProp}' (missing)`;
96
+ errors.push(message);
97
+ return;
98
+ }
99
+ }
100
+
101
+ // Improve "additional properties" errors
102
+ if (error.keyword === 'additionalProperties') {
103
+ const additionalProp = error.params.additionalProperty;
104
+ message = `unexpected property '${additionalProp}'`;
105
+ errors.push(`${field}: ${message}`);
106
+ return;
107
+ }
108
+
109
+ errors.push(`${field}: ${message}`);
110
+ });
111
+ }
112
+
113
+ // File naming convention check
114
+ const basename = path.basename(filePath);
115
+ if (!basename.match(/\.ripp\.(yaml|yml|json)$/)) {
116
+ warnings.push('File does not follow naming convention (*.ripp.yaml or *.ripp.json)');
117
+ }
118
+
119
+ // packet_id format check
120
+ if (packet.packet_id && !packet.packet_id.match(/^[a-z0-9-]+$/)) {
121
+ errors.push('packet_id must be lowercase with hyphens only (kebab-case)');
122
+ }
123
+
124
+ // Minimum level enforcement
125
+ if (options.minLevel && packet.level < options.minLevel) {
126
+ errors.push(
127
+ `Packet is Level ${packet.level}, but minimum Level ${options.minLevel} is required`
128
+ );
129
+ }
130
+
131
+ // Check template files (allow them to have placeholder values)
132
+ const isTemplate = basename.includes('template');
133
+ if (isTemplate) {
134
+ warnings.push('Template file detected - validation may show expected errors');
135
+ }
136
+
137
+ return { valid: errors.length === 0, errors, warnings, level: packet.level };
138
+ }
139
+
140
+ async function findRippFiles(pathArg) {
141
+ const stats = fs.statSync(pathArg);
142
+
143
+ if (stats.isFile()) {
144
+ return [pathArg];
145
+ } else if (stats.isDirectory()) {
146
+ const pattern = path.join(pathArg, '**/*.ripp.{yaml,yml,json}');
147
+ return await glob(pattern, { nodir: true });
148
+ } else {
149
+ throw new Error(`Invalid path: ${pathArg}`);
150
+ }
151
+ }
152
+
153
+ function printResults(results, options) {
154
+ console.log('');
155
+
156
+ let totalValid = 0;
157
+ let totalInvalid = 0;
158
+ const levelMissingFields = new Map(); // Track missing fields by level
159
+
160
+ // Show summary table for multiple files when not verbose
161
+ if (results.length >= MIN_FILES_FOR_TABLE && !options.verbose) {
162
+ console.log('┌──────────────────────────────────────────┬───────┬────────┬────────┐');
163
+ console.log('│ File │ Level │ Status │ Issues │');
164
+ console.log('├──────────────────────────────────────────┼───────┼────────┼────────┤');
165
+
166
+ results.forEach(result => {
167
+ const fileName =
168
+ result.file.length > MAX_FILENAME_WIDTH
169
+ ? '...' + result.file.slice(-(MAX_FILENAME_WIDTH - TRUNCATED_FILENAME_PREFIX_LENGTH))
170
+ : result.file;
171
+ const paddedFile = fileName.padEnd(MAX_FILENAME_WIDTH);
172
+ const level = result.level ? result.level.toString().padEnd(5) : 'N/A ';
173
+ const status = result.valid ? '✓ ' : '✗ ';
174
+ const statusColor = result.valid ? colors.green : colors.red;
175
+ const issues = result.errors.length.toString().padEnd(6);
176
+
177
+ console.log(
178
+ `│ ${paddedFile} │ ${level} │ ${statusColor}${status}${colors.reset} │ ${issues} │`
179
+ );
180
+
181
+ if (result.valid) {
182
+ totalValid++;
183
+ } else {
184
+ totalInvalid++;
185
+ }
186
+ });
187
+
188
+ console.log('└──────────────────────────────────────────┴───────┴────────┴────────┘');
189
+ console.log('');
190
+
191
+ if (totalInvalid > 0) {
192
+ log(
193
+ colors.red,
194
+ '✗',
195
+ `${totalInvalid} of ${results.length} failed. Run with --verbose for details.`
196
+ );
197
+ } else {
198
+ log(colors.green, '✓', `All ${totalValid} RIPP packets are valid.`);
199
+ }
200
+
201
+ console.log('');
202
+ return;
203
+ }
204
+
205
+ // Detailed output for verbose mode or small number of files
206
+ results.forEach(result => {
207
+ if (result.valid) {
208
+ totalValid++;
209
+ log(colors.green, '✓', `${result.file} is valid (Level ${result.level})`);
210
+ } else {
211
+ totalInvalid++;
212
+ const levelInfo = result.level ? ` (Level ${result.level})` : '';
213
+ log(colors.red, '✗', `${result.file}${levelInfo}`);
214
+ result.errors.forEach(error => {
215
+ console.log(` ${colors.red}•${colors.reset} ${error}`);
216
+
217
+ // Track level-based missing fields
218
+ const match = error.match(/Level (\d) requires '(\w+)'/);
219
+ if (match) {
220
+ const level = match[1];
221
+ const field = match[2];
222
+ if (!levelMissingFields.has(result.file)) {
223
+ levelMissingFields.set(result.file, { level, fields: [] });
224
+ }
225
+ levelMissingFields.get(result.file).fields.push(field);
226
+ }
227
+ });
228
+
229
+ // Add helpful tips for level-based errors
230
+ if (levelMissingFields.has(result.file)) {
231
+ const info = levelMissingFields.get(result.file);
232
+ console.log('');
233
+ console.log(
234
+ ` ${colors.blue}💡 Tip:${colors.reset} Use level: 1 for basic contracts, or add missing sections for Level ${info.level}`
235
+ );
236
+ console.log(
237
+ ` ${colors.blue}📖 Docs:${colors.reset} https://dylan-natter.github.io/ripp-protocol/ripp-levels.html`
238
+ );
239
+ }
240
+ }
241
+
242
+ if (result.warnings.length > 0 && !options.quiet) {
243
+ result.warnings.forEach(warning => {
244
+ console.log(` ${colors.yellow}⚠${colors.reset} ${warning}`);
245
+ });
246
+ }
247
+ });
248
+
249
+ console.log('');
250
+
251
+ if (totalInvalid > 0) {
252
+ log(colors.red, '✗', `${totalInvalid} of ${results.length} RIPP packets failed validation.`);
253
+ } else {
254
+ log(colors.green, '✓', `All ${totalValid} RIPP packets are valid.`);
255
+ }
256
+
257
+ console.log('');
258
+ }
259
+
260
+ function showHelp() {
261
+ const pkg = require('./package.json');
262
+ console.log(`
263
+ ${colors.blue}RIPP CLI v${pkg.version}${colors.reset}
264
+
265
+ ${colors.green}Commands:${colors.reset}
266
+ ripp init Initialize RIPP in your repository
267
+ ripp migrate Migrate legacy directory structure to new layout
268
+ ripp validate <path> Validate RIPP packets
269
+ ripp lint <path> Lint RIPP packets for best practices
270
+ ripp package --in <file> --out <file>
271
+ Package RIPP packet into normalized artifact
272
+ ripp analyze <input> --output <file>
273
+ Analyze code/schema and generate DRAFT RIPP packet
274
+
275
+ ${colors.blue}vNext - Intent Discovery Mode:${colors.reset}
276
+ ripp evidence build Build evidence pack from repository
277
+ ripp discover Infer candidate intent (requires AI enabled)
278
+ ripp confirm Confirm candidate intent (interactive)
279
+ ripp build Build canonical RIPP artifacts from confirmed intent
280
+
281
+ ripp --help Show this help message
282
+ ripp --version Show version
283
+
284
+ ${colors.green}Init Options:${colors.reset}
285
+ --force Overwrite existing files
286
+
287
+ ${colors.green}Migrate Options:${colors.reset}
288
+ --dry-run Preview changes without moving files
289
+
290
+ ${colors.green}Evidence Build Options:${colors.reset}
291
+ (Uses configuration from .ripp/config.yaml)
292
+
293
+ ${colors.green}Discover Options:${colors.reset}
294
+ --target-level <1|2|3> Target RIPP level (default: 1)
295
+ (Requires: ai.enabled=true in config AND RIPP_AI_ENABLED=true)
296
+
297
+ ${colors.green}Confirm Options:${colors.reset}
298
+ --interactive Interactive confirmation mode (default)
299
+ --checklist Generate markdown checklist for manual review
300
+ --user <id> User identifier for confirmation
301
+
302
+ ${colors.green}Build Options:${colors.reset}
303
+ --packet-id <id> Packet ID for generated RIPP (default: discovered-intent)
304
+ --title <title> Title for generated RIPP packet
305
+ --output-name <file> Output file name (default: handoff.ripp.yaml)
306
+
307
+ ${colors.green}Validate Options:${colors.reset}
308
+ --min-level <1|2|3> Enforce minimum RIPP level
309
+ --quiet Suppress warnings
310
+ --verbose Show detailed output for all files
311
+
312
+ ${colors.green}Lint Options:${colors.reset}
313
+ --strict Treat warnings as errors
314
+ --output <dir> Output directory for reports (default: reports/)
315
+
316
+ ${colors.green}Package Options:${colors.reset}
317
+ --in <file> Input RIPP packet file (required)
318
+ --out <file> Output file path (required)
319
+ --format <json|yaml|md> Output format (auto-detected from extension)
320
+ --package-version <version> Version string for the package (e.g., 1.0.0)
321
+ --force Overwrite existing output file without versioning
322
+ --skip-validation Skip validation entirely
323
+ --warn-on-invalid Validate but continue packaging on errors
324
+
325
+ ${colors.green}Analyze Options:${colors.reset}
326
+ <input> Input file (OpenAPI, JSON Schema)
327
+ --output <file> Output DRAFT RIPP packet file (required)
328
+ --packet-id <id> Packet ID for generated RIPP (default: analyzed)
329
+ --target-level <1|2|3> Target RIPP level (default: 1)
330
+
331
+ ${colors.green}Examples:${colors.reset}
332
+ ripp init
333
+ ripp init --force
334
+ ripp migrate
335
+ ripp migrate --dry-run
336
+ ripp validate my-feature.ripp.yaml
337
+ ripp validate ripp/intent/
338
+ ripp validate ripp/intent/ --min-level 2
339
+ ripp lint ripp/intent/
340
+ ripp lint ripp/intent/ --strict
341
+ ripp package --in feature.ripp.yaml --out handoff.md
342
+ ripp package --in feature.ripp.yaml --out handoff.md --package-version 1.0.0
343
+ ripp package --in feature.ripp.yaml --out handoff.md --force
344
+ ripp package --in feature.ripp.yaml --out handoff.md --warn-on-invalid
345
+ ripp package --in feature.ripp.yaml --out packaged.json --format json
346
+ ripp analyze openapi.json --output draft-api.ripp.yaml
347
+ ripp analyze openapi.json --output draft.ripp.yaml --target-level 2
348
+ ripp analyze schema.json --output draft.ripp.yaml --packet-id my-api
349
+
350
+ ${colors.blue}Intent Discovery Examples:${colors.reset}
351
+ ripp evidence build
352
+ RIPP_AI_ENABLED=true ripp discover --target-level 2
353
+ ripp confirm --interactive
354
+ ripp build --packet-id my-feature --title "My Feature"
355
+
356
+ ${colors.gray}Note: Legacy paths (features/, handoffs/, packages/) are supported for backward compatibility.${colors.reset}
357
+
358
+ ${colors.green}Exit Codes:${colors.reset}
359
+ 0 All checks passed
360
+ 1 Validation or lint failures found
361
+
362
+ ${colors.gray}Learn more: https://dylan-natter.github.io/ripp-protocol${colors.reset}
363
+ `);
364
+ }
365
+
366
+ function showVersion() {
367
+ const pkg = require('./package.json');
368
+ console.log(`ripp-cli v${pkg.version}`);
369
+ }
370
+
371
+ /**
372
+ * Apply a version string to a file path
373
+ * Examples:
374
+ * applyVersionToPath('handoff.zip', '1.0.0') => 'handoff-v1.0.0.zip'
375
+ * applyVersionToPath('handoff.md', '2.1.0') => 'handoff-v2.1.0.md'
376
+ */
377
+ function applyVersionToPath(filePath, version) {
378
+ const dir = path.dirname(filePath);
379
+ const ext = path.extname(filePath);
380
+ const base = path.basename(filePath, ext);
381
+
382
+ // Remove existing version suffix if present
383
+ const cleanBase = base.replace(/-v\d+(\.\d+)*$/, '');
384
+
385
+ // Add version prefix if not already present
386
+ const versionStr = version.startsWith('v') ? version : `v${version}`;
387
+
388
+ const newBase = `${cleanBase}-${versionStr}${ext}`;
389
+ return path.join(dir, newBase);
390
+ }
391
+
392
+ /**
393
+ * Get the next auto-increment version path
394
+ * Examples:
395
+ * handoff.zip exists => handoff-v2.zip
396
+ * handoff-v2.zip exists => handoff-v3.zip
397
+ */
398
+ function getNextVersionPath(filePath) {
399
+ const dir = path.dirname(filePath);
400
+ const ext = path.extname(filePath);
401
+ const base = path.basename(filePath, ext);
402
+
403
+ // Remove existing version suffix if present
404
+ const cleanBase = base.replace(/-v\d+$/, '');
405
+
406
+ let version = 2; // Start with v2 since v1 is the existing file
407
+ let newPath;
408
+
409
+ do {
410
+ newPath = path.join(dir, `${cleanBase}-v${version}${ext}`);
411
+ version++;
412
+ } while (fs.existsSync(newPath));
413
+
414
+ return newPath;
415
+ }
416
+
417
+ /**
418
+ * Get git information from the current repository
419
+ * Returns null if not in a git repo or git is not available
420
+ */
421
+ function getGitInfo() {
422
+ try {
423
+ // Check if we're in a git repo
424
+ execSync('git rev-parse --git-dir', { stdio: 'pipe' });
425
+
426
+ const commit = execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim();
427
+ const branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim();
428
+
429
+ return {
430
+ commit,
431
+ branch
432
+ };
433
+ } catch (error) {
434
+ // Not in a git repo or git not available
435
+ return null;
436
+ }
437
+ }
438
+
439
+ async function handleMigrateCommand(args) {
440
+ const dryRun = args.includes('--dry-run');
441
+
442
+ console.log(`${colors.blue}RIPP Directory Migration${colors.reset}\n`);
443
+ console.log('This will update your RIPP directory structure to the new layout:\n');
444
+ console.log(
445
+ ` ${colors.gray}ripp/features/ ${colors.reset}→ ${colors.green}ripp/intent/${colors.reset}`
446
+ );
447
+ console.log(
448
+ ` ${colors.gray}ripp/handoffs/ ${colors.reset}→ ${colors.green}ripp/output/handoffs/${colors.reset}`
449
+ );
450
+ console.log(
451
+ ` ${colors.gray}ripp/packages/ ${colors.reset}→ ${colors.green}ripp/output/packages/${colors.reset}\n`
452
+ );
453
+
454
+ if (dryRun) {
455
+ console.log(`${colors.yellow}ℹ DRY RUN MODE: No files will be moved${colors.reset}\n`);
456
+ }
457
+
458
+ try {
459
+ const results = migrateDirectoryStructure({ dryRun });
460
+
461
+ // Print moved directories
462
+ if (results.moved.length > 0) {
463
+ console.log(`${colors.green}✓ ${dryRun ? 'Would move' : 'Moved'}:${colors.reset}`);
464
+ results.moved.forEach(msg => {
465
+ console.log(` ${colors.green}→${colors.reset} ${msg}`);
466
+ });
467
+ console.log('');
468
+ }
469
+
470
+ // Print created directories
471
+ if (results.created.length > 0) {
472
+ console.log(`${colors.green}✓ ${dryRun ? 'Would create' : 'Created'}:${colors.reset}`);
473
+ results.created.forEach(msg => {
474
+ console.log(` ${colors.green}+${colors.reset} ${msg}`);
475
+ });
476
+ console.log('');
477
+ }
478
+
479
+ // Print skipped
480
+ if (results.skipped.length > 0) {
481
+ console.log(`${colors.blue}ℹ Info:${colors.reset}`);
482
+ results.skipped.forEach(msg => {
483
+ console.log(` ${colors.blue}•${colors.reset} ${msg}`);
484
+ });
485
+ console.log('');
486
+ }
487
+
488
+ // Print warnings
489
+ if (results.warnings.length > 0) {
490
+ console.log(`${colors.yellow}⚠ Warnings:${colors.reset}`);
491
+ results.warnings.forEach(msg => {
492
+ console.log(` ${colors.yellow}!${colors.reset} ${msg}`);
493
+ });
494
+ console.log('');
495
+ }
496
+
497
+ // Final summary
498
+ if (results.warnings.length > 0) {
499
+ log(
500
+ colors.yellow,
501
+ '⚠',
502
+ 'Migration completed with warnings. Please review conflicts manually.'
503
+ );
504
+ console.log('');
505
+ process.exit(0);
506
+ } else if (results.moved.length > 0 || results.created.length > 0) {
507
+ if (dryRun) {
508
+ log(colors.blue, 'ℹ', 'Dry run complete. Run without --dry-run to apply changes.');
509
+ } else {
510
+ log(colors.green, '✓', 'Migration complete!');
511
+ console.log('');
512
+ console.log(`${colors.blue}Next steps:${colors.reset}`);
513
+ console.log(' 1. Update your package.json scripts to use new paths');
514
+ console.log(' 2. Update any documentation referencing old paths');
515
+ console.log(' 3. Commit the changes to your repository');
516
+ }
517
+ console.log('');
518
+ process.exit(0);
519
+ } else {
520
+ log(colors.green, '✓', 'Already using new directory structure. No migration needed.');
521
+ console.log('');
522
+ process.exit(0);
523
+ }
524
+ } catch (error) {
525
+ console.error(`${colors.red}Migration failed: ${error.message}${colors.reset}`);
526
+ if (error.stack) {
527
+ console.error(`${colors.gray}${error.stack}${colors.reset}`);
528
+ }
529
+ process.exit(1);
530
+ }
531
+ }
532
+
533
+ async function main() {
534
+ const args = process.argv.slice(2);
535
+
536
+ if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
537
+ showHelp();
538
+ process.exit(0);
539
+ }
540
+
541
+ if (args.includes('--version') || args.includes('-v')) {
542
+ showVersion();
543
+ process.exit(0);
544
+ }
545
+
546
+ const command = args[0];
547
+
548
+ if (command === 'init') {
549
+ await handleInitCommand(args);
550
+ } else if (command === 'migrate') {
551
+ await handleMigrateCommand(args);
552
+ } else if (command === 'validate') {
553
+ await handleValidateCommand(args);
554
+ } else if (command === 'lint') {
555
+ await handleLintCommand(args);
556
+ } else if (command === 'package') {
557
+ await handlePackageCommand(args);
558
+ } else if (command === 'analyze') {
559
+ await handleAnalyzeCommand(args);
560
+ } else if (command === 'evidence' && args[1] === 'build') {
561
+ await handleEvidenceBuildCommand(args);
562
+ } else if (command === 'discover') {
563
+ await handleDiscoverCommand(args);
564
+ } else if (command === 'confirm') {
565
+ await handleConfirmCommand(args);
566
+ } else if (command === 'build') {
567
+ await handleBuildCommand(args);
568
+ } else {
569
+ console.error(`${colors.red}Error: Unknown command '${command}'${colors.reset}`);
570
+ console.error("Run 'ripp --help' for usage information.");
571
+ process.exit(1);
572
+ }
573
+ }
574
+
575
+ async function handleInitCommand(args) {
576
+ const options = {
577
+ force: args.includes('--force')
578
+ };
579
+
580
+ console.log(`${colors.blue}Initializing RIPP in repository...${colors.reset}\n`);
581
+
582
+ try {
583
+ const results = initRepository(options);
584
+
585
+ // Print created files
586
+ if (results.created.length > 0) {
587
+ console.log(`${colors.green}✓ Created:${colors.reset}`);
588
+ results.created.forEach(file => {
589
+ console.log(` ${colors.green}+${colors.reset} ${file}`);
590
+ });
591
+ console.log('');
592
+ }
593
+
594
+ // Print skipped files
595
+ if (results.skipped.length > 0) {
596
+ console.log(`${colors.yellow}ℹ Skipped:${colors.reset}`);
597
+ results.skipped.forEach(file => {
598
+ console.log(` ${colors.yellow}•${colors.reset} ${file}`);
599
+ });
600
+ console.log('');
601
+ }
602
+
603
+ // Print errors
604
+ if (results.errors.length > 0) {
605
+ console.log(`${colors.red}✗ Errors:${colors.reset}`);
606
+ results.errors.forEach(error => {
607
+ console.log(` ${colors.red}•${colors.reset} ${error}`);
608
+ });
609
+ console.log('');
610
+ }
611
+
612
+ // Final summary
613
+ if (results.errors.length > 0) {
614
+ log(colors.red, '✗', 'RIPP initialization completed with errors');
615
+ process.exit(1);
616
+ } else {
617
+ log(colors.green, '✓', 'RIPP initialization complete!');
618
+ console.log('');
619
+ console.log(`${colors.blue}Next steps:${colors.reset}`);
620
+ console.log(' 1. Add this script to your package.json:');
621
+ console.log('');
622
+ console.log(' "scripts": {');
623
+ console.log(' "ripp:validate": "ripp validate ripp/intent/"');
624
+ console.log(' }');
625
+ console.log('');
626
+ console.log(' 2. Create your first RIPP packet in ripp/intent/');
627
+ console.log(' 3. Validate it: npm run ripp:validate');
628
+ console.log(' 4. Commit the changes to your repository');
629
+ console.log('');
630
+ console.log(
631
+ `${colors.gray}Learn more: https://dylan-natter.github.io/ripp-protocol${colors.reset}`
632
+ );
633
+ console.log('');
634
+ process.exit(0);
635
+ }
636
+ } catch (error) {
637
+ console.error(`${colors.red}Error: ${error.message}${colors.reset}`);
638
+ process.exit(1);
639
+ }
640
+ }
641
+
642
+ async function handleValidateCommand(args) {
643
+ const pathArg = args[1];
644
+
645
+ if (!pathArg) {
646
+ console.error(`${colors.red}Error: Path argument required${colors.reset}`);
647
+ console.error('Usage: ripp validate <path>');
648
+ process.exit(1);
649
+ }
650
+
651
+ // Parse options
652
+ const options = {
653
+ minLevel: null,
654
+ quiet: args.includes('--quiet'),
655
+ verbose: args.includes('--verbose')
656
+ };
657
+
658
+ const minLevelIndex = args.indexOf('--min-level');
659
+ if (minLevelIndex !== -1 && args[minLevelIndex + 1]) {
660
+ options.minLevel = parseInt(args[minLevelIndex + 1]);
661
+ if (![1, 2, 3].includes(options.minLevel)) {
662
+ console.error(`${colors.red}Error: --min-level must be 1, 2, or 3${colors.reset}`);
663
+ process.exit(1);
664
+ }
665
+ }
666
+
667
+ // Check if path exists
668
+ if (!fs.existsSync(pathArg)) {
669
+ console.error(`${colors.red}Error: Path not found: ${pathArg}${colors.reset}`);
670
+ process.exit(1);
671
+ }
672
+
673
+ console.log(`${colors.blue}Validating RIPP packets...${colors.reset}`);
674
+
675
+ const schema = loadSchema();
676
+ const files = await findRippFiles(pathArg);
677
+
678
+ if (files.length === 0) {
679
+ console.log(`${colors.yellow}No RIPP files found in ${pathArg}${colors.reset}`);
680
+ process.exit(0);
681
+ }
682
+
683
+ const results = [];
684
+
685
+ for (const file of files) {
686
+ try {
687
+ const packet = loadPacket(file);
688
+ const result = validatePacket(packet, schema, file, options);
689
+ results.push({
690
+ file: path.relative(process.cwd(), file),
691
+ valid: result.valid,
692
+ errors: result.errors,
693
+ warnings: result.warnings,
694
+ level: result.level
695
+ });
696
+ } catch (error) {
697
+ results.push({
698
+ file: path.relative(process.cwd(), file),
699
+ valid: false,
700
+ errors: [error.message],
701
+ warnings: [],
702
+ level: null
703
+ });
704
+ }
705
+ }
706
+
707
+ printResults(results, options);
708
+
709
+ const hasFailures = results.some(r => !r.valid);
710
+ process.exit(hasFailures ? 1 : 0);
711
+ }
712
+
713
+ async function handleLintCommand(args) {
714
+ const pathArg = args[1];
715
+
716
+ if (!pathArg) {
717
+ console.error(`${colors.red}Error: Path argument required${colors.reset}`);
718
+ console.error('Usage: ripp lint <path>');
719
+ process.exit(1);
720
+ }
721
+
722
+ // Parse options
723
+ const options = {
724
+ strict: args.includes('--strict'),
725
+ outputDir: 'reports/'
726
+ };
727
+
728
+ const outputIndex = args.indexOf('--output');
729
+ if (outputIndex !== -1 && args[outputIndex + 1]) {
730
+ options.outputDir = args[outputIndex + 1];
731
+ }
732
+
733
+ // Ensure output directory ends with /
734
+ if (!options.outputDir.endsWith('/')) {
735
+ options.outputDir += '/';
736
+ }
737
+
738
+ // Check if path exists
739
+ if (!fs.existsSync(pathArg)) {
740
+ console.error(`${colors.red}Error: Path not found: ${pathArg}${colors.reset}`);
741
+ process.exit(1);
742
+ }
743
+
744
+ console.log(`${colors.blue}Linting RIPP packets...${colors.reset}`);
745
+
746
+ const schema = loadSchema();
747
+ const files = await findRippFiles(pathArg);
748
+
749
+ if (files.length === 0) {
750
+ console.log(`${colors.yellow}No RIPP files found in ${pathArg}${colors.reset}`);
751
+ process.exit(0);
752
+ }
753
+
754
+ const results = [];
755
+ let totalErrors = 0;
756
+ let totalWarnings = 0;
757
+
758
+ for (const file of files) {
759
+ try {
760
+ const packet = loadPacket(file);
761
+
762
+ // First validate against schema
763
+ const validation = validatePacket(packet, schema, file);
764
+
765
+ if (!validation.valid) {
766
+ // Skip linting if schema validation fails
767
+ console.log(
768
+ `${colors.yellow}⚠${colors.reset} ${path.relative(process.cwd(), file)} - Skipped (schema validation failed)`
769
+ );
770
+ continue;
771
+ }
772
+
773
+ // Run linter on valid packets
774
+ const lintResult = lintPacket(packet, file);
775
+
776
+ results.push({
777
+ file: path.relative(process.cwd(), file),
778
+ errors: lintResult.errors,
779
+ warnings: lintResult.warnings,
780
+ errorCount: lintResult.errorCount,
781
+ warningCount: lintResult.warningCount
782
+ });
783
+
784
+ totalErrors += lintResult.errorCount;
785
+ totalWarnings += lintResult.warningCount;
786
+
787
+ // Print inline results
788
+ if (lintResult.errorCount === 0 && lintResult.warningCount === 0) {
789
+ log(colors.green, '✓', `${path.relative(process.cwd(), file)} - No issues`);
790
+ } else {
791
+ if (lintResult.errorCount > 0) {
792
+ log(
793
+ colors.red,
794
+ '✗',
795
+ `${path.relative(process.cwd(), file)} - ${lintResult.errorCount} error(s), ${lintResult.warningCount} warning(s)`
796
+ );
797
+ } else {
798
+ log(
799
+ colors.yellow,
800
+ '⚠',
801
+ `${path.relative(process.cwd(), file)} - ${lintResult.warningCount} warning(s)`
802
+ );
803
+ }
804
+ }
805
+ } catch (error) {
806
+ console.log(
807
+ `${colors.red}✗${colors.reset} ${path.relative(process.cwd(), file)} - Parse error: ${error.message}`
808
+ );
809
+ }
810
+ }
811
+
812
+ console.log('');
813
+
814
+ // Create output directory if it doesn't exist
815
+ if (!fs.existsSync(options.outputDir)) {
816
+ fs.mkdirSync(options.outputDir, { recursive: true });
817
+ }
818
+
819
+ // Write JSON report
820
+ const jsonReport = generateJsonReport(results);
821
+ const jsonPath = path.join(options.outputDir, 'lint.json');
822
+ fs.writeFileSync(jsonPath, jsonReport);
823
+
824
+ // Write Markdown report
825
+ const mdReport = generateMarkdownReport(results);
826
+ const mdPath = path.join(options.outputDir, 'lint.md');
827
+ fs.writeFileSync(mdPath, mdReport);
828
+
829
+ console.log('');
830
+
831
+ // Summary
832
+ if (totalErrors > 0) {
833
+ log(colors.red, '✗', `Found ${totalErrors} error(s) and ${totalWarnings} warning(s)`);
834
+ } else if (totalWarnings > 0) {
835
+ log(colors.yellow, '⚠', `Found ${totalWarnings} warning(s)`);
836
+ } else {
837
+ log(colors.green, '✓', 'All packets passed linting checks');
838
+ }
839
+
840
+ console.log('');
841
+ console.log(`${colors.blue}📊 Reports generated:${colors.reset}`);
842
+ console.log(` ${colors.gray}•${colors.reset} ${jsonPath} (machine-readable)`);
843
+ console.log(` ${colors.gray}•${colors.reset} ${mdPath} (human-readable)`);
844
+ console.log('');
845
+ console.log(`${colors.gray}View: cat ${mdPath}${colors.reset}`);
846
+
847
+ console.log('');
848
+
849
+ // Exit with appropriate code
850
+ const hasFailures = totalErrors > 0 || (options.strict && totalWarnings > 0);
851
+ process.exit(hasFailures ? 1 : 0);
852
+ }
853
+
854
+ async function handlePackageCommand(args) {
855
+ // Parse options
856
+ const options = {
857
+ input: null,
858
+ output: null,
859
+ format: null,
860
+ version: null,
861
+ force: args.includes('--force'),
862
+ skipValidation: args.includes('--skip-validation'),
863
+ warnOnInvalid: args.includes('--warn-on-invalid')
864
+ };
865
+
866
+ const inIndex = args.indexOf('--in');
867
+ if (inIndex !== -1 && args[inIndex + 1]) {
868
+ options.input = args[inIndex + 1];
869
+ }
870
+
871
+ const outIndex = args.indexOf('--out');
872
+ if (outIndex !== -1 && args[outIndex + 1]) {
873
+ options.output = args[outIndex + 1];
874
+ }
875
+
876
+ const formatIndex = args.indexOf('--format');
877
+ if (formatIndex !== -1 && args[formatIndex + 1]) {
878
+ options.format = args[formatIndex + 1].toLowerCase();
879
+ }
880
+
881
+ const versionIndex = args.indexOf('--package-version');
882
+ if (versionIndex !== -1 && args[versionIndex + 1]) {
883
+ options.version = args[versionIndex + 1];
884
+ }
885
+
886
+ // Validate required options
887
+ if (!options.input) {
888
+ console.error(`${colors.red}Error: --in <file> is required${colors.reset}`);
889
+ console.error('Usage: ripp package --in <file> --out <file>');
890
+ process.exit(1);
891
+ }
892
+
893
+ if (!options.output) {
894
+ console.error(`${colors.red}Error: --out <file> is required${colors.reset}`);
895
+ console.error('Usage: ripp package --in <file> --out <file>');
896
+ process.exit(1);
897
+ }
898
+
899
+ // Check if input exists
900
+ if (!fs.existsSync(options.input)) {
901
+ console.error(`${colors.red}Error: Input file not found: ${options.input}${colors.reset}`);
902
+ process.exit(1);
903
+ }
904
+
905
+ // Auto-detect format from output extension if not specified
906
+ if (!options.format) {
907
+ const ext = path.extname(options.output).toLowerCase();
908
+ if (ext === '.json') {
909
+ options.format = 'json';
910
+ } else if (ext === '.yaml' || ext === '.yml') {
911
+ options.format = 'yaml';
912
+ } else if (ext === '.md') {
913
+ options.format = 'md';
914
+ } else {
915
+ console.error(
916
+ `${colors.red}Error: Unable to detect format from extension. Use --format <json|yaml|md>${colors.reset}`
917
+ );
918
+ process.exit(1);
919
+ }
920
+ }
921
+
922
+ // Validate format
923
+ if (!['json', 'yaml', 'md'].includes(options.format)) {
924
+ console.error(
925
+ `${colors.red}Error: Invalid format '${options.format}'. Must be json, yaml, or md${colors.reset}`
926
+ );
927
+ process.exit(1);
928
+ }
929
+
930
+ console.log(`${colors.blue}Packaging RIPP packet...${colors.reset}`);
931
+
932
+ try {
933
+ // Load the packet
934
+ const packet = loadPacket(options.input);
935
+ const schema = loadSchema();
936
+
937
+ // Validation handling
938
+ let validation = { valid: true, errors: [], warnings: [] };
939
+ let validationStatus = 'unvalidated';
940
+
941
+ if (!options.skipValidation) {
942
+ validation = validatePacket(packet, schema, options.input);
943
+
944
+ if (!validation.valid) {
945
+ validationStatus = 'invalid';
946
+
947
+ if (options.warnOnInvalid) {
948
+ // Warn but continue
949
+ console.log(
950
+ `${colors.yellow}⚠ Warning: Input packet has validation errors${colors.reset}`
951
+ );
952
+ validation.errors.forEach(error => {
953
+ console.log(` ${colors.yellow}•${colors.reset} ${error}`);
954
+ });
955
+ console.log('');
956
+ } else {
957
+ // Fail on validation error (default behavior)
958
+ console.error(`${colors.red}Error: Input packet failed validation${colors.reset}`);
959
+ validation.errors.forEach(error => {
960
+ console.error(` ${colors.red}•${colors.reset} ${error}`);
961
+ });
962
+ console.log('');
963
+ console.log(
964
+ `${colors.blue}💡 Tip:${colors.reset} Use --warn-on-invalid to package anyway, or --skip-validation to skip validation`
965
+ );
966
+ process.exit(1);
967
+ }
968
+ } else {
969
+ validationStatus = 'valid';
970
+ }
971
+ }
972
+
973
+ // Determine final output path with versioning
974
+ let finalOutputPath = options.output;
975
+
976
+ if (!options.force && fs.existsSync(options.output)) {
977
+ // File exists and --force not specified, apply versioning
978
+ if (options.version) {
979
+ // Explicit version provided
980
+ finalOutputPath = applyVersionToPath(options.output, options.version);
981
+ } else {
982
+ // Auto-increment version
983
+ finalOutputPath = getNextVersionPath(options.output);
984
+ }
985
+
986
+ console.log(
987
+ `${colors.yellow}ℹ${colors.reset} Output file exists. Versioning applied: ${path.basename(finalOutputPath)}`
988
+ );
989
+ console.log('');
990
+ } else if (options.version) {
991
+ // Explicit version provided, use it even if file doesn't exist
992
+ finalOutputPath = applyVersionToPath(options.output, options.version);
993
+ }
994
+
995
+ // Get git information if available
996
+ const gitInfo = getGitInfo();
997
+
998
+ // Package the packet with enhanced metadata
999
+ const packaged = packagePacket(packet, {
1000
+ version: options.version,
1001
+ gitInfo,
1002
+ validationStatus,
1003
+ validationErrors: validation.errors.length,
1004
+ sourceFile: path.basename(options.input)
1005
+ });
1006
+
1007
+ // Format according to requested format
1008
+ let output;
1009
+ if (options.format === 'json') {
1010
+ output = formatAsJson(packaged, { pretty: true });
1011
+ } else if (options.format === 'yaml') {
1012
+ output = formatAsYaml(packaged);
1013
+ } else if (options.format === 'md') {
1014
+ output = formatAsMarkdown(packaged);
1015
+ }
1016
+
1017
+ // Write to output file
1018
+ fs.writeFileSync(finalOutputPath, output);
1019
+
1020
+ log(colors.green, '✓', `Packaged successfully: ${finalOutputPath}`);
1021
+ console.log(` ${colors.gray}Format: ${options.format}${colors.reset}`);
1022
+ console.log(` ${colors.gray}Level: ${packet.level}${colors.reset}`);
1023
+ if (options.version) {
1024
+ console.log(` ${colors.gray}Package Version: ${options.version}${colors.reset}`);
1025
+ }
1026
+ if (validationStatus !== 'valid') {
1027
+ console.log(` ${colors.gray}Validation: ${validationStatus}${colors.reset}`);
1028
+ }
1029
+ console.log('');
1030
+
1031
+ process.exit(0);
1032
+ } catch (error) {
1033
+ console.error(`${colors.red}Error: ${error.message}${colors.reset}`);
1034
+ process.exit(1);
1035
+ }
1036
+ }
1037
+
1038
+ async function handleAnalyzeCommand(args) {
1039
+ const inputPath = args[1];
1040
+
1041
+ // Parse options
1042
+ const options = {
1043
+ output: null,
1044
+ packetId: 'analyzed',
1045
+ targetLevel: 1 // Default to Level 1
1046
+ };
1047
+
1048
+ const outputIndex = args.indexOf('--output');
1049
+ if (outputIndex !== -1 && args[outputIndex + 1]) {
1050
+ options.output = args[outputIndex + 1];
1051
+ }
1052
+
1053
+ const packetIdIndex = args.indexOf('--packet-id');
1054
+ if (packetIdIndex !== -1 && args[packetIdIndex + 1]) {
1055
+ options.packetId = args[packetIdIndex + 1];
1056
+ }
1057
+
1058
+ const targetLevelIndex = args.indexOf('--target-level');
1059
+ if (targetLevelIndex !== -1 && args[targetLevelIndex + 1]) {
1060
+ options.targetLevel = parseInt(args[targetLevelIndex + 1]);
1061
+ if (![1, 2, 3].includes(options.targetLevel)) {
1062
+ console.error(`${colors.red}Error: --target-level must be 1, 2, or 3${colors.reset}`);
1063
+ process.exit(1);
1064
+ }
1065
+ }
1066
+
1067
+ // Validate required options
1068
+ if (!inputPath) {
1069
+ console.error(`${colors.red}Error: Input file argument required${colors.reset}`);
1070
+ console.error('Usage: ripp analyze <input> --output <file>');
1071
+ process.exit(1);
1072
+ }
1073
+
1074
+ if (!options.output) {
1075
+ console.error(`${colors.red}Error: --output <file> is required${colors.reset}`);
1076
+ console.error('Usage: ripp analyze <input> --output <file>');
1077
+ process.exit(1);
1078
+ }
1079
+
1080
+ // Check if input exists
1081
+ if (!fs.existsSync(inputPath)) {
1082
+ console.error(`${colors.red}Error: Input file not found: ${inputPath}${colors.reset}`);
1083
+ process.exit(1);
1084
+ }
1085
+
1086
+ console.log(`${colors.blue}Analyzing input...${colors.reset}`);
1087
+ console.log(
1088
+ `${colors.yellow}⚠${colors.reset} Generated packets are DRAFTS and require human review\n`
1089
+ );
1090
+
1091
+ try {
1092
+ // Analyze the input
1093
+ const draftPacket = analyzeInput(inputPath, {
1094
+ packetId: options.packetId,
1095
+ targetLevel: options.targetLevel
1096
+ });
1097
+
1098
+ // Auto-detect output format
1099
+ const ext = path.extname(options.output).toLowerCase();
1100
+ let output;
1101
+
1102
+ if (ext === '.yaml' || ext === '.yml') {
1103
+ output = yaml.dump(draftPacket, { indent: 2, lineWidth: 100 });
1104
+ } else if (ext === '.json') {
1105
+ output = JSON.stringify(draftPacket, null, 2);
1106
+ } else {
1107
+ // Default to YAML
1108
+ output = yaml.dump(draftPacket, { indent: 2, lineWidth: 100 });
1109
+ }
1110
+
1111
+ // Write to output file
1112
+ fs.writeFileSync(options.output, output);
1113
+
1114
+ log(colors.green, '✓', `DRAFT packet generated: ${options.output}`);
1115
+ console.log(` ${colors.gray}Status: draft (requires human review)${colors.reset}`);
1116
+ console.log(` ${colors.gray}Level: ${draftPacket.level}${colors.reset}`);
1117
+ console.log('');
1118
+
1119
+ console.log(`${colors.yellow}⚠ IMPORTANT:${colors.reset}`);
1120
+ console.log(' This is a DRAFT generated from observable code/schema facts.');
1121
+ console.log(' Review and refine all TODO items before use.');
1122
+ console.log(' Pay special attention to:');
1123
+ console.log(' - Purpose (problem, solution, value)');
1124
+ console.log(' - UX Flow (user-facing steps)');
1125
+ if (draftPacket.level >= 2) {
1126
+ console.log(' - Permissions and failure modes');
1127
+ }
1128
+ console.log('');
1129
+
1130
+ process.exit(0);
1131
+ } catch (error) {
1132
+ console.error(`${colors.red}Error: ${error.message}${colors.reset}`);
1133
+
1134
+ // Provide better error messages for unsupported formats
1135
+ if (error.message.includes('Failed to parse') || error.message.includes('Unsupported')) {
1136
+ console.log('');
1137
+ console.log(`${colors.blue}ℹ Supported formats:${colors.reset}`);
1138
+ console.log(' • OpenAPI/Swagger (.json, .yaml)');
1139
+ console.log(' • JSON Schema (.json)');
1140
+ console.log('');
1141
+ }
1142
+
1143
+ process.exit(1);
1144
+ }
1145
+ }
1146
+
1147
+ async function handleEvidenceBuildCommand() {
1148
+ const cwd = process.cwd();
1149
+
1150
+ console.log(`${colors.blue}Building evidence pack...${colors.reset}\n`);
1151
+
1152
+ try {
1153
+ const config = loadConfig(cwd);
1154
+ const result = await buildEvidencePack(cwd, config);
1155
+
1156
+ log(colors.green, '✓', 'Evidence pack built successfully');
1157
+ console.log(` ${colors.gray}Index: ${result.indexPath}${colors.reset}`);
1158
+ console.log(` ${colors.gray}Files: ${result.index.stats.includedFiles}${colors.reset}`);
1159
+ console.log(
1160
+ ` ${colors.gray}Size: ${(result.index.stats.totalSize / 1024).toFixed(2)} KB${colors.reset}`
1161
+ );
1162
+ console.log('');
1163
+
1164
+ console.log(`${colors.blue}Evidence Summary:${colors.reset}`);
1165
+ console.log(
1166
+ ` ${colors.gray}Dependencies: ${result.index.evidence.dependencies.length}${colors.reset}`
1167
+ );
1168
+ console.log(` ${colors.gray}Routes: ${result.index.evidence.routes.length}${colors.reset}`);
1169
+ console.log(` ${colors.gray}Schemas: ${result.index.evidence.schemas.length}${colors.reset}`);
1170
+ console.log(
1171
+ ` ${colors.gray}Auth Signals: ${result.index.evidence.auth.length}${colors.reset}`
1172
+ );
1173
+ console.log(
1174
+ ` ${colors.gray}Workflows: ${result.index.evidence.workflows.length}${colors.reset}`
1175
+ );
1176
+ console.log('');
1177
+
1178
+ console.log(`${colors.yellow}⚠ Note:${colors.reset} Evidence pack contains code snippets.`);
1179
+ console.log(' Best-effort secret redaction applied, but review before sharing.');
1180
+ console.log('');
1181
+
1182
+ process.exit(0);
1183
+ } catch (error) {
1184
+ console.error(`${colors.red}Error: ${error.message}${colors.reset}`);
1185
+ process.exit(1);
1186
+ }
1187
+ }
1188
+
1189
+ async function handleDiscoverCommand(args) {
1190
+ const cwd = process.cwd();
1191
+
1192
+ // Parse options
1193
+ const options = {
1194
+ targetLevel: 1
1195
+ };
1196
+
1197
+ const targetLevelIndex = args.indexOf('--target-level');
1198
+ if (targetLevelIndex !== -1 && args[targetLevelIndex + 1]) {
1199
+ options.targetLevel = parseInt(args[targetLevelIndex + 1]);
1200
+ if (![1, 2, 3].includes(options.targetLevel)) {
1201
+ console.error(`${colors.red}Error: --target-level must be 1, 2, or 3${colors.reset}`);
1202
+ process.exit(1);
1203
+ }
1204
+ }
1205
+
1206
+ console.log(`${colors.blue}Discovering intent from evidence...${colors.reset}\n`);
1207
+
1208
+ try {
1209
+ // Check AI is enabled
1210
+ const config = loadConfig(cwd);
1211
+ const aiCheck = checkAiEnabled(config);
1212
+
1213
+ if (!aiCheck.enabled) {
1214
+ console.error(`${colors.red}Error: AI is not enabled${colors.reset}`);
1215
+ console.error(` ${aiCheck.reason}`);
1216
+ console.log('');
1217
+ console.log(`${colors.blue}To enable AI:${colors.reset}`);
1218
+ console.log(' 1. Set ai.enabled: true in .ripp/config.yaml');
1219
+ console.log(' 2. Set RIPP_AI_ENABLED=true environment variable');
1220
+ console.log(' 3. Set provider API key (e.g., OPENAI_API_KEY)');
1221
+ console.log('');
1222
+ process.exit(1);
1223
+ }
1224
+
1225
+ console.log(`${colors.gray}AI Provider: ${config.ai.provider}${colors.reset}`);
1226
+ console.log(`${colors.gray}Model: ${config.ai.model}${colors.reset}`);
1227
+ console.log(`${colors.gray}Target Level: ${options.targetLevel}${colors.reset}`);
1228
+ console.log('');
1229
+
1230
+ const result = await discoverIntent(cwd, options);
1231
+
1232
+ log(colors.green, '✓', 'Intent discovery complete');
1233
+ console.log(` ${colors.gray}Candidates: ${result.totalCandidates}${colors.reset}`);
1234
+ console.log(` ${colors.gray}Output: ${result.candidatesPath}${colors.reset}`);
1235
+ console.log('');
1236
+
1237
+ console.log(`${colors.yellow}⚠ IMPORTANT:${colors.reset}`);
1238
+ console.log(' All candidates are INFERRED and require human confirmation.');
1239
+ console.log(' Run "ripp confirm" to review and approve candidates.');
1240
+ console.log('');
1241
+
1242
+ process.exit(0);
1243
+ } catch (error) {
1244
+ console.error(`${colors.red}Error: ${error.message}${colors.reset}`);
1245
+ process.exit(1);
1246
+ }
1247
+ }
1248
+
1249
+ async function handleConfirmCommand(args) {
1250
+ const cwd = process.cwd();
1251
+
1252
+ // Parse options
1253
+ const options = {
1254
+ interactive: !args.includes('--checklist'),
1255
+ user: null
1256
+ };
1257
+
1258
+ const userIndex = args.indexOf('--user');
1259
+ if (userIndex !== -1 && args[userIndex + 1]) {
1260
+ options.user = args[userIndex + 1];
1261
+ }
1262
+
1263
+ console.log(`${colors.blue}Confirming candidate intent...${colors.reset}\n`);
1264
+
1265
+ try {
1266
+ const result = await confirmIntent(cwd, options);
1267
+
1268
+ if (result.checklistPath) {
1269
+ log(colors.green, '✓', `Checklist generated: ${result.checklistPath}`);
1270
+ console.log('');
1271
+ console.log(`${colors.blue}Next steps:${colors.reset}`);
1272
+ console.log(' 1. Review and edit the checklist');
1273
+ console.log(' 2. Mark accepted candidates with [x]');
1274
+ console.log(' 3. Save the file');
1275
+ console.log(' 4. Run "ripp build" to compile confirmed intent');
1276
+ console.log('');
1277
+ } else {
1278
+ log(colors.green, '✓', 'Intent confirmation complete');
1279
+ console.log(` ${colors.gray}Confirmed: ${result.confirmedCount}${colors.reset}`);
1280
+ console.log(` ${colors.gray}Rejected: ${result.rejectedCount}${colors.reset}`);
1281
+ console.log(` ${colors.gray}Output: ${result.confirmedPath}${colors.reset}`);
1282
+ console.log('');
1283
+
1284
+ if (result.confirmedCount > 0) {
1285
+ console.log(`${colors.blue}Next steps:${colors.reset}`);
1286
+ console.log(' Run "ripp build" to compile canonical RIPP artifacts');
1287
+ console.log('');
1288
+ }
1289
+ }
1290
+
1291
+ process.exit(0);
1292
+ } catch (error) {
1293
+ console.error(`${colors.red}Error: ${error.message}${colors.reset}`);
1294
+ process.exit(1);
1295
+ }
1296
+ }
1297
+
1298
+ async function handleBuildCommand(args) {
1299
+ const cwd = process.cwd();
1300
+
1301
+ // Parse options
1302
+ const options = {
1303
+ packetId: null,
1304
+ title: null,
1305
+ outputName: null
1306
+ };
1307
+
1308
+ const packetIdIndex = args.indexOf('--packet-id');
1309
+ if (packetIdIndex !== -1 && args[packetIdIndex + 1]) {
1310
+ options.packetId = args[packetIdIndex + 1];
1311
+ }
1312
+
1313
+ const titleIndex = args.indexOf('--title');
1314
+ if (titleIndex !== -1 && args[titleIndex + 1]) {
1315
+ options.title = args[titleIndex + 1];
1316
+ }
1317
+
1318
+ const outputNameIndex = args.indexOf('--output-name');
1319
+ if (outputNameIndex !== -1 && args[outputNameIndex + 1]) {
1320
+ options.outputName = args[outputNameIndex + 1];
1321
+ }
1322
+
1323
+ console.log(`${colors.blue}Building canonical RIPP artifacts...${colors.reset}\n`);
1324
+
1325
+ try {
1326
+ const result = buildCanonicalArtifacts(cwd, options);
1327
+
1328
+ log(colors.green, '✓', 'Build complete');
1329
+ console.log(` ${colors.gray}RIPP Packet: ${result.packetPath}${colors.reset}`);
1330
+ console.log(` ${colors.gray}Handoff MD: ${result.markdownPath}${colors.reset}`);
1331
+ console.log(` ${colors.gray}Level: ${result.level}${colors.reset}`);
1332
+ console.log('');
1333
+
1334
+ console.log(`${colors.blue}Next steps:${colors.reset}`);
1335
+ console.log(' 1. Review generated artifacts');
1336
+ console.log(' 2. Run "ripp validate .ripp/" to validate');
1337
+ console.log(' 3. Run "ripp package" to create handoff.zip');
1338
+ console.log('');
1339
+
1340
+ process.exit(0);
1341
+ } catch (error) {
1342
+ console.error(`${colors.red}Error: ${error.message}${colors.reset}`);
1343
+ process.exit(1);
1344
+ }
1345
+ }
1346
+
1347
+ main().catch(error => {
1348
+ console.error(`${colors.red}Fatal error: ${error.message}${colors.reset}`);
1349
+ process.exit(1);
1350
+ });