wogiflow 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/.workflow/agents/reviewer.md +81 -0
- package/.workflow/agents/security.md +94 -0
- package/.workflow/agents/story-writer.md +58 -0
- package/.workflow/bridges/base-bridge.js +395 -0
- package/.workflow/bridges/claude-bridge.js +434 -0
- package/.workflow/bridges/index.js +130 -0
- package/.workflow/lib/assumption-detector.js +481 -0
- package/.workflow/lib/config-substitution.js +371 -0
- package/.workflow/lib/failure-categories.js +478 -0
- package/.workflow/state/app-map.md.template +15 -0
- package/.workflow/state/architecture.md.template +24 -0
- package/.workflow/state/component-index.json.template +5 -0
- package/.workflow/state/decisions.md.template +15 -0
- package/.workflow/state/feedback-patterns.md.template +9 -0
- package/.workflow/state/knowledge-sync.json.template +6 -0
- package/.workflow/state/progress.md.template +14 -0
- package/.workflow/state/ready.json.template +7 -0
- package/.workflow/state/request-log.md.template +14 -0
- package/.workflow/state/session-state.json.template +11 -0
- package/.workflow/state/stack.md.template +33 -0
- package/.workflow/state/testing.md.template +36 -0
- package/.workflow/templates/claude-md.hbs +257 -0
- package/.workflow/templates/correction-report.md +67 -0
- package/.workflow/templates/gemini-md.hbs +52 -0
- package/README.md +1802 -0
- package/bin/flow +205 -0
- package/lib/index.js +33 -0
- package/lib/installer.js +467 -0
- package/lib/release-channel.js +269 -0
- package/lib/skill-registry.js +526 -0
- package/lib/upgrader.js +401 -0
- package/lib/utils.js +305 -0
- package/package.json +64 -0
- package/scripts/flow +985 -0
- package/scripts/flow-adaptive-learning.js +1259 -0
- package/scripts/flow-aggregate.js +488 -0
- package/scripts/flow-archive +133 -0
- package/scripts/flow-auto-context.js +1015 -0
- package/scripts/flow-auto-learn.js +615 -0
- package/scripts/flow-bridge.js +223 -0
- package/scripts/flow-browser-suggest.js +316 -0
- package/scripts/flow-bug.js +247 -0
- package/scripts/flow-cascade.js +711 -0
- package/scripts/flow-changelog +85 -0
- package/scripts/flow-checkpoint.js +483 -0
- package/scripts/flow-cli.js +403 -0
- package/scripts/flow-code-intelligence.js +760 -0
- package/scripts/flow-complexity.js +502 -0
- package/scripts/flow-config-set.js +152 -0
- package/scripts/flow-constants.js +157 -0
- package/scripts/flow-context +152 -0
- package/scripts/flow-context-init.js +482 -0
- package/scripts/flow-context-monitor.js +384 -0
- package/scripts/flow-context-scoring.js +886 -0
- package/scripts/flow-correct.js +458 -0
- package/scripts/flow-damage-control.js +985 -0
- package/scripts/flow-deps +101 -0
- package/scripts/flow-diff.js +700 -0
- package/scripts/flow-done +151 -0
- package/scripts/flow-done.js +489 -0
- package/scripts/flow-durable-session.js +1541 -0
- package/scripts/flow-entropy-monitor.js +345 -0
- package/scripts/flow-export-profile +349 -0
- package/scripts/flow-export-scanner.js +1046 -0
- package/scripts/flow-figma-confirm.js +400 -0
- package/scripts/flow-figma-extract.js +496 -0
- package/scripts/flow-figma-generate.js +683 -0
- package/scripts/flow-figma-index.js +909 -0
- package/scripts/flow-figma-match.js +617 -0
- package/scripts/flow-figma-mcp-server.js +518 -0
- package/scripts/flow-figma-pipeline.js +414 -0
- package/scripts/flow-file-ops.js +301 -0
- package/scripts/flow-gate-confidence.js +825 -0
- package/scripts/flow-guided-edit.js +659 -0
- package/scripts/flow-health +185 -0
- package/scripts/flow-health.js +413 -0
- package/scripts/flow-hooks.js +556 -0
- package/scripts/flow-http-client.js +249 -0
- package/scripts/flow-hybrid-detect.js +167 -0
- package/scripts/flow-hybrid-interactive.js +591 -0
- package/scripts/flow-hybrid-test.js +152 -0
- package/scripts/flow-import-profile +439 -0
- package/scripts/flow-init +253 -0
- package/scripts/flow-instruction-richness.js +827 -0
- package/scripts/flow-jira-integration.js +579 -0
- package/scripts/flow-knowledge-router.js +522 -0
- package/scripts/flow-knowledge-sync.js +589 -0
- package/scripts/flow-linear-integration.js +631 -0
- package/scripts/flow-links.js +774 -0
- package/scripts/flow-log-manager.js +559 -0
- package/scripts/flow-loop-enforcer.js +1246 -0
- package/scripts/flow-loop-retry-learning.js +630 -0
- package/scripts/flow-lsp.js +923 -0
- package/scripts/flow-map-index +348 -0
- package/scripts/flow-map-sync +201 -0
- package/scripts/flow-memory-blocks.js +668 -0
- package/scripts/flow-memory-compactor.js +350 -0
- package/scripts/flow-memory-db.js +1110 -0
- package/scripts/flow-memory-sync.js +484 -0
- package/scripts/flow-metrics.js +353 -0
- package/scripts/flow-migrate-ids.js +370 -0
- package/scripts/flow-model-adapter.js +802 -0
- package/scripts/flow-model-router.js +884 -0
- package/scripts/flow-models.js +1231 -0
- package/scripts/flow-morning.js +517 -0
- package/scripts/flow-multi-approach.js +660 -0
- package/scripts/flow-new-feature +86 -0
- package/scripts/flow-onboard +1042 -0
- package/scripts/flow-orchestrate-llm.js +459 -0
- package/scripts/flow-orchestrate.js +3592 -0
- package/scripts/flow-output.js +123 -0
- package/scripts/flow-parallel-detector.js +399 -0
- package/scripts/flow-parallel-dispatch.js +987 -0
- package/scripts/flow-parallel.js +428 -0
- package/scripts/flow-pattern-enforcer.js +600 -0
- package/scripts/flow-prd-manager.js +282 -0
- package/scripts/flow-progress.js +323 -0
- package/scripts/flow-project-analyzer.js +975 -0
- package/scripts/flow-prompt-composer.js +487 -0
- package/scripts/flow-providers.js +1381 -0
- package/scripts/flow-queue.js +308 -0
- package/scripts/flow-ready +82 -0
- package/scripts/flow-ready.js +189 -0
- package/scripts/flow-regression.js +396 -0
- package/scripts/flow-response-parser.js +450 -0
- package/scripts/flow-resume.js +284 -0
- package/scripts/flow-rules-sync.js +439 -0
- package/scripts/flow-run-trace.js +718 -0
- package/scripts/flow-safety.js +587 -0
- package/scripts/flow-search +104 -0
- package/scripts/flow-security.js +481 -0
- package/scripts/flow-session-end +106 -0
- package/scripts/flow-session-end.js +437 -0
- package/scripts/flow-session-state.js +671 -0
- package/scripts/flow-setup-hooks +216 -0
- package/scripts/flow-setup-hooks.js +377 -0
- package/scripts/flow-skill-create.js +329 -0
- package/scripts/flow-skill-creator.js +572 -0
- package/scripts/flow-skill-generator.js +1046 -0
- package/scripts/flow-skill-learn.js +880 -0
- package/scripts/flow-skill-matcher.js +578 -0
- package/scripts/flow-spec-generator.js +820 -0
- package/scripts/flow-stack-wizard.js +895 -0
- package/scripts/flow-standup +162 -0
- package/scripts/flow-start +74 -0
- package/scripts/flow-start.js +235 -0
- package/scripts/flow-status +110 -0
- package/scripts/flow-status.js +301 -0
- package/scripts/flow-step-browser.js +83 -0
- package/scripts/flow-step-changelog.js +217 -0
- package/scripts/flow-step-comments.js +306 -0
- package/scripts/flow-step-complexity.js +234 -0
- package/scripts/flow-step-coverage.js +218 -0
- package/scripts/flow-step-knowledge.js +193 -0
- package/scripts/flow-step-pr-tests.js +364 -0
- package/scripts/flow-step-regression.js +89 -0
- package/scripts/flow-step-review.js +516 -0
- package/scripts/flow-step-security.js +162 -0
- package/scripts/flow-step-silent-failures.js +290 -0
- package/scripts/flow-step-simplifier.js +346 -0
- package/scripts/flow-story +105 -0
- package/scripts/flow-story.js +500 -0
- package/scripts/flow-suspend.js +252 -0
- package/scripts/flow-sync-daemon.js +654 -0
- package/scripts/flow-task-analyzer.js +606 -0
- package/scripts/flow-team-dashboard.js +748 -0
- package/scripts/flow-team-sync.js +752 -0
- package/scripts/flow-team.js +977 -0
- package/scripts/flow-tech-options.js +528 -0
- package/scripts/flow-templates.js +812 -0
- package/scripts/flow-tiered-learning.js +728 -0
- package/scripts/flow-trace +204 -0
- package/scripts/flow-transcript-chunking.js +1106 -0
- package/scripts/flow-transcript-digest.js +7918 -0
- package/scripts/flow-transcript-language.js +465 -0
- package/scripts/flow-transcript-parsing.js +1085 -0
- package/scripts/flow-transcript-stories.js +2194 -0
- package/scripts/flow-update-map +224 -0
- package/scripts/flow-utils.js +2242 -0
- package/scripts/flow-verification.js +644 -0
- package/scripts/flow-verify.js +1177 -0
- package/scripts/flow-voice-input.js +638 -0
- package/scripts/flow-watch +168 -0
- package/scripts/flow-workflow-steps.js +521 -0
- package/scripts/flow-workflow.js +1029 -0
- package/scripts/flow-worktree.js +489 -0
- package/scripts/hooks/adapters/base-adapter.js +102 -0
- package/scripts/hooks/adapters/claude-code.js +359 -0
- package/scripts/hooks/adapters/index.js +79 -0
- package/scripts/hooks/core/component-check.js +341 -0
- package/scripts/hooks/core/index.js +35 -0
- package/scripts/hooks/core/loop-check.js +241 -0
- package/scripts/hooks/core/session-context.js +294 -0
- package/scripts/hooks/core/task-gate.js +177 -0
- package/scripts/hooks/core/validation.js +230 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
- package/scripts/hooks/entry/claude-code/session-end.js +87 -0
- package/scripts/hooks/entry/claude-code/session-start.js +46 -0
- package/scripts/hooks/entry/claude-code/stop.js +43 -0
- package/scripts/postinstall.js +139 -0
- package/templates/browser-test-flow.json +56 -0
- package/templates/bug-report.md +43 -0
- package/templates/component-detail.md +42 -0
- package/templates/component.stories.tsx +49 -0
- package/templates/context/constraints.md +83 -0
- package/templates/context/conventions.md +177 -0
- package/templates/context/stack.md +60 -0
- package/templates/correction-report.md +90 -0
- package/templates/feature-proposal.md +35 -0
- package/templates/hybrid/_base.md +254 -0
- package/templates/hybrid/_patterns.md +45 -0
- package/templates/hybrid/create-component.md +127 -0
- package/templates/hybrid/create-file.md +56 -0
- package/templates/hybrid/create-hook.md +145 -0
- package/templates/hybrid/create-service.md +70 -0
- package/templates/hybrid/fix-bug.md +33 -0
- package/templates/hybrid/modify-file.md +55 -0
- package/templates/story.md +68 -0
- package/templates/task.json +56 -0
- package/templates/trace.md +69 -0
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Diff Generation and Preview
|
|
5
|
+
*
|
|
6
|
+
* Generates unified diffs for file operations.
|
|
7
|
+
* Supports preview mode and interactive apply.
|
|
8
|
+
*
|
|
9
|
+
* Usage as module:
|
|
10
|
+
* const { generateDiff, applyDiffs } = require('./flow-diff');
|
|
11
|
+
*
|
|
12
|
+
* Usage as CLI:
|
|
13
|
+
* flow diff <file1> <file2> # Show diff between files
|
|
14
|
+
* flow diff --preview <operations.json> # Preview proposed changes
|
|
15
|
+
* flow diff --apply <operations.json> # Apply changes from JSON
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const readline = require('readline');
|
|
21
|
+
const { colors: c, getProjectRoot } = require('./flow-utils');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Simple diff implementation (line-based)
|
|
25
|
+
* Creates unified diff format
|
|
26
|
+
*/
|
|
27
|
+
function createUnifiedDiff(oldContent, newContent, oldPath, newPath) {
|
|
28
|
+
const oldLines = (oldContent || '').split('\n');
|
|
29
|
+
const newLines = (newContent || '').split('\n');
|
|
30
|
+
|
|
31
|
+
// LCS-based diff
|
|
32
|
+
const diff = computeDiff(oldLines, newLines);
|
|
33
|
+
|
|
34
|
+
// Format as unified diff
|
|
35
|
+
let output = '';
|
|
36
|
+
output += `--- ${oldPath}\n`;
|
|
37
|
+
output += `+++ ${newPath}\n`;
|
|
38
|
+
|
|
39
|
+
// Group changes into hunks
|
|
40
|
+
const hunks = groupIntoHunks(diff, oldLines, newLines);
|
|
41
|
+
|
|
42
|
+
for (const hunk of hunks) {
|
|
43
|
+
output += `@@ -${hunk.oldStart},${hunk.oldCount} +${hunk.newStart},${hunk.newCount} @@\n`;
|
|
44
|
+
output += hunk.lines.join('\n') + '\n';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return output;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Compute line-by-line diff using LCS algorithm
|
|
52
|
+
*/
|
|
53
|
+
function computeDiff(oldLines, newLines) {
|
|
54
|
+
const m = oldLines.length;
|
|
55
|
+
const n = newLines.length;
|
|
56
|
+
|
|
57
|
+
// Build LCS matrix
|
|
58
|
+
const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
|
|
59
|
+
|
|
60
|
+
for (let i = 1; i <= m; i++) {
|
|
61
|
+
for (let j = 1; j <= n; j++) {
|
|
62
|
+
if (oldLines[i - 1] === newLines[j - 1]) {
|
|
63
|
+
dp[i][j] = dp[i - 1][j - 1] + 1;
|
|
64
|
+
} else {
|
|
65
|
+
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Backtrack to find diff
|
|
71
|
+
const diff = [];
|
|
72
|
+
let i = m, j = n;
|
|
73
|
+
|
|
74
|
+
while (i > 0 || j > 0) {
|
|
75
|
+
if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
|
|
76
|
+
diff.unshift({ type: 'same', oldLine: i, newLine: j, content: oldLines[i - 1] });
|
|
77
|
+
i--;
|
|
78
|
+
j--;
|
|
79
|
+
} else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
|
|
80
|
+
diff.unshift({ type: 'add', newLine: j, content: newLines[j - 1] });
|
|
81
|
+
j--;
|
|
82
|
+
} else {
|
|
83
|
+
diff.unshift({ type: 'remove', oldLine: i, content: oldLines[i - 1] });
|
|
84
|
+
i--;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return diff;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Group diff entries into hunks with context
|
|
93
|
+
*/
|
|
94
|
+
function groupIntoHunks(diff, oldLines, newLines, contextLines = 3) {
|
|
95
|
+
const hunks = [];
|
|
96
|
+
let currentHunk = null;
|
|
97
|
+
let lastChangeIdx = -contextLines - 1;
|
|
98
|
+
|
|
99
|
+
for (let i = 0; i < diff.length; i++) {
|
|
100
|
+
const entry = diff[i];
|
|
101
|
+
const isChange = entry.type !== 'same';
|
|
102
|
+
|
|
103
|
+
if (isChange) {
|
|
104
|
+
// Start new hunk if too far from last change
|
|
105
|
+
if (i - lastChangeIdx > contextLines * 2 + 1) {
|
|
106
|
+
if (currentHunk) {
|
|
107
|
+
hunks.push(currentHunk);
|
|
108
|
+
}
|
|
109
|
+
currentHunk = {
|
|
110
|
+
oldStart: entry.oldLine || 1,
|
|
111
|
+
oldCount: 0,
|
|
112
|
+
newStart: entry.newLine || 1,
|
|
113
|
+
newCount: 0,
|
|
114
|
+
lines: []
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Add leading context
|
|
118
|
+
for (let c = Math.max(0, i - contextLines); c < i; c++) {
|
|
119
|
+
if (diff[c].type === 'same') {
|
|
120
|
+
currentHunk.lines.push(' ' + diff[c].content);
|
|
121
|
+
currentHunk.oldCount++;
|
|
122
|
+
currentHunk.newCount++;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
lastChangeIdx = i;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (currentHunk) {
|
|
131
|
+
if (entry.type === 'same') {
|
|
132
|
+
// Check if within trailing context
|
|
133
|
+
if (i - lastChangeIdx <= contextLines) {
|
|
134
|
+
currentHunk.lines.push(' ' + entry.content);
|
|
135
|
+
currentHunk.oldCount++;
|
|
136
|
+
currentHunk.newCount++;
|
|
137
|
+
}
|
|
138
|
+
} else if (entry.type === 'remove') {
|
|
139
|
+
currentHunk.lines.push('-' + entry.content);
|
|
140
|
+
currentHunk.oldCount++;
|
|
141
|
+
} else if (entry.type === 'add') {
|
|
142
|
+
currentHunk.lines.push('+' + entry.content);
|
|
143
|
+
currentHunk.newCount++;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (currentHunk && currentHunk.lines.length > 0) {
|
|
149
|
+
hunks.push(currentHunk);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return hunks;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Generate diff for a single file operation
|
|
157
|
+
*/
|
|
158
|
+
function generateDiff(filePath, originalContent, newContent) {
|
|
159
|
+
return createUnifiedDiff(
|
|
160
|
+
originalContent || '',
|
|
161
|
+
newContent,
|
|
162
|
+
`a/${filePath}`,
|
|
163
|
+
`b/${filePath}`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Parse file operations and generate diffs
|
|
169
|
+
*/
|
|
170
|
+
function generateDiffsForOperations(operations) {
|
|
171
|
+
const diffs = [];
|
|
172
|
+
|
|
173
|
+
for (const op of operations) {
|
|
174
|
+
const filePath = op.path;
|
|
175
|
+
|
|
176
|
+
if (op.type === 'write' || op.type === 'modify' || op.type === 'create') {
|
|
177
|
+
const originalContent = fs.existsSync(filePath)
|
|
178
|
+
? fs.readFileSync(filePath, 'utf-8')
|
|
179
|
+
: '';
|
|
180
|
+
|
|
181
|
+
const isNew = !fs.existsSync(filePath);
|
|
182
|
+
const diff = generateDiff(filePath, originalContent, op.content);
|
|
183
|
+
|
|
184
|
+
// Count additions and deletions
|
|
185
|
+
const diffLines = diff.split('\n');
|
|
186
|
+
const additions = diffLines.filter(l => l.startsWith('+') && !l.startsWith('+++')).length;
|
|
187
|
+
const deletions = diffLines.filter(l => l.startsWith('-') && !l.startsWith('---')).length;
|
|
188
|
+
|
|
189
|
+
diffs.push({
|
|
190
|
+
path: filePath,
|
|
191
|
+
operation: isNew ? 'create' : 'modify',
|
|
192
|
+
diff: diff,
|
|
193
|
+
additions,
|
|
194
|
+
deletions,
|
|
195
|
+
content: op.content
|
|
196
|
+
});
|
|
197
|
+
} else if (op.type === 'delete') {
|
|
198
|
+
const originalContent = fs.existsSync(filePath)
|
|
199
|
+
? fs.readFileSync(filePath, 'utf-8')
|
|
200
|
+
: '';
|
|
201
|
+
|
|
202
|
+
const diff = generateDiff(filePath, originalContent, '');
|
|
203
|
+
|
|
204
|
+
diffs.push({
|
|
205
|
+
path: filePath,
|
|
206
|
+
operation: 'delete',
|
|
207
|
+
diff: diff,
|
|
208
|
+
additions: 0,
|
|
209
|
+
deletions: originalContent.split('\n').length
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return diffs;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Format diffs for terminal display
|
|
219
|
+
*/
|
|
220
|
+
function formatDiffsForDisplay(diffs, options = {}) {
|
|
221
|
+
const showLineNumbers = options.showLineNumbers !== false;
|
|
222
|
+
let output = '';
|
|
223
|
+
|
|
224
|
+
for (const d of diffs) {
|
|
225
|
+
// Header
|
|
226
|
+
const opIcon = d.operation === 'create' ? '🆕' :
|
|
227
|
+
d.operation === 'delete' ? '🗑️' : '📝';
|
|
228
|
+
const opColor = d.operation === 'create' ? c.green :
|
|
229
|
+
d.operation === 'delete' ? c.red : c.yellow;
|
|
230
|
+
|
|
231
|
+
output += `\n${c.cyan}${'━'.repeat(60)}${c.reset}\n`;
|
|
232
|
+
output += `${opIcon} ${opColor}${c.bold}${d.operation.toUpperCase()}${c.reset}: ${d.path}\n`;
|
|
233
|
+
output += `${c.dim}+${d.additions} -${d.deletions}${c.reset}\n`;
|
|
234
|
+
output += `${c.cyan}${'─'.repeat(60)}${c.reset}\n`;
|
|
235
|
+
|
|
236
|
+
// Diff content
|
|
237
|
+
for (const line of d.diff.split('\n')) {
|
|
238
|
+
if (line.startsWith('+++') || line.startsWith('---')) {
|
|
239
|
+
output += `${c.bold}${line}${c.reset}\n`;
|
|
240
|
+
} else if (line.startsWith('@@')) {
|
|
241
|
+
output += `${c.cyan}${line}${c.reset}\n`;
|
|
242
|
+
} else if (line.startsWith('+')) {
|
|
243
|
+
output += `${c.green}${line}${c.reset}\n`;
|
|
244
|
+
} else if (line.startsWith('-')) {
|
|
245
|
+
output += `${c.red}${line}${c.reset}\n`;
|
|
246
|
+
} else {
|
|
247
|
+
output += `${line}\n`;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return output;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Save diffs to a run's artifacts
|
|
257
|
+
*/
|
|
258
|
+
function saveDiffsToRun(runId, diffs) {
|
|
259
|
+
const runDir = path.join(getProjectRoot(), '.workflow', 'runs', runId);
|
|
260
|
+
|
|
261
|
+
if (!fs.existsSync(runDir)) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const artifactsDir = path.join(runDir, 'artifacts');
|
|
266
|
+
if (!fs.existsSync(artifactsDir)) {
|
|
267
|
+
fs.mkdirSync(artifactsDir, { recursive: true });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Save combined diff
|
|
271
|
+
let combinedDiff = '';
|
|
272
|
+
for (const d of diffs) {
|
|
273
|
+
combinedDiff += d.diff + '\n';
|
|
274
|
+
}
|
|
275
|
+
fs.writeFileSync(path.join(artifactsDir, 'proposed-changes.diff'), combinedDiff);
|
|
276
|
+
|
|
277
|
+
// Save structured JSON
|
|
278
|
+
fs.writeFileSync(
|
|
279
|
+
path.join(artifactsDir, 'proposed-changes.json'),
|
|
280
|
+
JSON.stringify(diffs, null, 2)
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Parse SEARCH/REPLACE blocks from LLM output
|
|
288
|
+
* Format:
|
|
289
|
+
* <<<<<<< SEARCH
|
|
290
|
+
* old content
|
|
291
|
+
* =======
|
|
292
|
+
* new content
|
|
293
|
+
* >>>>>>> REPLACE
|
|
294
|
+
*
|
|
295
|
+
* IMPORTANT: Uses EXACT matching only - no fuzzy matching.
|
|
296
|
+
* If the search text doesn't match exactly, the operation fails.
|
|
297
|
+
*/
|
|
298
|
+
function parseSearchReplaceBlocks(content) {
|
|
299
|
+
const blocks = [];
|
|
300
|
+
const regex = /<<<<<<< SEARCH\n([\s\S]*?)\n=======\n([\s\S]*?)\n>>>>>>> REPLACE/g;
|
|
301
|
+
|
|
302
|
+
let match;
|
|
303
|
+
while ((match = regex.exec(content)) !== null) {
|
|
304
|
+
blocks.push({
|
|
305
|
+
search: match[1],
|
|
306
|
+
replace: match[2]
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return blocks;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Apply SEARCH/REPLACE blocks to file content
|
|
315
|
+
* EXACT MATCH ONLY - fails if search text not found exactly
|
|
316
|
+
*
|
|
317
|
+
* @returns { content: string, applied: number, failed: { search: string, reason: string }[] }
|
|
318
|
+
*/
|
|
319
|
+
function applySearchReplaceBlocks(fileContent, blocks) {
|
|
320
|
+
let content = fileContent;
|
|
321
|
+
let applied = 0;
|
|
322
|
+
const failed = [];
|
|
323
|
+
|
|
324
|
+
for (const block of blocks) {
|
|
325
|
+
// Exact match only - no fuzzy matching
|
|
326
|
+
if (content.includes(block.search)) {
|
|
327
|
+
// Count occurrences
|
|
328
|
+
const occurrences = content.split(block.search).length - 1;
|
|
329
|
+
|
|
330
|
+
if (occurrences > 1) {
|
|
331
|
+
// Multiple matches - fail to avoid unintended changes
|
|
332
|
+
failed.push({
|
|
333
|
+
search: block.search.slice(0, 100) + (block.search.length > 100 ? '...' : ''),
|
|
334
|
+
reason: `Found ${occurrences} matches - search text is not unique. Add more context.`
|
|
335
|
+
});
|
|
336
|
+
} else {
|
|
337
|
+
// Single exact match - apply
|
|
338
|
+
content = content.replace(block.search, block.replace);
|
|
339
|
+
applied++;
|
|
340
|
+
}
|
|
341
|
+
} else {
|
|
342
|
+
// No match found
|
|
343
|
+
failed.push({
|
|
344
|
+
search: block.search.slice(0, 100) + (block.search.length > 100 ? '...' : ''),
|
|
345
|
+
reason: 'Search text not found (exact match required)'
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return { content, applied, failed };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Apply SEARCH/REPLACE changes to a file
|
|
355
|
+
* Returns success status and details
|
|
356
|
+
*/
|
|
357
|
+
function applySearchReplaceToFile(filePath, searchReplaceContent) {
|
|
358
|
+
const blocks = parseSearchReplaceBlocks(searchReplaceContent);
|
|
359
|
+
|
|
360
|
+
if (blocks.length === 0) {
|
|
361
|
+
return {
|
|
362
|
+
success: false,
|
|
363
|
+
error: 'No SEARCH/REPLACE blocks found in content',
|
|
364
|
+
applied: 0,
|
|
365
|
+
failed: []
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (!fs.existsSync(filePath)) {
|
|
370
|
+
return {
|
|
371
|
+
success: false,
|
|
372
|
+
error: `File not found: ${filePath}`,
|
|
373
|
+
applied: 0,
|
|
374
|
+
failed: blocks.map(b => ({
|
|
375
|
+
search: b.search.slice(0, 50),
|
|
376
|
+
reason: 'File does not exist'
|
|
377
|
+
}))
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const originalContent = fs.readFileSync(filePath, 'utf-8');
|
|
382
|
+
const result = applySearchReplaceBlocks(originalContent, blocks);
|
|
383
|
+
|
|
384
|
+
if (result.failed.length === 0) {
|
|
385
|
+
// All blocks applied successfully
|
|
386
|
+
fs.writeFileSync(filePath, result.content);
|
|
387
|
+
return {
|
|
388
|
+
success: true,
|
|
389
|
+
applied: result.applied,
|
|
390
|
+
failed: [],
|
|
391
|
+
originalContent,
|
|
392
|
+
newContent: result.content
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (result.applied > 0) {
|
|
397
|
+
// Partial success - write what we could apply
|
|
398
|
+
fs.writeFileSync(filePath, result.content);
|
|
399
|
+
return {
|
|
400
|
+
success: false,
|
|
401
|
+
partial: true,
|
|
402
|
+
applied: result.applied,
|
|
403
|
+
failed: result.failed,
|
|
404
|
+
originalContent,
|
|
405
|
+
newContent: result.content
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Complete failure
|
|
410
|
+
return {
|
|
411
|
+
success: false,
|
|
412
|
+
applied: 0,
|
|
413
|
+
failed: result.failed
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Apply diffs (write files)
|
|
419
|
+
*/
|
|
420
|
+
function applyDiffs(operations) {
|
|
421
|
+
const results = [];
|
|
422
|
+
|
|
423
|
+
for (const op of operations) {
|
|
424
|
+
try {
|
|
425
|
+
if (op.type === 'delete') {
|
|
426
|
+
if (fs.existsSync(op.path)) {
|
|
427
|
+
fs.unlinkSync(op.path);
|
|
428
|
+
}
|
|
429
|
+
results.push({ path: op.path, success: true, operation: 'delete' });
|
|
430
|
+
} else {
|
|
431
|
+
// Ensure directory exists
|
|
432
|
+
const dir = path.dirname(op.path);
|
|
433
|
+
if (!fs.existsSync(dir)) {
|
|
434
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
435
|
+
}
|
|
436
|
+
fs.writeFileSync(op.path, op.content);
|
|
437
|
+
results.push({
|
|
438
|
+
path: op.path,
|
|
439
|
+
success: true,
|
|
440
|
+
operation: fs.existsSync(op.path) ? 'modify' : 'create'
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
} catch (error) {
|
|
444
|
+
results.push({
|
|
445
|
+
path: op.path,
|
|
446
|
+
success: false,
|
|
447
|
+
error: error.message
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return results;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Interactive confirmation prompt
|
|
457
|
+
*/
|
|
458
|
+
async function confirmApply(diffs) {
|
|
459
|
+
const rl = readline.createInterface({
|
|
460
|
+
input: process.stdin,
|
|
461
|
+
output: process.stdout
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
return new Promise((resolve) => {
|
|
465
|
+
rl.question(`\nApply these changes? [${c.green}y${c.reset}/${c.red}N${c.reset}]: `, (answer) => {
|
|
466
|
+
rl.close();
|
|
467
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Preview and optionally apply operations
|
|
474
|
+
*/
|
|
475
|
+
async function previewAndApply(operations, options = {}) {
|
|
476
|
+
const diffs = generateDiffsForOperations(operations);
|
|
477
|
+
|
|
478
|
+
// Show preview
|
|
479
|
+
console.log(formatDiffsForDisplay(diffs, options));
|
|
480
|
+
|
|
481
|
+
// Summary
|
|
482
|
+
const totalAdds = diffs.reduce((sum, d) => sum + d.additions, 0);
|
|
483
|
+
const totalDels = diffs.reduce((sum, d) => sum + d.deletions, 0);
|
|
484
|
+
console.log(`\n${c.bold}Summary:${c.reset} ${diffs.length} files, ${c.green}+${totalAdds}${c.reset} ${c.red}-${totalDels}${c.reset} lines\n`);
|
|
485
|
+
|
|
486
|
+
// Handle apply modes
|
|
487
|
+
if (options.dryRun) {
|
|
488
|
+
console.log(`${c.dim}(dry run - no changes applied)${c.reset}`);
|
|
489
|
+
return { applied: false, diffs };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (options.apply) {
|
|
493
|
+
// Auto-apply
|
|
494
|
+
const results = applyDiffs(operations);
|
|
495
|
+
const successCount = results.filter(r => r.success).length;
|
|
496
|
+
console.log(`${c.green}✅ Applied ${successCount}/${results.length} changes${c.reset}`);
|
|
497
|
+
return { applied: true, results, diffs };
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (options.nonInteractive) {
|
|
501
|
+
console.log(`${c.yellow}⚠️ Non-interactive mode: use --apply to apply changes${c.reset}`);
|
|
502
|
+
return { applied: false, diffs };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Interactive confirmation
|
|
506
|
+
const confirmed = await confirmApply(diffs);
|
|
507
|
+
|
|
508
|
+
if (confirmed) {
|
|
509
|
+
const results = applyDiffs(operations);
|
|
510
|
+
const successCount = results.filter(r => r.success).length;
|
|
511
|
+
console.log(`${c.green}✅ Applied ${successCount}/${results.length} changes${c.reset}`);
|
|
512
|
+
return { applied: true, results, diffs };
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
console.log(`${c.dim}Changes not applied.${c.reset}`);
|
|
516
|
+
return { applied: false, diffs };
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Module exports
|
|
520
|
+
module.exports = {
|
|
521
|
+
generateDiff,
|
|
522
|
+
generateDiffsForOperations,
|
|
523
|
+
formatDiffsForDisplay,
|
|
524
|
+
saveDiffsToRun,
|
|
525
|
+
applyDiffs,
|
|
526
|
+
previewAndApply,
|
|
527
|
+
confirmApply,
|
|
528
|
+
// SEARCH/REPLACE exact-match functions
|
|
529
|
+
parseSearchReplaceBlocks,
|
|
530
|
+
applySearchReplaceBlocks,
|
|
531
|
+
applySearchReplaceToFile
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
// CLI Handler
|
|
535
|
+
if (require.main === module) {
|
|
536
|
+
const args = process.argv.slice(2);
|
|
537
|
+
|
|
538
|
+
if (args.includes('--help') || args.includes('-h') || args.length === 0) {
|
|
539
|
+
console.log(`
|
|
540
|
+
${c.cyan}Wogi Flow - Diff Generation and Preview${c.reset}
|
|
541
|
+
|
|
542
|
+
${c.bold}Usage:${c.reset}
|
|
543
|
+
flow diff <file1> <file2> Show diff between two files
|
|
544
|
+
flow diff --preview <operations.json> Preview proposed changes
|
|
545
|
+
flow diff --apply <operations.json> Apply changes from JSON
|
|
546
|
+
flow diff --dry-run <operations.json> Show diff without prompting
|
|
547
|
+
flow diff --search-replace <file> <changes.txt> Apply SEARCH/REPLACE blocks
|
|
548
|
+
|
|
549
|
+
${c.bold}Options:${c.reset}
|
|
550
|
+
--apply Auto-apply without confirmation
|
|
551
|
+
--dry-run Show preview only, don't apply
|
|
552
|
+
--json Output diff in JSON format
|
|
553
|
+
--search-replace Apply SEARCH/REPLACE format changes (exact match only)
|
|
554
|
+
|
|
555
|
+
${c.bold}Operations JSON Format:${c.reset}
|
|
556
|
+
[
|
|
557
|
+
{ "type": "write", "path": "src/file.ts", "content": "..." },
|
|
558
|
+
{ "type": "delete", "path": "old-file.ts" }
|
|
559
|
+
]
|
|
560
|
+
|
|
561
|
+
${c.bold}SEARCH/REPLACE Format:${c.reset}
|
|
562
|
+
<<<<<<< SEARCH
|
|
563
|
+
old code to find (must match exactly)
|
|
564
|
+
=======
|
|
565
|
+
new code to replace with
|
|
566
|
+
>>>>>>> REPLACE
|
|
567
|
+
|
|
568
|
+
${c.yellow}Note: Uses EXACT matching only. No fuzzy matching.${c.reset}
|
|
569
|
+
If search text isn't found exactly, the operation fails.
|
|
570
|
+
`);
|
|
571
|
+
process.exit(0);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const jsonOutput = args.includes('--json');
|
|
575
|
+
const dryRun = args.includes('--dry-run');
|
|
576
|
+
const apply = args.includes('--apply');
|
|
577
|
+
|
|
578
|
+
// Filter out flags
|
|
579
|
+
const positionalArgs = args.filter(a => !a.startsWith('--'));
|
|
580
|
+
|
|
581
|
+
if (args.includes('--search-replace')) {
|
|
582
|
+
// Apply SEARCH/REPLACE blocks to a file
|
|
583
|
+
if (positionalArgs.length < 2) {
|
|
584
|
+
console.error(`${c.red}Error: --search-replace requires <target-file> <changes-file>${c.reset}`);
|
|
585
|
+
process.exit(1);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const [targetFile, changesFile] = positionalArgs;
|
|
589
|
+
|
|
590
|
+
if (!fs.existsSync(targetFile)) {
|
|
591
|
+
console.error(`${c.red}Error: Target file not found: ${targetFile}${c.reset}`);
|
|
592
|
+
process.exit(1);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
let changesContent;
|
|
596
|
+
if (fs.existsSync(changesFile)) {
|
|
597
|
+
changesContent = fs.readFileSync(changesFile, 'utf-8');
|
|
598
|
+
} else {
|
|
599
|
+
// Maybe it's inline content
|
|
600
|
+
changesContent = changesFile;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const blocks = parseSearchReplaceBlocks(changesContent);
|
|
604
|
+
console.log(`\n${c.cyan}Applying ${blocks.length} SEARCH/REPLACE block(s) to ${targetFile}${c.reset}\n`);
|
|
605
|
+
|
|
606
|
+
if (blocks.length === 0) {
|
|
607
|
+
console.error(`${c.red}No SEARCH/REPLACE blocks found in changes${c.reset}`);
|
|
608
|
+
process.exit(1);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (dryRun) {
|
|
612
|
+
// Show preview only
|
|
613
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
614
|
+
console.log(`${c.yellow}Block ${i + 1}:${c.reset}`);
|
|
615
|
+
console.log(`${c.red}- ${blocks[i].search.split('\n')[0]}...${c.reset}`);
|
|
616
|
+
console.log(`${c.green}+ ${blocks[i].replace.split('\n')[0]}...${c.reset}`);
|
|
617
|
+
console.log('');
|
|
618
|
+
}
|
|
619
|
+
console.log(`${c.dim}(dry run - no changes applied)${c.reset}`);
|
|
620
|
+
process.exit(0);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const result = applySearchReplaceToFile(targetFile, changesContent);
|
|
624
|
+
|
|
625
|
+
if (result.success) {
|
|
626
|
+
console.log(`${c.green}✅ Applied ${result.applied} change(s) successfully${c.reset}`);
|
|
627
|
+
} else if (result.partial) {
|
|
628
|
+
console.log(`${c.yellow}⚠️ Partial success: ${result.applied} applied, ${result.failed.length} failed${c.reset}`);
|
|
629
|
+
for (const f of result.failed) {
|
|
630
|
+
console.log(` ${c.red}✗${c.reset} ${f.reason}`);
|
|
631
|
+
console.log(` ${c.dim}Search: ${f.search}${c.reset}`);
|
|
632
|
+
}
|
|
633
|
+
process.exit(1);
|
|
634
|
+
} else {
|
|
635
|
+
console.log(`${c.red}❌ All changes failed${c.reset}`);
|
|
636
|
+
for (const f of result.failed) {
|
|
637
|
+
console.log(` ${c.red}✗${c.reset} ${f.reason}`);
|
|
638
|
+
console.log(` ${c.dim}Search: ${f.search}${c.reset}`);
|
|
639
|
+
}
|
|
640
|
+
process.exit(1);
|
|
641
|
+
}
|
|
642
|
+
} else if (args.includes('--preview') || args.includes('--apply') || args.includes('--dry-run')) {
|
|
643
|
+
// Preview/apply operations from JSON file
|
|
644
|
+
const jsonFile = positionalArgs[0];
|
|
645
|
+
if (!jsonFile || !fs.existsSync(jsonFile)) {
|
|
646
|
+
console.error(`${c.red}Error: Operations JSON file required${c.reset}`);
|
|
647
|
+
process.exit(1);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const operations = JSON.parse(fs.readFileSync(jsonFile, 'utf-8'));
|
|
651
|
+
|
|
652
|
+
previewAndApply(operations, {
|
|
653
|
+
dryRun,
|
|
654
|
+
apply,
|
|
655
|
+
nonInteractive: process.env.CI === 'true'
|
|
656
|
+
}).catch(err => {
|
|
657
|
+
console.error(`${c.red}Error: ${err.message}${c.reset}`);
|
|
658
|
+
process.exit(1);
|
|
659
|
+
});
|
|
660
|
+
} else if (positionalArgs.length >= 2) {
|
|
661
|
+
// Diff between two files
|
|
662
|
+
const [file1, file2] = positionalArgs;
|
|
663
|
+
|
|
664
|
+
if (!fs.existsSync(file1)) {
|
|
665
|
+
console.error(`${c.red}Error: File not found: ${file1}${c.reset}`);
|
|
666
|
+
process.exit(1);
|
|
667
|
+
}
|
|
668
|
+
if (!fs.existsSync(file2)) {
|
|
669
|
+
console.error(`${c.red}Error: File not found: ${file2}${c.reset}`);
|
|
670
|
+
process.exit(1);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const content1 = fs.readFileSync(file1, 'utf-8');
|
|
674
|
+
const content2 = fs.readFileSync(file2, 'utf-8');
|
|
675
|
+
const diff = generateDiff(file1, content1, content2);
|
|
676
|
+
|
|
677
|
+
if (jsonOutput) {
|
|
678
|
+
console.log(JSON.stringify({ diff }, null, 2));
|
|
679
|
+
} else {
|
|
680
|
+
// Colorize output
|
|
681
|
+
for (const line of diff.split('\n')) {
|
|
682
|
+
if (line.startsWith('+++') || line.startsWith('---')) {
|
|
683
|
+
console.log(`${c.bold}${line}${c.reset}`);
|
|
684
|
+
} else if (line.startsWith('@@')) {
|
|
685
|
+
console.log(`${c.cyan}${line}${c.reset}`);
|
|
686
|
+
} else if (line.startsWith('+')) {
|
|
687
|
+
console.log(`${c.green}${line}${c.reset}`);
|
|
688
|
+
} else if (line.startsWith('-')) {
|
|
689
|
+
console.log(`${c.red}${line}${c.reset}`);
|
|
690
|
+
} else {
|
|
691
|
+
console.log(line);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
} else {
|
|
696
|
+
console.error(`${c.red}Error: Not enough arguments${c.reset}`);
|
|
697
|
+
console.log(`${c.dim}Run "flow diff --help" for usage${c.reset}`);
|
|
698
|
+
process.exit(1);
|
|
699
|
+
}
|
|
700
|
+
}
|