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,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
|
+
};
|