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,414 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Figma Pipeline Orchestrator
5
+ *
6
+ * Coordinates the full Figma-to-code pipeline:
7
+ * 1. Extract - Parse Figma MCP output for atomic components
8
+ * 2. Match - Compare against codebase registry for reuse
9
+ * 3. Confirm - Interactive user confirmation of matches
10
+ * 4. Generate - Create code for new/modified components
11
+ *
12
+ * Usage:
13
+ * flow figma pipeline <figma-data.json> # Run full pipeline
14
+ * flow figma pipeline --step extract # Run specific step
15
+ * flow figma pipeline --auto # Non-interactive mode
16
+ */
17
+
18
+ const path = require('path');
19
+ const { FigmaExtractor, extractFromFile } = require('./flow-figma-extract');
20
+ const { ComponentMatcher, matchFromExtracted } = require('./flow-figma-match');
21
+ const { ConfirmationFlow, confirmMatches } = require('./flow-figma-confirm');
22
+ const { CodeGenerator, generateFromDecisions } = require('./flow-figma-generate');
23
+ const { getProjectRoot, readJson, writeJson, color } = require('./flow-utils');
24
+ const { readJson: safeReadJson } = require('./flow-file-ops');
25
+
26
+ const PROJECT_ROOT = getProjectRoot();
27
+ const WORKFLOW_DIR = path.join(PROJECT_ROOT, '.workflow');
28
+ const PIPELINE_STATE_PATH = path.join(WORKFLOW_DIR, 'state', 'figma-pipeline.json');
29
+
30
+ // ============================================================
31
+ // Pipeline State Management
32
+ // ============================================================
33
+
34
+ /**
35
+ * Load pipeline state from disk
36
+ */
37
+ function loadPipelineState() {
38
+ return safeReadJson(PIPELINE_STATE_PATH, {
39
+ lastRun: null,
40
+ currentStep: null,
41
+ extractedComponents: [],
42
+ matchResults: [],
43
+ decisions: [],
44
+ generatedFiles: []
45
+ });
46
+ }
47
+
48
+ /**
49
+ * Save pipeline state to disk
50
+ */
51
+ function savePipelineState(state) {
52
+ state.updatedAt = new Date().toISOString();
53
+ writeJson(PIPELINE_STATE_PATH, state);
54
+ }
55
+
56
+ // ============================================================
57
+ // Pipeline Orchestrator
58
+ // ============================================================
59
+
60
+ class FigmaPipeline {
61
+ constructor(options = {}) {
62
+ this.options = {
63
+ interactive: options.interactive !== false,
64
+ threshold: options.threshold || 80,
65
+ outputDir: options.outputDir || path.join(PROJECT_ROOT, 'src', 'components'),
66
+ verbose: options.verbose || false,
67
+ ...options
68
+ };
69
+ this.state = loadPipelineState();
70
+ }
71
+
72
+ /**
73
+ * Run the extract step
74
+ */
75
+ async extract(figmaData) {
76
+ if (this.options.verbose) console.log(color('blue', '📦 Step 1: Extracting components from Figma data...'));
77
+
78
+ const extractor = new FigmaExtractor();
79
+ const extracted = typeof figmaData === 'string'
80
+ ? extractFromFile(figmaData)
81
+ : extractor.extract(figmaData);
82
+
83
+ this.state.extractedComponents = extracted.components || [];
84
+ this.state.tokens = extracted.tokens || {};
85
+ this.state.currentStep = 'extract';
86
+ savePipelineState(this.state);
87
+
88
+ if (this.options.verbose) {
89
+ console.log(` ✓ Extracted ${this.state.extractedComponents.length} components`);
90
+ console.log(` ✓ Found ${Object.keys(this.state.tokens).length} design token categories`);
91
+ }
92
+
93
+ return extracted;
94
+ }
95
+
96
+ /**
97
+ * Run the match step
98
+ */
99
+ async match(components = null) {
100
+ if (this.options.verbose) console.log(color('blue', '🔍 Step 2: Matching against codebase registry...'));
101
+
102
+ const toMatch = components || this.state.extractedComponents;
103
+ if (!toMatch || toMatch.length === 0) {
104
+ throw new Error('No components to match. Run extract step first.');
105
+ }
106
+
107
+ const matcher = new ComponentMatcher({ threshold: this.options.threshold });
108
+ const matches = matchFromExtracted(toMatch);
109
+
110
+ this.state.matchResults = matches;
111
+ this.state.currentStep = 'match';
112
+ savePipelineState(this.state);
113
+
114
+ if (this.options.verbose) {
115
+ const exact = matches.filter(m => m.matchType === 'exact').length;
116
+ const partial = matches.filter(m => m.matchType === 'partial').length;
117
+ const create = matches.filter(m => m.matchType === 'create').length;
118
+ console.log(` ✓ Exact matches: ${exact}`);
119
+ console.log(` ✓ Partial matches: ${partial}`);
120
+ console.log(` ✓ New components: ${create}`);
121
+ }
122
+
123
+ return matches;
124
+ }
125
+
126
+ /**
127
+ * Run the confirm step (interactive)
128
+ */
129
+ async confirm(matches = null) {
130
+ if (this.options.verbose) console.log(color('blue', '✅ Step 3: Confirming component decisions...'));
131
+
132
+ const toConfirm = matches || this.state.matchResults;
133
+ if (!toConfirm || toConfirm.length === 0) {
134
+ throw new Error('No match results to confirm. Run match step first.');
135
+ }
136
+
137
+ let decisions;
138
+ if (this.options.interactive) {
139
+ decisions = await confirmMatches(toConfirm);
140
+ } else {
141
+ // Auto-confirm: use best match for exact/partial, create for new
142
+ decisions = toConfirm.map(m => ({
143
+ component: m.component,
144
+ action: m.matchType === 'create' ? 'create' : 'use',
145
+ match: m.bestMatch,
146
+ confirmed: true
147
+ }));
148
+ }
149
+
150
+ this.state.decisions = decisions;
151
+ this.state.currentStep = 'confirm';
152
+ savePipelineState(this.state);
153
+
154
+ if (this.options.verbose) {
155
+ const useExisting = decisions.filter(d => d.action === 'use').length;
156
+ const createNew = decisions.filter(d => d.action === 'create').length;
157
+ console.log(` ✓ Using existing: ${useExisting}`);
158
+ console.log(` ✓ Creating new: ${createNew}`);
159
+ }
160
+
161
+ return decisions;
162
+ }
163
+
164
+ /**
165
+ * Run the generate step
166
+ */
167
+ async generate(decisions = null) {
168
+ if (this.options.verbose) console.log(color('blue', '🛠️ Step 4: Generating code...'));
169
+
170
+ const toGenerate = decisions || this.state.decisions;
171
+ if (!toGenerate || toGenerate.length === 0) {
172
+ throw new Error('No decisions to generate from. Run confirm step first.');
173
+ }
174
+
175
+ const generator = new CodeGenerator({
176
+ outputDir: this.options.outputDir,
177
+ tokens: this.state.tokens
178
+ });
179
+
180
+ const generated = await generateFromDecisions(toGenerate, {
181
+ outputDir: this.options.outputDir
182
+ });
183
+
184
+ this.state.generatedFiles = generated.files || [];
185
+ this.state.currentStep = 'complete';
186
+ this.state.lastRun = new Date().toISOString();
187
+ savePipelineState(this.state);
188
+
189
+ if (this.options.verbose) {
190
+ console.log(` ✓ Generated ${this.state.generatedFiles.length} files`);
191
+ this.state.generatedFiles.forEach(f => console.log(` - ${f}`));
192
+ }
193
+
194
+ return generated;
195
+ }
196
+
197
+ /**
198
+ * Run the full pipeline
199
+ */
200
+ async runFull(figmaData) {
201
+ console.log(color('cyan', '═'.repeat(50)));
202
+ console.log(color('cyan', ' Figma-to-Code Pipeline'));
203
+ console.log(color('cyan', '═'.repeat(50)));
204
+ console.log();
205
+
206
+ try {
207
+ const extracted = await this.extract(figmaData);
208
+ const matches = await this.match();
209
+ const decisions = await this.confirm();
210
+ const generated = await this.generate();
211
+
212
+ console.log();
213
+ console.log(color('green', '✓ Pipeline complete!'));
214
+ console.log(` Components processed: ${extracted.components?.length || 0}`);
215
+ console.log(` Files generated: ${generated.files?.length || 0}`);
216
+
217
+ return {
218
+ success: true,
219
+ extracted,
220
+ matches,
221
+ decisions,
222
+ generated
223
+ };
224
+ } catch (err) {
225
+ console.error(color('red', `✗ Pipeline failed: ${err.message}`));
226
+ return {
227
+ success: false,
228
+ error: err.message,
229
+ step: this.state.currentStep
230
+ };
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Resume from last saved state
236
+ */
237
+ async resume() {
238
+ const step = this.state.currentStep;
239
+ if (!step) {
240
+ throw new Error('No previous pipeline state to resume from.');
241
+ }
242
+
243
+ console.log(color('yellow', `Resuming from step: ${step}`));
244
+
245
+ switch (step) {
246
+ case 'extract':
247
+ return this.runFromMatch();
248
+ case 'match':
249
+ return this.runFromConfirm();
250
+ case 'confirm':
251
+ return this.runFromGenerate();
252
+ default:
253
+ throw new Error(`Unknown step: ${step}`);
254
+ }
255
+ }
256
+
257
+ async runFromMatch() {
258
+ const matches = await this.match();
259
+ const decisions = await this.confirm();
260
+ const generated = await this.generate();
261
+ return { matches, decisions, generated };
262
+ }
263
+
264
+ async runFromConfirm() {
265
+ const decisions = await this.confirm();
266
+ const generated = await this.generate();
267
+ return { decisions, generated };
268
+ }
269
+
270
+ async runFromGenerate() {
271
+ const generated = await this.generate();
272
+ return { generated };
273
+ }
274
+
275
+ /**
276
+ * Get pipeline status
277
+ */
278
+ getStatus() {
279
+ return {
280
+ currentStep: this.state.currentStep,
281
+ lastRun: this.state.lastRun,
282
+ componentCount: this.state.extractedComponents?.length || 0,
283
+ matchCount: this.state.matchResults?.length || 0,
284
+ decisionCount: this.state.decisions?.length || 0,
285
+ generatedCount: this.state.generatedFiles?.length || 0
286
+ };
287
+ }
288
+
289
+ /**
290
+ * Reset pipeline state
291
+ */
292
+ reset() {
293
+ this.state = {
294
+ lastRun: null,
295
+ currentStep: null,
296
+ extractedComponents: [],
297
+ matchResults: [],
298
+ decisions: [],
299
+ generatedFiles: []
300
+ };
301
+ savePipelineState(this.state);
302
+ console.log(color('yellow', 'Pipeline state reset.'));
303
+ }
304
+ }
305
+
306
+ // ============================================================
307
+ // CLI Interface
308
+ // ============================================================
309
+
310
+ async function main() {
311
+ const args = process.argv.slice(2);
312
+
313
+ if (args.includes('--help') || args.includes('-h')) {
314
+ console.log(`
315
+ Figma Pipeline Orchestrator
316
+
317
+ Usage:
318
+ flow figma pipeline <figma-data.json> Run full pipeline
319
+ flow figma pipeline --resume Resume from last state
320
+ flow figma pipeline --status Show pipeline status
321
+ flow figma pipeline --reset Reset pipeline state
322
+
323
+ Options:
324
+ --step <name> Run specific step (extract|match|confirm|generate)
325
+ --auto Non-interactive mode (auto-confirm matches)
326
+ --threshold <n> Match threshold percentage (default: 80)
327
+ --output <dir> Output directory for generated code
328
+ --verbose Show detailed progress
329
+ `);
330
+ return;
331
+ }
332
+
333
+ const pipeline = new FigmaPipeline({
334
+ interactive: !args.includes('--auto'),
335
+ verbose: args.includes('--verbose') || args.includes('-v'),
336
+ threshold: args.includes('--threshold')
337
+ ? parseInt(args[args.indexOf('--threshold') + 1], 10)
338
+ : 80,
339
+ outputDir: args.includes('--output')
340
+ ? args[args.indexOf('--output') + 1]
341
+ : undefined
342
+ });
343
+
344
+ if (args.includes('--status')) {
345
+ const status = pipeline.getStatus();
346
+ console.log('Pipeline Status:');
347
+ console.log(` Current step: ${status.currentStep || 'not started'}`);
348
+ console.log(` Last run: ${status.lastRun || 'never'}`);
349
+ console.log(` Components: ${status.componentCount}`);
350
+ console.log(` Matches: ${status.matchCount}`);
351
+ console.log(` Decisions: ${status.decisionCount}`);
352
+ console.log(` Generated: ${status.generatedCount}`);
353
+ return;
354
+ }
355
+
356
+ if (args.includes('--reset')) {
357
+ pipeline.reset();
358
+ return;
359
+ }
360
+
361
+ if (args.includes('--resume')) {
362
+ await pipeline.resume();
363
+ return;
364
+ }
365
+
366
+ const stepIndex = args.indexOf('--step');
367
+ if (stepIndex !== -1) {
368
+ const step = args[stepIndex + 1];
369
+ const inputFile = args.find(a => !a.startsWith('--') && a.endsWith('.json'));
370
+
371
+ switch (step) {
372
+ case 'extract':
373
+ if (!inputFile) throw new Error('Input file required for extract step');
374
+ await pipeline.extract(inputFile);
375
+ break;
376
+ case 'match':
377
+ await pipeline.match();
378
+ break;
379
+ case 'confirm':
380
+ await pipeline.confirm();
381
+ break;
382
+ case 'generate':
383
+ await pipeline.generate();
384
+ break;
385
+ default:
386
+ throw new Error(`Unknown step: ${step}`);
387
+ }
388
+ return;
389
+ }
390
+
391
+ // Default: run full pipeline
392
+ const inputFile = args.find(a => !a.startsWith('--') && a.endsWith('.json'));
393
+ if (!inputFile) {
394
+ console.error('Error: Input file required');
395
+ console.log('Usage: flow figma pipeline <figma-data.json>');
396
+ process.exit(1);
397
+ }
398
+
399
+ await pipeline.runFull(inputFile);
400
+ }
401
+
402
+ // Run if executed directly
403
+ if (require.main === module) {
404
+ main().catch(err => {
405
+ console.error('Error:', err.message);
406
+ process.exit(1);
407
+ });
408
+ }
409
+
410
+ module.exports = {
411
+ FigmaPipeline,
412
+ loadPipelineState,
413
+ savePipelineState
414
+ };
@@ -0,0 +1,301 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - File Operations
5
+ *
6
+ * Safe file operations with atomic writes and error handling.
7
+ * Extracted from flow-utils.js for better modularity.
8
+ *
9
+ * Usage:
10
+ * const { readJson, writeJson, fileExists, dirExists } = require('./flow-file-ops');
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+
16
+ // ============================================================
17
+ // File Existence Checks
18
+ // ============================================================
19
+
20
+ /**
21
+ * Check if a file exists
22
+ */
23
+ function fileExists(filePath) {
24
+ try {
25
+ return fs.existsSync(filePath);
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Check if a directory exists
33
+ */
34
+ function dirExists(dirPath) {
35
+ try {
36
+ return fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory();
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Ensure a directory exists (create recursively if needed)
44
+ */
45
+ function ensureDir(dirPath) {
46
+ if (!dirExists(dirPath)) {
47
+ fs.mkdirSync(dirPath, { recursive: true });
48
+ }
49
+ }
50
+
51
+ // ============================================================
52
+ // JSON File Operations
53
+ // ============================================================
54
+
55
+ /**
56
+ * Read JSON file safely
57
+ * @param {string} filePath - Path to JSON file
58
+ * @param {*} [defaultValue=undefined] - Default value if file doesn't exist or is invalid
59
+ * @returns {*} Parsed JSON or defaultValue
60
+ * @throws {Error} If file cannot be read and no defaultValue provided
61
+ */
62
+ function readJson(filePath, defaultValue = undefined) {
63
+ try {
64
+ const content = fs.readFileSync(filePath, 'utf-8');
65
+ return JSON.parse(content);
66
+ } catch (err) {
67
+ if (defaultValue !== undefined) {
68
+ return defaultValue;
69
+ }
70
+ throw new Error(`Failed to read JSON from ${filePath}: ${err.message}`);
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Write JSON file with pretty formatting using atomic write pattern
76
+ * (writes to temp file, then renames for crash safety)
77
+ * @param {string} filePath - Path to JSON file
78
+ * @param {*} data - Data to serialize as JSON
79
+ * @returns {boolean} True on success
80
+ * @throws {Error} If file cannot be written
81
+ */
82
+ function writeJson(filePath, data) {
83
+ const tempPath = filePath + '.tmp.' + process.pid;
84
+ try {
85
+ ensureDir(path.dirname(filePath));
86
+ const content = JSON.stringify(data, null, 2) + '\n';
87
+ fs.writeFileSync(tempPath, content);
88
+ fs.renameSync(tempPath, filePath);
89
+ return true;
90
+ } catch (err) {
91
+ try { fs.unlinkSync(tempPath); } catch { /* ignore */ }
92
+ throw new Error(`Failed to write JSON to ${filePath}: ${err.message}`);
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Safely parse JSON with prototype pollution protection
98
+ * Use this for user-modifiable files (registry, stats, etc.)
99
+ * @param {string} filePath - Path to JSON file
100
+ * @param {*} [defaultValue=null] - Default value if parsing fails
101
+ * @returns {object|null} Parsed JSON or defaultValue on error
102
+ */
103
+ function safeJsonParse(filePath, defaultValue = null) {
104
+ try {
105
+ const content = fs.readFileSync(filePath, 'utf-8');
106
+
107
+ // Check for prototype pollution attempts
108
+ if (/__proto__|constructor\s*["'`:]|prototype\s*["'`:]/i.test(content)) {
109
+ console.error(`[safeJsonParse] Suspicious content detected in ${filePath}`);
110
+ return defaultValue;
111
+ }
112
+
113
+ const parsed = JSON.parse(content);
114
+
115
+ if (typeof parsed !== 'object' || parsed === null) {
116
+ console.error(`[safeJsonParse] Invalid JSON structure in ${filePath} (expected object)`);
117
+ return defaultValue;
118
+ }
119
+
120
+ const keys = Object.getOwnPropertyNames(parsed);
121
+ if (keys.includes('__proto__') || keys.includes('constructor') || keys.includes('prototype')) {
122
+ console.error(`[safeJsonParse] Prototype pollution attempt detected in ${filePath}`);
123
+ return defaultValue;
124
+ }
125
+
126
+ return parsed;
127
+ } catch (err) {
128
+ console.error(`[safeJsonParse] Failed to parse ${filePath}: ${err.message}`);
129
+ return defaultValue;
130
+ }
131
+ }
132
+
133
+ // ============================================================
134
+ // Text File Operations
135
+ // ============================================================
136
+
137
+ /**
138
+ * Read text file safely
139
+ * @param {string} filePath - Path to text file
140
+ * @param {*} [defaultValue=undefined] - Default value if file doesn't exist
141
+ * @returns {string|*} File contents or defaultValue
142
+ * @throws {Error} If file cannot be read and no defaultValue provided
143
+ */
144
+ function readFile(filePath, defaultValue = undefined) {
145
+ try {
146
+ return fs.readFileSync(filePath, 'utf-8');
147
+ } catch (err) {
148
+ if (defaultValue !== undefined) {
149
+ return defaultValue;
150
+ }
151
+ throw new Error(`Failed to read file ${filePath}: ${err.message}`);
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Write text file using atomic write pattern
157
+ */
158
+ function writeFile(filePath, content) {
159
+ const tempPath = filePath + '.tmp.' + process.pid;
160
+ try {
161
+ ensureDir(path.dirname(filePath));
162
+ fs.writeFileSync(tempPath, content);
163
+ fs.renameSync(tempPath, filePath);
164
+ return true;
165
+ } catch (err) {
166
+ try { fs.unlinkSync(tempPath); } catch { /* ignore */ }
167
+ throw new Error(`Failed to write file ${filePath}: ${err.message}`);
168
+ }
169
+ }
170
+
171
+ // ============================================================
172
+ // Path Validation
173
+ // ============================================================
174
+
175
+ /**
176
+ * Check if a path is within a base directory (prevents path traversal)
177
+ * @param {string} targetPath - Path to validate
178
+ * @param {string} baseDir - Base directory to check against
179
+ * @returns {boolean} True if path is within base directory
180
+ */
181
+ function isPathWithinDir(targetPath, baseDir) {
182
+ const resolved = path.resolve(targetPath);
183
+ const resolvedBase = path.resolve(baseDir);
184
+ return resolved === resolvedBase || resolved.startsWith(resolvedBase + path.sep);
185
+ }
186
+
187
+ /**
188
+ * Validate JSON file syntax
189
+ */
190
+ function validateJson(filePath) {
191
+ try {
192
+ const content = fs.readFileSync(filePath, 'utf-8');
193
+ JSON.parse(content);
194
+ return { valid: true };
195
+ } catch (err) {
196
+ return { valid: false, error: err.message };
197
+ }
198
+ }
199
+
200
+ // ============================================================
201
+ // Directory Operations
202
+ // ============================================================
203
+
204
+ /**
205
+ * List directories in a path
206
+ */
207
+ function listDirs(dirPath) {
208
+ try {
209
+ if (!dirExists(dirPath)) return [];
210
+ return fs.readdirSync(dirPath)
211
+ .filter(name => {
212
+ const fullPath = path.join(dirPath, name);
213
+ return fs.statSync(fullPath).isDirectory();
214
+ });
215
+ } catch {
216
+ return [];
217
+ }
218
+ }
219
+
220
+ /**
221
+ * List files matching a pattern in a directory
222
+ */
223
+ function listFiles(dirPath, extension = null) {
224
+ try {
225
+ if (!dirExists(dirPath)) return [];
226
+ return fs.readdirSync(dirPath)
227
+ .filter(name => {
228
+ const fullPath = path.join(dirPath, name);
229
+ if (!fs.statSync(fullPath).isFile()) return false;
230
+ if (extension && !name.endsWith(extension)) return false;
231
+ return true;
232
+ });
233
+ } catch {
234
+ return [];
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Count files recursively with depth limit and symlink protection
240
+ */
241
+ function countFiles(dirPath, extensions = [], maxDepth = 10) {
242
+ let count = 0;
243
+ const visited = new Set();
244
+
245
+ function walk(dir, depth) {
246
+ if (depth <= 0) return;
247
+
248
+ try {
249
+ const realPath = fs.realpathSync(dir);
250
+ if (visited.has(realPath)) return;
251
+ visited.add(realPath);
252
+
253
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
254
+ for (const entry of entries) {
255
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
256
+
257
+ const fullPath = path.join(dir, entry.name);
258
+ if (entry.isDirectory() && !entry.isSymbolicLink()) {
259
+ walk(fullPath, depth - 1);
260
+ } else if (entry.isFile()) {
261
+ if (extensions.length === 0 || extensions.some(ext => entry.name.endsWith(ext))) {
262
+ count++;
263
+ }
264
+ }
265
+ }
266
+ } catch (err) {
267
+ if (process.env.DEBUG) console.error(`[DEBUG] countFiles: ${err.message}`);
268
+ }
269
+ }
270
+
271
+ if (dirExists(dirPath)) {
272
+ walk(dirPath, maxDepth);
273
+ }
274
+
275
+ return count;
276
+ }
277
+
278
+ module.exports = {
279
+ // Existence checks
280
+ fileExists,
281
+ dirExists,
282
+ ensureDir,
283
+
284
+ // JSON operations
285
+ readJson,
286
+ writeJson,
287
+ safeJsonParse,
288
+ validateJson,
289
+
290
+ // Text operations
291
+ readFile,
292
+ writeFile,
293
+
294
+ // Path validation
295
+ isPathWithinDir,
296
+
297
+ // Directory operations
298
+ listDirs,
299
+ listFiles,
300
+ countFiles,
301
+ };