@sarjallab09/figma-intelligence 1.0.1 → 1.2.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/README.md +67 -36
- package/dist/bin/cli.js +2 -0
- package/dist/design-bridge/bridge.js +2 -0
- package/dist/figma-bridge-plugin/bridge-relay.js +2 -0
- package/dist/figma-bridge-plugin/code.js +1 -0
- package/{figma-bridge-plugin → dist/figma-bridge-plugin}/package-lock.json +0 -3
- package/dist/figma-bridge-plugin/ui.html +4970 -0
- package/dist/figma-intelligence-layer/dist/index.js +2 -0
- package/dist/scripts/clean-existing-chunks.js +2 -0
- package/dist/scripts/connect-ai-tool.js +2 -0
- package/dist/scripts/convert-hub-pdfs.js +2 -0
- package/dist/scripts/figma-mcp-status.js +2 -0
- package/dist/scripts/register-codex-mcp.js +2 -0
- package/dist/scripts/test-copilot-chat.js +2 -0
- package/package.json +11 -8
- package/bin/cli.js +0 -859
- package/design-bridge/bridge.js +0 -196
- package/design-bridge/lib/assets.js +0 -367
- package/design-bridge/lib/prompt.js +0 -85
- package/design-bridge/lib/server.js +0 -66
- package/design-bridge/lib/stitch.js +0 -37
- package/design-bridge/lib/tokens.js +0 -82
- package/design-bridge/package-lock.json +0 -579
- package/figma-bridge-plugin/README.md +0 -97
- package/figma-bridge-plugin/anthropic-chat-runner.js +0 -192
- package/figma-bridge-plugin/bridge-relay.js +0 -2363
- package/figma-bridge-plugin/chat-runner.js +0 -459
- package/figma-bridge-plugin/code.js +0 -1528
- package/figma-bridge-plugin/codex-runner.js +0 -505
- package/figma-bridge-plugin/component-schemas.js +0 -110
- package/figma-bridge-plugin/content-context.js +0 -869
- package/figma-bridge-plugin/create-button.js +0 -216
- package/figma-bridge-plugin/gemini-cli-runner.js +0 -291
- package/figma-bridge-plugin/gemini-runner.js +0 -187
- package/figma-bridge-plugin/html-to-figma.js +0 -927
- package/figma-bridge-plugin/knowledge-hub/.gitkeep +0 -0
- package/figma-bridge-plugin/knowledge-hub/uspec-references/anatomy-spec.md +0 -159
- package/figma-bridge-plugin/knowledge-hub/uspec-references/api-spec.md +0 -162
- package/figma-bridge-plugin/knowledge-hub/uspec-references/color-spec.md +0 -148
- package/figma-bridge-plugin/knowledge-hub/uspec-references/full-spec-template.md +0 -314
- package/figma-bridge-plugin/knowledge-hub/uspec-references/property-spec.md +0 -175
- package/figma-bridge-plugin/knowledge-hub/uspec-references/screen-reader-spec.md +0 -180
- package/figma-bridge-plugin/knowledge-hub/uspec-references/structure-spec.md +0 -165
- package/figma-bridge-plugin/perplexity-runner.js +0 -188
- package/figma-bridge-plugin/references/SKILL.md +0 -178
- package/figma-bridge-plugin/references/anatomy-spec.md +0 -159
- package/figma-bridge-plugin/references/api-spec.md +0 -162
- package/figma-bridge-plugin/references/color-spec.md +0 -148
- package/figma-bridge-plugin/references/full-spec-template.md +0 -314
- package/figma-bridge-plugin/references/property-spec.md +0 -175
- package/figma-bridge-plugin/references/screen-reader-spec.md +0 -180
- package/figma-bridge-plugin/references/structure-spec.md +0 -165
- package/figma-bridge-plugin/shared-prompt-config.js +0 -604
- package/figma-bridge-plugin/spec-helpers/build-table.js +0 -269
- package/figma-bridge-plugin/spec-helpers/classify-elements.js +0 -189
- package/figma-bridge-plugin/spec-helpers/index.js +0 -35
- package/figma-bridge-plugin/spec-helpers/parse-figma-link.js +0 -49
- package/figma-bridge-plugin/spec-helpers/position-markers.js +0 -158
- package/figma-bridge-plugin/stitch-auth.js +0 -322
- package/figma-bridge-plugin/stitch-runner.js +0 -1427
- package/figma-bridge-plugin/token-resolver.js +0 -107
- package/figma-bridge-plugin/ui.html +0 -4467
- package/figma-intelligence-layer/.env.example +0 -39
- package/figma-intelligence-layer/docs/local-image-generation.md +0 -60
- package/figma-intelligence-layer/examples/comfyui-workflow-template.example.json +0 -101
- package/figma-intelligence-layer/jest.config.js +0 -14
- package/figma-intelligence-layer/mcp-config.json +0 -19
- package/figma-intelligence-layer/package-lock.json +0 -5892
- package/figma-intelligence-layer/scripts/setup-comfyui-local.sh +0 -67
- package/figma-intelligence-layer/scripts/start-comfyui.sh +0 -33
- package/figma-intelligence-layer/src/index.ts +0 -2233
- package/figma-intelligence-layer/src/shared/auto-layout-validator.ts +0 -404
- package/figma-intelligence-layer/src/shared/cache.ts +0 -187
- package/figma-intelligence-layer/src/shared/color-operations.ts +0 -533
- package/figma-intelligence-layer/src/shared/color-utils.ts +0 -138
- package/figma-intelligence-layer/src/shared/component-script-builder.ts +0 -413
- package/figma-intelligence-layer/src/shared/component-templates.ts +0 -2767
- package/figma-intelligence-layer/src/shared/concept-taxonomy.ts +0 -694
- package/figma-intelligence-layer/src/shared/decision-log.ts +0 -128
- package/figma-intelligence-layer/src/shared/design-system-context.ts +0 -568
- package/figma-intelligence-layer/src/shared/design-system-intelligence.ts +0 -131
- package/figma-intelligence-layer/src/shared/design-system-matcher.ts +0 -184
- package/figma-intelligence-layer/src/shared/design-system-normalizers.ts +0 -196
- package/figma-intelligence-layer/src/shared/design-system-tokens.ts +0 -295
- package/figma-intelligence-layer/src/shared/dtcg-validator.ts +0 -530
- package/figma-intelligence-layer/src/shared/enrichment-pipeline.ts +0 -671
- package/figma-intelligence-layer/src/shared/figma-bridge.ts +0 -1408
- package/figma-intelligence-layer/src/shared/font-config.ts +0 -126
- package/figma-intelligence-layer/src/shared/icon-catalog.ts +0 -360
- package/figma-intelligence-layer/src/shared/icon-fetch.ts +0 -80
- package/figma-intelligence-layer/src/shared/prototype-script-builder.ts +0 -162
- package/figma-intelligence-layer/src/shared/response-compression.ts +0 -440
- package/figma-intelligence-layer/src/shared/semantic-token-catalog.ts +0 -324
- package/figma-intelligence-layer/src/shared/token-binder.ts +0 -505
- package/figma-intelligence-layer/src/shared/token-math.ts +0 -427
- package/figma-intelligence-layer/src/shared/token-naming.ts +0 -468
- package/figma-intelligence-layer/src/shared/token-utils.ts +0 -420
- package/figma-intelligence-layer/src/shared/types.ts +0 -346
- package/figma-intelligence-layer/src/shared/typography-presets.ts +0 -94
- package/figma-intelligence-layer/src/shared/unsplash.ts +0 -165
- package/figma-intelligence-layer/src/shared/vision-client.ts +0 -607
- package/figma-intelligence-layer/src/shared/vision-provider-anthropic.ts +0 -334
- package/figma-intelligence-layer/src/shared/vision-provider-openai.ts +0 -446
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/a11y-annotate-handler.ts +0 -782
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/a11y-annotate-renderer.ts +0 -496
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/a11y-annotation-kit.ts +0 -230
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/colorblind-sim.ts +0 -66
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/index.ts +0 -810
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/keyboard-sr-order-analyzer.ts +0 -1191
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/keyboard-sr-order-figma-page.ts +0 -1346
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/keyboard-sr-order-handler.ts +0 -148
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/vpat-figma-page.ts +0 -499
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/vpat-report.ts +0 -910
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/wcag-checker.ts +0 -989
- package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/wcag-criteria.ts +0 -1160
- package/figma-intelligence-layer/src/tools/phase1-vision/design-from-ref/index.ts +0 -424
- package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/component-recognizer.ts +0 -38
- package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/ds-matcher.ts +0 -111
- package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/font-matcher.ts +0 -114
- package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/icon-resolver.ts +0 -103
- package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/index.ts +0 -1060
- package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/layout-segmenter.ts +0 -18
- package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/token-inferencer.ts +0 -39
- package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/vision-pipeline.ts +0 -58
- package/figma-intelligence-layer/src/tools/phase1-vision/sketch-to-design/index.ts +0 -298
- package/figma-intelligence-layer/src/tools/phase1-vision/visual-audit/index.ts +0 -197
- package/figma-intelligence-layer/src/tools/phase2-accuracy/component-audit/index.ts +0 -494
- package/figma-intelligence-layer/src/tools/phase2-accuracy/intent-translator/index.ts +0 -356
- package/figma-intelligence-layer/src/tools/phase2-accuracy/layout-intelligence/container-patterns.ts +0 -123
- package/figma-intelligence-layer/src/tools/phase2-accuracy/layout-intelligence/index.ts +0 -663
- package/figma-intelligence-layer/src/tools/phase2-accuracy/lint-rules/built-in-rules.yaml +0 -56
- package/figma-intelligence-layer/src/tools/phase2-accuracy/lint-rules/index.ts +0 -614
- package/figma-intelligence-layer/src/tools/phase2-accuracy/lint-rules/rule-engine.ts +0 -113
- package/figma-intelligence-layer/src/tools/phase2-accuracy/theme-generator/color-theory.ts +0 -178
- package/figma-intelligence-layer/src/tools/phase2-accuracy/theme-generator/index.ts +0 -470
- package/figma-intelligence-layer/src/tools/phase2-accuracy/variant-expander/index.ts +0 -429
- package/figma-intelligence-layer/src/tools/phase2-accuracy/variant-expander/token-override-maps.ts +0 -226
- package/figma-intelligence-layer/src/tools/phase3-generation/ai-image-insert/index.ts +0 -535
- package/figma-intelligence-layer/src/tools/phase3-generation/component-archaeologist/index.ts +0 -660
- package/figma-intelligence-layer/src/tools/phase3-generation/component-archaeologist/pattern-fingerprints.ts +0 -209
- package/figma-intelligence-layer/src/tools/phase3-generation/composition-builder/index.ts +0 -540
- package/figma-intelligence-layer/src/tools/phase3-generation/figma-animated-build.ts +0 -391
- package/figma-intelligence-layer/src/tools/phase3-generation/page-architect/index.ts +0 -2019
- package/figma-intelligence-layer/src/tools/phase3-generation/page-architect/screen-templates.ts +0 -131
- package/figma-intelligence-layer/src/tools/phase3-generation/prototype-map/index.ts +0 -381
- package/figma-intelligence-layer/src/tools/phase3-generation/prototype-wire/index.ts +0 -565
- package/figma-intelligence-layer/src/tools/phase3-generation/swarm-build/index.ts +0 -764
- package/figma-intelligence-layer/src/tools/phase3-generation/system-drift/index.ts +0 -535
- package/figma-intelligence-layer/src/tools/phase3-generation/unsplash-search/index.ts +0 -84
- package/figma-intelligence-layer/src/tools/phase3-generation/url-to-frame/index.ts +0 -401
- package/figma-intelligence-layer/src/tools/phase4-sync/animation-specifier/code-generators/css-animations.ts +0 -68
- package/figma-intelligence-layer/src/tools/phase4-sync/animation-specifier/code-generators/framer-motion.ts +0 -78
- package/figma-intelligence-layer/src/tools/phase4-sync/animation-specifier/code-generators/swift-animations.ts +0 -93
- package/figma-intelligence-layer/src/tools/phase4-sync/animation-specifier/index.ts +0 -596
- package/figma-intelligence-layer/src/tools/phase4-sync/ci-check/index.ts +0 -462
- package/figma-intelligence-layer/src/tools/phase4-sync/export-tokens/index.ts +0 -1470
- package/figma-intelligence-layer/src/tools/phase4-sync/generate-component-code/index.ts +0 -829
- package/figma-intelligence-layer/src/tools/phase4-sync/handoff-spec/index.ts +0 -702
- package/figma-intelligence-layer/src/tools/phase4-sync/icon-library-sync/index.ts +0 -483
- package/figma-intelligence-layer/src/tools/phase4-sync/sync-from-code/index.ts +0 -501
- package/figma-intelligence-layer/src/tools/phase4-sync/sync-from-code/storybook-parser.ts +0 -106
- package/figma-intelligence-layer/src/tools/phase4-sync/watch-docs/index.ts +0 -676
- package/figma-intelligence-layer/src/tools/phase4-sync/webhook-listener/index.ts +0 -560
- package/figma-intelligence-layer/src/tools/phase5-governance/apg-doc/index.ts +0 -1043
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/component-detection.ts +0 -620
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/anatomy.ts +0 -331
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/color-tokens.ts +0 -77
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/properties.ts +0 -54
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/snapshot.ts +0 -287
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/spacing.ts +0 -71
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/states.ts +0 -43
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/typography.ts +0 -71
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/index.ts +0 -221
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/_default.ts +0 -166
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/accordion.ts +0 -232
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/alert.ts +0 -234
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/avatar-group.ts +0 -270
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/avatar.ts +0 -249
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/badge.ts +0 -231
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/banner.ts +0 -293
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/breadcrumb.ts +0 -240
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/button.ts +0 -243
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/calendar.ts +0 -307
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/card.ts +0 -143
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/checkbox.ts +0 -227
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/chip.ts +0 -233
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/combobox.ts +0 -282
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/datepicker.ts +0 -276
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/divider.ts +0 -223
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/drawer.ts +0 -255
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/dropdown-menu.ts +0 -289
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/empty-state.ts +0 -261
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/file-uploader.ts +0 -290
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/form.ts +0 -265
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/grid.ts +0 -238
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/icon.ts +0 -255
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/index.ts +0 -128
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/inline-edit.ts +0 -286
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/inline-message.ts +0 -255
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/input.ts +0 -330
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/link.ts +0 -247
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/list.ts +0 -250
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/menu.ts +0 -247
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/modal.ts +0 -144
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/navbar.ts +0 -264
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/navigation.ts +0 -251
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/number-input.ts +0 -261
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/pagination.ts +0 -248
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/popover.ts +0 -270
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/progress.ts +0 -251
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/radio.ts +0 -142
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/range-slider.ts +0 -282
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/rating.ts +0 -250
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/search.ts +0 -258
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/segmented-control.ts +0 -265
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/select.ts +0 -319
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/skeleton.ts +0 -256
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/slider.ts +0 -232
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/spinner.ts +0 -239
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/status-dot.ts +0 -252
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/stepper.ts +0 -270
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/table.ts +0 -244
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/tabs.ts +0 -143
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/tag.ts +0 -243
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/textarea.ts +0 -259
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/time-picker.ts +0 -293
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/toast.ts +0 -144
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/toggle.ts +0 -289
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/toolbar.ts +0 -267
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/tooltip.ts +0 -232
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/treeview.ts +0 -257
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/typography.ts +0 -319
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/legacy-compat.ts +0 -121
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/renderers/anatomy-diagram.ts +0 -430
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/renderers/figma-page.ts +0 -312
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/renderers/json.ts +0 -129
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/renderers/markdown.ts +0 -78
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/renderers/visual-doc.ts +0 -2333
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/accessibility.ts +0 -100
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/anatomy.ts +0 -32
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/color-tokens.ts +0 -59
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/content-guidance.ts +0 -18
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/design-tokens.ts +0 -53
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/interaction-rules.ts +0 -19
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/overview.ts +0 -91
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/properties-api.ts +0 -71
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/qa-criteria.ts +0 -19
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/related-components.ts +0 -110
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/responsive.ts +0 -19
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/size-specs.ts +0 -67
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/spacing-structure.ts +0 -58
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/state-specs.ts +0 -79
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/states.ts +0 -50
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/type-hierarchy.ts +0 -33
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/typography.ts +0 -55
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/usage-guidelines.ts +0 -73
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/variants.ts +0 -81
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/types.ts +0 -409
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec-sheet/index.ts +0 -198
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec-sheet/renderer.ts +0 -701
- package/figma-intelligence-layer/src/tools/phase5-governance/component-spec-sheet/types.ts +0 -88
- package/figma-intelligence-layer/src/tools/phase5-governance/decision-log/index.ts +0 -135
- package/figma-intelligence-layer/src/tools/phase5-governance/design-decision-log/index.ts +0 -491
- package/figma-intelligence-layer/src/tools/phase5-governance/ds-primitives/index.ts +0 -416
- package/figma-intelligence-layer/src/tools/phase5-governance/ds-scaffolder/index.ts +0 -722
- package/figma-intelligence-layer/src/tools/phase5-governance/ds-variables/index.ts +0 -449
- package/figma-intelligence-layer/src/tools/phase5-governance/health-report/index.ts +0 -393
- package/figma-intelligence-layer/src/tools/phase5-governance/taxonomy-docs/index.ts +0 -406
- package/figma-intelligence-layer/src/tools/phase5-governance/taxonomy-docs/renderers/figma-page.ts +0 -292
- package/figma-intelligence-layer/src/tools/phase5-governance/taxonomy-docs/renderers/json.ts +0 -24
- package/figma-intelligence-layer/src/tools/phase5-governance/taxonomy-docs/renderers/markdown.ts +0 -172
- package/figma-intelligence-layer/src/tools/phase5-governance/taxonomy-docs/renderers/naming-guide.ts +0 -409
- package/figma-intelligence-layer/src/tools/phase5-governance/token-analytics/index.ts +0 -594
- package/figma-intelligence-layer/src/tools/phase5-governance/token-docs/index.ts +0 -710
- package/figma-intelligence-layer/src/tools/phase5-governance/token-migrate/index.ts +0 -458
- package/figma-intelligence-layer/src/tools/phase5-governance/token-naming/index.ts +0 -134
- package/figma-intelligence-layer/tests/apg-doc.test.ts +0 -101
- package/figma-intelligence-layer/tests/design-system-context.test.ts +0 -152
- package/figma-intelligence-layer/tests/design-system-matcher.test.ts +0 -144
- package/figma-intelligence-layer/tests/figma-bridge.test.ts +0 -83
- package/figma-intelligence-layer/tests/generate-image-and-insert.test.ts +0 -56
- package/figma-intelligence-layer/tests/screen-cloner-regression.test.ts +0 -69
- package/figma-intelligence-layer/tests/smoke.test.ts +0 -174
- package/figma-intelligence-layer/tests/spec-generator.test.ts +0 -127
- package/figma-intelligence-layer/tests/token-migrate.test.ts +0 -21
- package/figma-intelligence-layer/tests/token-naming.test.ts +0 -30
- package/figma-intelligence-layer/tsconfig.json +0 -19
- package/scripts/clean-existing-chunks.js +0 -179
- package/scripts/connect-ai-tool.js +0 -490
- package/scripts/convert-hub-pdfs.js +0 -425
- package/scripts/figma-mcp-status.js +0 -349
- package/scripts/register-codex-mcp.js +0 -96
- /package/{design-bridge → dist/design-bridge}/.env.example +0 -0
- /package/{design-bridge → dist/design-bridge}/package.json +0 -0
- /package/{figma-bridge-plugin → dist/figma-bridge-plugin}/manifest.json +0 -0
- /package/{figma-bridge-plugin → dist/figma-bridge-plugin}/package.json +0 -0
- /package/{figma-intelligence-layer → dist/figma-intelligence-layer}/package.json +0 -0
|
@@ -1,1427 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* stitch-runner.js — Uses the official @google/stitch-sdk to generate UI designs,
|
|
4
|
-
* then converts the Tailwind HTML output to native Figma frames.
|
|
5
|
-
*
|
|
6
|
-
* SDK: @google/stitch-sdk (ESM-only, loaded via dynamic import)
|
|
7
|
-
* MCP endpoint: https://stitch.googleapis.com/mcp
|
|
8
|
-
* Auth: API key from Stitch Settings (stitch.withgoogle.com)
|
|
9
|
-
*
|
|
10
|
-
* Pipeline (mirrors actual Stitch environment):
|
|
11
|
-
* 1. StitchToolClient.connect() → authenticate
|
|
12
|
-
* 2. Stitch.createProject() → workspace (or reuse existing)
|
|
13
|
-
* 3. Project.generate(prompt) → Screen with HTML + screenshot
|
|
14
|
-
* 4. extract_design_context → Design DNA (colors, typography, spacing, components)
|
|
15
|
-
* 5. Save as design.md → persist for future screen consistency
|
|
16
|
-
* 6. Screen.getHtml() → download URL → fetch HTML
|
|
17
|
-
* 7. htmlToFigmaCommands() → convert Tailwind HTML to Figma frames
|
|
18
|
-
*
|
|
19
|
-
* The design.md file is saved per-project so subsequent screen generations
|
|
20
|
-
* in the same workspace reuse the same design DNA — maintaining visual
|
|
21
|
-
* consistency across screens (same palette, fonts, spacing, patterns).
|
|
22
|
-
*
|
|
23
|
-
* Emits the same event shape as chat-runner.js / perplexity-runner.js
|
|
24
|
-
* so bridge-relay can use it interchangeably.
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
const http = require("http");
|
|
28
|
-
const https = require("https");
|
|
29
|
-
const { EventEmitter } = require("events");
|
|
30
|
-
const { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } = require("fs");
|
|
31
|
-
const { join } = require("path");
|
|
32
|
-
const { homedir } = require("os");
|
|
33
|
-
const { htmlToFigmaExecuteCode } = require("./html-to-figma");
|
|
34
|
-
const { getStitchAccessToken, hasStitchAuth } = require("./stitch-auth");
|
|
35
|
-
const STITCH_LOG = join(homedir(), ".claude", "stitch", "debug.log");
|
|
36
|
-
function stitchLog(msg, data) {
|
|
37
|
-
try {
|
|
38
|
-
const ts = new Date().toISOString();
|
|
39
|
-
const line = `[${ts}] ${msg}${data !== undefined ? ": " + JSON.stringify(data, null, 2) : ""}\n`;
|
|
40
|
-
if (!existsSync(join(homedir(), ".claude", "stitch"))) mkdirSync(join(homedir(), ".claude", "stitch"), { recursive: true });
|
|
41
|
-
appendFileSync(STITCH_LOG, line);
|
|
42
|
-
} catch {}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// ── Local preview server ──────────────────────────────────────────────────────
|
|
46
|
-
|
|
47
|
-
const PREVIEW_DIR = join(homedir(), ".claude", "stitch", "previews");
|
|
48
|
-
let _previewServer = null;
|
|
49
|
-
let _previewPort = null;
|
|
50
|
-
const _previewFiles = new Map(); // slug → { html, title, createdAt }
|
|
51
|
-
|
|
52
|
-
function ensurePreviewServer() {
|
|
53
|
-
return new Promise((resolve, reject) => {
|
|
54
|
-
if (_previewServer && _previewPort) {
|
|
55
|
-
resolve(_previewPort);
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (!existsSync(PREVIEW_DIR)) mkdirSync(PREVIEW_DIR, { recursive: true });
|
|
60
|
-
|
|
61
|
-
const server = http.createServer((req, res) => {
|
|
62
|
-
const url = new URL(req.url, `http://localhost`);
|
|
63
|
-
const slug = url.pathname.slice(1) || "index";
|
|
64
|
-
|
|
65
|
-
// Serve the index listing all previews
|
|
66
|
-
if (slug === "index") {
|
|
67
|
-
const items = Array.from(_previewFiles.entries())
|
|
68
|
-
.sort((a, b) => b[1].createdAt - a[1].createdAt)
|
|
69
|
-
.map(([s, f]) => `<li style="margin:8px 0"><a href="/${s}" style="color:#4ade80;font-size:18px">${f.title || s}</a> <span style="color:#666;font-size:13px">${new Date(f.createdAt).toLocaleTimeString()}</span></li>`)
|
|
70
|
-
.join("");
|
|
71
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
72
|
-
res.end(`<!DOCTYPE html><html><head><title>Stitch Previews</title>
|
|
73
|
-
<style>body{font-family:-apple-system,sans-serif;background:#0a0a0a;color:#fff;padding:40px;max-width:600px;margin:0 auto}
|
|
74
|
-
a{text-decoration:none}a:hover{text-decoration:underline}h1{color:#4ade80}ul{list-style:none;padding:0}</style></head>
|
|
75
|
-
<body><h1>Stitch Previews</h1><ul>${items || "<li style='color:#666'>No previews yet</li>"}</ul></body></html>`);
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const entry = _previewFiles.get(slug);
|
|
80
|
-
if (!entry) {
|
|
81
|
-
res.writeHead(404, { "Content-Type": "text/html" });
|
|
82
|
-
res.end("<h1>Preview not found</h1><p><a href='/'>View all previews</a></p>");
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
res.writeHead(200, {
|
|
87
|
-
"Content-Type": "text/html",
|
|
88
|
-
"Cache-Control": "no-cache",
|
|
89
|
-
});
|
|
90
|
-
res.end(entry.html);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
server.listen(0, "127.0.0.1", () => {
|
|
94
|
-
_previewPort = server.address().port;
|
|
95
|
-
_previewServer = server;
|
|
96
|
-
console.log(` Stitch preview server running at http://localhost:${_previewPort}`);
|
|
97
|
-
resolve(_previewPort);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
server.on("error", (err) => {
|
|
101
|
-
console.error(" Stitch preview server error:", err.message);
|
|
102
|
-
reject(err);
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function addPreview(html, title) {
|
|
108
|
-
const slug = (title || "screen")
|
|
109
|
-
.toLowerCase()
|
|
110
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
111
|
-
.replace(/^-|-$/g, "")
|
|
112
|
-
.slice(0, 60)
|
|
113
|
-
+ "-" + Date.now().toString(36);
|
|
114
|
-
|
|
115
|
-
_previewFiles.set(slug, { html, title, createdAt: Date.now() });
|
|
116
|
-
|
|
117
|
-
// Also save to disk for persistence
|
|
118
|
-
try {
|
|
119
|
-
writeFileSync(join(PREVIEW_DIR, slug + ".html"), html);
|
|
120
|
-
} catch {}
|
|
121
|
-
|
|
122
|
-
return slug;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// ── SDK loader (ESM → CJS bridge) ─────────────────────────────────────────────
|
|
126
|
-
|
|
127
|
-
let _sdkPromise = null;
|
|
128
|
-
|
|
129
|
-
function loadSdk() {
|
|
130
|
-
if (!_sdkPromise) {
|
|
131
|
-
_sdkPromise = import("@google/stitch-sdk");
|
|
132
|
-
}
|
|
133
|
-
return _sdkPromise;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// ── Design context (design.md) persistence ─────────────────────────────────────
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Directory where per-project design.md files are stored.
|
|
140
|
-
* ~/.claude/stitch/<sanitized-project-name>/design.md
|
|
141
|
-
*/
|
|
142
|
-
const STITCH_DIR = join(homedir(), ".claude", "stitch");
|
|
143
|
-
|
|
144
|
-
function sanitizeName(name) {
|
|
145
|
-
return (name || "default").replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 60);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function getDesignMdPath(projectName) {
|
|
149
|
-
const dir = join(STITCH_DIR, sanitizeName(projectName));
|
|
150
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
151
|
-
return join(dir, "design.md");
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Load an existing design.md for a project. Returns null if not found.
|
|
156
|
-
*/
|
|
157
|
-
function loadDesignMd(projectName) {
|
|
158
|
-
const filePath = getDesignMdPath(projectName);
|
|
159
|
-
try {
|
|
160
|
-
if (existsSync(filePath)) return readFileSync(filePath, "utf8");
|
|
161
|
-
} catch {}
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Save design.md for a project.
|
|
167
|
-
*/
|
|
168
|
-
function saveDesignMd(projectName, content) {
|
|
169
|
-
const filePath = getDesignMdPath(projectName);
|
|
170
|
-
try {
|
|
171
|
-
writeFileSync(filePath, content, "utf8");
|
|
172
|
-
return filePath;
|
|
173
|
-
} catch (err) {
|
|
174
|
-
console.error(" ⚠ Could not save design.md:", err.message);
|
|
175
|
-
return null;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Build a design.md from the extract_design_context result.
|
|
181
|
-
* The result is typically structured JSON with colors, typography, spacing, etc.
|
|
182
|
-
*/
|
|
183
|
-
function buildDesignMd(context, projectName, screenPrompt, theme) {
|
|
184
|
-
const lines = [];
|
|
185
|
-
lines.push(`# Design System — ${projectName}`);
|
|
186
|
-
lines.push("");
|
|
187
|
-
lines.push(`> Extracted by Google Stitch from: "${screenPrompt}"`);
|
|
188
|
-
lines.push(`> Generated: ${new Date().toISOString().split("T")[0]}`);
|
|
189
|
-
lines.push("");
|
|
190
|
-
|
|
191
|
-
if (typeof context === "string") {
|
|
192
|
-
// If Stitch returned plain text/markdown, use it directly
|
|
193
|
-
lines.push(context);
|
|
194
|
-
|
|
195
|
-
// Append structured Figma Variables section from the theme's namedColors
|
|
196
|
-
// so the .md parser can create proper variable collections
|
|
197
|
-
if (theme?.namedColors && typeof theme.namedColors === "object") {
|
|
198
|
-
lines.push("");
|
|
199
|
-
lines.push("---");
|
|
200
|
-
lines.push("");
|
|
201
|
-
lines.push("## Primitives");
|
|
202
|
-
lines.push("");
|
|
203
|
-
lines.push("### Colors");
|
|
204
|
-
lines.push("");
|
|
205
|
-
for (const [name, hex] of Object.entries(theme.namedColors)) {
|
|
206
|
-
lines.push(`- **color/${name.replace(/_/g, "/")}**: \`${hex}\``);
|
|
207
|
-
}
|
|
208
|
-
lines.push("");
|
|
209
|
-
|
|
210
|
-
// Add fonts
|
|
211
|
-
if (theme.font || theme.bodyFont || theme.headlineFont) {
|
|
212
|
-
lines.push("### Strings");
|
|
213
|
-
lines.push("");
|
|
214
|
-
if (theme.font) lines.push(`- **fontFamily/primary**: \`${theme.font}\``);
|
|
215
|
-
if (theme.bodyFont && theme.bodyFont !== theme.font) lines.push(`- **fontFamily/body**: \`${theme.bodyFont}\``);
|
|
216
|
-
if (theme.headlineFont && theme.headlineFont !== theme.font) lines.push(`- **fontFamily/headline**: \`${theme.headlineFont}\``);
|
|
217
|
-
lines.push("");
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Add spacing scale
|
|
221
|
-
if (theme.spacingScale) {
|
|
222
|
-
lines.push("### spacing");
|
|
223
|
-
lines.push("");
|
|
224
|
-
const base = [0, 2, 4, 6, 8, 10, 12, 16, 20, 24, 32, 40, 48];
|
|
225
|
-
const names = ["0", "0.5", "1", "1.5", "2", "2.5", "3", "4", "5", "6", "8", "10", "12"];
|
|
226
|
-
base.forEach((val, i) => {
|
|
227
|
-
const scaled = Math.round(val * (theme.spacingScale || 1));
|
|
228
|
-
lines.push(`- **spacing/${names[i]}**: \`${scaled}px\``);
|
|
229
|
-
});
|
|
230
|
-
lines.push("");
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Semantic aliases for common roles
|
|
234
|
-
lines.push("## Semantic");
|
|
235
|
-
lines.push("");
|
|
236
|
-
lines.push("### Colors");
|
|
237
|
-
lines.push("");
|
|
238
|
-
const semanticMap = {
|
|
239
|
-
"color/action/primary": "color/primary",
|
|
240
|
-
"color/action/primary/hover": "color/primary/container",
|
|
241
|
-
"color/text/primary": "color/on/surface",
|
|
242
|
-
"color/text/secondary": "color/on/surface/variant",
|
|
243
|
-
"color/text/onAction": "color/on/primary",
|
|
244
|
-
"color/surface/default": "color/surface",
|
|
245
|
-
"color/surface/muted": "color/surface/container",
|
|
246
|
-
"color/border/default": "color/outline",
|
|
247
|
-
"color/border/muted": "color/outline/variant",
|
|
248
|
-
"color/action/danger": "color/error",
|
|
249
|
-
"color/action/success": "color/tertiary",
|
|
250
|
-
};
|
|
251
|
-
for (const [sem, prim] of Object.entries(semanticMap)) {
|
|
252
|
-
// Only add if the target primitive exists in namedColors
|
|
253
|
-
const primKey = prim.replace("color/", "").replace(/\//g, "_");
|
|
254
|
-
if (theme.namedColors[primKey]) {
|
|
255
|
-
lines.push(`- **${sem}**: → \`${prim}\``);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
lines.push("");
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
return lines.join("\n");
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Structured context — format as readable markdown
|
|
265
|
-
if (context.colors || context.colorPalette || context.palette) {
|
|
266
|
-
lines.push("## Colors");
|
|
267
|
-
lines.push("");
|
|
268
|
-
const colors = context.colors || context.colorPalette || context.palette;
|
|
269
|
-
if (Array.isArray(colors)) {
|
|
270
|
-
for (const c of colors) {
|
|
271
|
-
const name = c.name || c.role || c.label || "Color";
|
|
272
|
-
const value = c.value || c.hex || c.color || "";
|
|
273
|
-
lines.push(`- **${name}**: \`${value}\``);
|
|
274
|
-
}
|
|
275
|
-
} else if (typeof colors === "object") {
|
|
276
|
-
for (const [key, val] of Object.entries(colors)) {
|
|
277
|
-
lines.push(`- **${key}**: \`${typeof val === "object" ? val.hex || val.value || JSON.stringify(val) : val}\``);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
lines.push("");
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
if (context.typography || context.fonts || context.textStyles) {
|
|
284
|
-
lines.push("## Typography");
|
|
285
|
-
lines.push("");
|
|
286
|
-
const typo = context.typography || context.fonts || context.textStyles;
|
|
287
|
-
if (Array.isArray(typo)) {
|
|
288
|
-
for (const t of typo) {
|
|
289
|
-
const name = t.name || t.role || "Text";
|
|
290
|
-
const family = t.fontFamily || t.family || "";
|
|
291
|
-
const size = t.fontSize || t.size || "";
|
|
292
|
-
const weight = t.fontWeight || t.weight || "";
|
|
293
|
-
lines.push(`- **${name}**: ${family} ${size}${size ? "px" : ""} / ${weight}`);
|
|
294
|
-
}
|
|
295
|
-
} else if (typeof typo === "object") {
|
|
296
|
-
for (const [key, val] of Object.entries(typo)) {
|
|
297
|
-
lines.push(`- **${key}**: ${typeof val === "object" ? JSON.stringify(val) : val}`);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
lines.push("");
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
if (context.spacing || context.layout) {
|
|
304
|
-
lines.push("## Spacing & Layout");
|
|
305
|
-
lines.push("");
|
|
306
|
-
const sp = context.spacing || context.layout;
|
|
307
|
-
if (typeof sp === "object") {
|
|
308
|
-
for (const [key, val] of Object.entries(sp)) {
|
|
309
|
-
lines.push(`- **${key}**: ${typeof val === "object" ? JSON.stringify(val) : val}`);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
lines.push("");
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (context.components || context.patterns || context.componentPatterns) {
|
|
316
|
-
lines.push("## Component Patterns");
|
|
317
|
-
lines.push("");
|
|
318
|
-
const comps = context.components || context.patterns || context.componentPatterns;
|
|
319
|
-
if (Array.isArray(comps)) {
|
|
320
|
-
for (const c of comps) {
|
|
321
|
-
const name = c.name || c.type || "Component";
|
|
322
|
-
const desc = c.description || c.style || "";
|
|
323
|
-
lines.push(`- **${name}**: ${desc}`);
|
|
324
|
-
}
|
|
325
|
-
} else if (typeof comps === "object") {
|
|
326
|
-
for (const [key, val] of Object.entries(comps)) {
|
|
327
|
-
lines.push(`- **${key}**: ${typeof val === "object" ? JSON.stringify(val) : val}`);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
lines.push("");
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
if (context.borderRadius || context.radius || context.radii) {
|
|
334
|
-
lines.push("## Border Radius");
|
|
335
|
-
lines.push("");
|
|
336
|
-
const radii = context.borderRadius || context.radius || context.radii;
|
|
337
|
-
if (typeof radii === "object") {
|
|
338
|
-
for (const [key, val] of Object.entries(radii)) {
|
|
339
|
-
lines.push(`- **${key}**: ${val}`);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
lines.push("");
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
if (context.shadows || context.effects) {
|
|
346
|
-
lines.push("## Shadows & Effects");
|
|
347
|
-
lines.push("");
|
|
348
|
-
const effects = context.shadows || context.effects;
|
|
349
|
-
if (typeof effects === "object") {
|
|
350
|
-
for (const [key, val] of Object.entries(effects)) {
|
|
351
|
-
lines.push(`- **${key}**: ${typeof val === "object" ? JSON.stringify(val) : val}`);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
lines.push("");
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Dump any remaining top-level keys we didn't handle
|
|
358
|
-
const handled = new Set(["colors","colorPalette","palette","typography","fonts","textStyles","spacing","layout","components","patterns","componentPatterns","borderRadius","radius","radii","shadows","effects"]);
|
|
359
|
-
const remaining = Object.keys(context).filter(k => !handled.has(k));
|
|
360
|
-
if (remaining.length > 0) {
|
|
361
|
-
lines.push("## Additional Properties");
|
|
362
|
-
lines.push("");
|
|
363
|
-
for (const key of remaining) {
|
|
364
|
-
const val = context[key];
|
|
365
|
-
if (val !== null && val !== undefined) {
|
|
366
|
-
lines.push(`- **${key}**: ${typeof val === "object" ? JSON.stringify(val) : val}`);
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
lines.push("");
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
return lines.join("\n");
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// ── GCP project auto-detection ──────────────────────────────────────────────
|
|
376
|
-
|
|
377
|
-
/**
|
|
378
|
-
* Auto-detect the user's Google Cloud project ID from their account.
|
|
379
|
-
* Queries the Cloud Resource Manager API and returns the first active project.
|
|
380
|
-
*/
|
|
381
|
-
function detectGcpProject(accessToken) {
|
|
382
|
-
return new Promise((resolve) => {
|
|
383
|
-
https.get("https://cloudresourcemanager.googleapis.com/v1/projects?pageSize=5", {
|
|
384
|
-
headers: { "Authorization": `Bearer ${accessToken}` },
|
|
385
|
-
}, (res) => {
|
|
386
|
-
let data = "";
|
|
387
|
-
res.on("data", (c) => { data += c; });
|
|
388
|
-
res.on("end", () => {
|
|
389
|
-
try {
|
|
390
|
-
const json = JSON.parse(data);
|
|
391
|
-
const active = (json.projects || []).find(p => p.lifecycleState === "ACTIVE");
|
|
392
|
-
if (active) {
|
|
393
|
-
console.log(` Stitch: auto-detected GCP project: ${active.projectId}`);
|
|
394
|
-
resolve(active.projectId);
|
|
395
|
-
} else {
|
|
396
|
-
resolve("figma-plugin");
|
|
397
|
-
}
|
|
398
|
-
} catch {
|
|
399
|
-
resolve("figma-plugin");
|
|
400
|
-
}
|
|
401
|
-
});
|
|
402
|
-
}).on("error", () => resolve("figma-plugin"));
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// ── Intent detection ────────────────────────────────────────────────────────────
|
|
407
|
-
|
|
408
|
-
function detectStitchIntent(message) {
|
|
409
|
-
const m = message.toLowerCase();
|
|
410
|
-
|
|
411
|
-
// List projects — broad patterns
|
|
412
|
-
if (/list\s+(all\s+)?(my\s+)?(stitch\s+)?projects|show\s+(all\s+)?(me\s+)?(my\s+)?(stitch\s+)?projects|stitch\s+projects|what\s+projects|my\s+projects/.test(m))
|
|
413
|
-
return { type: "list_projects" };
|
|
414
|
-
|
|
415
|
-
// Use/convert an existing screen to something (React, webpage, Figma, interactive, etc.)
|
|
416
|
-
// Key signal: user mentions "screen" + an action/conversion word
|
|
417
|
-
// Catches: "use my stitch screen", "create interactive web with the selected screen",
|
|
418
|
-
// "convert stitch to react", "make the screen interactive", "web app from my screen", etc.
|
|
419
|
-
if (/use\s+(my|the|this|that|selected|current|latest)?\s*(stitch\s+)?screen/.test(m) ||
|
|
420
|
-
/from\s+(my\s+)?stitch.*(?:create|build|make|convert|generate)/.test(m) ||
|
|
421
|
-
/(?:create|build|make|convert|generate)\b.*\b(?:from|using|with)\s+(?:my\s+)?(?:the\s+)?(?:stitch\s+)?(?:selected\s+)?screen/.test(m) ||
|
|
422
|
-
/(?:with|from|using)\s+(?:the\s+)?(?:my\s+)?(?:selected\s+|current\s+|latest\s+)?(?:stitch\s+)?screen/.test(m) ||
|
|
423
|
-
/stitch\s+(?:screen\s+)?(?:to|into)\s+(?:react|figma|webpage|html|code|web|interactive)/.test(m) ||
|
|
424
|
-
/(?:screen|design)\s+(?:i|I)\s+(?:selected|created|made|have)/.test(m) ||
|
|
425
|
-
/take\s+(?:the|my|this)?\s*(?:stitch\s+)?screen/.test(m) ||
|
|
426
|
-
/(?:selected|current|latest)\s+screen\b/.test(m) ||
|
|
427
|
-
/\bscreen\b.*(?:interactive|react|web\s*app|webpage|website|convert|code)/.test(m) ||
|
|
428
|
-
/(?:interactive|react|web\s*app|webpage|website|convert)\b.*\bscreen\b/.test(m))
|
|
429
|
-
return { type: "get_screen", screenHint: extractScreenHint(m), projectHint: extractProjectHint(m), wantsCode: detectCodeIntent(m) };
|
|
430
|
-
|
|
431
|
-
// Get screen HTML / fetch specific screen by name
|
|
432
|
-
if (/get\s+(the\s+)?html|screen\s+code|fetch\s+screen|download\s+screen|html\s+from\s+screen/.test(m))
|
|
433
|
-
return { type: "get_screen", screenHint: extractScreenHint(m), projectHint: extractProjectHint(m), wantsCode: detectCodeIntent(m) };
|
|
434
|
-
|
|
435
|
-
// Show screenshot / preview — check BEFORE list_screens
|
|
436
|
-
if (/show\s+screenshot|preview\s+screen|screen\s+image|screenshot\s+of/.test(m))
|
|
437
|
-
return { type: "get_screen_image", screenHint: extractScreenHint(m), projectHint: extractProjectHint(m) };
|
|
438
|
-
|
|
439
|
-
// List screens in a project
|
|
440
|
-
if (/list\s+(all\s+)?screens|show\s+(all\s+)?screens|screens?\s+(in|of|from)\s+/.test(m))
|
|
441
|
-
return { type: "list_screens", projectHint: extractProjectHint(m) };
|
|
442
|
-
|
|
443
|
-
// Default: generation
|
|
444
|
-
return { type: "generate" };
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
/**
|
|
448
|
-
* Detect if the user wants code output (React, HTML, etc.) vs Figma frames.
|
|
449
|
-
*/
|
|
450
|
-
function detectCodeIntent(message) {
|
|
451
|
-
if (/react|component|webpage|web\s*page|web\s*app|website|html\s+code|code|next\.?js|typescript|jsx|tsx|interactive\s+web|interactive\s+page|interactive\s+app|live\s+preview|preview\s+link|run\s+locally|localhost|interactive\b.*\b(?:using|with|from|create|build|make|generate)/.test(message))
|
|
452
|
-
return "react";
|
|
453
|
-
return null; // default: Figma frames
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
function extractProjectHint(message) {
|
|
457
|
-
// Match: "in project X", "from project X", "project named X", "in X project"
|
|
458
|
-
let match = message.match(/(?:in|from|of)\s+(?:project\s+)?["']?([^"',\n]+?)["']?\s*(?:project)?(?:\s*$|\s+(?:to|and|screen|create|get|show|list))/i);
|
|
459
|
-
if (match) { const h = stripArticles(match[1].trim()); if (h) return h; }
|
|
460
|
-
match = message.match(/project\s+(?:named?\s+)?["']?([^"',\n]+?)["']?(?:\s*$|\s+(?:to|and|screen|create|get|show))/i);
|
|
461
|
-
if (match) { const h = stripArticles(match[1].trim()); if (h) return h; }
|
|
462
|
-
return null;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
/**
|
|
466
|
-
* Strip leading articles (the, my, this, that, a, an) from hints.
|
|
467
|
-
*/
|
|
468
|
-
function stripArticles(s) {
|
|
469
|
-
// Remove leading articles and also filter out "stitch" (provider name, not a project)
|
|
470
|
-
return s.replace(/^(the|my|this|that|a|an)\s+/i, "").replace(/^stitch\s*/i, "").trim();
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
/**
|
|
474
|
-
* Get a human-readable label for a screen.
|
|
475
|
-
* The API often only returns a `name` like "projects/123/screens/abc" with no displayName.
|
|
476
|
-
*/
|
|
477
|
-
function getScreenLabel(screen, index) {
|
|
478
|
-
const d = screen.data || {};
|
|
479
|
-
if (d.displayName) return d.displayName;
|
|
480
|
-
if (d.title) return d.title;
|
|
481
|
-
// Extract short ID from name like "projects/123/screens/abc..."
|
|
482
|
-
const id = screen.id || screen.screenId || "";
|
|
483
|
-
return `Screen ${typeof index === "number" ? index : 1}` + (id ? ` (${id.slice(0, 8)}...)` : "");
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
function extractScreenHint(message) {
|
|
487
|
-
// Match: "screen X", "the X screen", "screen named X", "use X"
|
|
488
|
-
let match = message.match(/(?:screen\s+(?:named?\s+)?|use\s+(?:the\s+)?)["']?([^"',\n]+?)["']?\s*(?:screen)?(?:\s*$|\s+(?:to|from|in|and|create))/i);
|
|
489
|
-
if (match) return match[1].trim();
|
|
490
|
-
return null;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
async function findProject(stitchApi, hint) {
|
|
494
|
-
const projects = await stitchApi.projects();
|
|
495
|
-
if (!hint) return { projects, matched: null };
|
|
496
|
-
|
|
497
|
-
const lower = stripArticles(hint).toLowerCase();
|
|
498
|
-
const matched = projects.find(p => {
|
|
499
|
-
const title = (p.data?.title || p.data?.displayName || "").toLowerCase();
|
|
500
|
-
// Bidirectional: "beehive" matches "Beehive Project" and vice versa
|
|
501
|
-
return title === lower || title.includes(lower) || lower.includes(title) || p.id === hint || p.projectId === hint;
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
return { projects, matched };
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
/**
|
|
508
|
-
* List screens for a project — bypasses SDK's project.screens() to handle
|
|
509
|
-
* the raw MCP response directly and avoid crashes on unexpected formats.
|
|
510
|
-
*/
|
|
511
|
-
async function listScreensDirect(client, projectId) {
|
|
512
|
-
try {
|
|
513
|
-
const raw = await client.callTool("list_screens", { projectId });
|
|
514
|
-
console.log(" Stitch list_screens raw:", JSON.stringify(raw)?.slice(0, 300));
|
|
515
|
-
if (!raw) return [];
|
|
516
|
-
const arr = raw.screens || raw.items || (Array.isArray(raw) ? raw : []);
|
|
517
|
-
return arr;
|
|
518
|
-
} catch (err) {
|
|
519
|
-
console.error(" Stitch: list_screens failed:", err.message);
|
|
520
|
-
return [];
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
async function findScreen(client, projectId, hint) {
|
|
525
|
-
const screenDataList = await listScreensDirect(client, projectId);
|
|
526
|
-
|
|
527
|
-
// Wrap raw screen data into objects with a consistent shape
|
|
528
|
-
const screens = screenDataList.map(s => ({
|
|
529
|
-
data: s,
|
|
530
|
-
id: s.id || (s.name ? s.name.split("/screens/").pop() : undefined),
|
|
531
|
-
screenId: s.id || (s.name ? s.name.split("/screens/").pop() : undefined),
|
|
532
|
-
projectId,
|
|
533
|
-
// Keep raw data for getHtml/getImage later
|
|
534
|
-
_raw: s,
|
|
535
|
-
}));
|
|
536
|
-
|
|
537
|
-
if (!hint) return { screens, matched: null };
|
|
538
|
-
|
|
539
|
-
const lower = stripArticles(hint).toLowerCase();
|
|
540
|
-
const matched = screens.find(s => {
|
|
541
|
-
const name = (s.data?.displayName || s.data?.name || s.data?.title || "").toLowerCase();
|
|
542
|
-
return name === lower || name.includes(lower) || lower.includes(name) || s.id === hint || s.screenId === hint;
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
return { screens, matched };
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
// ── Query handlers ──────────────────────────────────────────────────────────────
|
|
549
|
-
|
|
550
|
-
async function handleListProjects(stitchApi, requestId, onEvent) {
|
|
551
|
-
onEvent({ type: "phase_start", id: requestId, phase: "Listing Stitch projects..." });
|
|
552
|
-
onEvent({ type: "tool_start", id: requestId, tool: "list_projects" });
|
|
553
|
-
|
|
554
|
-
const projects = await stitchApi.projects();
|
|
555
|
-
|
|
556
|
-
onEvent({ type: "tool_done", id: requestId, tool: "list_projects", isError: false });
|
|
557
|
-
|
|
558
|
-
if (projects.length === 0) {
|
|
559
|
-
const msg = "You don't have any Stitch projects yet. Ask me to create a screen and I'll set one up!\n";
|
|
560
|
-
onEvent({ type: "text_delta", id: requestId, delta: msg });
|
|
561
|
-
onEvent({ type: "done", id: requestId, fullText: msg });
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
const lines = [`Your Stitch Projects (${projects.length}):\n\n`];
|
|
566
|
-
for (let i = 0; i < projects.length; i++) {
|
|
567
|
-
const p = projects[i];
|
|
568
|
-
const title = p.data?.title || p.data?.displayName || `Project ${i + 1}`;
|
|
569
|
-
const id = p.id || p.projectId || "unknown";
|
|
570
|
-
const created = p.data?.createTime ? new Date(p.data.createTime).toLocaleDateString() : "";
|
|
571
|
-
lines.push(`${i + 1}. **${title}** (ID: ${id})${created ? `\n Created: ${created}` : ""}\n`);
|
|
572
|
-
}
|
|
573
|
-
lines.push(`\nSay "list screens in [project name]" to see screens in a project.\n`);
|
|
574
|
-
|
|
575
|
-
const msg = lines.join("");
|
|
576
|
-
onEvent({ type: "text_delta", id: requestId, delta: msg });
|
|
577
|
-
onEvent({ type: "done", id: requestId, fullText: msg });
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
async function handleListScreens(stitchApi, client, projectHint, requestId, onEvent) {
|
|
581
|
-
onEvent({ type: "phase_start", id: requestId, phase: "Finding project..." });
|
|
582
|
-
|
|
583
|
-
const { projects, matched } = await findProject(stitchApi, projectHint);
|
|
584
|
-
|
|
585
|
-
if (!matched) {
|
|
586
|
-
if (!projectHint) {
|
|
587
|
-
const names = projects.map(p => p.data?.title || p.data?.displayName || p.id).join(", ");
|
|
588
|
-
const msg = `Which project? Your projects: ${names}\n\nSay "list screens in [project name]"\n`;
|
|
589
|
-
onEvent({ type: "text_delta", id: requestId, delta: msg });
|
|
590
|
-
onEvent({ type: "done", id: requestId, fullText: msg });
|
|
591
|
-
return;
|
|
592
|
-
}
|
|
593
|
-
const msg = `Could not find project "${projectHint}". Your projects:\n` +
|
|
594
|
-
projects.map(p => `- ${p.data?.title || p.data?.displayName || p.id}`).join("\n") + "\n";
|
|
595
|
-
onEvent({ type: "text_delta", id: requestId, delta: msg });
|
|
596
|
-
onEvent({ type: "done", id: requestId, fullText: msg });
|
|
597
|
-
return;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
const projectTitle = matched.data?.title || matched.data?.displayName || matched.id;
|
|
601
|
-
onEvent({ type: "phase_start", id: requestId, phase: `Listing screens in "${projectTitle}"...` });
|
|
602
|
-
onEvent({ type: "tool_start", id: requestId, tool: "list_screens" });
|
|
603
|
-
|
|
604
|
-
const { screens } = await findScreen(client, matched.projectId || matched.id, null);
|
|
605
|
-
|
|
606
|
-
onEvent({ type: "tool_done", id: requestId, tool: "list_screens", isError: false });
|
|
607
|
-
|
|
608
|
-
if (screens.length === 0) {
|
|
609
|
-
const msg = `No screens in "${projectTitle}" yet. Ask me to generate one!\n`;
|
|
610
|
-
onEvent({ type: "text_delta", id: requestId, delta: msg });
|
|
611
|
-
onEvent({ type: "done", id: requestId, fullText: msg });
|
|
612
|
-
return;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
const lines = [`Screens in "${projectTitle}" (${screens.length}):\n\n`];
|
|
616
|
-
for (let i = 0; i < screens.length; i++) {
|
|
617
|
-
const s = screens[i];
|
|
618
|
-
const name = getScreenLabel(s, i + 1);
|
|
619
|
-
const id = s.id || s.screenId || "unknown";
|
|
620
|
-
lines.push(`${i + 1}. **${name}** (ID: ${id.slice(0, 12)})\n`);
|
|
621
|
-
}
|
|
622
|
-
lines.push(`\nSay "use screen [name] from ${projectTitle}" to create a Figma frame from it.\n`);
|
|
623
|
-
lines.push(`Or "get HTML from screen [name] in ${projectTitle}" to get the raw code.\n`);
|
|
624
|
-
|
|
625
|
-
const msg = lines.join("");
|
|
626
|
-
onEvent({ type: "text_delta", id: requestId, delta: msg });
|
|
627
|
-
onEvent({ type: "done", id: requestId, fullText: msg });
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
async function handleGetScreen(stitchApi, client, projectHint, screenHint, requestId, onEvent, deviceType, wantsCode) {
|
|
631
|
-
onEvent({ type: "phase_start", id: requestId, phase: "Finding project and screen..." });
|
|
632
|
-
|
|
633
|
-
const { projects, matched: matchedProject } = await findProject(stitchApi, projectHint);
|
|
634
|
-
|
|
635
|
-
if (projects.length === 0) {
|
|
636
|
-
const msg = "No Stitch projects found. Create a screen first!\n";
|
|
637
|
-
onEvent({ type: "text_delta", id: requestId, delta: msg });
|
|
638
|
-
onEvent({ type: "done", id: requestId, fullText: msg });
|
|
639
|
-
return;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
if (matchedProject) {
|
|
643
|
-
return handleGetScreenFromProject(matchedProject, stitchApi, client, screenHint, requestId, onEvent, deviceType, wantsCode);
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
// No exact project match — search ALL projects for the screen
|
|
647
|
-
// The user might have said the screen name (e.g. "Beehive Management Dashboard")
|
|
648
|
-
// which could be in any project, or the project hint itself might be the screen name
|
|
649
|
-
const combinedHint = screenHint || projectHint;
|
|
650
|
-
onEvent({ type: "phase_start", id: requestId, phase: "Searching all projects for screen..." });
|
|
651
|
-
|
|
652
|
-
for (const proj of projects) {
|
|
653
|
-
const projId = proj.projectId || proj.id;
|
|
654
|
-
try {
|
|
655
|
-
const { screens, matched: matchedScreen } = await findScreen(client, projId, combinedHint);
|
|
656
|
-
if (matchedScreen) {
|
|
657
|
-
const projectTitle = proj.data?.title || proj.data?.displayName || proj.id;
|
|
658
|
-
onEvent({ type: "text_delta", id: requestId, delta: `Found screen in "${projectTitle}"\n\n` });
|
|
659
|
-
return fetchAndProcessScreen(client, matchedScreen, projectTitle, requestId, onEvent, deviceType, wantsCode);
|
|
660
|
-
}
|
|
661
|
-
// If no hint but project has screens, use the latest
|
|
662
|
-
if (!combinedHint && screens.length > 0) {
|
|
663
|
-
const latest = screens[screens.length - 1];
|
|
664
|
-
const projectTitle = proj.data?.title || proj.data?.displayName || proj.id;
|
|
665
|
-
const latestName = getScreenLabel(latest, screens.length);
|
|
666
|
-
onEvent({ type: "text_delta", id: requestId, delta: `Auto-selecting latest screen: "${latestName}" from "${projectTitle}"\n\n` });
|
|
667
|
-
return fetchAndProcessScreen(client, latest, projectTitle, requestId, onEvent, deviceType, wantsCode);
|
|
668
|
-
}
|
|
669
|
-
} catch {}
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
// Nothing found — try the first project with any screens as fallback
|
|
673
|
-
for (const proj of projects) {
|
|
674
|
-
const projId = proj.projectId || proj.id;
|
|
675
|
-
try {
|
|
676
|
-
const { screens } = await findScreen(client, projId, null);
|
|
677
|
-
if (screens.length > 0) {
|
|
678
|
-
return handleGetScreenFromProject(proj, stitchApi, client, screenHint, requestId, onEvent, deviceType, wantsCode);
|
|
679
|
-
}
|
|
680
|
-
} catch {}
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
const names = projects.map(p => p.data?.title || p.data?.displayName || p.id).join(", ");
|
|
684
|
-
const msg = `No screens found in any project. Your projects: ${names}\nGenerate a screen first!\n`;
|
|
685
|
-
onEvent({ type: "text_delta", id: requestId, delta: msg });
|
|
686
|
-
onEvent({ type: "done", id: requestId, fullText: msg });
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
async function handleGetScreenFromProject(project, stitchApi, client, screenHint, requestId, onEvent, deviceType, wantsCode) {
|
|
690
|
-
const projectTitle = project.data?.title || project.data?.displayName || project.id;
|
|
691
|
-
const projId = project.projectId || project.id;
|
|
692
|
-
|
|
693
|
-
onEvent({ type: "phase_start", id: requestId, phase: `Looking up screens in "${projectTitle}"...` });
|
|
694
|
-
onEvent({ type: "tool_start", id: requestId, tool: "list_screens" });
|
|
695
|
-
|
|
696
|
-
const { screens, matched: matchedScreen } = await findScreen(client, projId, screenHint);
|
|
697
|
-
|
|
698
|
-
onEvent({ type: "tool_done", id: requestId, tool: "list_screens", isError: false });
|
|
699
|
-
|
|
700
|
-
if (!matchedScreen) {
|
|
701
|
-
if (screens.length === 0) {
|
|
702
|
-
const msg = `No screens in "${projectTitle}". Generate one first!\n`;
|
|
703
|
-
onEvent({ type: "text_delta", id: requestId, delta: msg });
|
|
704
|
-
onEvent({ type: "done", id: requestId, fullText: msg });
|
|
705
|
-
return;
|
|
706
|
-
}
|
|
707
|
-
// Auto-pick the latest (last) screen — user said "selected/the/my screen"
|
|
708
|
-
// or hint didn't match any screen name, so just use the most recent one
|
|
709
|
-
const latest = screens[screens.length - 1];
|
|
710
|
-
const latestName = getScreenLabel(latest, screens.length);
|
|
711
|
-
onEvent({ type: "text_delta", id: requestId, delta: `Auto-selecting latest screen: "${latestName}"\n\n` });
|
|
712
|
-
return fetchAndProcessScreen(client, latest, projectTitle, requestId, onEvent, deviceType, wantsCode);
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
return fetchAndProcessScreen(client, matchedScreen, projectTitle, requestId, onEvent, deviceType, wantsCode);
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
async function fetchAndProcessScreen(client, screen, projectTitle, requestId, onEvent, deviceType, wantsCode) {
|
|
719
|
-
const screenName = getScreenLabel(screen, 1);
|
|
720
|
-
const projId = screen.projectId;
|
|
721
|
-
const scrId = screen.screenId || screen.id;
|
|
722
|
-
|
|
723
|
-
onEvent({ type: "phase_start", id: requestId, phase: `Fetching HTML for "${screenName}"...` });
|
|
724
|
-
onEvent({ type: "tool_start", id: requestId, tool: "get_screen" });
|
|
725
|
-
|
|
726
|
-
// Fetch screen details directly via MCP tool (bypass SDK wrapper)
|
|
727
|
-
let htmlUrl = null;
|
|
728
|
-
let imageUrl = null;
|
|
729
|
-
|
|
730
|
-
// Check if cached data already has URLs
|
|
731
|
-
if (screen.data?.htmlCode?.downloadUrl) {
|
|
732
|
-
htmlUrl = screen.data.htmlCode.downloadUrl;
|
|
733
|
-
}
|
|
734
|
-
if (screen.data?.screenshot?.downloadUrl) {
|
|
735
|
-
imageUrl = screen.data.screenshot.downloadUrl;
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
// If no cached URLs, fetch via get_screen tool
|
|
739
|
-
if (!htmlUrl) {
|
|
740
|
-
try {
|
|
741
|
-
const raw = await client.callTool("get_screen", {
|
|
742
|
-
projectId: projId,
|
|
743
|
-
screenId: scrId,
|
|
744
|
-
name: `projects/${projId}/screens/${scrId}`,
|
|
745
|
-
});
|
|
746
|
-
console.log(" Stitch get_screen raw:", JSON.stringify(raw)?.slice(0, 400));
|
|
747
|
-
if (raw) {
|
|
748
|
-
htmlUrl = raw.htmlCode?.downloadUrl || raw.html?.downloadUrl || null;
|
|
749
|
-
imageUrl = imageUrl || raw.screenshot?.downloadUrl || null;
|
|
750
|
-
}
|
|
751
|
-
} catch (err) {
|
|
752
|
-
console.error(" Stitch: get_screen failed:", err.message);
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
onEvent({ type: "tool_done", id: requestId, tool: "get_screen", isError: false });
|
|
757
|
-
|
|
758
|
-
if (!htmlUrl) {
|
|
759
|
-
const msg = `Screen "${screenName}" has no HTML available. Raw screen data logged to console.\n`;
|
|
760
|
-
onEvent({ type: "text_delta", id: requestId, delta: msg });
|
|
761
|
-
onEvent({ type: "done", id: requestId, fullText: msg });
|
|
762
|
-
return;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
const html = await fetchUrl(htmlUrl);
|
|
766
|
-
|
|
767
|
-
if (!html || html.length < 20) {
|
|
768
|
-
const msg = `Screen "${screenName}" returned empty HTML.\n`;
|
|
769
|
-
onEvent({ type: "text_delta", id: requestId, delta: msg });
|
|
770
|
-
onEvent({ type: "done", id: requestId, fullText: msg });
|
|
771
|
-
return;
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
if (wantsCode === "react") {
|
|
775
|
-
// ── Code + live preview mode ──
|
|
776
|
-
onEvent({
|
|
777
|
-
type: "phase_start",
|
|
778
|
-
id: requestId,
|
|
779
|
-
phase: "Starting live preview...",
|
|
780
|
-
});
|
|
781
|
-
|
|
782
|
-
try {
|
|
783
|
-
const port = await ensurePreviewServer();
|
|
784
|
-
const slug = addPreview(html, screenName);
|
|
785
|
-
const previewUrl = `http://localhost:${port}/${slug}`;
|
|
786
|
-
|
|
787
|
-
const reactCode = htmlToReactComponent(html, screenName);
|
|
788
|
-
const componentName = pascalCase(screenName) || "StitchScreen";
|
|
789
|
-
|
|
790
|
-
onEvent({
|
|
791
|
-
type: "text_delta",
|
|
792
|
-
id: requestId,
|
|
793
|
-
delta: `Fetched "${screenName}" from "${projectTitle}"\n` +
|
|
794
|
-
(imageUrl ? `Screenshot: ${imageUrl}\n\n` : "\n") +
|
|
795
|
-
`**Live Preview:** [${previewUrl}](${previewUrl})\n` +
|
|
796
|
-
`Open in your browser to see the interactive page.\n\n` +
|
|
797
|
-
`React component available for download:\n\n` +
|
|
798
|
-
"```download:" + componentName + ".tsx\n" + reactCode + "\n```\n\n",
|
|
799
|
-
});
|
|
800
|
-
onEvent({ type: "done", id: requestId, fullText: `Live preview at ${previewUrl}` });
|
|
801
|
-
} catch (err) {
|
|
802
|
-
const reactCode = htmlToReactComponent(html, screenName);
|
|
803
|
-
const componentName = pascalCase(screenName) || "StitchScreen";
|
|
804
|
-
onEvent({
|
|
805
|
-
type: "text_delta",
|
|
806
|
-
id: requestId,
|
|
807
|
-
delta: `Fetched "${screenName}" from "${projectTitle}"\n\n` +
|
|
808
|
-
"```download:" + componentName + ".tsx\n" + reactCode + "\n```\n",
|
|
809
|
-
});
|
|
810
|
-
onEvent({ type: "done", id: requestId, fullText: `React component — ${reactCode.length} chars` });
|
|
811
|
-
}
|
|
812
|
-
} else {
|
|
813
|
-
// ── Figma output mode: convert to native frames ──
|
|
814
|
-
onEvent({
|
|
815
|
-
type: "text_delta",
|
|
816
|
-
id: requestId,
|
|
817
|
-
delta: `Fetched "${screenName}" from "${projectTitle}" (${html.length} chars)\n` +
|
|
818
|
-
(imageUrl ? `Screenshot: ${imageUrl}\n\n` : "\n") +
|
|
819
|
-
`Converting to Figma frame...\n\n`,
|
|
820
|
-
});
|
|
821
|
-
processStitchHtml(html, "", requestId, screenName, deviceType, onEvent);
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
async function handleGetScreenImage(stitchApi, client, projectHint, screenHint, requestId, onEvent) {
|
|
826
|
-
onEvent({ type: "phase_start", id: requestId, phase: "Finding screen..." });
|
|
827
|
-
|
|
828
|
-
const { projects, matched: matchedProject } = await findProject(stitchApi, projectHint);
|
|
829
|
-
const project = matchedProject || (projects.length === 1 ? projects[0] : null);
|
|
830
|
-
|
|
831
|
-
if (!project) {
|
|
832
|
-
const names = projects.map(p => p.data?.title || p.data?.displayName || p.id).join(", ");
|
|
833
|
-
const msg = `Which project? Your projects: ${names}\n`;
|
|
834
|
-
onEvent({ type: "text_delta", id: requestId, delta: msg });
|
|
835
|
-
onEvent({ type: "done", id: requestId, fullText: msg });
|
|
836
|
-
return;
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
const projId = project.projectId || project.id;
|
|
840
|
-
const { screens, matched: matchedScreen } = await findScreen(client, projId, screenHint);
|
|
841
|
-
const screen = matchedScreen || (screens.length === 1 ? screens[0] : null);
|
|
842
|
-
|
|
843
|
-
if (!screen) {
|
|
844
|
-
const names = screens.map((s, i) => getScreenLabel(s, i + 1)).join(", ");
|
|
845
|
-
const msg = `Which screen? Available: ${names}\n`;
|
|
846
|
-
onEvent({ type: "text_delta", id: requestId, delta: msg });
|
|
847
|
-
onEvent({ type: "done", id: requestId, fullText: msg });
|
|
848
|
-
return;
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
const scrId = screen.screenId || screen.id;
|
|
852
|
-
onEvent({ type: "tool_start", id: requestId, tool: "get_screen" });
|
|
853
|
-
|
|
854
|
-
let imageUrl = screen.data?.screenshot?.downloadUrl || null;
|
|
855
|
-
if (!imageUrl) {
|
|
856
|
-
try {
|
|
857
|
-
const raw = await client.callTool("get_screen", { projectId: projId, screenId: scrId, name: `projects/${projId}/screens/${scrId}` });
|
|
858
|
-
imageUrl = raw?.screenshot?.downloadUrl || null;
|
|
859
|
-
} catch {}
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
onEvent({ type: "tool_done", id: requestId, tool: "get_screen", isError: false });
|
|
863
|
-
|
|
864
|
-
const screenName = getScreenLabel(screen, 1);
|
|
865
|
-
const msg = imageUrl
|
|
866
|
-
? `Screenshot of "${screenName}":\n${imageUrl}\n`
|
|
867
|
-
: `No screenshot available for "${screenName}".\n`;
|
|
868
|
-
|
|
869
|
-
onEvent({ type: "text_delta", id: requestId, delta: msg });
|
|
870
|
-
onEvent({ type: "done", id: requestId, fullText: msg });
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
// ── HTML → React conversion ─────────────────────────────────────────────────────
|
|
874
|
-
|
|
875
|
-
function pascalCase(str) {
|
|
876
|
-
return (str || "Screen")
|
|
877
|
-
.replace(/[^a-zA-Z0-9\s]/g, " ")
|
|
878
|
-
.split(/\s+/)
|
|
879
|
-
.filter(Boolean)
|
|
880
|
-
.map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
|
881
|
-
.join("");
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
/**
|
|
885
|
-
* Convert Stitch-generated Tailwind HTML into a React functional component.
|
|
886
|
-
* Handles: class→className, for→htmlFor, style strings→objects,
|
|
887
|
-
* self-closing tags, inline event handlers, etc.
|
|
888
|
-
*/
|
|
889
|
-
function htmlToReactComponent(html, screenName) {
|
|
890
|
-
const componentName = pascalCase(screenName) || "StitchScreen";
|
|
891
|
-
|
|
892
|
-
// Extract just the <body> content if it's a full HTML document
|
|
893
|
-
let body = html;
|
|
894
|
-
const bodyMatch = html.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
|
|
895
|
-
if (bodyMatch) body = bodyMatch[1].trim();
|
|
896
|
-
|
|
897
|
-
// Extract <style> or <link> tags for reference
|
|
898
|
-
const styles = [];
|
|
899
|
-
html.replace(/<style[^>]*>([\s\S]*?)<\/style>/gi, (_, css) => { styles.push(css.trim()); });
|
|
900
|
-
|
|
901
|
-
// Convert HTML attributes to JSX
|
|
902
|
-
let jsx = body
|
|
903
|
-
// class → className
|
|
904
|
-
.replace(/\bclass="/g, 'className="')
|
|
905
|
-
.replace(/\bclass='/g, "className='")
|
|
906
|
-
// for → htmlFor
|
|
907
|
-
.replace(/\bfor="/g, 'htmlFor="')
|
|
908
|
-
// Self-close void elements: <img ...>, <input ...>, <br>, <hr>, etc.
|
|
909
|
-
.replace(/<(img|input|br|hr|meta|link|source|area|embed|col|wbr)(\s[^>]*?)?\s*\/?>/gi,
|
|
910
|
-
(_, tag, attrs) => `<${tag}${attrs || ""} />`)
|
|
911
|
-
// tabindex → tabIndex
|
|
912
|
-
.replace(/\btabindex=/g, "tabIndex=")
|
|
913
|
-
// autocomplete → autoComplete
|
|
914
|
-
.replace(/\bautocomplete=/g, "autoComplete=")
|
|
915
|
-
// maxlength → maxLength
|
|
916
|
-
.replace(/\bmaxlength=/g, "maxLength=")
|
|
917
|
-
// Convert inline style="..." strings to style objects
|
|
918
|
-
.replace(/\bstyle="([^"]*)"/g, (_, styleStr) => {
|
|
919
|
-
const obj = styleStr.split(";").filter(Boolean).map(s => {
|
|
920
|
-
const [prop, ...valParts] = s.split(":");
|
|
921
|
-
if (!prop || !valParts.length) return null;
|
|
922
|
-
const camelProp = prop.trim().replace(/-([a-z])/g, (__, c) => c.toUpperCase());
|
|
923
|
-
const val = valParts.join(":").trim();
|
|
924
|
-
// Numbers without units
|
|
925
|
-
const numVal = /^-?\d+(\.\d+)?$/.test(val) ? val : `"${val}"`;
|
|
926
|
-
return `${camelProp}: ${numVal}`;
|
|
927
|
-
}).filter(Boolean).join(", ");
|
|
928
|
-
return `style={{${obj}}}`;
|
|
929
|
-
})
|
|
930
|
-
// Remove onclick/onchange etc. (can't convert inline JS reliably)
|
|
931
|
-
.replace(/\bon\w+="[^"]*"/g, "");
|
|
932
|
-
|
|
933
|
-
// Build the component
|
|
934
|
-
const lines = [];
|
|
935
|
-
lines.push(`import React from "react";`);
|
|
936
|
-
lines.push(``);
|
|
937
|
-
|
|
938
|
-
// If there were inline styles, include them as a <style> in the component
|
|
939
|
-
if (styles.length > 0) {
|
|
940
|
-
lines.push(`const inlineStyles = \`${styles.join("\n")}\`;`);
|
|
941
|
-
lines.push(``);
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
lines.push(`export default function ${componentName}() {`);
|
|
945
|
-
lines.push(` return (`);
|
|
946
|
-
|
|
947
|
-
if (styles.length > 0) {
|
|
948
|
-
lines.push(` <>`);
|
|
949
|
-
lines.push(` <style dangerouslySetInnerHTML={{ __html: inlineStyles }} />`);
|
|
950
|
-
lines.push(` ${jsx}`);
|
|
951
|
-
lines.push(` </>`);
|
|
952
|
-
} else {
|
|
953
|
-
// Wrap in a fragment if multiple root elements
|
|
954
|
-
const rootTags = (jsx.match(/^<[a-zA-Z]/gm) || []).length;
|
|
955
|
-
if (rootTags > 1) {
|
|
956
|
-
lines.push(` <>`);
|
|
957
|
-
lines.push(` ${jsx}`);
|
|
958
|
-
lines.push(` </>`);
|
|
959
|
-
} else {
|
|
960
|
-
lines.push(` ${jsx}`);
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
lines.push(` );`);
|
|
965
|
-
lines.push(`}`);
|
|
966
|
-
|
|
967
|
-
return lines.join("\n");
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
// ── Main runner ────────────────────────────────────────────────────────────────
|
|
971
|
-
|
|
972
|
-
function runStitch({ message, requestId, apiKey, projectId, model, designContext, onEvent }) {
|
|
973
|
-
const emitter = new EventEmitter();
|
|
974
|
-
let aborted = false;
|
|
975
|
-
|
|
976
|
-
// Auto-get access token from OAuth if not provided directly
|
|
977
|
-
if (!apiKey && hasStitchAuth()) {
|
|
978
|
-
// Will be fetched async inside the main flow
|
|
979
|
-
} else if (!apiKey) {
|
|
980
|
-
onEvent({
|
|
981
|
-
type: "error",
|
|
982
|
-
id: requestId,
|
|
983
|
-
error: 'Not signed in to Stitch. Click "Sign in with Google" on the login screen to connect your Google account.',
|
|
984
|
-
});
|
|
985
|
-
onEvent({ type: "done", id: requestId, fullText: "" });
|
|
986
|
-
setTimeout(() => emitter.emit("close", 1), 0);
|
|
987
|
-
emitter.kill = () => {};
|
|
988
|
-
return emitter;
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
const prompt = (message || "").trim() || "Create a simple landing page";
|
|
992
|
-
const workspaceName = projectId || "Figma Intelligence";
|
|
993
|
-
|
|
994
|
-
const deviceType = /mobile|ios|android|phone|app\s+screen/i.test(prompt)
|
|
995
|
-
? "MOBILE"
|
|
996
|
-
: "DESKTOP";
|
|
997
|
-
|
|
998
|
-
function abort() {
|
|
999
|
-
aborted = true;
|
|
1000
|
-
emitter.emit("close", null);
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
(async () => {
|
|
1004
|
-
let client = null;
|
|
1005
|
-
|
|
1006
|
-
try {
|
|
1007
|
-
// ── Step 1: Load SDK & connect ─────────────────────────────────────
|
|
1008
|
-
onEvent({
|
|
1009
|
-
type: "phase_start",
|
|
1010
|
-
id: requestId,
|
|
1011
|
-
phase: "Connecting to Stitch...",
|
|
1012
|
-
});
|
|
1013
|
-
|
|
1014
|
-
const { Stitch, StitchToolClient } = await loadSdk();
|
|
1015
|
-
|
|
1016
|
-
// Get access token: from direct input or OAuth refresh
|
|
1017
|
-
let accessToken = apiKey;
|
|
1018
|
-
if (!accessToken) {
|
|
1019
|
-
accessToken = await getStitchAccessToken();
|
|
1020
|
-
}
|
|
1021
|
-
if (!accessToken) {
|
|
1022
|
-
onEvent({ type: "error", id: requestId, error: 'Stitch session expired. Click "Sign in with Google" to reconnect.' });
|
|
1023
|
-
onEvent({ type: "done", id: requestId, fullText: "" });
|
|
1024
|
-
return;
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
// SDK requires accessToken + projectId for OAuth auth.
|
|
1028
|
-
// projectId is Google Cloud project ID (for X-Goog-User-Project header).
|
|
1029
|
-
// Auto-detect from user's GCP projects if not provided.
|
|
1030
|
-
let gcpProjectId = projectId || process.env.GOOGLE_CLOUD_PROJECT;
|
|
1031
|
-
if (!gcpProjectId) {
|
|
1032
|
-
gcpProjectId = await detectGcpProject(accessToken);
|
|
1033
|
-
}
|
|
1034
|
-
client = new StitchToolClient({ accessToken, projectId: gcpProjectId });
|
|
1035
|
-
await client.connect();
|
|
1036
|
-
|
|
1037
|
-
if (aborted) return;
|
|
1038
|
-
|
|
1039
|
-
const stitchApi = new Stitch(client);
|
|
1040
|
-
|
|
1041
|
-
// ── Step 2: Detect intent ──────────────────────────────────────────
|
|
1042
|
-
const intent = detectStitchIntent(prompt);
|
|
1043
|
-
|
|
1044
|
-
switch (intent.type) {
|
|
1045
|
-
case "list_projects":
|
|
1046
|
-
await handleListProjects(stitchApi, requestId, onEvent);
|
|
1047
|
-
return;
|
|
1048
|
-
|
|
1049
|
-
case "list_screens":
|
|
1050
|
-
await handleListScreens(stitchApi, client, intent.projectHint, requestId, onEvent);
|
|
1051
|
-
return;
|
|
1052
|
-
|
|
1053
|
-
case "get_screen":
|
|
1054
|
-
await handleGetScreen(stitchApi, client, intent.projectHint, intent.screenHint, requestId, onEvent, deviceType, intent.wantsCode);
|
|
1055
|
-
return;
|
|
1056
|
-
|
|
1057
|
-
case "get_screen_image":
|
|
1058
|
-
await handleGetScreenImage(stitchApi, client, intent.projectHint, intent.screenHint, requestId, onEvent);
|
|
1059
|
-
return;
|
|
1060
|
-
|
|
1061
|
-
// default: fall through to generation pipeline below
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
if (aborted) return;
|
|
1065
|
-
|
|
1066
|
-
// ── Generation pipeline (existing flow) ────────────────────────────
|
|
1067
|
-
|
|
1068
|
-
// Find or create project
|
|
1069
|
-
onEvent({
|
|
1070
|
-
type: "phase_start",
|
|
1071
|
-
id: requestId,
|
|
1072
|
-
phase: "Setting up project...",
|
|
1073
|
-
});
|
|
1074
|
-
|
|
1075
|
-
let project;
|
|
1076
|
-
try {
|
|
1077
|
-
const projects = await stitchApi.projects();
|
|
1078
|
-
const existing = projects.find(p =>
|
|
1079
|
-
p.data?.title === workspaceName || p.data?.displayName === workspaceName
|
|
1080
|
-
);
|
|
1081
|
-
project = existing || await stitchApi.createProject(workspaceName);
|
|
1082
|
-
} catch {
|
|
1083
|
-
project = await stitchApi.createProject(workspaceName);
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
if (aborted) return;
|
|
1087
|
-
|
|
1088
|
-
// Check for design context: attached .md file takes priority over saved one
|
|
1089
|
-
const existingDesignMd = designContext || loadDesignMd(workspaceName);
|
|
1090
|
-
let generationPrompt = prompt;
|
|
1091
|
-
|
|
1092
|
-
if (existingDesignMd) {
|
|
1093
|
-
onEvent({
|
|
1094
|
-
type: "phase_start",
|
|
1095
|
-
id: requestId,
|
|
1096
|
-
phase: designContext ? "Loading attached design context..." : "Loading design context (design.md)...",
|
|
1097
|
-
});
|
|
1098
|
-
generationPrompt =
|
|
1099
|
-
`Use the following design system for visual consistency:\n\n` +
|
|
1100
|
-
`${existingDesignMd}\n\n---\n\n` +
|
|
1101
|
-
`Now generate: ${prompt}`;
|
|
1102
|
-
|
|
1103
|
-
onEvent({
|
|
1104
|
-
type: "text_delta",
|
|
1105
|
-
id: requestId,
|
|
1106
|
-
delta: designContext ? "Using attached .md design context.\n\n" : "Loaded existing design.md for visual consistency.\n\n",
|
|
1107
|
-
});
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
// Generate the screen
|
|
1111
|
-
onEvent({
|
|
1112
|
-
type: "phase_start",
|
|
1113
|
-
id: requestId,
|
|
1114
|
-
phase: `Generating UI with Stitch (${deviceType.toLowerCase()})...`,
|
|
1115
|
-
});
|
|
1116
|
-
onEvent({ type: "tool_start", id: requestId, tool: "generate_screen_from_text" });
|
|
1117
|
-
|
|
1118
|
-
// Bypass SDK's project.generate() — it crashes when the API response
|
|
1119
|
-
// structure doesn't match: raw.outputComponents[0].design.screens[0]
|
|
1120
|
-
// throws "Cannot read properties of undefined (reading 'screens')".
|
|
1121
|
-
// Instead, call the tool directly and handle the raw response safely.
|
|
1122
|
-
const genRaw = await client.callTool("generate_screen_from_text", {
|
|
1123
|
-
projectId: project.id,
|
|
1124
|
-
prompt: generationPrompt,
|
|
1125
|
-
deviceType,
|
|
1126
|
-
});
|
|
1127
|
-
stitchLog("generate raw", genRaw);
|
|
1128
|
-
|
|
1129
|
-
// Extract screen data from whichever response structure the API returns.
|
|
1130
|
-
// outputComponents is an array where some entries are design systems
|
|
1131
|
-
// (have `designSystem` key) and others contain actual screens
|
|
1132
|
-
// (have `design.screens` key). We need the screen, not the design system.
|
|
1133
|
-
let screenData = null;
|
|
1134
|
-
if (genRaw?.outputComponents) {
|
|
1135
|
-
for (const comp of genRaw.outputComponents) {
|
|
1136
|
-
if (comp?.design?.screens?.[0]) {
|
|
1137
|
-
screenData = comp.design.screens[0];
|
|
1138
|
-
break;
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
if (!screenData && genRaw?.screens?.[0]) {
|
|
1143
|
-
screenData = genRaw.screens[0];
|
|
1144
|
-
}
|
|
1145
|
-
if (!screenData && (genRaw?.id || genRaw?.htmlCode)) {
|
|
1146
|
-
screenData = genRaw;
|
|
1147
|
-
}
|
|
1148
|
-
stitchLog("screenData extracted", screenData);
|
|
1149
|
-
|
|
1150
|
-
if (!screenData) {
|
|
1151
|
-
onEvent({ type: "tool_done", id: requestId, tool: "generate_screen_from_text", isError: true });
|
|
1152
|
-
const msg = "Stitch generation returned an unexpected response. Try again or use a different prompt.\n";
|
|
1153
|
-
onEvent({ type: "text_delta", id: requestId, delta: msg });
|
|
1154
|
-
onEvent({ type: "done", id: requestId, fullText: msg });
|
|
1155
|
-
return;
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
// Build a screen-like object with the same interface we use downstream
|
|
1159
|
-
const screen = {
|
|
1160
|
-
id: screenData.id || (screenData.name ? screenData.name.split("/screens/").pop() : null),
|
|
1161
|
-
screenId: screenData.id || (screenData.name ? screenData.name.split("/screens/").pop() : null),
|
|
1162
|
-
data: screenData,
|
|
1163
|
-
getHtml: async () => {
|
|
1164
|
-
if (screenData.htmlCode?.downloadUrl) return screenData.htmlCode.downloadUrl;
|
|
1165
|
-
const scrId = screenData.id || (screenData.name ? screenData.name.split("/screens/").pop() : null);
|
|
1166
|
-
stitchLog("getHtml: no cached URL, fetching screen " + scrId);
|
|
1167
|
-
|
|
1168
|
-
// Try fetching, with one retry after a short delay (HTML may not be ready immediately)
|
|
1169
|
-
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1170
|
-
try {
|
|
1171
|
-
if (attempt > 0) await new Promise(r => setTimeout(r, 3000));
|
|
1172
|
-
const raw = await client.callTool("get_screen", {
|
|
1173
|
-
projectId: project.id,
|
|
1174
|
-
screenId: scrId,
|
|
1175
|
-
name: `projects/${project.id}/screens/${scrId}`,
|
|
1176
|
-
});
|
|
1177
|
-
stitchLog("get_screen raw (attempt " + attempt + ")", raw);
|
|
1178
|
-
const url = raw?.htmlCode?.downloadUrl || raw?.html?.downloadUrl || null;
|
|
1179
|
-
if (url) return url;
|
|
1180
|
-
} catch (err) {
|
|
1181
|
-
stitchLog("getHtml attempt " + attempt + " failed: " + err.message);
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
// Last resort: list screens and find ours
|
|
1186
|
-
try {
|
|
1187
|
-
const listRaw = await client.callTool("list_screens", { projectId: project.id });
|
|
1188
|
-
const allScreens = listRaw?.screens || listRaw?.items || (Array.isArray(listRaw) ? listRaw : []);
|
|
1189
|
-
const ours = allScreens.find(s => s.id === scrId || (s.name && s.name.includes(scrId)));
|
|
1190
|
-
if (ours?.htmlCode?.downloadUrl) return ours.htmlCode.downloadUrl;
|
|
1191
|
-
stitchLog("screen found in list but no htmlCode", ours);
|
|
1192
|
-
} catch {}
|
|
1193
|
-
|
|
1194
|
-
return null;
|
|
1195
|
-
},
|
|
1196
|
-
};
|
|
1197
|
-
|
|
1198
|
-
onEvent({ type: "tool_done", id: requestId, tool: "generate_screen_from_text", isError: false });
|
|
1199
|
-
|
|
1200
|
-
if (aborted) return;
|
|
1201
|
-
|
|
1202
|
-
// Extract Design DNA & save design.md (first generation only)
|
|
1203
|
-
if (!existingDesignMd) {
|
|
1204
|
-
onEvent({
|
|
1205
|
-
type: "phase_start",
|
|
1206
|
-
id: requestId,
|
|
1207
|
-
phase: "Extracting design context (Design DNA)...",
|
|
1208
|
-
});
|
|
1209
|
-
onEvent({ type: "tool_start", id: requestId, tool: "extract_design_context" });
|
|
1210
|
-
|
|
1211
|
-
try {
|
|
1212
|
-
const designContext = await client.callTool("extract_design_context", {
|
|
1213
|
-
projectId: project.id,
|
|
1214
|
-
screenId: screen.id,
|
|
1215
|
-
});
|
|
1216
|
-
|
|
1217
|
-
onEvent({ type: "tool_done", id: requestId, tool: "extract_design_context", isError: false });
|
|
1218
|
-
|
|
1219
|
-
const designMdContent = buildDesignMd(designContext, workspaceName, prompt, screen.data?.theme);
|
|
1220
|
-
const savedPath = saveDesignMd(workspaceName, designMdContent);
|
|
1221
|
-
|
|
1222
|
-
if (savedPath) {
|
|
1223
|
-
onEvent({
|
|
1224
|
-
type: "text_delta",
|
|
1225
|
-
id: requestId,
|
|
1226
|
-
delta: `Design context saved to design.md\n` +
|
|
1227
|
-
` Path: ${savedPath}\n` +
|
|
1228
|
-
` Future screens in "${workspaceName}" will use this design system.\n\n`,
|
|
1229
|
-
});
|
|
1230
|
-
}
|
|
1231
|
-
} catch (err) {
|
|
1232
|
-
onEvent({ type: "tool_done", id: requestId, tool: "extract_design_context", isError: true });
|
|
1233
|
-
console.log(` Warning: extract_design_context failed: ${err.message}`);
|
|
1234
|
-
onEvent({
|
|
1235
|
-
type: "text_delta",
|
|
1236
|
-
id: requestId,
|
|
1237
|
-
delta: `Could not extract design context: ${err.message}\n\n`,
|
|
1238
|
-
});
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
if (aborted) return;
|
|
1243
|
-
|
|
1244
|
-
// Get HTML download URL & fetch it
|
|
1245
|
-
onEvent({
|
|
1246
|
-
type: "phase_start",
|
|
1247
|
-
id: requestId,
|
|
1248
|
-
phase: "Fetching generated code...",
|
|
1249
|
-
});
|
|
1250
|
-
onEvent({ type: "tool_start", id: requestId, tool: "fetch_screen_code" });
|
|
1251
|
-
|
|
1252
|
-
const htmlUrl = await screen.getHtml();
|
|
1253
|
-
|
|
1254
|
-
onEvent({ type: "tool_done", id: requestId, tool: "fetch_screen_code", isError: false });
|
|
1255
|
-
|
|
1256
|
-
if (!htmlUrl) {
|
|
1257
|
-
onEvent({
|
|
1258
|
-
type: "text_delta",
|
|
1259
|
-
id: requestId,
|
|
1260
|
-
delta: "Stitch generated a screen but returned no HTML URL. Try a different prompt.",
|
|
1261
|
-
});
|
|
1262
|
-
onEvent({ type: "done", id: requestId, fullText: "" });
|
|
1263
|
-
return;
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
const html = await fetchUrl(htmlUrl);
|
|
1267
|
-
|
|
1268
|
-
if (aborted) return;
|
|
1269
|
-
|
|
1270
|
-
if (!html || html.length < 20) {
|
|
1271
|
-
onEvent({
|
|
1272
|
-
type: "text_delta",
|
|
1273
|
-
id: requestId,
|
|
1274
|
-
delta: "Stitch HTML download returned empty content.",
|
|
1275
|
-
});
|
|
1276
|
-
onEvent({ type: "done", id: requestId, fullText: "" });
|
|
1277
|
-
return;
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
// Check if user wants code output (React/HTML/web) vs Figma frames
|
|
1281
|
-
const codeIntent = detectCodeIntent(prompt.toLowerCase());
|
|
1282
|
-
|
|
1283
|
-
if (codeIntent === "react") {
|
|
1284
|
-
// ── Code + live preview mode ──
|
|
1285
|
-
const screenName = screen.data?.title || screen.data?.displayName || "Generated Screen";
|
|
1286
|
-
|
|
1287
|
-
// Start preview server and serve the HTML
|
|
1288
|
-
onEvent({
|
|
1289
|
-
type: "phase_start",
|
|
1290
|
-
id: requestId,
|
|
1291
|
-
phase: "Starting live preview...",
|
|
1292
|
-
});
|
|
1293
|
-
|
|
1294
|
-
try {
|
|
1295
|
-
const port = await ensurePreviewServer();
|
|
1296
|
-
const slug = addPreview(html, screenName);
|
|
1297
|
-
const previewUrl = `http://localhost:${port}/${slug}`;
|
|
1298
|
-
|
|
1299
|
-
// Convert to React component for download
|
|
1300
|
-
const reactCode = htmlToReactComponent(html, screenName);
|
|
1301
|
-
const componentName = pascalCase(screenName) || "StitchScreen";
|
|
1302
|
-
|
|
1303
|
-
onEvent({
|
|
1304
|
-
type: "text_delta",
|
|
1305
|
-
id: requestId,
|
|
1306
|
-
delta: `**Live Preview:** [${previewUrl}](${previewUrl})\n` +
|
|
1307
|
-
`Open in your browser to see the interactive page.\n\n` +
|
|
1308
|
-
`React component available for download:\n\n` +
|
|
1309
|
-
"```download:" + componentName + ".tsx\n" + reactCode + "\n```\n\n" +
|
|
1310
|
-
`**To run locally as a React app:**\n` +
|
|
1311
|
-
`1. Install Tailwind CSS: \`npm install tailwindcss @tailwindcss/vite\`\n` +
|
|
1312
|
-
`2. Save as \`${componentName}.tsx\`\n` +
|
|
1313
|
-
`3. Import: \`import ${componentName} from './${componentName}'\`\n`,
|
|
1314
|
-
});
|
|
1315
|
-
onEvent({ type: "done", id: requestId, fullText: `Live preview at ${previewUrl}` });
|
|
1316
|
-
} catch (err) {
|
|
1317
|
-
// Fallback: just show the code if preview server fails
|
|
1318
|
-
const reactCode = htmlToReactComponent(html, screenName);
|
|
1319
|
-
const componentName = pascalCase(screenName) || "StitchScreen";
|
|
1320
|
-
onEvent({
|
|
1321
|
-
type: "text_delta",
|
|
1322
|
-
id: requestId,
|
|
1323
|
-
delta: `React component (preview server unavailable):\n\n` +
|
|
1324
|
-
"```download:" + componentName + ".tsx\n" + reactCode + "\n```\n",
|
|
1325
|
-
});
|
|
1326
|
-
onEvent({ type: "done", id: requestId, fullText: `React component — ${reactCode.length} chars` });
|
|
1327
|
-
}
|
|
1328
|
-
} else {
|
|
1329
|
-
// ── Figma output mode: convert to native frames ──
|
|
1330
|
-
processStitchHtml(html, "", requestId, prompt, deviceType, onEvent);
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
} catch (err) {
|
|
1334
|
-
if (!aborted) {
|
|
1335
|
-
let errMsg = err.message || String(err);
|
|
1336
|
-
|
|
1337
|
-
if (errMsg.includes("401") || errMsg.includes("403") || errMsg.includes("PERMISSION") || errMsg.includes("API keys are not supported")) {
|
|
1338
|
-
errMsg = `Stitch auth failed. This API requires an OAuth2 access token, not an API key. Get one at stitch.withgoogle.com → Settings, or run: gcloud auth print-access-token`;
|
|
1339
|
-
} else if (errMsg.includes("429") || errMsg.includes("RATE_LIMITED")) {
|
|
1340
|
-
errMsg = "Stitch rate limit exceeded. Free tier: 350 generations/month. Please wait and try again.";
|
|
1341
|
-
} else if (errMsg.includes("ENOTFOUND") || errMsg.includes("ECONNREFUSED")) {
|
|
1342
|
-
errMsg = "Cannot reach Stitch servers. Check your internet connection.";
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
|
-
onEvent({ type: "error", id: requestId, error: `Stitch: ${errMsg}` });
|
|
1346
|
-
onEvent({ type: "done", id: requestId, fullText: "" });
|
|
1347
|
-
}
|
|
1348
|
-
} finally {
|
|
1349
|
-
if (client) {
|
|
1350
|
-
try { await client.close(); } catch {}
|
|
1351
|
-
}
|
|
1352
|
-
emitter.emit("close", 0);
|
|
1353
|
-
}
|
|
1354
|
-
})();
|
|
1355
|
-
|
|
1356
|
-
emitter.kill = abort;
|
|
1357
|
-
return emitter;
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
1361
|
-
|
|
1362
|
-
function fetchUrl(url) {
|
|
1363
|
-
return new Promise((resolve, reject) => {
|
|
1364
|
-
const mod = url.startsWith("https") ? https : require("http");
|
|
1365
|
-
mod.get(url, (res) => {
|
|
1366
|
-
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
1367
|
-
fetchUrl(res.headers.location).then(resolve).catch(reject);
|
|
1368
|
-
return;
|
|
1369
|
-
}
|
|
1370
|
-
if (res.statusCode !== 200) {
|
|
1371
|
-
reject(new Error(`HTTP ${res.statusCode} fetching ${url}`));
|
|
1372
|
-
return;
|
|
1373
|
-
}
|
|
1374
|
-
let data = "";
|
|
1375
|
-
res.on("data", (chunk) => { data += chunk.toString(); });
|
|
1376
|
-
res.on("end", () => resolve(data));
|
|
1377
|
-
res.on("error", reject);
|
|
1378
|
-
}).on("error", reject);
|
|
1379
|
-
});
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
function processStitchHtml(html, css, requestId, prompt, deviceType, onEvent) {
|
|
1383
|
-
onEvent({
|
|
1384
|
-
type: "phase_start",
|
|
1385
|
-
id: requestId,
|
|
1386
|
-
phase: "Converting to Figma frames…",
|
|
1387
|
-
});
|
|
1388
|
-
|
|
1389
|
-
let result;
|
|
1390
|
-
try {
|
|
1391
|
-
result = htmlToFigmaExecuteCode(html, css, prompt, deviceType || "DESKTOP");
|
|
1392
|
-
} catch (err) {
|
|
1393
|
-
onEvent({
|
|
1394
|
-
type: "error",
|
|
1395
|
-
id: requestId,
|
|
1396
|
-
error: `HTML-to-Figma conversion failed: ${err.message}`,
|
|
1397
|
-
});
|
|
1398
|
-
onEvent({ type: "done", id: requestId, fullText: "" });
|
|
1399
|
-
return;
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
if (!result || !result.code) {
|
|
1403
|
-
onEvent({
|
|
1404
|
-
type: "text_delta",
|
|
1405
|
-
id: requestId,
|
|
1406
|
-
delta: "Could not convert Stitch output to Figma frames. The generated HTML may be too complex.",
|
|
1407
|
-
});
|
|
1408
|
-
onEvent({ type: "done", id: requestId, fullText: "" });
|
|
1409
|
-
return;
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
// Emit a single execute command with the full Figma Plugin API code
|
|
1413
|
-
onEvent({ type: "tool_start", id: requestId, tool: "figma_execute" });
|
|
1414
|
-
onEvent({
|
|
1415
|
-
type: "figma_command",
|
|
1416
|
-
id: requestId,
|
|
1417
|
-
method: "execute",
|
|
1418
|
-
params: { code: result.code },
|
|
1419
|
-
});
|
|
1420
|
-
onEvent({ type: "tool_done", id: requestId, tool: "figma_execute", isError: false });
|
|
1421
|
-
|
|
1422
|
-
const summary = `Generated ${result.commandCount} Figma element(s) from Stitch for: "${prompt}"`;
|
|
1423
|
-
onEvent({ type: "text_delta", id: requestId, delta: summary });
|
|
1424
|
-
onEvent({ type: "done", id: requestId, fullText: summary });
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1427
|
-
module.exports = { runStitch };
|