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.
Files changed (221) hide show
  1. package/.workflow/agents/reviewer.md +81 -0
  2. package/.workflow/agents/security.md +94 -0
  3. package/.workflow/agents/story-writer.md +58 -0
  4. package/.workflow/bridges/base-bridge.js +395 -0
  5. package/.workflow/bridges/claude-bridge.js +434 -0
  6. package/.workflow/bridges/index.js +130 -0
  7. package/.workflow/lib/assumption-detector.js +481 -0
  8. package/.workflow/lib/config-substitution.js +371 -0
  9. package/.workflow/lib/failure-categories.js +478 -0
  10. package/.workflow/state/app-map.md.template +15 -0
  11. package/.workflow/state/architecture.md.template +24 -0
  12. package/.workflow/state/component-index.json.template +5 -0
  13. package/.workflow/state/decisions.md.template +15 -0
  14. package/.workflow/state/feedback-patterns.md.template +9 -0
  15. package/.workflow/state/knowledge-sync.json.template +6 -0
  16. package/.workflow/state/progress.md.template +14 -0
  17. package/.workflow/state/ready.json.template +7 -0
  18. package/.workflow/state/request-log.md.template +14 -0
  19. package/.workflow/state/session-state.json.template +11 -0
  20. package/.workflow/state/stack.md.template +33 -0
  21. package/.workflow/state/testing.md.template +36 -0
  22. package/.workflow/templates/claude-md.hbs +257 -0
  23. package/.workflow/templates/correction-report.md +67 -0
  24. package/.workflow/templates/gemini-md.hbs +52 -0
  25. package/README.md +1802 -0
  26. package/bin/flow +205 -0
  27. package/lib/index.js +33 -0
  28. package/lib/installer.js +467 -0
  29. package/lib/release-channel.js +269 -0
  30. package/lib/skill-registry.js +526 -0
  31. package/lib/upgrader.js +401 -0
  32. package/lib/utils.js +305 -0
  33. package/package.json +64 -0
  34. package/scripts/flow +985 -0
  35. package/scripts/flow-adaptive-learning.js +1259 -0
  36. package/scripts/flow-aggregate.js +488 -0
  37. package/scripts/flow-archive +133 -0
  38. package/scripts/flow-auto-context.js +1015 -0
  39. package/scripts/flow-auto-learn.js +615 -0
  40. package/scripts/flow-bridge.js +223 -0
  41. package/scripts/flow-browser-suggest.js +316 -0
  42. package/scripts/flow-bug.js +247 -0
  43. package/scripts/flow-cascade.js +711 -0
  44. package/scripts/flow-changelog +85 -0
  45. package/scripts/flow-checkpoint.js +483 -0
  46. package/scripts/flow-cli.js +403 -0
  47. package/scripts/flow-code-intelligence.js +760 -0
  48. package/scripts/flow-complexity.js +502 -0
  49. package/scripts/flow-config-set.js +152 -0
  50. package/scripts/flow-constants.js +157 -0
  51. package/scripts/flow-context +152 -0
  52. package/scripts/flow-context-init.js +482 -0
  53. package/scripts/flow-context-monitor.js +384 -0
  54. package/scripts/flow-context-scoring.js +886 -0
  55. package/scripts/flow-correct.js +458 -0
  56. package/scripts/flow-damage-control.js +985 -0
  57. package/scripts/flow-deps +101 -0
  58. package/scripts/flow-diff.js +700 -0
  59. package/scripts/flow-done +151 -0
  60. package/scripts/flow-done.js +489 -0
  61. package/scripts/flow-durable-session.js +1541 -0
  62. package/scripts/flow-entropy-monitor.js +345 -0
  63. package/scripts/flow-export-profile +349 -0
  64. package/scripts/flow-export-scanner.js +1046 -0
  65. package/scripts/flow-figma-confirm.js +400 -0
  66. package/scripts/flow-figma-extract.js +496 -0
  67. package/scripts/flow-figma-generate.js +683 -0
  68. package/scripts/flow-figma-index.js +909 -0
  69. package/scripts/flow-figma-match.js +617 -0
  70. package/scripts/flow-figma-mcp-server.js +518 -0
  71. package/scripts/flow-figma-pipeline.js +414 -0
  72. package/scripts/flow-file-ops.js +301 -0
  73. package/scripts/flow-gate-confidence.js +825 -0
  74. package/scripts/flow-guided-edit.js +659 -0
  75. package/scripts/flow-health +185 -0
  76. package/scripts/flow-health.js +413 -0
  77. package/scripts/flow-hooks.js +556 -0
  78. package/scripts/flow-http-client.js +249 -0
  79. package/scripts/flow-hybrid-detect.js +167 -0
  80. package/scripts/flow-hybrid-interactive.js +591 -0
  81. package/scripts/flow-hybrid-test.js +152 -0
  82. package/scripts/flow-import-profile +439 -0
  83. package/scripts/flow-init +253 -0
  84. package/scripts/flow-instruction-richness.js +827 -0
  85. package/scripts/flow-jira-integration.js +579 -0
  86. package/scripts/flow-knowledge-router.js +522 -0
  87. package/scripts/flow-knowledge-sync.js +589 -0
  88. package/scripts/flow-linear-integration.js +631 -0
  89. package/scripts/flow-links.js +774 -0
  90. package/scripts/flow-log-manager.js +559 -0
  91. package/scripts/flow-loop-enforcer.js +1246 -0
  92. package/scripts/flow-loop-retry-learning.js +630 -0
  93. package/scripts/flow-lsp.js +923 -0
  94. package/scripts/flow-map-index +348 -0
  95. package/scripts/flow-map-sync +201 -0
  96. package/scripts/flow-memory-blocks.js +668 -0
  97. package/scripts/flow-memory-compactor.js +350 -0
  98. package/scripts/flow-memory-db.js +1110 -0
  99. package/scripts/flow-memory-sync.js +484 -0
  100. package/scripts/flow-metrics.js +353 -0
  101. package/scripts/flow-migrate-ids.js +370 -0
  102. package/scripts/flow-model-adapter.js +802 -0
  103. package/scripts/flow-model-router.js +884 -0
  104. package/scripts/flow-models.js +1231 -0
  105. package/scripts/flow-morning.js +517 -0
  106. package/scripts/flow-multi-approach.js +660 -0
  107. package/scripts/flow-new-feature +86 -0
  108. package/scripts/flow-onboard +1042 -0
  109. package/scripts/flow-orchestrate-llm.js +459 -0
  110. package/scripts/flow-orchestrate.js +3592 -0
  111. package/scripts/flow-output.js +123 -0
  112. package/scripts/flow-parallel-detector.js +399 -0
  113. package/scripts/flow-parallel-dispatch.js +987 -0
  114. package/scripts/flow-parallel.js +428 -0
  115. package/scripts/flow-pattern-enforcer.js +600 -0
  116. package/scripts/flow-prd-manager.js +282 -0
  117. package/scripts/flow-progress.js +323 -0
  118. package/scripts/flow-project-analyzer.js +975 -0
  119. package/scripts/flow-prompt-composer.js +487 -0
  120. package/scripts/flow-providers.js +1381 -0
  121. package/scripts/flow-queue.js +308 -0
  122. package/scripts/flow-ready +82 -0
  123. package/scripts/flow-ready.js +189 -0
  124. package/scripts/flow-regression.js +396 -0
  125. package/scripts/flow-response-parser.js +450 -0
  126. package/scripts/flow-resume.js +284 -0
  127. package/scripts/flow-rules-sync.js +439 -0
  128. package/scripts/flow-run-trace.js +718 -0
  129. package/scripts/flow-safety.js +587 -0
  130. package/scripts/flow-search +104 -0
  131. package/scripts/flow-security.js +481 -0
  132. package/scripts/flow-session-end +106 -0
  133. package/scripts/flow-session-end.js +437 -0
  134. package/scripts/flow-session-state.js +671 -0
  135. package/scripts/flow-setup-hooks +216 -0
  136. package/scripts/flow-setup-hooks.js +377 -0
  137. package/scripts/flow-skill-create.js +329 -0
  138. package/scripts/flow-skill-creator.js +572 -0
  139. package/scripts/flow-skill-generator.js +1046 -0
  140. package/scripts/flow-skill-learn.js +880 -0
  141. package/scripts/flow-skill-matcher.js +578 -0
  142. package/scripts/flow-spec-generator.js +820 -0
  143. package/scripts/flow-stack-wizard.js +895 -0
  144. package/scripts/flow-standup +162 -0
  145. package/scripts/flow-start +74 -0
  146. package/scripts/flow-start.js +235 -0
  147. package/scripts/flow-status +110 -0
  148. package/scripts/flow-status.js +301 -0
  149. package/scripts/flow-step-browser.js +83 -0
  150. package/scripts/flow-step-changelog.js +217 -0
  151. package/scripts/flow-step-comments.js +306 -0
  152. package/scripts/flow-step-complexity.js +234 -0
  153. package/scripts/flow-step-coverage.js +218 -0
  154. package/scripts/flow-step-knowledge.js +193 -0
  155. package/scripts/flow-step-pr-tests.js +364 -0
  156. package/scripts/flow-step-regression.js +89 -0
  157. package/scripts/flow-step-review.js +516 -0
  158. package/scripts/flow-step-security.js +162 -0
  159. package/scripts/flow-step-silent-failures.js +290 -0
  160. package/scripts/flow-step-simplifier.js +346 -0
  161. package/scripts/flow-story +105 -0
  162. package/scripts/flow-story.js +500 -0
  163. package/scripts/flow-suspend.js +252 -0
  164. package/scripts/flow-sync-daemon.js +654 -0
  165. package/scripts/flow-task-analyzer.js +606 -0
  166. package/scripts/flow-team-dashboard.js +748 -0
  167. package/scripts/flow-team-sync.js +752 -0
  168. package/scripts/flow-team.js +977 -0
  169. package/scripts/flow-tech-options.js +528 -0
  170. package/scripts/flow-templates.js +812 -0
  171. package/scripts/flow-tiered-learning.js +728 -0
  172. package/scripts/flow-trace +204 -0
  173. package/scripts/flow-transcript-chunking.js +1106 -0
  174. package/scripts/flow-transcript-digest.js +7918 -0
  175. package/scripts/flow-transcript-language.js +465 -0
  176. package/scripts/flow-transcript-parsing.js +1085 -0
  177. package/scripts/flow-transcript-stories.js +2194 -0
  178. package/scripts/flow-update-map +224 -0
  179. package/scripts/flow-utils.js +2242 -0
  180. package/scripts/flow-verification.js +644 -0
  181. package/scripts/flow-verify.js +1177 -0
  182. package/scripts/flow-voice-input.js +638 -0
  183. package/scripts/flow-watch +168 -0
  184. package/scripts/flow-workflow-steps.js +521 -0
  185. package/scripts/flow-workflow.js +1029 -0
  186. package/scripts/flow-worktree.js +489 -0
  187. package/scripts/hooks/adapters/base-adapter.js +102 -0
  188. package/scripts/hooks/adapters/claude-code.js +359 -0
  189. package/scripts/hooks/adapters/index.js +79 -0
  190. package/scripts/hooks/core/component-check.js +341 -0
  191. package/scripts/hooks/core/index.js +35 -0
  192. package/scripts/hooks/core/loop-check.js +241 -0
  193. package/scripts/hooks/core/session-context.js +294 -0
  194. package/scripts/hooks/core/task-gate.js +177 -0
  195. package/scripts/hooks/core/validation.js +230 -0
  196. package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
  197. package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
  198. package/scripts/hooks/entry/claude-code/session-end.js +87 -0
  199. package/scripts/hooks/entry/claude-code/session-start.js +46 -0
  200. package/scripts/hooks/entry/claude-code/stop.js +43 -0
  201. package/scripts/postinstall.js +139 -0
  202. package/templates/browser-test-flow.json +56 -0
  203. package/templates/bug-report.md +43 -0
  204. package/templates/component-detail.md +42 -0
  205. package/templates/component.stories.tsx +49 -0
  206. package/templates/context/constraints.md +83 -0
  207. package/templates/context/conventions.md +177 -0
  208. package/templates/context/stack.md +60 -0
  209. package/templates/correction-report.md +90 -0
  210. package/templates/feature-proposal.md +35 -0
  211. package/templates/hybrid/_base.md +254 -0
  212. package/templates/hybrid/_patterns.md +45 -0
  213. package/templates/hybrid/create-component.md +127 -0
  214. package/templates/hybrid/create-file.md +56 -0
  215. package/templates/hybrid/create-hook.md +145 -0
  216. package/templates/hybrid/create-service.md +70 -0
  217. package/templates/hybrid/fix-bug.md +33 -0
  218. package/templates/hybrid/modify-file.md +55 -0
  219. package/templates/story.md +68 -0
  220. package/templates/task.json +56 -0
  221. 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
+ }