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/README.md +292 -0
- package/index.js +1350 -0
- package/lib/ai-provider.js +354 -0
- package/lib/analyzer.js +394 -0
- package/lib/build.js +338 -0
- package/lib/config.js +277 -0
- package/lib/confirmation.js +183 -0
- package/lib/discovery.js +119 -0
- package/lib/evidence.js +368 -0
- package/lib/init.js +488 -0
- package/lib/linter.js +309 -0
- package/lib/migrate.js +203 -0
- package/lib/packager.js +374 -0
- package/package.json +40 -0
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
|
+
});
|