wogiflow 1.0.12 → 1.0.13
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/.workflow/specs/architecture.md.template +24 -0
- package/.workflow/specs/stack.md.template +33 -0
- package/.workflow/specs/testing.md.template +36 -0
- package/README.md +90 -1
- package/package.json +1 -1
- package/scripts/MEMORY-ARCHITECTURE.md +150 -0
- package/scripts/flow +20 -19
- package/scripts/flow-auto-context.js +97 -3
- package/scripts/flow-conflict-resolver.js +735 -0
- package/scripts/flow-context-gatherer.js +520 -0
- package/scripts/flow-context-monitor.js +148 -19
- package/scripts/flow-damage-control.js +5 -1
- package/scripts/flow-export-profile +168 -1
- package/scripts/flow-import-profile +257 -6
- package/scripts/flow-instruction-richness.js +182 -18
- package/scripts/flow-knowledge-router.js +2 -0
- package/scripts/flow-knowledge-sync.js +2 -0
- package/scripts/{flow-transcript-chunking.js → flow-long-input-chunking.js} +4 -2
- package/scripts/{flow-transcript-parsing.js → flow-long-input-parsing.js} +35 -0
- package/scripts/{flow-transcript-stories.js → flow-long-input-stories.js} +86 -38
- package/scripts/{flow-transcript-digest.js → flow-long-input.js} +231 -15
- package/scripts/flow-memory-db.js +386 -1
- package/scripts/flow-memory-sync.js +2 -0
- package/scripts/flow-model-adapter.js +53 -29
- package/scripts/flow-model-router.js +246 -1
- package/scripts/flow-morning.js +94 -0
- package/scripts/flow-onboard +223 -10
- package/scripts/flow-orchestrate-validation.js +539 -0
- package/scripts/flow-orchestrate.js +16 -507
- package/scripts/flow-pattern-extractor.js +1265 -0
- package/scripts/flow-prompt-composer.js +222 -2
- package/scripts/flow-quality-guard.js +594 -0
- package/scripts/flow-section-index.js +713 -0
- package/scripts/flow-section-resolver.js +484 -0
- package/scripts/flow-session-end.js +188 -2
- package/scripts/flow-skill-create.js +19 -3
- package/scripts/flow-skill-matcher.js +122 -7
- package/scripts/flow-statusline-setup.js +218 -0
- package/scripts/flow-step-review.js +19 -0
- package/scripts/flow-tech-debt.js +734 -0
- package/scripts/flow-utils.js +2 -0
- package/scripts/hooks/core/long-input-gate.js +293 -0
- package/scripts/flow-parallel-detector.js +0 -399
- package/scripts/flow-parallel-dispatch.js +0 -987
- /package/scripts/{flow-transcript-language.js → flow-long-input-language.js} +0 -0
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Interactive Conflict Resolution
|
|
5
|
+
*
|
|
6
|
+
* Presents pattern conflicts to user for resolution with recommendations.
|
|
7
|
+
* Uses arrow key navigation and real-time feedback.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* Programmatic:
|
|
11
|
+
* const { resolveConflicts } = require('./flow-conflict-resolver');
|
|
12
|
+
* const resolved = await resolveConflicts(conflicts);
|
|
13
|
+
*
|
|
14
|
+
* CLI (reads conflicts from file):
|
|
15
|
+
* flow conflict-resolve --input conflicts.json --output resolved.json
|
|
16
|
+
* node scripts/flow-conflict-resolver.js --input conflicts.json
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const readline = require('readline');
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Constants
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
// Display configuration
|
|
27
|
+
const DEFAULT_TERMINAL_WIDTH = 80;
|
|
28
|
+
const MAX_BOX_WIDTH = 62;
|
|
29
|
+
const MAX_LINE_WIDTH = 60;
|
|
30
|
+
const DEFAULT_MAX_PATH_LENGTH = 50;
|
|
31
|
+
const DAYS_PER_WEEK = 7;
|
|
32
|
+
const DAYS_PER_MONTH = 30;
|
|
33
|
+
const DAYS_PER_YEAR = 365;
|
|
34
|
+
const MS_PER_DAY = 1000 * 60 * 60 * 24;
|
|
35
|
+
|
|
36
|
+
// Colors for CLI output
|
|
37
|
+
// TODO: Consider using flow-output.js for shared color definitions
|
|
38
|
+
const c = {
|
|
39
|
+
reset: '\x1b[0m',
|
|
40
|
+
dim: '\x1b[2m',
|
|
41
|
+
bold: '\x1b[1m',
|
|
42
|
+
red: '\x1b[31m',
|
|
43
|
+
green: '\x1b[32m',
|
|
44
|
+
yellow: '\x1b[33m',
|
|
45
|
+
blue: '\x1b[34m',
|
|
46
|
+
cyan: '\x1b[36m',
|
|
47
|
+
magenta: '\x1b[35m',
|
|
48
|
+
bgBlue: '\x1b[44m',
|
|
49
|
+
bgGreen: '\x1b[42m',
|
|
50
|
+
underline: '\x1b[4m'
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Resolution values
|
|
54
|
+
const RESOLUTION = {
|
|
55
|
+
A: 'A',
|
|
56
|
+
B: 'B',
|
|
57
|
+
SKIP: 'SKIP'
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// ============================================================================
|
|
61
|
+
// Terminal Utilities
|
|
62
|
+
// ============================================================================
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Clear the terminal screen
|
|
66
|
+
*/
|
|
67
|
+
function clearScreen() {
|
|
68
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Hide cursor
|
|
73
|
+
*/
|
|
74
|
+
function hideCursor() {
|
|
75
|
+
process.stdout.write('\x1b[?25l');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Show cursor
|
|
80
|
+
*/
|
|
81
|
+
function showCursor() {
|
|
82
|
+
process.stdout.write('\x1b[?25h');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get terminal width
|
|
87
|
+
*/
|
|
88
|
+
function getTerminalWidth() {
|
|
89
|
+
return process.stdout.columns || DEFAULT_TERMINAL_WIDTH;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Create a horizontal line
|
|
94
|
+
*/
|
|
95
|
+
function horizontalLine(char = '─', width = null) {
|
|
96
|
+
const w = width || getTerminalWidth();
|
|
97
|
+
return char.repeat(Math.min(w, MAX_LINE_WIDTH));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Box drawing for headers
|
|
102
|
+
*/
|
|
103
|
+
function boxHeader(text) {
|
|
104
|
+
const width = Math.min(getTerminalWidth(), MAX_BOX_WIDTH);
|
|
105
|
+
const textWidth = width - 4;
|
|
106
|
+
const paddedText = text.padEnd(textWidth).slice(0, textWidth);
|
|
107
|
+
|
|
108
|
+
return [
|
|
109
|
+
`${c.cyan}╔${'═'.repeat(width - 2)}╗${c.reset}`,
|
|
110
|
+
`${c.cyan}║${c.reset} ${c.bold}${paddedText}${c.reset}${c.cyan}║${c.reset}`,
|
|
111
|
+
`${c.cyan}╚${'═'.repeat(width - 2)}╝${c.reset}`
|
|
112
|
+
].join('\n');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Format a file path for display
|
|
117
|
+
*/
|
|
118
|
+
function formatFilePath(filePath, maxLength = DEFAULT_MAX_PATH_LENGTH) {
|
|
119
|
+
if (!filePath) return '';
|
|
120
|
+
if (filePath.length <= maxLength) return filePath;
|
|
121
|
+
|
|
122
|
+
const parts = filePath.split('/');
|
|
123
|
+
if (parts.length <= 2) return filePath.slice(-maxLength);
|
|
124
|
+
|
|
125
|
+
// Show first and last parts
|
|
126
|
+
return '.../' + parts.slice(-2).join('/');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Format time ago
|
|
131
|
+
*/
|
|
132
|
+
function formatTimeAgo(date) {
|
|
133
|
+
if (!date) return 'Unknown';
|
|
134
|
+
|
|
135
|
+
const now = new Date();
|
|
136
|
+
const then = new Date(date);
|
|
137
|
+
const diffMs = now - then;
|
|
138
|
+
const diffDays = Math.floor(diffMs / MS_PER_DAY);
|
|
139
|
+
|
|
140
|
+
if (diffDays === 0) return 'Today';
|
|
141
|
+
if (diffDays === 1) return 'Yesterday';
|
|
142
|
+
if (diffDays < DAYS_PER_WEEK) return `${diffDays} days ago`;
|
|
143
|
+
if (diffDays < DAYS_PER_MONTH) return `${Math.floor(diffDays / DAYS_PER_WEEK)} weeks ago`;
|
|
144
|
+
if (diffDays < DAYS_PER_YEAR) return `${Math.floor(diffDays / DAYS_PER_MONTH)} months ago`;
|
|
145
|
+
return `${Math.floor(diffDays / DAYS_PER_YEAR)} years ago`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ============================================================================
|
|
149
|
+
// Conflict Display
|
|
150
|
+
// ============================================================================
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Render a single conflict for display
|
|
154
|
+
*/
|
|
155
|
+
function renderConflict(conflict, index, total, selectedOption = null) {
|
|
156
|
+
const lines = [];
|
|
157
|
+
const width = Math.min(getTerminalWidth(), MAX_BOX_WIDTH);
|
|
158
|
+
|
|
159
|
+
// Header
|
|
160
|
+
lines.push('');
|
|
161
|
+
lines.push(boxHeader(`Pattern Conflict Resolution`));
|
|
162
|
+
lines.push('');
|
|
163
|
+
|
|
164
|
+
// Progress indicator
|
|
165
|
+
lines.push(`${c.dim}Found ${total} conflicts requiring your decision:${c.reset}`);
|
|
166
|
+
lines.push('');
|
|
167
|
+
|
|
168
|
+
// Conflict header
|
|
169
|
+
lines.push(`${c.yellow}${'━'.repeat(width - 4)}${c.reset}`);
|
|
170
|
+
lines.push(`${c.bold}CONFLICT ${index + 1}/${total}: ${conflict.description}${c.reset}`);
|
|
171
|
+
lines.push(`${c.yellow}${'━'.repeat(width - 4)}${c.reset}`);
|
|
172
|
+
lines.push('');
|
|
173
|
+
|
|
174
|
+
// Option A
|
|
175
|
+
const isARecommended = conflict.recommendation === 'A';
|
|
176
|
+
const isASelected = selectedOption === 'A';
|
|
177
|
+
const aBg = isASelected ? c.bgBlue : '';
|
|
178
|
+
const aPrefix = isASelected ? '▶ ' : ' ';
|
|
179
|
+
|
|
180
|
+
lines.push(`${aBg}${aPrefix}${c.bold}Option A: ${conflict.patternA.pattern.name}${isARecommended ? ` ${c.green}(Recommended)${c.reset}${aBg}${c.bold}` : ''}${c.reset}`);
|
|
181
|
+
|
|
182
|
+
// Option A examples
|
|
183
|
+
if (conflict.patternA.pattern.examples && conflict.patternA.pattern.examples.length > 0) {
|
|
184
|
+
const examples = conflict.patternA.pattern.examples.slice(0, 3);
|
|
185
|
+
lines.push(`${c.dim} Examples: ${examples.join(', ')}${c.reset}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Option A stats
|
|
189
|
+
const totalOccurrences = conflict.patternA.occurrences + conflict.patternB.occurrences;
|
|
190
|
+
const aPercentage = Math.round((conflict.patternA.occurrences / totalOccurrences) * 100);
|
|
191
|
+
lines.push(`${c.dim} Usage: ${conflict.patternA.occurrences} files (${aPercentage}%)${c.reset}`);
|
|
192
|
+
lines.push(`${c.dim} Last used: ${formatTimeAgo(conflict.patternA.newestOccurrence)}${c.reset}`);
|
|
193
|
+
|
|
194
|
+
// Option A file examples
|
|
195
|
+
if (conflict.patternA.files && conflict.patternA.files.length > 0) {
|
|
196
|
+
lines.push(`${c.dim} Files: ${conflict.patternA.files.slice(0, 2).map(f => formatFilePath(f, 30)).join(', ')}${c.reset}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
lines.push('');
|
|
200
|
+
|
|
201
|
+
// Option B
|
|
202
|
+
const isBRecommended = conflict.recommendation === 'B';
|
|
203
|
+
const isBSelected = selectedOption === 'B';
|
|
204
|
+
const bBg = isBSelected ? c.bgBlue : '';
|
|
205
|
+
const bPrefix = isBSelected ? '▶ ' : ' ';
|
|
206
|
+
|
|
207
|
+
lines.push(`${bBg}${bPrefix}${c.bold}Option B: ${conflict.patternB.pattern.name}${isBRecommended ? ` ${c.green}(Recommended)${c.reset}${bBg}${c.bold}` : ''}${c.reset}`);
|
|
208
|
+
|
|
209
|
+
// Option B examples
|
|
210
|
+
if (conflict.patternB.pattern.examples && conflict.patternB.pattern.examples.length > 0) {
|
|
211
|
+
const examples = conflict.patternB.pattern.examples.slice(0, 3);
|
|
212
|
+
lines.push(`${c.dim} Examples: ${examples.join(', ')}${c.reset}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Option B stats
|
|
216
|
+
const bPercentage = Math.round((conflict.patternB.occurrences / totalOccurrences) * 100);
|
|
217
|
+
lines.push(`${c.dim} Usage: ${conflict.patternB.occurrences} files (${bPercentage}%)${c.reset}`);
|
|
218
|
+
lines.push(`${c.dim} Last used: ${formatTimeAgo(conflict.patternB.newestOccurrence)}${c.reset}`);
|
|
219
|
+
|
|
220
|
+
// Option B file examples
|
|
221
|
+
if (conflict.patternB.files && conflict.patternB.files.length > 0) {
|
|
222
|
+
lines.push(`${c.dim} Files: ${conflict.patternB.files.slice(0, 2).map(f => formatFilePath(f, 30)).join(', ')}${c.reset}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
lines.push('');
|
|
226
|
+
|
|
227
|
+
// Why recommended
|
|
228
|
+
if (conflict.recommendationReason) {
|
|
229
|
+
const recOption = conflict.recommendation;
|
|
230
|
+
lines.push(`${c.green} Why ${recOption} is recommended:${c.reset}`);
|
|
231
|
+
lines.push(`${c.dim} ${conflict.recommendationReason}${c.reset}`);
|
|
232
|
+
lines.push('');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Skip option
|
|
236
|
+
const isSkipSelected = selectedOption === 'SKIP';
|
|
237
|
+
const skipBg = isSkipSelected ? c.bgBlue : '';
|
|
238
|
+
const skipPrefix = isSkipSelected ? '▶ ' : ' ';
|
|
239
|
+
lines.push(`${skipBg}${skipPrefix}${c.dim}[S] Skip - Don't enforce either pattern${c.reset}`);
|
|
240
|
+
|
|
241
|
+
lines.push('');
|
|
242
|
+
|
|
243
|
+
// Controls
|
|
244
|
+
lines.push(`${c.dim}${horizontalLine()}${c.reset}`);
|
|
245
|
+
lines.push(`${c.cyan} ↑/↓${c.reset} Navigate ${c.cyan}Enter${c.reset} Select ${c.cyan}A/B/S${c.reset} Quick select ${c.cyan}Q${c.reset} Quit`);
|
|
246
|
+
|
|
247
|
+
return lines.join('\n');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Render completion summary
|
|
252
|
+
*/
|
|
253
|
+
function renderSummary(resolutions) {
|
|
254
|
+
const lines = [];
|
|
255
|
+
|
|
256
|
+
lines.push('');
|
|
257
|
+
lines.push(boxHeader(`Resolution Complete`));
|
|
258
|
+
lines.push('');
|
|
259
|
+
|
|
260
|
+
const countA = resolutions.filter(r => r.resolution === 'A').length;
|
|
261
|
+
const countB = resolutions.filter(r => r.resolution === 'B').length;
|
|
262
|
+
const countSkip = resolutions.filter(r => r.resolution === 'SKIP').length;
|
|
263
|
+
|
|
264
|
+
lines.push(`${c.green}✓${c.reset} Resolved ${resolutions.length} conflicts:`);
|
|
265
|
+
lines.push('');
|
|
266
|
+
|
|
267
|
+
if (countA > 0) {
|
|
268
|
+
lines.push(` ${c.cyan}${countA}${c.reset} chose Option A (primary pattern)`);
|
|
269
|
+
}
|
|
270
|
+
if (countB > 0) {
|
|
271
|
+
lines.push(` ${c.cyan}${countB}${c.reset} chose Option B (alternative pattern)`);
|
|
272
|
+
}
|
|
273
|
+
if (countSkip > 0) {
|
|
274
|
+
lines.push(` ${c.dim}${countSkip}${c.reset} skipped (no preference)`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
lines.push('');
|
|
278
|
+
|
|
279
|
+
// List resolutions
|
|
280
|
+
lines.push(`${c.dim}${horizontalLine()}${c.reset}`);
|
|
281
|
+
lines.push(`${c.bold}Decisions made:${c.reset}`);
|
|
282
|
+
lines.push('');
|
|
283
|
+
|
|
284
|
+
for (const res of resolutions) {
|
|
285
|
+
if (res.resolution === 'SKIP') {
|
|
286
|
+
lines.push(` ${c.dim}○ ${res.description}: Skipped${c.reset}`);
|
|
287
|
+
} else {
|
|
288
|
+
const chosen = res.resolution === 'A' ? res.patternA.pattern.name : res.patternB.pattern.name;
|
|
289
|
+
lines.push(` ${c.green}●${c.reset} ${res.description}: ${c.cyan}${chosen}${c.reset}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
lines.push('');
|
|
294
|
+
|
|
295
|
+
return lines.join('\n');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ============================================================================
|
|
299
|
+
// Interactive Resolution
|
|
300
|
+
// ============================================================================
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Resolve conflicts interactively using arrow keys
|
|
304
|
+
*/
|
|
305
|
+
async function resolveConflictsInteractive(conflicts) {
|
|
306
|
+
if (!conflicts || conflicts.length === 0) {
|
|
307
|
+
console.log(`${c.yellow}No conflicts to resolve.${c.reset}`);
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Check if we're in a TTY
|
|
312
|
+
if (!process.stdin.isTTY) {
|
|
313
|
+
console.error(`${c.red}Error: Interactive mode requires a TTY.${c.reset}`);
|
|
314
|
+
console.error(`${c.dim}Use --auto-accept for non-interactive resolution.${c.reset}`);
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const resolutions = [];
|
|
319
|
+
let currentIndex = 0;
|
|
320
|
+
|
|
321
|
+
// Process each conflict
|
|
322
|
+
while (currentIndex < conflicts.length) {
|
|
323
|
+
const conflict = conflicts[currentIndex];
|
|
324
|
+
const resolution = await resolveOneConflict(conflict, currentIndex, conflicts.length);
|
|
325
|
+
|
|
326
|
+
if (resolution === 'QUIT') {
|
|
327
|
+
showCursor();
|
|
328
|
+
console.log(`\n${c.yellow}Resolution cancelled. No changes saved.${c.reset}`);
|
|
329
|
+
process.exit(0);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Store resolution
|
|
333
|
+
resolutions.push({
|
|
334
|
+
...conflict,
|
|
335
|
+
resolution: resolution,
|
|
336
|
+
resolvedAt: new Date().toISOString()
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
currentIndex++;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Show summary
|
|
343
|
+
clearScreen();
|
|
344
|
+
console.log(renderSummary(resolutions));
|
|
345
|
+
|
|
346
|
+
return resolutions;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Resolve a single conflict with arrow key navigation
|
|
351
|
+
*/
|
|
352
|
+
async function resolveOneConflict(conflict, index, total) {
|
|
353
|
+
return new Promise((resolve) => {
|
|
354
|
+
const options = ['A', 'B', 'SKIP'];
|
|
355
|
+
let selectedIndex = conflict.recommendation === 'B' ? 1 : 0; // Pre-select recommended
|
|
356
|
+
|
|
357
|
+
function render() {
|
|
358
|
+
clearScreen();
|
|
359
|
+
console.log(renderConflict(conflict, index, total, options[selectedIndex]));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Initial render
|
|
363
|
+
hideCursor();
|
|
364
|
+
render();
|
|
365
|
+
|
|
366
|
+
// Setup raw mode for key input
|
|
367
|
+
readline.emitKeypressEvents(process.stdin);
|
|
368
|
+
if (process.stdin.setRawMode) {
|
|
369
|
+
process.stdin.setRawMode(true);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function handleKeypress(str, key) {
|
|
373
|
+
if (!key) return;
|
|
374
|
+
|
|
375
|
+
// Handle arrow keys
|
|
376
|
+
if (key.name === 'up') {
|
|
377
|
+
selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : options.length - 1;
|
|
378
|
+
render();
|
|
379
|
+
} else if (key.name === 'down') {
|
|
380
|
+
selectedIndex = selectedIndex < options.length - 1 ? selectedIndex + 1 : 0;
|
|
381
|
+
render();
|
|
382
|
+
} else if (key.name === 'return') {
|
|
383
|
+
// Enter key - select current option
|
|
384
|
+
cleanup();
|
|
385
|
+
resolve(options[selectedIndex]);
|
|
386
|
+
} else if (str === 'a' || str === 'A') {
|
|
387
|
+
cleanup();
|
|
388
|
+
resolve('A');
|
|
389
|
+
} else if (str === 'b' || str === 'B') {
|
|
390
|
+
cleanup();
|
|
391
|
+
resolve('B');
|
|
392
|
+
} else if (str === 's' || str === 'S') {
|
|
393
|
+
cleanup();
|
|
394
|
+
resolve('SKIP');
|
|
395
|
+
} else if (str === 'q' || str === 'Q' || (key.ctrl && key.name === 'c')) {
|
|
396
|
+
cleanup();
|
|
397
|
+
resolve('QUIT');
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function cleanup() {
|
|
402
|
+
process.stdin.removeListener('keypress', handleKeypress);
|
|
403
|
+
if (process.stdin.setRawMode) {
|
|
404
|
+
process.stdin.setRawMode(false);
|
|
405
|
+
}
|
|
406
|
+
showCursor();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
process.stdin.on('keypress', handleKeypress);
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Auto-accept all recommendations (non-interactive)
|
|
415
|
+
*/
|
|
416
|
+
function resolveConflictsAuto(conflicts) {
|
|
417
|
+
if (!conflicts || conflicts.length === 0) {
|
|
418
|
+
return [];
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return conflicts.map(conflict => ({
|
|
422
|
+
...conflict,
|
|
423
|
+
resolution: conflict.recommendation || 'SKIP',
|
|
424
|
+
resolvedAt: new Date().toISOString(),
|
|
425
|
+
autoResolved: true
|
|
426
|
+
}));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ============================================================================
|
|
430
|
+
// Main API
|
|
431
|
+
// ============================================================================
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Main entry point for resolving conflicts
|
|
435
|
+
*
|
|
436
|
+
* @param {Array} conflicts - Array of conflict objects from pattern extractor
|
|
437
|
+
* @param {Object} options - Resolution options
|
|
438
|
+
* @param {boolean} options.interactive - Use interactive mode (default: true if TTY)
|
|
439
|
+
* @param {boolean} options.autoAccept - Auto-accept all recommendations
|
|
440
|
+
* @returns {Promise<Array>} Resolved conflicts with user decisions
|
|
441
|
+
*/
|
|
442
|
+
async function resolveConflicts(conflicts, options = {}) {
|
|
443
|
+
const {
|
|
444
|
+
interactive = process.stdin.isTTY,
|
|
445
|
+
autoAccept = false
|
|
446
|
+
} = options;
|
|
447
|
+
|
|
448
|
+
if (autoAccept) {
|
|
449
|
+
return resolveConflictsAuto(conflicts);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (interactive) {
|
|
453
|
+
return resolveConflictsInteractive(conflicts);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Non-interactive, non-auto: just return with null resolutions
|
|
457
|
+
return conflicts.map(conflict => ({
|
|
458
|
+
...conflict,
|
|
459
|
+
resolution: null
|
|
460
|
+
}));
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Convert resolutions to decisions.md format
|
|
465
|
+
*/
|
|
466
|
+
function resolutionsToDecisions(resolutions) {
|
|
467
|
+
const lines = ['## Pattern Decisions', ''];
|
|
468
|
+
|
|
469
|
+
for (const res of resolutions) {
|
|
470
|
+
if (res.resolution === 'SKIP') continue;
|
|
471
|
+
|
|
472
|
+
const chosenPattern = res.resolution === 'A'
|
|
473
|
+
? res.patternA.pattern
|
|
474
|
+
: res.patternB.pattern;
|
|
475
|
+
|
|
476
|
+
lines.push(`### ${chosenPattern.subcategory.replace('.', ': ').replace(/\b\w/g, l => l.toUpperCase())}`);
|
|
477
|
+
lines.push('');
|
|
478
|
+
lines.push(`**Pattern**: ${chosenPattern.name}`);
|
|
479
|
+
|
|
480
|
+
if (chosenPattern.description) {
|
|
481
|
+
lines.push(`**Description**: ${chosenPattern.description}`);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (chosenPattern.examples && chosenPattern.examples.length > 0) {
|
|
485
|
+
lines.push(`**Examples**: \`${chosenPattern.examples.slice(0, 3).join('`, `')}\``);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
lines.push('');
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return lines.join('\n');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Safe JSON parsing with prototype pollution prevention
|
|
496
|
+
*/
|
|
497
|
+
function safeJsonParse(content, defaultValue = null) {
|
|
498
|
+
try {
|
|
499
|
+
// Check for prototype pollution attempts in raw content
|
|
500
|
+
if (/__proto__|constructor\s*["'`:]|prototype\s*["'`:]/i.test(content)) {
|
|
501
|
+
console.error(`${c.red}Suspicious content detected in JSON${c.reset}`);
|
|
502
|
+
return defaultValue;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const parsed = JSON.parse(content);
|
|
506
|
+
|
|
507
|
+
// Validate it's an array or object
|
|
508
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
509
|
+
return defaultValue;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Recursive check for prototype pollution in nested structures
|
|
513
|
+
function hasPrototypePollution(obj) {
|
|
514
|
+
if (typeof obj !== 'object' || obj === null) {
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
if (!Array.isArray(obj)) {
|
|
518
|
+
const keys = Object.getOwnPropertyNames(obj);
|
|
519
|
+
if (keys.includes('__proto__') || keys.includes('constructor') || keys.includes('prototype')) {
|
|
520
|
+
return true;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
// Recursively check all values (array elements or object properties)
|
|
524
|
+
for (const value of Object.values(obj)) {
|
|
525
|
+
if (hasPrototypePollution(value)) {
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (hasPrototypePollution(parsed)) {
|
|
533
|
+
console.error(`${c.red}Prototype pollution attempt detected${c.reset}`);
|
|
534
|
+
return defaultValue;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return parsed;
|
|
538
|
+
} catch (err) {
|
|
539
|
+
console.error(`${c.red}JSON parse error: ${err.message}${c.reset}`);
|
|
540
|
+
return defaultValue;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Load conflicts from JSON file
|
|
546
|
+
*/
|
|
547
|
+
function loadConflictsFromFile(filePath) {
|
|
548
|
+
try {
|
|
549
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
550
|
+
const data = safeJsonParse(content);
|
|
551
|
+
|
|
552
|
+
if (!data) {
|
|
553
|
+
console.error(`${c.red}Error: Failed to parse conflicts file.${c.reset}`);
|
|
554
|
+
process.exit(1);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Handle both direct array and wrapped format
|
|
558
|
+
if (Array.isArray(data)) {
|
|
559
|
+
return data;
|
|
560
|
+
}
|
|
561
|
+
if (data.conflicts && Array.isArray(data.conflicts)) {
|
|
562
|
+
return data.conflicts;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
console.error(`${c.red}Error: Invalid conflicts file format.${c.reset}`);
|
|
566
|
+
process.exit(1);
|
|
567
|
+
} catch (err) {
|
|
568
|
+
console.error(`${c.red}Error loading conflicts: ${err.message}${c.reset}`);
|
|
569
|
+
process.exit(1);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Save resolutions to JSON file
|
|
575
|
+
*/
|
|
576
|
+
function saveResolutionsToFile(resolutions, filePath) {
|
|
577
|
+
try {
|
|
578
|
+
fs.writeFileSync(filePath, JSON.stringify(resolutions, null, 2), 'utf-8');
|
|
579
|
+
} catch (err) {
|
|
580
|
+
console.error(`${c.red}Error saving resolutions: ${err.message}${c.reset}`);
|
|
581
|
+
process.exit(1);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// ============================================================================
|
|
586
|
+
// CLI Interface
|
|
587
|
+
// ============================================================================
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Parse command line arguments
|
|
591
|
+
*/
|
|
592
|
+
function parseArgs(args) {
|
|
593
|
+
const options = {
|
|
594
|
+
input: null,
|
|
595
|
+
output: null,
|
|
596
|
+
autoAccept: false,
|
|
597
|
+
format: 'json',
|
|
598
|
+
help: false
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
for (let i = 0; i < args.length; i++) {
|
|
602
|
+
const arg = args[i];
|
|
603
|
+
|
|
604
|
+
if (arg === '--input' || arg === '-i') {
|
|
605
|
+
options.input = args[++i];
|
|
606
|
+
} else if (arg === '--output' || arg === '-o') {
|
|
607
|
+
options.output = args[++i];
|
|
608
|
+
} else if (arg === '--auto-accept' || arg === '--auto') {
|
|
609
|
+
options.autoAccept = true;
|
|
610
|
+
} else if (arg === '--format' || arg === '-f') {
|
|
611
|
+
options.format = args[++i];
|
|
612
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
613
|
+
options.help = true;
|
|
614
|
+
} else if (!arg.startsWith('-') && !options.input) {
|
|
615
|
+
options.input = arg;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return options;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Print help message
|
|
624
|
+
*/
|
|
625
|
+
function printHelp() {
|
|
626
|
+
console.log(`
|
|
627
|
+
${c.bold}Wogi Flow - Conflict Resolver${c.reset}
|
|
628
|
+
|
|
629
|
+
Interactive resolution of pattern conflicts with recommendations.
|
|
630
|
+
|
|
631
|
+
${c.cyan}Usage:${c.reset}
|
|
632
|
+
flow conflict-resolve [options] [input-file]
|
|
633
|
+
node scripts/flow-conflict-resolver.js [options]
|
|
634
|
+
|
|
635
|
+
${c.cyan}Options:${c.reset}
|
|
636
|
+
-i, --input <file> Input file with conflicts JSON
|
|
637
|
+
-o, --output <file> Output file for resolutions (default: stdout)
|
|
638
|
+
--auto-accept Auto-accept all recommendations (non-interactive)
|
|
639
|
+
-f, --format <format> Output format: json, decisions (default: json)
|
|
640
|
+
-h, --help Show this help message
|
|
641
|
+
|
|
642
|
+
${c.cyan}Examples:${c.reset}
|
|
643
|
+
${c.dim}# Interactive resolution${c.reset}
|
|
644
|
+
flow conflict-resolve conflicts.json -o resolved.json
|
|
645
|
+
|
|
646
|
+
${c.dim}# Auto-accept recommendations${c.reset}
|
|
647
|
+
flow conflict-resolve conflicts.json --auto-accept
|
|
648
|
+
|
|
649
|
+
${c.dim}# Output as decisions.md format${c.reset}
|
|
650
|
+
flow conflict-resolve conflicts.json --format decisions
|
|
651
|
+
|
|
652
|
+
${c.cyan}Navigation:${c.reset}
|
|
653
|
+
↑/↓ Navigate between options
|
|
654
|
+
Enter Select current option
|
|
655
|
+
A/B/S Quick select Option A, B, or Skip
|
|
656
|
+
Q Quit without saving
|
|
657
|
+
`);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Main CLI entry point
|
|
662
|
+
*/
|
|
663
|
+
async function main() {
|
|
664
|
+
const args = process.argv.slice(2);
|
|
665
|
+
const options = parseArgs(args);
|
|
666
|
+
|
|
667
|
+
if (options.help) {
|
|
668
|
+
printHelp();
|
|
669
|
+
process.exit(0);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (!options.input) {
|
|
673
|
+
console.error(`${c.red}Error: No input file specified.${c.reset}`);
|
|
674
|
+
console.error(`${c.dim}Use --help for usage information.${c.reset}`);
|
|
675
|
+
process.exit(1);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Load conflicts
|
|
679
|
+
const conflicts = loadConflictsFromFile(options.input);
|
|
680
|
+
|
|
681
|
+
if (conflicts.length === 0) {
|
|
682
|
+
console.log(`${c.green}No conflicts found in input file.${c.reset}`);
|
|
683
|
+
process.exit(0);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
console.log(`${c.cyan}Loaded ${conflicts.length} conflicts from ${options.input}${c.reset}`);
|
|
687
|
+
|
|
688
|
+
// Resolve conflicts
|
|
689
|
+
const resolutions = await resolveConflicts(conflicts, {
|
|
690
|
+
autoAccept: options.autoAccept
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
// Format output
|
|
694
|
+
let output;
|
|
695
|
+
if (options.format === 'decisions') {
|
|
696
|
+
output = resolutionsToDecisions(resolutions);
|
|
697
|
+
} else {
|
|
698
|
+
output = JSON.stringify(resolutions, null, 2);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Write output
|
|
702
|
+
if (options.output) {
|
|
703
|
+
fs.writeFileSync(options.output, output, 'utf-8');
|
|
704
|
+
console.log(`${c.green}Resolutions saved to ${options.output}${c.reset}`);
|
|
705
|
+
} else if (!options.autoAccept) {
|
|
706
|
+
// Only print to stdout in auto mode or when explicitly requested
|
|
707
|
+
console.log(output);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
process.exit(0);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// ============================================================================
|
|
714
|
+
// Module Exports
|
|
715
|
+
// ============================================================================
|
|
716
|
+
|
|
717
|
+
module.exports = {
|
|
718
|
+
resolveConflicts,
|
|
719
|
+
resolveConflictsInteractive,
|
|
720
|
+
resolveConflictsAuto,
|
|
721
|
+
resolutionsToDecisions,
|
|
722
|
+
loadConflictsFromFile,
|
|
723
|
+
saveResolutionsToFile,
|
|
724
|
+
renderConflict,
|
|
725
|
+
renderSummary,
|
|
726
|
+
RESOLUTION
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
// Run CLI if executed directly
|
|
730
|
+
if (require.main === module) {
|
|
731
|
+
main().catch(err => {
|
|
732
|
+
console.error(`${c.red}Error: ${err.message}${c.reset}`);
|
|
733
|
+
process.exit(1);
|
|
734
|
+
});
|
|
735
|
+
}
|