@mmerterden/multi-agent-pipeline 11.2.0 → 11.3.1
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/CHANGELOG.md +68 -0
- package/README.md +2 -2
- package/docs/FIGMA_PIPELINE.md +12 -25
- package/docs/architecture.md +3 -5
- package/docs/features.md +8 -8
- package/install/_dev-only-files.mjs +0 -6
- package/install/claude.mjs +39 -17
- package/install/copilot.mjs +22 -16
- package/install/templates/copilot-instructions.md +3 -3
- package/package.json +1 -1
- package/pipeline/commands/{multi-agent.md → multi-agent/SKILL.md} +39 -39
- package/pipeline/commands/multi-agent/{analysis.md → analysis/SKILL.md} +18 -18
- package/pipeline/commands/multi-agent/{analysis-resolve.md → analysis-resolve/SKILL.md} +4 -4
- package/pipeline/commands/multi-agent/{autopilot.md → autopilot/SKILL.md} +4 -4
- package/pipeline/commands/multi-agent/{build-optimize.md → build-optimize/SKILL.md} +1 -1
- package/pipeline/commands/multi-agent/{channels.md → channels/SKILL.md} +12 -12
- package/pipeline/commands/multi-agent/{dev.md → dev/SKILL.md} +4 -4
- package/pipeline/commands/multi-agent/{dev-autopilot.md → dev-autopilot/SKILL.md} +4 -4
- package/pipeline/commands/multi-agent/{dev-local.md → dev-local/SKILL.md} +5 -5
- package/pipeline/commands/multi-agent/{dev-local-autopilot.md → dev-local-autopilot/SKILL.md} +4 -4
- package/pipeline/commands/multi-agent/{finish.md → finish/SKILL.md} +6 -6
- package/pipeline/commands/multi-agent/{generate.md → generate/SKILL.md} +3 -3
- package/pipeline/commands/multi-agent/{help.md → help/SKILL.md} +4 -4
- package/pipeline/commands/multi-agent/{local.md → local/SKILL.md} +5 -5
- package/pipeline/commands/multi-agent/{local-autopilot.md → local-autopilot/SKILL.md} +5 -5
- package/pipeline/commands/multi-agent/{manual-test.md → manual-test/SKILL.md} +1 -1
- package/pipeline/commands/multi-agent/refactor/SKILL.md +257 -0
- package/pipeline/commands/multi-agent/{resume.md → resume/SKILL.md} +1 -1
- package/pipeline/commands/multi-agent/{review.md → review/SKILL.md} +5 -5
- package/pipeline/commands/multi-agent/{setup.md → setup/SKILL.md} +1 -1
- package/pipeline/commands/multi-agent/{sync.md → sync/SKILL.md} +20 -58
- package/pipeline/lib/credential-store.sh +8 -4
- package/pipeline/{commands/multi-agent → multi-agent-refs}/_account-picker.md +2 -2
- package/pipeline/{commands/multi-agent → multi-agent-refs}/_dev-context.md +2 -2
- package/pipeline/{commands/multi-agent → multi-agent-refs}/_repo-picker.md +2 -2
- package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/android-guide.md +1 -1
- package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/channels/jira.md +1 -1
- package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/channels/wiki.md +7 -7
- package/pipeline/multi-agent-refs/component-dispatch.md +92 -0
- package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/cross-cli-contract.md +10 -77
- package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/features/verify-by-test.md +1 -1
- package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/generate-issue.md +2 -2
- package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/issue-jira-triad.md +1 -1
- package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/keychain.md +1 -1
- package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/phases/log-format.md +1 -1
- package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/phases/modes.md +2 -2
- package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/phases/operations.md +1 -1
- package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/phases/phase-0-init.md +10 -10
- package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/phases/phase-1-analysis.md +6 -6
- package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/phases/phase-2-planning.md +3 -3
- package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/phases/phase-3-dev.md +10 -10
- package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/phases/phase-4-review.md +8 -8
- package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/phases/phase-5-test.md +3 -3
- package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/phases/phase-6-commit.md +4 -4
- package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/phases/phase-7-report.md +6 -6
- package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/phases.md +12 -12
- package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/progress-contract.md +4 -4
- package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/swiftui-guide.md +1 -1
- package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/tracker-contract.md +3 -3
- package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/wiki-capture.md +4 -4
- package/pipeline/preferences-template.json +17 -1
- package/pipeline/rules/figma-pipeline.md +2 -2
- package/pipeline/schemas/prefs.schema.json +0 -34
- package/pipeline/scripts/README.md +1 -3
- package/pipeline/scripts/build-skills-index.mjs +3 -6
- package/pipeline/scripts/eval-mine-corpus.mjs +187 -0
- package/pipeline/scripts/fixtures/install-layout.tsv +6 -5
- package/pipeline/scripts/gen-mode-dispatch.mjs +3 -3
- package/pipeline/scripts/lint-skills.mjs +26 -1
- package/pipeline/scripts/phase-banner.sh +2 -2
- package/pipeline/scripts/review-scope.mjs +94 -0
- package/pipeline/scripts/scan-agent-config.sh +2 -2
- package/pipeline/scripts/smoke-agent-log-cost.sh +1 -1
- package/pipeline/scripts/smoke-agent-model-routing.sh +4 -4
- package/pipeline/scripts/smoke-autopilot-circuit-breaker.sh +2 -2
- package/pipeline/scripts/smoke-channels-approval-gate.sh +2 -2
- package/pipeline/scripts/smoke-channels-flow.sh +5 -5
- package/pipeline/scripts/smoke-clarify.sh +1 -1
- package/pipeline/scripts/smoke-commands-skills-parity.sh +3 -3
- package/pipeline/scripts/smoke-community-gates.sh +1 -1
- package/pipeline/scripts/smoke-compliance-skills.sh +2 -2
- package/pipeline/scripts/smoke-cost-summary.sh +1 -1
- package/pipeline/scripts/smoke-cross-cli-behavior.sh +17 -43
- package/pipeline/scripts/smoke-cross-phase-cohesion.sh +1 -1
- package/pipeline/scripts/smoke-dev-critic.sh +1 -1
- package/pipeline/scripts/smoke-diff-explain.sh +1 -1
- package/pipeline/scripts/smoke-diff-risk.sh +1 -1
- package/pipeline/scripts/smoke-evidence-gate.sh +1 -1
- package/pipeline/scripts/smoke-figma-dispatch.sh +46 -57
- package/pipeline/scripts/smoke-gate-hooks.sh +1 -1
- package/pipeline/scripts/smoke-generate-issue.sh +8 -8
- package/pipeline/scripts/smoke-handoff-contract.sh +3 -3
- package/pipeline/scripts/smoke-identity-isolation.sh +2 -2
- package/pipeline/scripts/smoke-intent-guard.sh +1 -1
- package/pipeline/scripts/smoke-issue-comment-template.sh +2 -2
- package/pipeline/scripts/smoke-issue-jira-triad.sh +4 -4
- package/pipeline/scripts/smoke-language-axis.sh +6 -6
- package/pipeline/scripts/smoke-learnings-ledger.sh +3 -3
- package/pipeline/scripts/smoke-mode-dispatch-drift.sh +16 -16
- package/pipeline/scripts/smoke-model-fallback.sh +3 -3
- package/pipeline/scripts/smoke-multi-repo-integration.sh +2 -2
- package/pipeline/scripts/smoke-multi-repo-worktree.sh +1 -1
- package/pipeline/scripts/smoke-no-token-prompt.sh +5 -5
- package/pipeline/scripts/smoke-phase-0-multi-repo.sh +1 -1
- package/pipeline/scripts/smoke-phase-6-multi.sh +1 -1
- package/pipeline/scripts/smoke-phase4-gates.sh +45 -0
- package/pipeline/scripts/smoke-plan-approval-gate.sh +5 -5
- package/pipeline/scripts/smoke-plan-todos.sh +3 -3
- package/pipeline/scripts/smoke-pr-review-actions.sh +3 -3
- package/pipeline/scripts/smoke-prefs-language.sh +1 -1
- package/pipeline/scripts/smoke-progress-contract.sh +2 -2
- package/pipeline/scripts/smoke-push-retry.sh +1 -1
- package/pipeline/scripts/smoke-repo-map.sh +2 -2
- package/pipeline/scripts/smoke-shadow-git.sh +1 -1
- package/pipeline/scripts/smoke-skill-authoring.sh +4 -9
- package/pipeline/scripts/smoke-tasklist-ordering.sh +10 -10
- package/pipeline/scripts/smoke-test-gap.sh +1 -1
- package/pipeline/scripts/smoke-token-budget.sh +1 -1
- package/pipeline/scripts/smoke-token-preflight.sh +2 -2
- package/pipeline/scripts/smoke-tracker-contract.sh +13 -13
- package/pipeline/scripts/smoke-tracker-tokens-invocation.sh +5 -5
- package/pipeline/scripts/smoke-triage-memory.sh +2 -2
- package/pipeline/scripts/smoke-update-check.sh +1 -1
- package/pipeline/scripts/smoke-url-enrichment.sh +2 -2
- package/pipeline/scripts/smoke-validator-gates.sh +1 -1
- package/pipeline/scripts/smoke-verify-by-test.sh +3 -3
- package/pipeline/scripts/smoke-wiki-integration.sh +9 -33
- package/pipeline/scripts/smoke-work-summary.sh +1 -1
- package/pipeline/scripts/smoke-worktree-path-convention.sh +4 -4
- package/pipeline/scripts/test-integrity-gate.mjs +83 -0
- package/pipeline/scripts/uninstall.mjs +17 -6
- package/pipeline/skills/.skill-manifest.json +40 -160
- package/pipeline/skills/.skills-index.json +3 -373
- package/pipeline/skills/shared/README.md +1 -1
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-analysis/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-analysis-resolve/SKILL.md +3 -3
- package/pipeline/skills/shared/core/multi-agent-build-optimize/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-channels/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-finish/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-refactor/SKILL.md +116 -51
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +8 -30
- package/pipeline/skills/shared/external/compose-components/SKILL.md +14 -14
- package/pipeline/skills/skills-index.md +3 -44
- package/pipeline/commands/multi-agent/refactor.md +0 -188
- package/pipeline/commands/multi-agent/refs/component-dispatch.md +0 -92
- package/pipeline/scripts/smoke-existing-discovery-gate.sh +0 -71
- package/pipeline/scripts/smoke-figma-android-parity.sh +0 -148
- package/pipeline/scripts/smoke-figma-credential-store.sh +0 -105
- package/pipeline/scripts/smoke-figma-cross-cli-inventory.sh +0 -177
- package/pipeline/scripts/smoke-figma-sync.sh +0 -149
- package/pipeline/scripts/smoke-sync-delegation.sh +0 -74
- package/pipeline/skills/figma-android/README.md +0 -38
- package/pipeline/skills/figma-android/figma-component-code-connect/SKILL.md +0 -62
- package/pipeline/skills/figma-android/figma-component-implement/SKILL.md +0 -158
- package/pipeline/skills/figma-android/figma-component-test/SKILL.md +0 -120
- package/pipeline/skills/figma-android/figma-component-wiki/SKILL.md +0 -35
- package/pipeline/skills/figma-android/figma-to-component/SKILL.md +0 -124
- package/pipeline/skills/figma-common/README.md +0 -64
- package/pipeline/skills/figma-common/figma-bottom-sheets/SKILL.md +0 -152
- package/pipeline/skills/figma-common/figma-cli-iterate/SKILL.md +0 -282
- package/pipeline/skills/figma-common/figma-cli-iterate-mend/SKILL.md +0 -503
- package/pipeline/skills/figma-common/figma-cli-lean-iterate/SKILL.md +0 -288
- package/pipeline/skills/figma-common/figma-cli-skip/SKILL.md +0 -362
- package/pipeline/skills/figma-common/figma-commit/COMMON_REBASE.md +0 -206
- package/pipeline/skills/figma-common/figma-commit/REVIEW.md +0 -337
- package/pipeline/skills/figma-common/figma-commit/SKILL.md +0 -216
- package/pipeline/skills/figma-common/figma-component-confluence-sync/SKILL.md +0 -223
- package/pipeline/skills/figma-common/figma-component-start/SKILL.md +0 -251
- package/pipeline/skills/figma-common/figma-component-status-update/SKILL.md +0 -78
- package/pipeline/skills/figma-common/figma-evolve-component/SKILL.md +0 -61
- package/pipeline/skills/figma-common/figma-fix/SKILL.md +0 -321
- package/pipeline/skills/figma-common/figma-form-integration/SKILL.md +0 -542
- package/pipeline/skills/figma-common/figma-issue/SKILL.md +0 -750
- package/pipeline/skills/figma-common/figma-iterate/SKILL.md +0 -208
- package/pipeline/skills/figma-common/figma-iteration-commit/SKILL.md +0 -1020
- package/pipeline/skills/figma-common/figma-mend/SKILL.md +0 -331
- package/pipeline/skills/figma-common/figma-navigation/SKILL.md +0 -156
- package/pipeline/skills/figma-common/figma-overlays/SKILL.md +0 -142
- package/pipeline/skills/figma-common/figma-price-integration/SKILL.md +0 -398
- package/pipeline/skills/figma-common/figma-remote-mcp-auth/SKILL.md +0 -104
- package/pipeline/skills/figma-common/figma-review/SKILL.md +0 -400
- package/pipeline/skills/figma-common/figma-setup/SKILL.md +0 -514
- package/pipeline/skills/figma-common/figma-setup/scripts/fetch-mcp-token.py +0 -592
- package/pipeline/skills/figma-common/figma-skip/SKILL.md +0 -129
- package/pipeline/skills/figma-common/figma-ui-patterns/SKILL.md +0 -105
- package/pipeline/skills/figma-common/figma-ui-patterns/patterns/animated-gradient-border.md +0 -116
- package/pipeline/skills/figma-common/figma-utility/SKILL.md +0 -274
- package/pipeline/skills/figma-common/figma-utility/scripts/figma-utility.py +0 -808
- package/pipeline/skills/figma-common/figma-validate/SKILL.md +0 -633
- package/pipeline/skills/figma-common/performance-iteration-commit-all/SKILL.md +0 -716
- package/pipeline/skills/figma-common/performance-review-next/SKILL.md +0 -238
- package/pipeline/skills/figma-common/performance-start/SKILL.md +0 -430
- package/pipeline/skills/figma-common/performance-swiftui/SKILL.md +0 -706
- package/pipeline/skills/figma-common/performance-tour/SKILL.md +0 -423
- package/pipeline/skills/figma-ios/REVIEW_CHECKLIST.md +0 -67
- package/pipeline/skills/figma-ios/figma-component-code-connect/SKILL.md +0 -178
- package/pipeline/skills/figma-ios/figma-component-implement/SKILL.md +0 -184
- package/pipeline/skills/figma-ios/figma-component-test/SKILL.md +0 -219
- package/pipeline/skills/figma-ios/figma-component-wiki/SKILL.md +0 -274
- package/pipeline/skills/figma-ios/figma-to-component/SKILL.md +0 -416
- package/pipeline/skills/figma-ios/figma-to-component/halt-return-protocol.md +0 -57
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-0-init.md +0 -307
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-1-gathering.md +0 -119
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-1.5-existing-discovery.md +0 -174
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-2-orchestrator.md +0 -333
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-2a-testing-identifiers.md +0 -368
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-2b-localization.md +0 -393
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-2c-accessibility.md +0 -617
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-2d-analytics.md +0 -352
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-3-orchestrator.md +0 -337
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-3a-location.md +0 -206
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-3b-tokens.md +0 -235
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-3c-nested.md +0 -214
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-3d-patterns.md +0 -902
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-3e-assets.md +0 -156
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-3f-utilities.md +0 -175
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-3g-property-coverage.md +0 -176
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-3h-variant-config.md +0 -333
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-4-orchestrator.md +0 -412
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-4a-configuration.md +0 -336
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-4b-view.md +0 -705
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-4c-documentation.md +0 -332
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-4d-preview.md +0 -380
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-4e-modifiers.md +0 -262
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-5-orchestrator.md +0 -482
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-5a-viewinspector.md +0 -274
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-5b-snapshot.md +0 -636
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-5c-unit.md +0 -142
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-6-code-connect.md +0 -547
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-7-wiki.md +0 -39
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-7a-confluence-generate.md +0 -659
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-7a-wiki-generate.md +0 -580
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-8-cleanup.md +0 -51
- package/pipeline/skills/figma-ios/figma-to-component/reference/accessibility.md +0 -183
- package/pipeline/skills/figma-ios/figma-to-component/reference/analytics-events.md +0 -64
- package/pipeline/skills/figma-ios/figma-to-component/reference/code-connect.md +0 -531
- package/pipeline/skills/figma-ios/figma-to-component/reference/confluence-api.md +0 -89
- package/pipeline/skills/figma-ios/figma-to-component/reference/confluence-xhtml.md +0 -155
- package/pipeline/skills/figma-ios/figma-to-component/reference/figma-to-swiftui-effects.md +0 -196
- package/pipeline/skills/figma-ios/figma-to-component/reference/halt-return-protocol.md +0 -57
- package/pipeline/skills/figma-ios/figma-to-component/reference/localization-naming.md +0 -89
- package/pipeline/skills/figma-ios/figma-to-component/reference/macros.md +0 -227
- package/pipeline/skills/figma-ios/figma-to-component/reference/missing-tokens.md +0 -157
- package/pipeline/skills/figma-ios/figma-to-component/reference/orchestrator-discipline.md +0 -90
- package/pipeline/skills/figma-ios/figma-to-component/reference/registry.md +0 -116
- package/pipeline/skills/figma-ios/figma-to-component/reference/remote-mcp-script.md +0 -153
- package/pipeline/skills/figma-ios/figma-to-component/reference/rest-api-script.md +0 -130
- package/pipeline/skills/figma-ios/figma-to-component/reference/scripts-inventory.md +0 -218
- package/pipeline/skills/figma-ios/figma-to-component/reference/snapshot-testing.md +0 -188
- package/pipeline/skills/figma-ios/figma-to-component/reference/subcomponent-graph.md +0 -93
- package/pipeline/skills/figma-ios/figma-to-component/reference/testing-identifiers-naming.md +0 -98
- package/pipeline/skills/figma-ios/figma-to-component/reference/tools.md +0 -261
- package/pipeline/skills/figma-ios/figma-to-component/reference/viewinspector.md +0 -147
- package/pipeline/skills/figma-ios/figma-to-component/reference/wiki-to-confluence-mapping.md +0 -182
- package/pipeline/skills/figma-ios/figma-to-component/scripts/apply-author-login-map.py +0 -185
- package/pipeline/skills/figma-ios/figma-to-component/scripts/backfill-status.py +0 -609
- package/pipeline/skills/figma-ios/figma-to-component/scripts/build-author-registry.py +0 -332
- package/pipeline/skills/figma-ios/figma-to-component/scripts/bulk-sync-issues.py +0 -261
- package/pipeline/skills/figma-ios/figma-to-component/scripts/code-connect-data-gather.py +0 -184
- package/pipeline/skills/figma-ios/figma-to-component/scripts/code-connect-publish.sh +0 -188
- package/pipeline/skills/figma-ios/figma-to-component/scripts/confluence-component-status-upload.py +0 -768
- package/pipeline/skills/figma-ios/figma-to-component/scripts/confluence-component-status.py +0 -191
- package/pipeline/skills/figma-ios/figma-to-component/scripts/confluence-data-gather.py +0 -420
- package/pipeline/skills/figma-ios/figma-to-component/scripts/confluence-page-ids.example.json +0 -9
- package/pipeline/skills/figma-ios/figma-to-component/scripts/confluence-publish.py +0 -336
- package/pipeline/skills/figma-ios/figma-to-component/scripts/figma-subcomponent-graph.py +0 -391
- package/pipeline/skills/figma-ios/figma-to-component/scripts/figma-update.py +0 -292
- package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/__init__.py +0 -1
- package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/issue_sync_propagate.py +0 -93
- package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/registry_writer.py +0 -299
- package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/test_backfill_status.py +0 -343
- package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/test_figma_update.py +0 -206
- package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/test_figma_update_http.py +0 -149
- package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/test_phase_clis.py +0 -281
- package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/test_registry_writer.py +0 -332
- package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/test_skill_figma_issue.py +0 -176
- package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/test_skill_figma_review.py +0 -98
- package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/test_update_issue.py +0 -298
- package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/test_update_issue_gh.py +0 -195
- package/pipeline/skills/figma-ios/figma-to-component/scripts/phase1-gather.py +0 -1298
- package/pipeline/skills/figma-ios/figma-to-component/scripts/phase2-finalize.py +0 -228
- package/pipeline/skills/figma-ios/figma-to-component/scripts/phase3-scripts.py +0 -1089
- package/pipeline/skills/figma-ios/figma-to-component/scripts/phase4-finalize.py +0 -141
- package/pipeline/skills/figma-ios/figma-to-component/scripts/phase5-finalize.py +0 -106
- package/pipeline/skills/figma-ios/figma-to-component/scripts/phase6-finalize.py +0 -162
- package/pipeline/skills/figma-ios/figma-to-component/scripts/phase7-finalize.py +0 -105
- package/pipeline/skills/figma-ios/figma-to-component/scripts/register-icons-codeconnect.py +0 -179
- package/pipeline/skills/figma-ios/figma-to-component/scripts/remote-mcp-fetch.py +0 -260
- package/pipeline/skills/figma-ios/figma-to-component/scripts/resolve-author-logins.py +0 -260
- package/pipeline/skills/figma-ios/figma-to-component/scripts/run-uicomponents-tests.sh +0 -86
- package/pipeline/skills/figma-ios/figma-to-component/scripts/sidebar-generator.py +0 -321
- package/pipeline/skills/figma-ios/figma-to-component/scripts/update-issue-from-registry.py +0 -1470
- package/pipeline/skills/figma-ios/figma-to-component/scripts/validate-phase4.sh +0 -176
- package/pipeline/skills/figma-ios/figma-to-component/scripts/validate-phase6.sh +0 -147
- package/pipeline/skills/figma-ios/figma-to-component/scripts/validate-phase7a.py +0 -629
- /package/pipeline/commands/multi-agent/{delete.md → delete/SKILL.md} +0 -0
- /package/pipeline/commands/multi-agent/{diff-explain.md → diff-explain/SKILL.md} +0 -0
- /package/pipeline/commands/multi-agent/{garbage-collect.md → garbage-collect/SKILL.md} +0 -0
- /package/pipeline/commands/multi-agent/{issue.md → issue/SKILL.md} +0 -0
- /package/pipeline/commands/multi-agent/{jira.md → jira/SKILL.md} +0 -0
- /package/pipeline/commands/multi-agent/{kill.md → kill/SKILL.md} +0 -0
- /package/pipeline/commands/multi-agent/{language.md → language/SKILL.md} +0 -0
- /package/pipeline/commands/multi-agent/{log.md → log/SKILL.md} +0 -0
- /package/pipeline/commands/multi-agent/{prune-logs.md → prune-logs/SKILL.md} +0 -0
- /package/pipeline/commands/multi-agent/{purge.md → purge/SKILL.md} +0 -0
- /package/pipeline/commands/multi-agent/{scan.md → scan/SKILL.md} +0 -0
- /package/pipeline/commands/multi-agent/{search.md → search/SKILL.md} +0 -0
- /package/pipeline/commands/multi-agent/{stack.md → stack/SKILL.md} +0 -0
- /package/pipeline/commands/multi-agent/{status.md → status/SKILL.md} +0 -0
- /package/pipeline/commands/multi-agent/{test.md → test/SKILL.md} +0 -0
- /package/pipeline/commands/multi-agent/{update.md → update/SKILL.md} +0 -0
- /package/pipeline/{commands/multi-agent → multi-agent-refs}/_input-parser.md +0 -0
- /package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/analysis-template.md +0 -0
- /package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/audit-guide.md +0 -0
- /package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/backend-guide.md +0 -0
- /package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/channels/confluence.md +0 -0
- /package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/channels/issue-comment.md +0 -0
- /package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/channels/pr-review-actions.md +0 -0
- /package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/channels/pr.md +0 -0
- /package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/conventions-defaults.md +0 -0
- /package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/features/autopilot-circuit-breaker.md +0 -0
- /package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/features/dev-critic.md +0 -0
- /package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/features/external-context-injection.md +0 -0
- /package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/features/model-fallback.md +0 -0
- /package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/features/plan-todos.md +0 -0
- /package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/features/prior-fix-detection.md +0 -0
- /package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/features/repo-map.md +0 -0
- /package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/features/shadow-git.md +0 -0
- /package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/frontend-guide.md +0 -0
- /package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/knowledge.md +0 -0
- /package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/multi-repo-integration-build.md +0 -0
- /package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/picker-contract.md +0 -0
- /package/pipeline/{commands/multi-agent/refs → multi-agent-refs}/rules.md +0 -0
|
@@ -1,1298 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Phase 1 Data Gathering Script
|
|
4
|
-
Replaces the LLM subagent with deterministic Python calls.
|
|
5
|
-
Expected runtime: ~5-15 seconds (vs ~20 minutes with subagent).
|
|
6
|
-
|
|
7
|
-
Usage:
|
|
8
|
-
python3 phase1-gather.py \
|
|
9
|
-
--file-key "ps4xrJzhn7WNbfkZs895Bi" \
|
|
10
|
-
--node-id "9047:349" \
|
|
11
|
-
--artifact-path "path/to/ComponentName/" \
|
|
12
|
-
--component-name "AvatarPassengers" \
|
|
13
|
-
[--figma-token TOKEN] \
|
|
14
|
-
[--mcp-token TOKEN] \
|
|
15
|
-
[--mcp-url "http://127.0.0.1:3845/mcp"]
|
|
16
|
-
|
|
17
|
-
Exit 0 = success (prints JSON summary to stdout)
|
|
18
|
-
Exit 1 = failure (prints JSON error to stdout)
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
import argparse
|
|
22
|
-
import concurrent.futures
|
|
23
|
-
import http.client
|
|
24
|
-
import json
|
|
25
|
-
import os
|
|
26
|
-
import re
|
|
27
|
-
import ssl
|
|
28
|
-
import subprocess
|
|
29
|
-
import sys
|
|
30
|
-
import time
|
|
31
|
-
from datetime import datetime
|
|
32
|
-
from pathlib import Path
|
|
33
|
-
from urllib.error import HTTPError, URLError
|
|
34
|
-
from urllib.parse import urlparse
|
|
35
|
-
from urllib.request import Request, urlopen
|
|
36
|
-
|
|
37
|
-
# ─── Globals ─────────────────────────────────────────────────────────────────
|
|
38
|
-
MCP_MODE = None # "remote" or "local"
|
|
39
|
-
MCP_URL = None # local MCP URL (http://127.0.0.1:3845/mcp)
|
|
40
|
-
MCP_TOKEN = None # OAuth token for remote MCP
|
|
41
|
-
SESSION_ID = None # local MCP session ID
|
|
42
|
-
FIGMA_TOKEN = None
|
|
43
|
-
FILE_KEY = None
|
|
44
|
-
NODE_ID = None
|
|
45
|
-
COMPONENT_NAME = None
|
|
46
|
-
ARTIFACT_PATH = None
|
|
47
|
-
|
|
48
|
-
SUBLAYER_CAP = 20 # max sublayers to fetch for get_design_context
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def log(msg):
|
|
52
|
-
"""Log to stderr so stdout stays clean for JSON output."""
|
|
53
|
-
print(f"[phase1] {msg}", file=sys.stderr)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def halt(gate, reason, completed_gates, artifacts, user_action=""):
|
|
57
|
-
"""Print HALT JSON and exit 1."""
|
|
58
|
-
print(json.dumps({
|
|
59
|
-
"status": "HALT",
|
|
60
|
-
"phase": "phase-1",
|
|
61
|
-
"gate": gate,
|
|
62
|
-
"reason": reason,
|
|
63
|
-
"completed_gates": completed_gates,
|
|
64
|
-
"artifacts_created": artifacts,
|
|
65
|
-
"user_action_required": user_action,
|
|
66
|
-
}, indent=2))
|
|
67
|
-
sys.exit(1)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
# ─── HTTP Helpers ────────────────────────────────────────────────────────────
|
|
71
|
-
|
|
72
|
-
def figma_get(endpoint):
|
|
73
|
-
"""GET from Figma REST API. Returns parsed JSON."""
|
|
74
|
-
url = f"https://api.figma.com/v1/{endpoint}"
|
|
75
|
-
req = Request(url, headers={"X-Figma-Token": FIGMA_TOKEN})
|
|
76
|
-
with urlopen(req, timeout=30) as resp:
|
|
77
|
-
return json.loads(resp.read().decode())
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def download_file(url, dest):
|
|
81
|
-
"""Download a URL to a local file path. Returns True on success."""
|
|
82
|
-
try:
|
|
83
|
-
req = Request(url)
|
|
84
|
-
with urlopen(req, timeout=60) as resp:
|
|
85
|
-
Path(dest).parent.mkdir(parents=True, exist_ok=True)
|
|
86
|
-
with open(dest, "wb") as f:
|
|
87
|
-
f.write(resp.read())
|
|
88
|
-
return True
|
|
89
|
-
except (URLError, HTTPError) as exc:
|
|
90
|
-
log(f"Download failed ({dest}): {exc}")
|
|
91
|
-
return False
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
# ─── MCP Token Resolution ────────────────────────────────────────────────────
|
|
95
|
-
|
|
96
|
-
def get_mcp_token(cli_token=None):
|
|
97
|
-
"""Resolve MCP OAuth token: CLI arg > Keychain FIGMA_MCP_TOKEN > Claude Code credentials."""
|
|
98
|
-
if cli_token:
|
|
99
|
-
return cli_token
|
|
100
|
-
# Keychain: dedicated entry
|
|
101
|
-
try:
|
|
102
|
-
token = subprocess.check_output(
|
|
103
|
-
["security", "find-generic-password", "-a", os.environ["USER"],
|
|
104
|
-
"-s", "FIGMA_MCP_TOKEN", "-w"],
|
|
105
|
-
stderr=subprocess.DEVNULL,
|
|
106
|
-
).decode().strip()
|
|
107
|
-
if token:
|
|
108
|
-
log("MCP token from Keychain: FIGMA_MCP_TOKEN")
|
|
109
|
-
return token
|
|
110
|
-
except (subprocess.CalledProcessError, KeyError):
|
|
111
|
-
pass
|
|
112
|
-
# Keychain: extract from Claude Code credentials
|
|
113
|
-
try:
|
|
114
|
-
raw = subprocess.check_output(
|
|
115
|
-
["security", "find-generic-password", "-s", "Claude Code-credentials",
|
|
116
|
-
"-a", os.environ["USER"], "-w"],
|
|
117
|
-
stderr=subprocess.DEVNULL,
|
|
118
|
-
).decode()
|
|
119
|
-
match = re.search(
|
|
120
|
-
r'"figma-remote\|[^"]+":\{[^}]*"accessToken":"([^"]+)"', raw
|
|
121
|
-
)
|
|
122
|
-
if match:
|
|
123
|
-
log("MCP token extracted from Claude Code credentials")
|
|
124
|
-
return match.group(1)
|
|
125
|
-
except (subprocess.CalledProcessError, KeyError):
|
|
126
|
-
pass
|
|
127
|
-
return None
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def _try_refresh_mcp_token():
|
|
131
|
-
"""Attempt to refresh MCP token using stored refresh_token and fetch-mcp-token.py."""
|
|
132
|
-
global MCP_TOKEN
|
|
133
|
-
script = Path(__file__).parent.parent.parent / "figma-setup" / "scripts" / "fetch-mcp-token.py"
|
|
134
|
-
if not script.exists():
|
|
135
|
-
# Try relative to repo root
|
|
136
|
-
script = Path(__file__).resolve()
|
|
137
|
-
for _ in range(10):
|
|
138
|
-
script = script.parent
|
|
139
|
-
candidate = script / ".instructions" / "figma" / "figma-setup" / "scripts" / "fetch-mcp-token.py"
|
|
140
|
-
if candidate.exists():
|
|
141
|
-
script = candidate
|
|
142
|
-
break
|
|
143
|
-
else:
|
|
144
|
-
return False
|
|
145
|
-
try:
|
|
146
|
-
result = subprocess.run(
|
|
147
|
-
[sys.executable, str(script), "--verify", "--save-keychain", "--quiet"],
|
|
148
|
-
capture_output=True, text=True, timeout=30,
|
|
149
|
-
)
|
|
150
|
-
if result.returncode == 0 and result.stdout.strip():
|
|
151
|
-
MCP_TOKEN = result.stdout.strip()
|
|
152
|
-
log("MCP token auto-refreshed via fetch-mcp-token.py")
|
|
153
|
-
return True
|
|
154
|
-
else:
|
|
155
|
-
log(f"MCP token refresh failed (exit {result.returncode})")
|
|
156
|
-
except Exception as exc:
|
|
157
|
-
log(f"MCP token refresh error: {exc}")
|
|
158
|
-
return False
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
# ─── MCP Helpers ─────────────────────────────────────────────────────────────
|
|
162
|
-
|
|
163
|
-
def _parse_sse_json(raw):
|
|
164
|
-
"""Parse JSON from SSE response. Handles multiline data payloads."""
|
|
165
|
-
# Find the "data: " prefix
|
|
166
|
-
data_start = raw.find("\ndata: ")
|
|
167
|
-
if data_start == -1:
|
|
168
|
-
data_start = raw.find("data: ")
|
|
169
|
-
if data_start == -1:
|
|
170
|
-
return None
|
|
171
|
-
|
|
172
|
-
json_str = raw[raw.index("data: ", data_start) + 6:].strip()
|
|
173
|
-
try:
|
|
174
|
-
return json.loads(json_str)
|
|
175
|
-
except json.JSONDecodeError:
|
|
176
|
-
# JSON may have trailing SSE noise - find balanced braces
|
|
177
|
-
depth = 0
|
|
178
|
-
end = 0
|
|
179
|
-
for i, ch in enumerate(json_str):
|
|
180
|
-
if ch == "{":
|
|
181
|
-
depth += 1
|
|
182
|
-
elif ch == "}":
|
|
183
|
-
depth -= 1
|
|
184
|
-
if depth == 0:
|
|
185
|
-
end = i + 1
|
|
186
|
-
break
|
|
187
|
-
if end > 0:
|
|
188
|
-
try:
|
|
189
|
-
return json.loads(json_str[:end])
|
|
190
|
-
except json.JSONDecodeError:
|
|
191
|
-
pass
|
|
192
|
-
return None
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
def _mcp_post_local(body_dict):
|
|
196
|
-
"""POST to local MCP (HTTP, session-based)."""
|
|
197
|
-
parsed = urlparse(MCP_URL)
|
|
198
|
-
conn = http.client.HTTPConnection(parsed.hostname, parsed.port, timeout=120)
|
|
199
|
-
headers = {
|
|
200
|
-
"Content-Type": "application/json",
|
|
201
|
-
"Accept": "application/json, text/event-stream",
|
|
202
|
-
}
|
|
203
|
-
if SESSION_ID:
|
|
204
|
-
headers["mcp-session-id"] = SESSION_ID
|
|
205
|
-
|
|
206
|
-
conn.request("POST", parsed.path, body=json.dumps(body_dict), headers=headers)
|
|
207
|
-
resp = conn.getresponse()
|
|
208
|
-
session = resp.getheader("mcp-session-id")
|
|
209
|
-
raw = resp.read().decode()
|
|
210
|
-
conn.close()
|
|
211
|
-
|
|
212
|
-
result_json = _parse_sse_json(raw)
|
|
213
|
-
return result_json, session
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
def _mcp_post_remote(body_dict):
|
|
217
|
-
"""POST to remote MCP (HTTPS, Bearer auth, stateless)."""
|
|
218
|
-
conn = http.client.HTTPSConnection("mcp.figma.com", timeout=120)
|
|
219
|
-
headers = {
|
|
220
|
-
"Content-Type": "application/json",
|
|
221
|
-
"Accept": "application/json, text/event-stream",
|
|
222
|
-
"Authorization": f"Bearer {MCP_TOKEN}",
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
conn.request("POST", "/mcp", body=json.dumps(body_dict), headers=headers)
|
|
226
|
-
resp = conn.getresponse()
|
|
227
|
-
raw = resp.read().decode()
|
|
228
|
-
conn.close()
|
|
229
|
-
|
|
230
|
-
if resp.status != 200:
|
|
231
|
-
raise RuntimeError(f"Remote MCP HTTP {resp.status}: {raw[:200]}")
|
|
232
|
-
|
|
233
|
-
result_json = _parse_sse_json(raw)
|
|
234
|
-
return result_json, None
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
def _mcp_post(body_dict):
|
|
238
|
-
"""Route to remote or local MCP based on MCP_MODE."""
|
|
239
|
-
if MCP_MODE == "remote":
|
|
240
|
-
return _mcp_post_remote(body_dict)
|
|
241
|
-
else:
|
|
242
|
-
return _mcp_post_local(body_dict)
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
def mcp_init():
|
|
246
|
-
"""Initialize MCP connection. Tries remote first, falls back to local."""
|
|
247
|
-
global MCP_MODE, MCP_TOKEN, SESSION_ID
|
|
248
|
-
|
|
249
|
-
init_body = {
|
|
250
|
-
"jsonrpc": "2.0",
|
|
251
|
-
"method": "initialize",
|
|
252
|
-
"params": {
|
|
253
|
-
"capabilities": {},
|
|
254
|
-
"clientInfo": {"name": "phase1-gather", "version": "1.0.0"},
|
|
255
|
-
"protocolVersion": "2025-03-26",
|
|
256
|
-
},
|
|
257
|
-
"id": 1,
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
# Try remote first
|
|
261
|
-
if MCP_TOKEN:
|
|
262
|
-
try:
|
|
263
|
-
log("MCP: trying remote (mcp.figma.com)...")
|
|
264
|
-
result, _ = _mcp_post_remote(init_body)
|
|
265
|
-
if result:
|
|
266
|
-
MCP_MODE = "remote"
|
|
267
|
-
log("MCP: remote connected")
|
|
268
|
-
return
|
|
269
|
-
except Exception as exc:
|
|
270
|
-
log(f"MCP: remote failed ({exc}), attempting token refresh...")
|
|
271
|
-
# Try auto-refresh before falling back to local
|
|
272
|
-
if _try_refresh_mcp_token():
|
|
273
|
-
try:
|
|
274
|
-
result, _ = _mcp_post_remote(init_body)
|
|
275
|
-
if result:
|
|
276
|
-
MCP_MODE = "remote"
|
|
277
|
-
log("MCP: remote connected (after token refresh)")
|
|
278
|
-
return
|
|
279
|
-
except Exception as exc2:
|
|
280
|
-
log(f"MCP: remote still failed after refresh ({exc2}), trying local...")
|
|
281
|
-
|
|
282
|
-
# Fall back to local
|
|
283
|
-
try:
|
|
284
|
-
log(f"MCP: trying local ({MCP_URL})...")
|
|
285
|
-
result, session = _mcp_post_local(init_body)
|
|
286
|
-
if session:
|
|
287
|
-
SESSION_ID = session
|
|
288
|
-
MCP_MODE = "local"
|
|
289
|
-
log(f"MCP: local connected (session: {SESSION_ID[:12]}...)")
|
|
290
|
-
return
|
|
291
|
-
except Exception as exc:
|
|
292
|
-
raise RuntimeError(f"MCP init failed - remote and local both unavailable. Local error: {exc}")
|
|
293
|
-
|
|
294
|
-
raise RuntimeError("MCP init failed - no session from local, no response from remote")
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
_MCP_CALL_ID = 10 # auto-increment per call
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
def mcp_call(tool_name, arguments):
|
|
301
|
-
"""Call an MCP tool. Returns the text content from the result."""
|
|
302
|
-
global _MCP_CALL_ID
|
|
303
|
-
_MCP_CALL_ID += 1
|
|
304
|
-
|
|
305
|
-
# Remote MCP requires fileKey in every call
|
|
306
|
-
if MCP_MODE == "remote" and "fileKey" not in arguments:
|
|
307
|
-
arguments = {**arguments, "fileKey": FILE_KEY}
|
|
308
|
-
|
|
309
|
-
body = {
|
|
310
|
-
"jsonrpc": "2.0",
|
|
311
|
-
"method": "tools/call",
|
|
312
|
-
"params": {"name": tool_name, "arguments": arguments},
|
|
313
|
-
"id": _MCP_CALL_ID,
|
|
314
|
-
}
|
|
315
|
-
result_json, _ = _mcp_post(body)
|
|
316
|
-
if not result_json:
|
|
317
|
-
raise RuntimeError(f"MCP {tool_name}: empty response")
|
|
318
|
-
if "error" in result_json:
|
|
319
|
-
raise RuntimeError(f"MCP {tool_name}: {result_json['error']}")
|
|
320
|
-
|
|
321
|
-
# Extract text from content array
|
|
322
|
-
content_items = (result_json.get("result") or {}).get("content", [])
|
|
323
|
-
texts = [item.get("text", "") for item in content_items if item.get("type") == "text"]
|
|
324
|
-
return "\n".join(texts)
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
# ─── Batch 1: Parallel Fetchers ─────────────────────────────────────────────
|
|
328
|
-
|
|
329
|
-
def fetch_screenshot_url():
|
|
330
|
-
"""Figma REST -> screenshot image URL."""
|
|
331
|
-
log("REST: fetching screenshot URL...")
|
|
332
|
-
data = figma_get(f"images/{FILE_KEY}?ids={NODE_ID}&format=png&scale=2")
|
|
333
|
-
url = (data.get("images") or {}).get(NODE_ID)
|
|
334
|
-
if not url:
|
|
335
|
-
raise RuntimeError("No screenshot URL returned from images API")
|
|
336
|
-
log("REST: screenshot URL obtained")
|
|
337
|
-
return url
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
def fetch_node_data():
|
|
341
|
-
"""Figma REST -> full node document (for component properties + variants)."""
|
|
342
|
-
log("REST: fetching node data...")
|
|
343
|
-
data = figma_get(f"files/{FILE_KEY}/nodes?ids={NODE_ID}")
|
|
344
|
-
node = (data.get("nodes") or {}).get(NODE_ID, {}).get("document")
|
|
345
|
-
if not node:
|
|
346
|
-
raise RuntimeError("No document in nodes response")
|
|
347
|
-
log(f"REST: node type={node.get('type')}")
|
|
348
|
-
return node
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
def fetch_metadata():
|
|
352
|
-
log("MCP: get_metadata...")
|
|
353
|
-
text = mcp_call("get_metadata", {
|
|
354
|
-
"nodeId": NODE_ID,
|
|
355
|
-
"clientLanguages": "swift",
|
|
356
|
-
"clientFrameworks": "swiftui",
|
|
357
|
-
})
|
|
358
|
-
log(f"MCP: metadata {len(text)} chars")
|
|
359
|
-
return text
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
def _is_too_large_response(text):
|
|
363
|
-
"""Check if MCP returned XML metadata instead of code (design too large)."""
|
|
364
|
-
return "too large" in text.lower() and "<frame" in text[:300]
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
def _parse_sublayer_ids(text):
|
|
368
|
-
"""Extract sublayer node IDs from the XML metadata response."""
|
|
369
|
-
# Match <symbol id="X:Y" ...> or <frame id="X:Y" ...> (children only)
|
|
370
|
-
ids = re.findall(r'<(?:symbol|frame)\s+id="([^"]+)"', text)
|
|
371
|
-
# Skip the root frame (first match is the container itself)
|
|
372
|
-
root_match = re.search(r'<frame\s+id="([^"]+)"', text)
|
|
373
|
-
root_id = root_match.group(1) if root_match else None
|
|
374
|
-
return [nid for nid in ids if nid != root_id]
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
def _build_design_context_args(node_id, assets_dir=None):
|
|
378
|
-
"""Build arguments for get_design_context. Skips dirForAssetWrites on remote."""
|
|
379
|
-
args = {
|
|
380
|
-
"nodeId": node_id,
|
|
381
|
-
"clientLanguages": "swift",
|
|
382
|
-
"clientFrameworks": "swiftui",
|
|
383
|
-
# Disable Code Connect in design context - when Code Connect is enabled,
|
|
384
|
-
# the returned snippets override the original Tailwind reference code we
|
|
385
|
-
# need for layout/style extraction. We fetch Code Connect separately
|
|
386
|
-
# via get_code_connect_map.
|
|
387
|
-
"disableCodeConnect": True,
|
|
388
|
-
}
|
|
389
|
-
if assets_dir and MCP_MODE == "local":
|
|
390
|
-
args["dirForAssetWrites"] = assets_dir
|
|
391
|
-
return args
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
def _fetch_sublayer_context(node_id, assets_dir, depth=0):
|
|
395
|
-
"""Fetch design context for a single sublayer. Recurses if also too large."""
|
|
396
|
-
if depth > 3:
|
|
397
|
-
log(f" sublayer {node_id}: max recursion depth, returning raw")
|
|
398
|
-
return {"nodeId": node_id, "error": "max_recursion_depth"}
|
|
399
|
-
|
|
400
|
-
text = mcp_call("get_design_context", _build_design_context_args(node_id, assets_dir))
|
|
401
|
-
|
|
402
|
-
if _is_too_large_response(text):
|
|
403
|
-
log(f" sublayer {node_id}: also too large, splitting further (depth={depth+1})")
|
|
404
|
-
child_ids = _parse_sublayer_ids(text)
|
|
405
|
-
if not child_ids:
|
|
406
|
-
return {"nodeId": node_id, "response": text}
|
|
407
|
-
child_responses = {}
|
|
408
|
-
for cid in child_ids:
|
|
409
|
-
child_responses[cid] = _fetch_sublayer_context(cid, assets_dir, depth + 1)
|
|
410
|
-
return {"nodeId": node_id, "sublayers": True, "responses": child_responses}
|
|
411
|
-
|
|
412
|
-
log(f" sublayer {node_id}: {len(text)} chars")
|
|
413
|
-
return {"nodeId": node_id, "response": text}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
def _fetch_sublayers_parallel(sublayer_ids, assets_dir, max_workers=10):
|
|
417
|
-
"""Fetch all sublayer design contexts in parallel using a thread pool."""
|
|
418
|
-
results = {}
|
|
419
|
-
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as pool:
|
|
420
|
-
future_to_sid = {
|
|
421
|
-
pool.submit(_fetch_sublayer_context, sid, assets_dir): sid
|
|
422
|
-
for sid in sublayer_ids
|
|
423
|
-
}
|
|
424
|
-
for future in concurrent.futures.as_completed(future_to_sid):
|
|
425
|
-
sid = future_to_sid[future]
|
|
426
|
-
try:
|
|
427
|
-
results[sid] = future.result()
|
|
428
|
-
except Exception as e:
|
|
429
|
-
log(f" sublayer {sid}: ERROR {e}")
|
|
430
|
-
results[sid] = {"nodeId": sid, "error": str(e)}
|
|
431
|
-
return results
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
def fetch_design_context():
|
|
435
|
-
assets_dir = str((Path(ARTIFACT_PATH) / "pass1" / "assets").resolve())
|
|
436
|
-
Path(assets_dir).mkdir(parents=True, exist_ok=True)
|
|
437
|
-
|
|
438
|
-
log("MCP: get_design_context...")
|
|
439
|
-
text = mcp_call("get_design_context", _build_design_context_args(NODE_ID, assets_dir))
|
|
440
|
-
log(f"MCP: design_context {len(text)} chars")
|
|
441
|
-
|
|
442
|
-
if _is_too_large_response(text):
|
|
443
|
-
sublayer_ids = _parse_sublayer_ids(text)
|
|
444
|
-
|
|
445
|
-
# Cap check: if too many sublayers, defer to manual resolution
|
|
446
|
-
if len(sublayer_ids) > SUBLAYER_CAP:
|
|
447
|
-
log(f"MCP: {len(sublayer_ids)} sublayers exceeds cap ({SUBLAYER_CAP}) - deferring")
|
|
448
|
-
return {
|
|
449
|
-
"__deferred": True,
|
|
450
|
-
"__sublayer_count": len(sublayer_ids),
|
|
451
|
-
"__sublayer_ids": sublayer_ids,
|
|
452
|
-
"__metadata_xml": text,
|
|
453
|
-
"__cap": SUBLAYER_CAP,
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
log(f"MCP: design too large - fetching {len(sublayer_ids)} sublayers in parallel")
|
|
457
|
-
if sublayer_ids:
|
|
458
|
-
sublayer_responses = _fetch_sublayers_parallel(sublayer_ids, assets_dir)
|
|
459
|
-
total = sum(
|
|
460
|
-
len(r.get("response", "")) for r in sublayer_responses.values()
|
|
461
|
-
if isinstance(r, dict) and "response" in r
|
|
462
|
-
)
|
|
463
|
-
log(f"MCP: sublayer design_context total {total} chars across {len(sublayer_ids)} sublayers")
|
|
464
|
-
return {
|
|
465
|
-
"__sublayers": True,
|
|
466
|
-
"__sublayer_ids": sublayer_ids,
|
|
467
|
-
"__metadata_xml": text,
|
|
468
|
-
"__responses": sublayer_responses,
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
return text
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
def fetch_variable_defs():
|
|
475
|
-
log("MCP: get_variable_defs...")
|
|
476
|
-
text = mcp_call("get_variable_defs", {
|
|
477
|
-
"nodeId": NODE_ID,
|
|
478
|
-
"clientLanguages": "swift",
|
|
479
|
-
"clientFrameworks": "swiftui",
|
|
480
|
-
})
|
|
481
|
-
log(f"MCP: variable_defs {len(text)} chars")
|
|
482
|
-
return text
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
def fetch_code_connect_map():
|
|
486
|
-
log("MCP: get_code_connect_map...")
|
|
487
|
-
# Filter by SwiftUI label - without this, MCP returns all platforms
|
|
488
|
-
# (e.g. Compose) and we get Android snippets instead of iOS ones.
|
|
489
|
-
text = mcp_call("get_code_connect_map", {
|
|
490
|
-
"nodeId": NODE_ID,
|
|
491
|
-
"codeConnectLabel": "SwiftUI",
|
|
492
|
-
})
|
|
493
|
-
log(f"MCP: code_connect_map {len(text)} chars")
|
|
494
|
-
return text
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
# ─── Batch 2: Downloads & Post-Processing ───────────────────────────────────
|
|
498
|
-
|
|
499
|
-
def download_screenshot(screenshot_url):
|
|
500
|
-
dest = str(Path(ARTIFACT_PATH) / "pass1" / "screenshot.png")
|
|
501
|
-
log("Downloading screenshot PNG...")
|
|
502
|
-
if not download_file(screenshot_url, dest):
|
|
503
|
-
raise RuntimeError("Screenshot PNG download failed")
|
|
504
|
-
log("Screenshot saved")
|
|
505
|
-
return dest
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
def download_variant_screenshots(node_data):
|
|
509
|
-
"""Batch-download variant screenshots. Returns list of variant info dicts."""
|
|
510
|
-
if node_data.get("type") != "COMPONENT_SET":
|
|
511
|
-
log("Not COMPONENT_SET -- skipping variants")
|
|
512
|
-
return []
|
|
513
|
-
|
|
514
|
-
children = [c for c in node_data.get("children", []) if c.get("type") == "COMPONENT"]
|
|
515
|
-
if not children:
|
|
516
|
-
log("No COMPONENT children found")
|
|
517
|
-
return []
|
|
518
|
-
|
|
519
|
-
log(f"Found {len(children)} variants -- batch fetching image URLs...")
|
|
520
|
-
|
|
521
|
-
# Single batch API call for all variant IDs
|
|
522
|
-
all_ids = ",".join(c["id"] for c in children)
|
|
523
|
-
data = figma_get(f"images/{FILE_KEY}?ids={all_ids}&format=png&scale=2")
|
|
524
|
-
image_urls = data.get("images") or {}
|
|
525
|
-
|
|
526
|
-
screenshots_dir = Path(ARTIFACT_PATH) / "pass1" / "screenshots"
|
|
527
|
-
screenshots_dir.mkdir(parents=True, exist_ok=True)
|
|
528
|
-
|
|
529
|
-
# Build download list
|
|
530
|
-
downloads = []
|
|
531
|
-
for child in children:
|
|
532
|
-
url = image_urls.get(child["id"])
|
|
533
|
-
if not url:
|
|
534
|
-
continue
|
|
535
|
-
name = child.get("name", child["id"])
|
|
536
|
-
# Single-property: "State=Success" -> "Success"
|
|
537
|
-
# Multi-property: "Size=Large, Loyalty Type=Classic" -> "Size=Large_LoyaltyType=Classic"
|
|
538
|
-
parts = [p.strip() for p in name.split(",")]
|
|
539
|
-
if len(parts) == 1 and "=" in parts[0]:
|
|
540
|
-
safe = parts[0].split("=", 1)[1].strip()
|
|
541
|
-
else:
|
|
542
|
-
# "Size=Large, Loyalty Type=Classic Plus" -> "Size=Large_LoyaltyType=Classic_Plus"
|
|
543
|
-
formatted = []
|
|
544
|
-
for p in parts:
|
|
545
|
-
if "=" in p:
|
|
546
|
-
k, v = p.split("=", 1)
|
|
547
|
-
k = k.strip().replace(" ", "") # remove spaces in key
|
|
548
|
-
v = v.strip().replace(" ", "_") # spaces -> underscores in value
|
|
549
|
-
formatted.append(f"{k}={v}")
|
|
550
|
-
else:
|
|
551
|
-
formatted.append(p.replace(" ", ""))
|
|
552
|
-
safe = "_".join(formatted)
|
|
553
|
-
safe = re.sub(r"[/\\:]", "_", safe)
|
|
554
|
-
downloads.append({
|
|
555
|
-
"id": child["id"],
|
|
556
|
-
"name": name,
|
|
557
|
-
"safe_name": safe,
|
|
558
|
-
"file": f"{safe}.png",
|
|
559
|
-
"url": url,
|
|
560
|
-
"dest": str(screenshots_dir / f"{safe}.png"),
|
|
561
|
-
})
|
|
562
|
-
|
|
563
|
-
# Parallel download
|
|
564
|
-
results = []
|
|
565
|
-
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as pool:
|
|
566
|
-
futs = {pool.submit(download_file, d["url"], d["dest"]): d for d in downloads}
|
|
567
|
-
for fut in concurrent.futures.as_completed(futs):
|
|
568
|
-
info = futs[fut]
|
|
569
|
-
if fut.result():
|
|
570
|
-
results.append(info)
|
|
571
|
-
log(f" variant: {info['safe_name']}")
|
|
572
|
-
|
|
573
|
-
# Write variants.md
|
|
574
|
-
results.sort(key=lambda x: x["name"])
|
|
575
|
-
lines = [
|
|
576
|
-
"# Variant Screenshots\n",
|
|
577
|
-
"| Variant | Node ID | File |",
|
|
578
|
-
"|---------|---------|------|",
|
|
579
|
-
]
|
|
580
|
-
for r in results:
|
|
581
|
-
lines.append(f"| {r['name']} | {r['id']} | {r['file']} |")
|
|
582
|
-
lines += ["", "## Usage", "- Confluence documentation", "- Snapshot testing reference", ""]
|
|
583
|
-
(screenshots_dir / "variants.md").write_text("\n".join(lines))
|
|
584
|
-
|
|
585
|
-
log(f"Downloaded {len(results)}/{len(downloads)} variant screenshots")
|
|
586
|
-
return results
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
def sanitize_svgs():
|
|
590
|
-
"""Remove Xcode-incompatible attributes from downloaded SVGs."""
|
|
591
|
-
assets_dir = Path(ARTIFACT_PATH) / "pass1" / "assets"
|
|
592
|
-
if not assets_dir.exists():
|
|
593
|
-
return 0
|
|
594
|
-
|
|
595
|
-
count = 0
|
|
596
|
-
for svg in assets_dir.glob("*.svg"):
|
|
597
|
-
content = svg.read_text()
|
|
598
|
-
original = content
|
|
599
|
-
content = re.sub(r' width="100%"', "", content)
|
|
600
|
-
content = re.sub(r' height="100%"', "", content)
|
|
601
|
-
content = re.sub(r' preserveAspectRatio="none"', "", content)
|
|
602
|
-
content = re.sub(r' style="display: block;"', "", content)
|
|
603
|
-
content = re.sub(r' overflow="visible"', "", content)
|
|
604
|
-
content = re.sub(r"var\(--fill-0, ([^)]*)\)", r"\1", content)
|
|
605
|
-
content = re.sub(r"var\(--stroke-0, ([^)]*)\)", r"\1", content)
|
|
606
|
-
if content != original:
|
|
607
|
-
svg.write_text(content)
|
|
608
|
-
count += 1
|
|
609
|
-
if count:
|
|
610
|
-
log(f"Sanitized {count} SVGs")
|
|
611
|
-
return count
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
# ─── File Writers ────────────────────────────────────────────────────────────
|
|
615
|
-
|
|
616
|
-
def save_mcp_text(filename, text, try_json=True):
|
|
617
|
-
"""Write MCP text output to a file in pass1/."""
|
|
618
|
-
dest = Path(ARTIFACT_PATH) / "pass1" / filename
|
|
619
|
-
if try_json:
|
|
620
|
-
try:
|
|
621
|
-
parsed = json.loads(text)
|
|
622
|
-
dest.write_text(json.dumps(parsed, indent=2, ensure_ascii=False) + "\n")
|
|
623
|
-
return
|
|
624
|
-
except (json.JSONDecodeError, TypeError):
|
|
625
|
-
pass
|
|
626
|
-
dest.write_text(text)
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
def write_design_context_deferred(data):
|
|
630
|
-
"""Write deferred artifact when sublayer count exceeds cap."""
|
|
631
|
-
pass1 = Path(ARTIFACT_PATH) / "pass1"
|
|
632
|
-
|
|
633
|
-
# Write design_context_response.json with metadata XML only
|
|
634
|
-
envelope = {
|
|
635
|
-
"rootNodeId": NODE_ID,
|
|
636
|
-
"source": MCP_MODE,
|
|
637
|
-
"deferred": True,
|
|
638
|
-
"sublayers": True,
|
|
639
|
-
"sublayerCount": data["__sublayer_count"],
|
|
640
|
-
"cap": data["__cap"],
|
|
641
|
-
"sublayerNodeIds": data["__sublayer_ids"],
|
|
642
|
-
"metadataXml": data["__metadata_xml"],
|
|
643
|
-
"responses": {},
|
|
644
|
-
}
|
|
645
|
-
(pass1 / "design_context_response.json").write_text(
|
|
646
|
-
json.dumps(envelope, indent=2, ensure_ascii=False) + "\n"
|
|
647
|
-
)
|
|
648
|
-
|
|
649
|
-
# Write design_context_deferred.json - resolution instructions
|
|
650
|
-
cmd_template = (
|
|
651
|
-
f"python3 .instructions/figma/figma-to-swiftui/scripts/phase1-gather.py "
|
|
652
|
-
f"--fetch-deferred "
|
|
653
|
-
f"--file-key {FILE_KEY} "
|
|
654
|
-
f"--node-id {NODE_ID} "
|
|
655
|
-
f"--artifact-path {ARTIFACT_PATH} "
|
|
656
|
-
f"--component-name {COMPONENT_NAME} "
|
|
657
|
-
f"--sublayer-ids \"{{comma_separated_ids}}\""
|
|
658
|
-
)
|
|
659
|
-
deferred = {
|
|
660
|
-
"status": "deferred",
|
|
661
|
-
"reason": "sublayer_count_exceeds_cap",
|
|
662
|
-
"sublayerCount": data["__sublayer_count"],
|
|
663
|
-
"cap": data["__cap"],
|
|
664
|
-
"sublayerIds": data["__sublayer_ids"],
|
|
665
|
-
"fileKey": FILE_KEY,
|
|
666
|
-
"rootNodeId": NODE_ID,
|
|
667
|
-
"instruction": (
|
|
668
|
-
"Design context has too many sublayers to fetch automatically. "
|
|
669
|
-
"Use --fetch-deferred mode with selected sublayer IDs to resolve."
|
|
670
|
-
),
|
|
671
|
-
"commandTemplate": cmd_template,
|
|
672
|
-
}
|
|
673
|
-
(pass1 / "design_context_deferred.json").write_text(
|
|
674
|
-
json.dumps(deferred, indent=2, ensure_ascii=False) + "\n"
|
|
675
|
-
)
|
|
676
|
-
log(f"design_context_deferred.json written ({data['__sublayer_count']} sublayers)")
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
def write_design_context_file(data):
|
|
680
|
-
"""Save design context -- handles normal text, sublayer dict, and deferred."""
|
|
681
|
-
dest = Path(ARTIFACT_PATH) / "pass1" / "design_context_response.json"
|
|
682
|
-
|
|
683
|
-
# Deferred case: sublayer count exceeded cap
|
|
684
|
-
if isinstance(data, dict) and data.get("__deferred"):
|
|
685
|
-
write_design_context_deferred(data)
|
|
686
|
-
return
|
|
687
|
-
|
|
688
|
-
# Sublayer case: data is a dict with __sublayers=True
|
|
689
|
-
if isinstance(data, dict) and data.get("__sublayers"):
|
|
690
|
-
sublayer_ids = data["__sublayer_ids"]
|
|
691
|
-
responses = {}
|
|
692
|
-
for sid, resp in data["__responses"].items():
|
|
693
|
-
if isinstance(resp, dict) and "response" in resp:
|
|
694
|
-
responses[sid] = resp["response"]
|
|
695
|
-
else:
|
|
696
|
-
responses[sid] = resp # nested sublayer or error
|
|
697
|
-
envelope = {
|
|
698
|
-
"rootNodeId": NODE_ID,
|
|
699
|
-
"source": MCP_MODE,
|
|
700
|
-
"sublayers": True,
|
|
701
|
-
"sublayerNodeIds": sublayer_ids,
|
|
702
|
-
"responses": responses,
|
|
703
|
-
}
|
|
704
|
-
dest.write_text(json.dumps(envelope, indent=2, ensure_ascii=False) + "\n")
|
|
705
|
-
return
|
|
706
|
-
|
|
707
|
-
# Normal case: data is a text string
|
|
708
|
-
text = data
|
|
709
|
-
try:
|
|
710
|
-
parsed = json.loads(text)
|
|
711
|
-
dest.write_text(json.dumps(parsed, indent=2, ensure_ascii=False) + "\n")
|
|
712
|
-
except (json.JSONDecodeError, TypeError):
|
|
713
|
-
envelope = {
|
|
714
|
-
"rootNodeId": NODE_ID,
|
|
715
|
-
"source": MCP_MODE,
|
|
716
|
-
"sublayers": False,
|
|
717
|
-
"response": text,
|
|
718
|
-
}
|
|
719
|
-
dest.write_text(json.dumps(envelope, indent=2, ensure_ascii=False) + "\n")
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
def write_design_context_assets():
|
|
723
|
-
"""List downloaded assets in design_context_assets.txt."""
|
|
724
|
-
dest = Path(ARTIFACT_PATH) / "pass1" / "design_context_assets.txt"
|
|
725
|
-
assets_dir = Path(ARTIFACT_PATH) / "pass1" / "assets"
|
|
726
|
-
files = sorted(assets_dir.glob("*")) if assets_dir.exists() else []
|
|
727
|
-
files = [f for f in files if f.is_file()]
|
|
728
|
-
|
|
729
|
-
if files:
|
|
730
|
-
lines = [
|
|
731
|
-
"# Asset References from Design Context",
|
|
732
|
-
f"# {len(files)} assets downloaded by MCP",
|
|
733
|
-
"",
|
|
734
|
-
]
|
|
735
|
-
for f in files:
|
|
736
|
-
lines.append(f"{f.name} | local:{f}")
|
|
737
|
-
else:
|
|
738
|
-
lines = [
|
|
739
|
-
"# Asset References from Design Context",
|
|
740
|
-
"# No assets referenced in design context response.",
|
|
741
|
-
]
|
|
742
|
-
dest.write_text("\n".join(lines) + "\n")
|
|
743
|
-
return len(files)
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
def write_screenshot_response():
|
|
747
|
-
dest = Path(ARTIFACT_PATH) / "pass1" / "screenshot_response.md"
|
|
748
|
-
today = datetime.now().strftime("%Y-%m-%d")
|
|
749
|
-
dest.write_text(
|
|
750
|
-
f"# Screenshot Response\n\n"
|
|
751
|
-
f"## Screenshot File\n"
|
|
752
|
-
f"- File: screenshot.png\n"
|
|
753
|
-
f"- Node ID: {NODE_ID}\n"
|
|
754
|
-
f"- Source: local\n"
|
|
755
|
-
f"- Scale: 2x\n"
|
|
756
|
-
f"- Timestamp: {today}\n"
|
|
757
|
-
)
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
def generate_component_properties(node_data):
|
|
761
|
-
"""Build component_properties.json from the REST API node document."""
|
|
762
|
-
node_type = node_data.get("type", "UNKNOWN")
|
|
763
|
-
is_component = node_type in ("COMPONENT", "COMPONENT_SET")
|
|
764
|
-
prop_defs = node_data.get("componentPropertyDefinitions") or {}
|
|
765
|
-
|
|
766
|
-
variants_list = []
|
|
767
|
-
boolean_props = []
|
|
768
|
-
text_slots = []
|
|
769
|
-
nested_comps = []
|
|
770
|
-
cc_mappings = {}
|
|
771
|
-
|
|
772
|
-
for key, defn in prop_defs.items():
|
|
773
|
-
ptype = defn.get("type")
|
|
774
|
-
# Display name: "Text#6893:1" -> "Text", "\u21aa Left Icon#13152:11" -> "\u21aa Left Icon"
|
|
775
|
-
display = key.split("#")[0]
|
|
776
|
-
clean = display.lstrip("\u21aa ").strip()
|
|
777
|
-
|
|
778
|
-
if ptype == "VARIANT":
|
|
779
|
-
values = defn.get("variantOptions", [])
|
|
780
|
-
variants_list.append({"property": display, "values": values, "type": "VARIANT"})
|
|
781
|
-
if values:
|
|
782
|
-
pairs = ", ".join(
|
|
783
|
-
f'"{v}": .{v.lower().replace(" ", "")}' for v in values
|
|
784
|
-
)
|
|
785
|
-
cc_mappings[key] = f'@FigmaEnum("{display}", mapping: [{pairs}])'
|
|
786
|
-
|
|
787
|
-
elif ptype == "BOOLEAN":
|
|
788
|
-
boolean_props.append({
|
|
789
|
-
"property": clean,
|
|
790
|
-
"default": defn.get("defaultValue", False),
|
|
791
|
-
"figmaKey": key,
|
|
792
|
-
})
|
|
793
|
-
cc_mappings[key] = f'@FigmaBoolean("{clean}")'
|
|
794
|
-
|
|
795
|
-
elif ptype == "TEXT":
|
|
796
|
-
text_slots.append({
|
|
797
|
-
"name": clean,
|
|
798
|
-
"defaultValue": defn.get("defaultValue", ""),
|
|
799
|
-
"figmaKey": key,
|
|
800
|
-
})
|
|
801
|
-
cc_mappings[key] = f'@FigmaString("{clean}")'
|
|
802
|
-
|
|
803
|
-
elif ptype == "INSTANCE_SWAP":
|
|
804
|
-
nested_comps.append({
|
|
805
|
-
"name": clean,
|
|
806
|
-
"nodeId": defn.get("defaultValue", ""),
|
|
807
|
-
"figmaKey": key,
|
|
808
|
-
"type": "INSTANCE_SWAP",
|
|
809
|
-
})
|
|
810
|
-
cc_mappings[key] = f'@FigmaInstance("{display}")'
|
|
811
|
-
|
|
812
|
-
result = {
|
|
813
|
-
"componentName": COMPONENT_NAME,
|
|
814
|
-
"nodeId": NODE_ID,
|
|
815
|
-
"nodeType": node_type,
|
|
816
|
-
"isComponent": is_component,
|
|
817
|
-
"componentPropertyDefinitions": prop_defs,
|
|
818
|
-
"variants": variants_list,
|
|
819
|
-
"booleanProperties": boolean_props,
|
|
820
|
-
"textSlots": text_slots,
|
|
821
|
-
"nestedComponents": nested_comps,
|
|
822
|
-
"codeConnectStrategy": {
|
|
823
|
-
"isComponent": is_component,
|
|
824
|
-
"hasPropertyDefinitions": bool(prop_defs),
|
|
825
|
-
"mappings": cc_mappings,
|
|
826
|
-
},
|
|
827
|
-
}
|
|
828
|
-
dest = Path(ARTIFACT_PATH) / "pass1" / "component_properties.json"
|
|
829
|
-
dest.write_text(json.dumps(result, indent=2, ensure_ascii=False) + "\n")
|
|
830
|
-
log("component_properties.json written")
|
|
831
|
-
return result
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
def write_component_summary(comp_props, node_data):
|
|
835
|
-
"""Condensed summary (~100 lines) for downstream phases."""
|
|
836
|
-
prop_defs = comp_props.get("componentPropertyDefinitions") or {}
|
|
837
|
-
properties = []
|
|
838
|
-
for key, defn in prop_defs.items():
|
|
839
|
-
ptype = defn.get("type")
|
|
840
|
-
name = key.split("#")[0].lstrip("\u21aa ").strip()
|
|
841
|
-
prop = {"name": name, "type": ptype, "figmaKey": key, "default": defn.get("defaultValue")}
|
|
842
|
-
if ptype == "VARIANT":
|
|
843
|
-
prop["values"] = defn.get("variantOptions", [])
|
|
844
|
-
properties.append(prop)
|
|
845
|
-
|
|
846
|
-
children = node_data.get("children", [])
|
|
847
|
-
variant_count = len([c for c in children if c.get("type") == "COMPONENT"])
|
|
848
|
-
bbox = node_data.get("absoluteBoundingBox") or {}
|
|
849
|
-
|
|
850
|
-
assets_dir = Path(ARTIFACT_PATH) / "pass1" / "assets"
|
|
851
|
-
svgs = sorted(assets_dir.glob("*.svg")) if assets_dir.exists() else []
|
|
852
|
-
screenshots_dir = Path(ARTIFACT_PATH) / "pass1" / "screenshots"
|
|
853
|
-
pngs = sorted(screenshots_dir.glob("*.png")) if screenshots_dir.exists() else []
|
|
854
|
-
|
|
855
|
-
ntype = node_data.get("type", "UNKNOWN")
|
|
856
|
-
layer_line = (
|
|
857
|
-
f"{ntype} > COMPONENT({variant_count} variants)"
|
|
858
|
-
if variant_count
|
|
859
|
-
else ntype
|
|
860
|
-
)
|
|
861
|
-
|
|
862
|
-
summary = {
|
|
863
|
-
"componentName": COMPONENT_NAME,
|
|
864
|
-
"nodeId": NODE_ID,
|
|
865
|
-
"fileKey": FILE_KEY,
|
|
866
|
-
"nodeType": ntype,
|
|
867
|
-
"variantCount": variant_count,
|
|
868
|
-
"dimensions": {"width": bbox.get("width", 0), "height": bbox.get("height", 0)},
|
|
869
|
-
"properties": properties,
|
|
870
|
-
"layerSummary": [layer_line],
|
|
871
|
-
"variableBindingCount": 0,
|
|
872
|
-
"variableBindingSummary": [],
|
|
873
|
-
"assets": {
|
|
874
|
-
"svgCount": len(svgs),
|
|
875
|
-
"screenshotCount": len(pngs),
|
|
876
|
-
"fileList": [s.name for s in svgs],
|
|
877
|
-
},
|
|
878
|
-
"hasExistingCodeConnect": False,
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
dest = Path(ARTIFACT_PATH) / "pass1" / "component_summary.json"
|
|
882
|
-
dest.write_text(json.dumps(summary, indent=2, ensure_ascii=False) + "\n")
|
|
883
|
-
log("component_summary.json written")
|
|
884
|
-
return summary
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
def write_index(variant_results, asset_count):
|
|
888
|
-
today = datetime.now().strftime("%Y-%m-%d")
|
|
889
|
-
screenshots_dir = Path(ARTIFACT_PATH) / "pass1" / "screenshots"
|
|
890
|
-
var_count = len(list(screenshots_dir.glob("*.png"))) if screenshots_dir.exists() else 0
|
|
891
|
-
assets_dir = Path(ARTIFACT_PATH) / "pass1" / "assets"
|
|
892
|
-
svg_count = len(list(assets_dir.glob("*.svg"))) if assets_dir.exists() else 0
|
|
893
|
-
|
|
894
|
-
content = f"""# Pass 1 -- Data Gathering Output
|
|
895
|
-
|
|
896
|
-
Component: {COMPONENT_NAME}
|
|
897
|
-
Node ID: {NODE_ID}
|
|
898
|
-
File Key: {FILE_KEY}
|
|
899
|
-
Gathered: {today}
|
|
900
|
-
|
|
901
|
-
## Files
|
|
902
|
-
|
|
903
|
-
| File | Description |
|
|
904
|
-
|------|-------------|
|
|
905
|
-
| `pass1/screenshot.png` | Component screenshot (2x scale) |
|
|
906
|
-
| `pass1/screenshot_response.md` | Screenshot metadata |
|
|
907
|
-
| `pass1/screenshots/` | Variant screenshots ({var_count} variants) |
|
|
908
|
-
| `pass1/screenshots/variants.md` | Variant screenshot index |
|
|
909
|
-
| `pass1/metadata_response.xml` | Layer hierarchy (XML) |
|
|
910
|
-
| `pass1/design_context_response.json` | Generated code + structure |
|
|
911
|
-
| `pass1/design_context_assets.txt` | Asset download URLs ({svg_count} vectors) |
|
|
912
|
-
| `pass1/assets/` | Downloaded SVG assets (sanitized for Xcode) |
|
|
913
|
-
| `pass1/variable_defs_response.json` | Design tokens / variables |
|
|
914
|
-
| `pass1/component_properties.json` | Variant properties, booleans, text slots, Code Connect strategy |
|
|
915
|
-
| `pass1/code_connect_map_response.json` | Existing Code Connect mappings |
|
|
916
|
-
| `pass1/component_summary.json` | **Condensed summary -- read this FIRST** |
|
|
917
|
-
|
|
918
|
-
## Read Instructions
|
|
919
|
-
|
|
920
|
-
**All downstream phases: read `component_summary.json` FIRST.** It contains a condensed view of the component's properties, structure, and assets in ~100 lines. Only read raw artifacts (design_context_response.json, etc.) if you need detail the summary does not provide.
|
|
921
|
-
|
|
922
|
-
- Phase 2A (Testing Identifiers): `component_summary.json`, then `design_context_response.json` if needed, `component_properties.json`
|
|
923
|
-
- Phase 2B (Localization): `component_summary.json`, then `design_context_response.json` if needed, `component_properties.json`
|
|
924
|
-
- Phase 2C (Accessibility): `component_summary.json`, then `screenshot_response.md`, `design_context_response.json` if needed
|
|
925
|
-
- Phase 2D (Analytics): `component_summary.json`, then `design_context_response.json` if needed
|
|
926
|
-
- Phase 3 sub-phases: `component_summary.json` FIRST, then phase-specific raw artifacts as needed
|
|
927
|
-
- Phase 4 sub-phases: `component_summary.json` for cross-reference, pass3/ analysis files as primary input
|
|
928
|
-
"""
|
|
929
|
-
(Path(ARTIFACT_PATH) / "pass1" / "_index.md").write_text(content)
|
|
930
|
-
log("_index.md written")
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
def update_implementation_history(variant_count, svg_count):
|
|
934
|
-
hist = Path(ARTIFACT_PATH) / "IMPLEMENTATION_HISTORY.md"
|
|
935
|
-
if not hist.exists():
|
|
936
|
-
log("WARNING: IMPLEMENTATION_HISTORY.md not found -- skipping update")
|
|
937
|
-
return
|
|
938
|
-
|
|
939
|
-
text = hist.read_text()
|
|
940
|
-
|
|
941
|
-
phase1_block = f"""
|
|
942
|
-
## Phase 1 Data
|
|
943
|
-
- Screenshot: pass1/screenshot_response.md
|
|
944
|
-
- Screenshot PNG: pass1/screenshot.png
|
|
945
|
-
- Metadata: pass1/metadata_response.xml
|
|
946
|
-
- Design Context: pass1/design_context_response.json
|
|
947
|
-
- Design Assets: pass1/design_context_assets.txt
|
|
948
|
-
- Variable Defs: pass1/variable_defs_response.json
|
|
949
|
-
- Component Properties: pass1/component_properties.json (for Code Connect)
|
|
950
|
-
- Code Connect: pass1/code_connect_map_response.json
|
|
951
|
-
- Assets: pass1/assets/ (SVGs sanitized)
|
|
952
|
-
- Variant Screenshots: pass1/screenshots/ ({variant_count} variants)
|
|
953
|
-
- Component Summary: pass1/component_summary.json
|
|
954
|
-
"""
|
|
955
|
-
|
|
956
|
-
if "## Phase 1 Data" in text:
|
|
957
|
-
text = re.sub(r"\n## Phase 1 Data.*?(?=\n## |\Z)", "", text, flags=re.DOTALL)
|
|
958
|
-
|
|
959
|
-
text = text.rstrip() + "\n" + phase1_block
|
|
960
|
-
hist.write_text(text)
|
|
961
|
-
log("IMPLEMENTATION_HISTORY.md updated")
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
# ─── Deferred Resolution ─────────────────────────────────────────────────────
|
|
965
|
-
|
|
966
|
-
def fetch_deferred(sublayer_ids):
|
|
967
|
-
"""Fetch design context for selected sublayer IDs and merge into standard format."""
|
|
968
|
-
t0 = time.time()
|
|
969
|
-
log(f"Deferred resolution: fetching {len(sublayer_ids)} sublayers...")
|
|
970
|
-
|
|
971
|
-
pass1 = Path(ARTIFACT_PATH) / "pass1"
|
|
972
|
-
assets_dir = str((pass1 / "assets").resolve())
|
|
973
|
-
Path(assets_dir).mkdir(parents=True, exist_ok=True)
|
|
974
|
-
|
|
975
|
-
# Read existing deferred JSON to get all sublayer IDs
|
|
976
|
-
deferred_path = pass1 / "design_context_deferred.json"
|
|
977
|
-
if deferred_path.exists():
|
|
978
|
-
deferred = json.loads(deferred_path.read_text())
|
|
979
|
-
all_sublayer_ids = deferred.get("sublayerIds", [])
|
|
980
|
-
else:
|
|
981
|
-
all_sublayer_ids = sublayer_ids
|
|
982
|
-
|
|
983
|
-
# Fetch each sublayer in parallel (reuses existing infrastructure)
|
|
984
|
-
sublayer_responses = _fetch_sublayers_parallel(sublayer_ids, assets_dir)
|
|
985
|
-
|
|
986
|
-
# Build responses dict (text only, same format as normal sublayer path)
|
|
987
|
-
responses = {}
|
|
988
|
-
for sid, resp in sublayer_responses.items():
|
|
989
|
-
if isinstance(resp, dict) and "response" in resp:
|
|
990
|
-
responses[sid] = resp["response"]
|
|
991
|
-
elif isinstance(resp, dict) and "error" in resp:
|
|
992
|
-
responses[sid] = f"<!-- Error: {resp['error']} -->"
|
|
993
|
-
log(f" WARNING: sublayer {sid} had error: {resp['error']}")
|
|
994
|
-
else:
|
|
995
|
-
responses[sid] = str(resp)
|
|
996
|
-
|
|
997
|
-
# Build merged design_context_response.json - same format as non-deferred
|
|
998
|
-
envelope = {
|
|
999
|
-
"rootNodeId": NODE_ID,
|
|
1000
|
-
"source": MCP_MODE,
|
|
1001
|
-
"sublayers": True,
|
|
1002
|
-
"representativeSelection": True,
|
|
1003
|
-
"sublayerNodeIds": all_sublayer_ids,
|
|
1004
|
-
"selectedNodeIds": sublayer_ids,
|
|
1005
|
-
"responses": responses,
|
|
1006
|
-
# Flat concatenated response for Phase 2 NORMALIZE_DESIGN_CONTEXT compatibility
|
|
1007
|
-
"response": "\n\n".join(
|
|
1008
|
-
f"// --- Sublayer: {sid} ---\n{text}"
|
|
1009
|
-
for sid, text in responses.items()
|
|
1010
|
-
if not text.startswith("<!-- Error")
|
|
1011
|
-
),
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
dest = pass1 / "design_context_response.json"
|
|
1015
|
-
dest.write_text(json.dumps(envelope, indent=2, ensure_ascii=False) + "\n")
|
|
1016
|
-
log(f"design_context_response.json written ({len(responses)} sublayers, {len(envelope['response'])} chars)")
|
|
1017
|
-
|
|
1018
|
-
# Update component_summary.json
|
|
1019
|
-
summary_path = pass1 / "component_summary.json"
|
|
1020
|
-
if summary_path.exists():
|
|
1021
|
-
summary = json.loads(summary_path.read_text())
|
|
1022
|
-
summary["designContextStatus"] = "representative"
|
|
1023
|
-
summary["selectedSublayerCount"] = len(sublayer_ids)
|
|
1024
|
-
summary_path.write_text(json.dumps(summary, indent=2, ensure_ascii=False) + "\n")
|
|
1025
|
-
log("component_summary.json updated with designContextStatus")
|
|
1026
|
-
|
|
1027
|
-
# Sanitize any new SVGs
|
|
1028
|
-
sanitize_svgs()
|
|
1029
|
-
|
|
1030
|
-
# Update asset list
|
|
1031
|
-
write_design_context_assets()
|
|
1032
|
-
|
|
1033
|
-
duration = time.time() - t0
|
|
1034
|
-
log(f"=== DEFERRED COMPLETE in {duration:.1f}s (MCP: {MCP_MODE}) ===")
|
|
1035
|
-
|
|
1036
|
-
result = {
|
|
1037
|
-
"status": "COMPLETE",
|
|
1038
|
-
"phase": "phase-1-deferred",
|
|
1039
|
-
"mcpMode": MCP_MODE,
|
|
1040
|
-
"sublayersFetched": len(sublayer_ids),
|
|
1041
|
-
"sublayersSucceeded": len([v for v in responses.values() if not v.startswith("<!-- Error")]),
|
|
1042
|
-
"responseChars": len(envelope["response"]),
|
|
1043
|
-
"duration_seconds": round(duration, 1),
|
|
1044
|
-
}
|
|
1045
|
-
print(json.dumps(result, indent=2))
|
|
1046
|
-
sys.exit(0)
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
# ─── Main ────────────────────────────────────────────────────────────────────
|
|
1050
|
-
|
|
1051
|
-
def main():
|
|
1052
|
-
global MCP_URL, MCP_TOKEN, FIGMA_TOKEN, FILE_KEY, NODE_ID, COMPONENT_NAME, ARTIFACT_PATH
|
|
1053
|
-
|
|
1054
|
-
parser = argparse.ArgumentParser(description="Phase 1 -- Data Gathering")
|
|
1055
|
-
parser.add_argument("--file-key", required=True)
|
|
1056
|
-
parser.add_argument("--node-id", required=True, help="colon-separated, e.g. 6893:6879")
|
|
1057
|
-
parser.add_argument("--artifact-path", required=True)
|
|
1058
|
-
parser.add_argument("--component-name", required=True)
|
|
1059
|
-
parser.add_argument("--figma-token", help="REST API token (falls back to macOS Keychain)")
|
|
1060
|
-
parser.add_argument("--mcp-token", help="MCP OAuth token (falls back to Keychain/Claude Code creds)")
|
|
1061
|
-
parser.add_argument("--mcp-url", default="http://127.0.0.1:3845/mcp")
|
|
1062
|
-
parser.add_argument("--fetch-deferred", action="store_true",
|
|
1063
|
-
help="Deferred resolution mode: fetch design context for selected sublayer IDs")
|
|
1064
|
-
parser.add_argument("--sublayer-ids", help="Comma-separated sublayer node IDs (requires --fetch-deferred)")
|
|
1065
|
-
args = parser.parse_args()
|
|
1066
|
-
|
|
1067
|
-
FILE_KEY = args.file_key
|
|
1068
|
-
NODE_ID = args.node_id
|
|
1069
|
-
COMPONENT_NAME = args.component_name
|
|
1070
|
-
ARTIFACT_PATH = args.artifact_path
|
|
1071
|
-
MCP_URL = args.mcp_url
|
|
1072
|
-
|
|
1073
|
-
# ─── Deferred Resolution Mode ───
|
|
1074
|
-
if args.fetch_deferred:
|
|
1075
|
-
if not args.sublayer_ids:
|
|
1076
|
-
halt("PRE_CHECK", "--fetch-deferred requires --sublayer-ids", [], [],
|
|
1077
|
-
"Pass comma-separated sublayer node IDs: --sublayer-ids 'id1,id2,id3'")
|
|
1078
|
-
sublayer_ids = [sid.strip() for sid in args.sublayer_ids.split(",") if sid.strip()]
|
|
1079
|
-
if not sublayer_ids:
|
|
1080
|
-
halt("PRE_CHECK", "No valid sublayer IDs provided", [], [],
|
|
1081
|
-
"Pass comma-separated sublayer node IDs: --sublayer-ids 'id1,id2,id3'")
|
|
1082
|
-
# Need MCP token for fetching
|
|
1083
|
-
MCP_TOKEN = get_mcp_token(args.mcp_token)
|
|
1084
|
-
try:
|
|
1085
|
-
mcp_init()
|
|
1086
|
-
except Exception as exc:
|
|
1087
|
-
halt("PRE_CHECK", f"MCP init failed: {exc}", [], [],
|
|
1088
|
-
"Ensure Figma Desktop is running or run /figma-setup for remote MCP")
|
|
1089
|
-
fetch_deferred(sublayer_ids)
|
|
1090
|
-
return # fetch_deferred calls sys.exit(0)
|
|
1091
|
-
|
|
1092
|
-
# Resolve Figma REST API token
|
|
1093
|
-
FIGMA_TOKEN = args.figma_token
|
|
1094
|
-
if not FIGMA_TOKEN:
|
|
1095
|
-
try:
|
|
1096
|
-
FIGMA_TOKEN = subprocess.check_output(
|
|
1097
|
-
["security", "find-generic-password", "-a", os.environ["USER"],
|
|
1098
|
-
"-s", "FIGMA_ACCESS_TOKEN", "-w"],
|
|
1099
|
-
stderr=subprocess.DEVNULL,
|
|
1100
|
-
).decode().strip()
|
|
1101
|
-
except (subprocess.CalledProcessError, KeyError):
|
|
1102
|
-
halt("PRE_CHECK", "FIGMA_ACCESS_TOKEN not found", [], [],
|
|
1103
|
-
"Pass --figma-token or: "$HOME/.claude/lib/credential-store.sh" set \1 \2
|
|
1104
|
-
|
|
1105
|
-
# Resolve MCP OAuth token (for remote MCP)
|
|
1106
|
-
MCP_TOKEN = get_mcp_token(args.mcp_token)
|
|
1107
|
-
|
|
1108
|
-
t0 = time.time()
|
|
1109
|
-
log(f"Phase 1: {COMPONENT_NAME} node={NODE_ID} file={FILE_KEY}")
|
|
1110
|
-
|
|
1111
|
-
# Ensure directories
|
|
1112
|
-
pass1 = Path(ARTIFACT_PATH) / "pass1"
|
|
1113
|
-
pass1.mkdir(parents=True, exist_ok=True)
|
|
1114
|
-
(pass1 / "assets").mkdir(exist_ok=True)
|
|
1115
|
-
(pass1 / "screenshots").mkdir(exist_ok=True)
|
|
1116
|
-
|
|
1117
|
-
gates = ["PRE_CHECK"]
|
|
1118
|
-
artifacts = []
|
|
1119
|
-
|
|
1120
|
-
# -- MCP Init (remote first, local fallback) --
|
|
1121
|
-
try:
|
|
1122
|
-
mcp_init()
|
|
1123
|
-
except Exception as exc:
|
|
1124
|
-
halt("PRE_CHECK", f"MCP init failed: {exc}", [], [],
|
|
1125
|
-
"Ensure Figma Desktop is running or run /figma-setup for remote MCP")
|
|
1126
|
-
|
|
1127
|
-
# -- Batch 1: parallel data fetching --
|
|
1128
|
-
log("=== Batch 1: parallel data fetching ===")
|
|
1129
|
-
b1 = {}
|
|
1130
|
-
b1_err = {}
|
|
1131
|
-
|
|
1132
|
-
with concurrent.futures.ThreadPoolExecutor(max_workers=6) as pool:
|
|
1133
|
-
jobs = {
|
|
1134
|
-
pool.submit(fetch_screenshot_url): "screenshot_url",
|
|
1135
|
-
pool.submit(fetch_node_data): "node_data",
|
|
1136
|
-
pool.submit(fetch_metadata): "metadata",
|
|
1137
|
-
pool.submit(fetch_design_context): "design_context",
|
|
1138
|
-
pool.submit(fetch_variable_defs): "variable_defs",
|
|
1139
|
-
pool.submit(fetch_code_connect_map): "code_connect_map",
|
|
1140
|
-
}
|
|
1141
|
-
for fut in concurrent.futures.as_completed(jobs):
|
|
1142
|
-
key = jobs[fut]
|
|
1143
|
-
try:
|
|
1144
|
-
b1[key] = fut.result()
|
|
1145
|
-
except Exception as exc:
|
|
1146
|
-
b1_err[key] = str(exc)
|
|
1147
|
-
log(f" ERROR {key}: {exc}")
|
|
1148
|
-
|
|
1149
|
-
# Critical checks
|
|
1150
|
-
if "screenshot_url" in b1_err:
|
|
1151
|
-
halt("SCREENSHOT", b1_err["screenshot_url"], gates, artifacts,
|
|
1152
|
-
"Check FIGMA_ACCESS_TOKEN and node ID")
|
|
1153
|
-
if "node_data" in b1_err:
|
|
1154
|
-
halt("COMPONENT_PROPERTIES", b1_err["node_data"], gates, artifacts,
|
|
1155
|
-
"Check FIGMA_ACCESS_TOKEN and node ID")
|
|
1156
|
-
|
|
1157
|
-
# -- Batch 2: downloads + processing --
|
|
1158
|
-
log("=== Batch 2: downloads & processing ===")
|
|
1159
|
-
|
|
1160
|
-
# Screenshot
|
|
1161
|
-
try:
|
|
1162
|
-
download_screenshot(b1["screenshot_url"])
|
|
1163
|
-
gates.append("SCREENSHOT")
|
|
1164
|
-
artifacts.append("pass1/screenshot.png")
|
|
1165
|
-
except Exception as exc:
|
|
1166
|
-
halt("SCREENSHOT", str(exc), gates, artifacts)
|
|
1167
|
-
|
|
1168
|
-
write_screenshot_response()
|
|
1169
|
-
artifacts.append("pass1/screenshot_response.md")
|
|
1170
|
-
|
|
1171
|
-
# Variant screenshots
|
|
1172
|
-
node_data = b1["node_data"]
|
|
1173
|
-
try:
|
|
1174
|
-
variant_results = download_variant_screenshots(node_data)
|
|
1175
|
-
gates.append("VARIANT_SCREENSHOTS")
|
|
1176
|
-
if variant_results:
|
|
1177
|
-
artifacts.extend(["pass1/screenshots/", "pass1/screenshots/variants.md"])
|
|
1178
|
-
except Exception as exc:
|
|
1179
|
-
log(f"WARNING: variant screenshots failed: {exc}")
|
|
1180
|
-
variant_results = []
|
|
1181
|
-
gates.append("VARIANT_SCREENSHOTS")
|
|
1182
|
-
|
|
1183
|
-
# Metadata
|
|
1184
|
-
if "metadata" in b1:
|
|
1185
|
-
save_mcp_text("metadata_response.xml", b1["metadata"], try_json=False)
|
|
1186
|
-
gates.append("METADATA")
|
|
1187
|
-
artifacts.append("pass1/metadata_response.xml")
|
|
1188
|
-
elif "metadata" in b1_err:
|
|
1189
|
-
(pass1 / "metadata_response.xml").write_text(f"<!-- Error: {b1_err['metadata']} -->\n")
|
|
1190
|
-
gates.append("METADATA")
|
|
1191
|
-
artifacts.append("pass1/metadata_response.xml")
|
|
1192
|
-
|
|
1193
|
-
# Design context
|
|
1194
|
-
design_context_deferred = False
|
|
1195
|
-
if "design_context" in b1:
|
|
1196
|
-
dc_data = b1["design_context"]
|
|
1197
|
-
write_design_context_file(dc_data)
|
|
1198
|
-
gates.append("DESIGN_CONTEXT")
|
|
1199
|
-
artifacts.append("pass1/design_context_response.json")
|
|
1200
|
-
if isinstance(dc_data, dict) and dc_data.get("__deferred"):
|
|
1201
|
-
design_context_deferred = True
|
|
1202
|
-
artifacts.append("pass1/design_context_deferred.json")
|
|
1203
|
-
elif "design_context" in b1_err:
|
|
1204
|
-
halt("DESIGN_CONTEXT", b1_err["design_context"], gates, artifacts,
|
|
1205
|
-
"Check Figma connection and node ID")
|
|
1206
|
-
|
|
1207
|
-
# Sanitize SVGs
|
|
1208
|
-
sanitize_svgs()
|
|
1209
|
-
gates.append("SANITIZE_SVGS")
|
|
1210
|
-
|
|
1211
|
-
# Asset list
|
|
1212
|
-
asset_count = write_design_context_assets()
|
|
1213
|
-
artifacts.append("pass1/design_context_assets.txt")
|
|
1214
|
-
|
|
1215
|
-
# Variable defs
|
|
1216
|
-
if "variable_defs" in b1:
|
|
1217
|
-
save_mcp_text("variable_defs_response.json", b1["variable_defs"])
|
|
1218
|
-
else:
|
|
1219
|
-
# REST API fallback (requires Enterprise plan - may 403)
|
|
1220
|
-
log("MCP variable_defs failed - trying REST API fallback...")
|
|
1221
|
-
try:
|
|
1222
|
-
rest_vars = figma_get(f"files/{FILE_KEY}/variables/local")
|
|
1223
|
-
(pass1 / "variable_defs_response.json").write_text(
|
|
1224
|
-
json.dumps(rest_vars, indent=2, ensure_ascii=False) + "\n"
|
|
1225
|
-
)
|
|
1226
|
-
log("REST API variable_defs fallback succeeded")
|
|
1227
|
-
except HTTPError as exc:
|
|
1228
|
-
if exc.code == 403:
|
|
1229
|
-
log("REST API variable_defs: 403 (requires Enterprise plan) - skipping")
|
|
1230
|
-
else:
|
|
1231
|
-
log(f"REST API variable_defs fallback failed: {exc}")
|
|
1232
|
-
(pass1 / "variable_defs_response.json").write_text(json.dumps({
|
|
1233
|
-
"error": "variable_defs_unavailable",
|
|
1234
|
-
"source": "none",
|
|
1235
|
-
"note": "Variable definitions could not be retrieved. MCP unavailable, REST API returned 403 (Enterprise plan required).",
|
|
1236
|
-
}, indent=2) + "\n")
|
|
1237
|
-
except Exception as exc:
|
|
1238
|
-
log(f"REST API variable_defs fallback failed: {exc}")
|
|
1239
|
-
(pass1 / "variable_defs_response.json").write_text(json.dumps({
|
|
1240
|
-
"error": "variable_defs_unavailable",
|
|
1241
|
-
"source": "none",
|
|
1242
|
-
"note": f"Variable definitions could not be retrieved via MCP or REST API: {exc}",
|
|
1243
|
-
}, indent=2) + "\n")
|
|
1244
|
-
gates.append("VARIABLE_DEFS")
|
|
1245
|
-
artifacts.append("pass1/variable_defs_response.json")
|
|
1246
|
-
|
|
1247
|
-
# Component properties
|
|
1248
|
-
comp_props = generate_component_properties(node_data)
|
|
1249
|
-
gates.append("COMPONENT_PROPERTIES")
|
|
1250
|
-
artifacts.append("pass1/component_properties.json")
|
|
1251
|
-
|
|
1252
|
-
# Code connect map
|
|
1253
|
-
if "code_connect_map" in b1:
|
|
1254
|
-
save_mcp_text("code_connect_map_response.json", b1["code_connect_map"])
|
|
1255
|
-
else:
|
|
1256
|
-
(pass1 / "code_connect_map_response.json").write_text("{}\n")
|
|
1257
|
-
gates.append("CODE_CONNECT_MAP")
|
|
1258
|
-
artifacts.append("pass1/code_connect_map_response.json")
|
|
1259
|
-
|
|
1260
|
-
# -- Finalize --
|
|
1261
|
-
log("=== Finalize ===")
|
|
1262
|
-
|
|
1263
|
-
write_component_summary(comp_props, node_data)
|
|
1264
|
-
artifacts.append("pass1/component_summary.json")
|
|
1265
|
-
|
|
1266
|
-
write_index(variant_results, asset_count)
|
|
1267
|
-
artifacts.append("pass1/_index.md")
|
|
1268
|
-
|
|
1269
|
-
update_implementation_history(
|
|
1270
|
-
len(variant_results),
|
|
1271
|
-
len(list((pass1 / "assets").glob("*.svg"))) if (pass1 / "assets").exists() else 0,
|
|
1272
|
-
)
|
|
1273
|
-
|
|
1274
|
-
gates.append("FINALIZE")
|
|
1275
|
-
duration = time.time() - t0
|
|
1276
|
-
|
|
1277
|
-
# -- Success --
|
|
1278
|
-
status = "COMPLETE_WITH_DEFERRED" if design_context_deferred else "COMPLETE"
|
|
1279
|
-
log(f"=== {status} in {duration:.1f}s (MCP: {MCP_MODE}) ===")
|
|
1280
|
-
summary = {
|
|
1281
|
-
"status": status,
|
|
1282
|
-
"phase": "phase-1",
|
|
1283
|
-
"mcpMode": MCP_MODE,
|
|
1284
|
-
"gates_passed": gates,
|
|
1285
|
-
"artifacts_created": artifacts,
|
|
1286
|
-
"duration_seconds": round(duration, 1),
|
|
1287
|
-
}
|
|
1288
|
-
if design_context_deferred:
|
|
1289
|
-
summary["deferred"] = {
|
|
1290
|
-
"artifact": "pass1/design_context_deferred.json",
|
|
1291
|
-
"reason": "sublayer_count_exceeds_cap",
|
|
1292
|
-
}
|
|
1293
|
-
print(json.dumps(summary, indent=2))
|
|
1294
|
-
sys.exit(0)
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
if __name__ == "__main__":
|
|
1298
|
-
main()
|