@polymorphism-tech/morph-spec 4.20.0 → 4.22.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 +2 -2
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/agents.json +15 -0
- package/framework/hooks/shared/phase-utils.js +3 -0
- package/framework/phases.json +1 -1
- package/framework/skills/level-0-meta/mcp-registry.json +56 -10
- package/framework/skills/level-0-meta/morph-frontend-review/SKILL.md +1 -1
- package/framework/skills/level-0-meta/morph-gemini-image/SKILL.md +290 -0
- package/framework/skills/level-0-meta/morph-post-implementation/SKILL.md +19 -0
- package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +10 -10
- package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +146 -5
- package/framework/skills/level-1-workflows/morph-phase-implement/prompts/implementer-prompt.md +8 -0
- package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +216 -9
- package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +1 -1
- package/framework/standards/STANDARDS.json +32 -0
- package/framework/standards/frontend/design-system/ai-image-generation.md +244 -0
- package/framework/standards/integration/mcp/mcp-tools.md +48 -0
- package/package.json +1 -1
- package/src/commands/mcp/mcp-setup.js +99 -2
- package/src/core/paths/output-schema.js +1 -0
- package/src/lib/agents/output-collector.js +1 -0
- package/src/lib/detectors/index.js +1 -1
- package/src/lib/installers/mcp-installer.js +140 -1
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# AI Image Generation
|
|
2
|
+
|
|
3
|
+
> **Scope:** nextjs
|
|
4
|
+
> **Layer:** 1 (load when relevant)
|
|
5
|
+
> **Keywords:** image, gemini, asset, ai-generation, placeholder, mcp, next/image
|
|
6
|
+
> **Load When:** feature has UI components requiring image assets
|
|
7
|
+
|
|
8
|
+
## When to Use AI-Generated Images
|
|
9
|
+
|
|
10
|
+
| Use Case | Approach | Examples |
|
|
11
|
+
|----------|----------|---------|
|
|
12
|
+
| **AI generation** | Gemini Image MCP | Hero images, brand illustrations, feature-specific visuals, empty states, background panels |
|
|
13
|
+
| **Stock photos** | External sources | Generic content (people in offices, nature scenes, stock portraits) |
|
|
14
|
+
| **SVG/CSS** | Code-based | Icons, patterns, gradients, geometric shapes, loading animations |
|
|
15
|
+
| **Skip** | Other tools | Screenshots (Playwright), diagrams (Mermaid), logos (vector tools) |
|
|
16
|
+
|
|
17
|
+
**Decision rule:** If the image is unique to the project's brand/aesthetic and doesn't exist as stock, use AI generation. If it's generic and widely available, use stock. If it's geometric or icon-based, use SVG/CSS.
|
|
18
|
+
|
|
19
|
+
## Gemini Image MCP API
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
// Generate a new image
|
|
23
|
+
mcp__gemini_image_mcp__generate_image({
|
|
24
|
+
prompt: string, // Text description (max 5000 chars, narrative style)
|
|
25
|
+
aspectRatio?: "1:1" | "2:3" | "3:2" | "3:4" | "4:3" | "9:16" | "16:9",
|
|
26
|
+
outputPath?: string, // File path to save (e.g., "public/images/feature/hero.png")
|
|
27
|
+
model?: string, // Default: gemini-2.5-flash-image
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
// Edit an existing image
|
|
31
|
+
mcp__gemini_image_mcp__edit_image({
|
|
32
|
+
imageSource: string, // Path to image or image ID
|
|
33
|
+
prompt: string, // Short, targeted edit instructions (1-2 sentences)
|
|
34
|
+
outputPath?: string,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// Continue editing last image
|
|
38
|
+
mcp__gemini_image_mcp__continue_editing({
|
|
39
|
+
prompt: string, // One adjustment at a time
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// List generated images
|
|
43
|
+
mcp__gemini_image_mcp__list_images({
|
|
44
|
+
type?: "generated" | "edited" | "all",
|
|
45
|
+
limit?: number,
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// Get image details
|
|
49
|
+
mcp__gemini_image_mcp__get_image_info({
|
|
50
|
+
imageId?: string, // Defaults to last image
|
|
51
|
+
})
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Prompt Engineering Patterns
|
|
55
|
+
|
|
56
|
+
### Design-System-Aware Prompts
|
|
57
|
+
|
|
58
|
+
Read `design-system.md` or `globals.css` to extract project tokens. Inject into prompts:
|
|
59
|
+
|
|
60
|
+
- **Background color** → use as image background
|
|
61
|
+
- **Primary/accent colors** → use as highlight colors
|
|
62
|
+
- **Font pair** → mention for text elements
|
|
63
|
+
- **Aesthetic direction** → "Modern Luxury", "Kinetic Atelier", etc.
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
"{subject description}. Dark charcoal background (#10131a) with copper (#D2802D)
|
|
67
|
+
and gold (#FFB779) accent colors. {style modifiers}. Premium luxury aesthetic.
|
|
68
|
+
{composition rules}."
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Component-Contextual Prompts
|
|
72
|
+
|
|
73
|
+
Describe what the image will BE in the UI, not just the subject:
|
|
74
|
+
|
|
75
|
+
- **Hero image** → "floating tablet showing app dashboard on dark background" (not just "fitness dashboard")
|
|
76
|
+
- **Auth background** → "gym equipment with moody lighting" (matches brand feel)
|
|
77
|
+
- **Empty state** → "artistic wireframe body silhouette" (matches feature context)
|
|
78
|
+
|
|
79
|
+
### Narrative Style
|
|
80
|
+
|
|
81
|
+
Gemini responds best to coherent paragraphs. Build prompts by layering:
|
|
82
|
+
|
|
83
|
+
1. **Subject** — hyper-specific description
|
|
84
|
+
2. **Environment** — foreground, mid-ground, background (layered for depth)
|
|
85
|
+
3. **Lighting** — multiple sources with different colors/directions
|
|
86
|
+
4. **Style** — photography terms ("85mm portrait lens") or art style ("cel-shaded")
|
|
87
|
+
5. **Color palette** — dominant colors and contrast relationships
|
|
88
|
+
6. **Composition** — camera angle, framing, negative space
|
|
89
|
+
|
|
90
|
+
**Never:** keyword spam ("beautiful, stunning, 4k, masterpiece"), JSON prompts, negative prompts ("no cars").
|
|
91
|
+
|
|
92
|
+
## Iterative Editing Workflows
|
|
93
|
+
|
|
94
|
+
**Cardinal rule: generation demands depth; editing demands restraint.**
|
|
95
|
+
|
|
96
|
+
### Adding / Removing Elements
|
|
97
|
+
One sentence. Don't describe how to integrate — the model handles that.
|
|
98
|
+
```
|
|
99
|
+
"Add a knitted wizard hat to the cat."
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Background Replacement
|
|
103
|
+
State the new background simply. Don't over-describe.
|
|
104
|
+
```
|
|
105
|
+
"Replace the background with a tropical beach at sunset."
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Inpainting (Targeted Changes)
|
|
109
|
+
Name what changes and what stays in one line.
|
|
110
|
+
```
|
|
111
|
+
"Change the blue sofa to a brown leather chesterfield. Keep everything else."
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Style Transfer
|
|
115
|
+
Name the target style.
|
|
116
|
+
```
|
|
117
|
+
"Transform this into Van Gogh's Starry Night style."
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Iterative Refinement with `continue_editing`
|
|
121
|
+
One change at a time:
|
|
122
|
+
- "Make the lighting warmer"
|
|
123
|
+
- "More contrast in the shadows"
|
|
124
|
+
- "Zoom out a bit"
|
|
125
|
+
- "More serious expression"
|
|
126
|
+
|
|
127
|
+
### Text Rendering
|
|
128
|
+
The model frequently misspells words. After generating any image with text:
|
|
129
|
+
1. View the result
|
|
130
|
+
2. If text is wrong: `continue_editing` with "Fix the text — it should read '{correct text}' exactly. Keep everything else."
|
|
131
|
+
3. Expect 2-3 iterations for correct text
|
|
132
|
+
|
|
133
|
+
## Aspect Ratio Guide
|
|
134
|
+
|
|
135
|
+
| Use Case | Ratio | Examples |
|
|
136
|
+
|----------|-------|---------|
|
|
137
|
+
| Hero / banner | `16:9` | Landing page hero, YouTube thumbnail, desktop wallpaper |
|
|
138
|
+
| Card / blog header | `3:2` | Feature cards, blog post headers, landscape photo |
|
|
139
|
+
| Avatar / icon | `1:1` | Profile pictures, social media posts, square icons |
|
|
140
|
+
| Auth panel / sidebar | `3:4` | Login brand image, Pinterest-style, portrait poster |
|
|
141
|
+
| Portrait | `2:3` | Book cover, tall illustration, portrait photo |
|
|
142
|
+
| Story / reel | `9:16` | Mobile wallpaper, social story, vertical banner |
|
|
143
|
+
| Standard display | `4:3` | Documentation, slides, standard display |
|
|
144
|
+
|
|
145
|
+
## File Naming & Directory
|
|
146
|
+
|
|
147
|
+
**Convention:** `public/images/{feature}/{purpose}.png`
|
|
148
|
+
|
|
149
|
+
- Lowercase, kebab-case, descriptive names
|
|
150
|
+
- Group by feature, not by type
|
|
151
|
+
- Examples:
|
|
152
|
+
- `public/images/ui-redesign/hero-mockup.png`
|
|
153
|
+
- `public/images/ui-redesign/gym-equipment.png`
|
|
154
|
+
- `public/images/ui-redesign/body-analysis.png`
|
|
155
|
+
|
|
156
|
+
## Next.js Image Integration
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
import Image from 'next/image';
|
|
160
|
+
|
|
161
|
+
// Above-the-fold hero image (priority for LCP)
|
|
162
|
+
<Image
|
|
163
|
+
src="/images/{feature}/hero-mockup.png"
|
|
164
|
+
alt="AI fitness analysis dashboard"
|
|
165
|
+
width={1440}
|
|
166
|
+
height={810}
|
|
167
|
+
priority
|
|
168
|
+
sizes="100vw"
|
|
169
|
+
/>
|
|
170
|
+
|
|
171
|
+
// Card image with responsive sizing
|
|
172
|
+
<Image
|
|
173
|
+
src="/images/{feature}/card-image.png"
|
|
174
|
+
alt="Feature card illustration"
|
|
175
|
+
width={600}
|
|
176
|
+
height={400}
|
|
177
|
+
sizes="(max-width: 768px) 100vw, 50vw"
|
|
178
|
+
className="rounded-lg"
|
|
179
|
+
/>
|
|
180
|
+
|
|
181
|
+
// Background image with fill
|
|
182
|
+
<div className="relative h-full">
|
|
183
|
+
<Image
|
|
184
|
+
src="/images/{feature}/auth-background.png"
|
|
185
|
+
alt=""
|
|
186
|
+
fill
|
|
187
|
+
className="object-cover"
|
|
188
|
+
priority={false}
|
|
189
|
+
/>
|
|
190
|
+
</div>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## `assets.md` Format
|
|
194
|
+
|
|
195
|
+
The `assets.md` file is created during the UI/UX phase (Step 4.5) and consumed by the implement phase.
|
|
196
|
+
|
|
197
|
+
```markdown
|
|
198
|
+
# Image Assets — {feature}
|
|
199
|
+
|
|
200
|
+
## Design Context
|
|
201
|
+
- Aesthetic: {from design-system.md}
|
|
202
|
+
- Primary: {--color-primary value}
|
|
203
|
+
- Background: {--color-background value}
|
|
204
|
+
- Accent: {--color-accent value}
|
|
205
|
+
- Font pair: {display + body fonts}
|
|
206
|
+
|
|
207
|
+
## Assets
|
|
208
|
+
|
|
209
|
+
### IMG-001: {Descriptive Name}
|
|
210
|
+
- **Purpose:** {What this image represents in the UI}
|
|
211
|
+
- **Route:** {App route where it appears}
|
|
212
|
+
- **Component:** {React component name}
|
|
213
|
+
- **Aspect Ratio:** {1:1 | 3:2 | 16:9 | etc.}
|
|
214
|
+
- **Output Path:** `public/images/{feature}/{kebab-name}.png`
|
|
215
|
+
- **Prompt:** "{Narrative prompt with design system colors and aesthetic direction injected}"
|
|
216
|
+
- **Priority:** {high | medium | low}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**Example entry:**
|
|
220
|
+
|
|
221
|
+
```markdown
|
|
222
|
+
### IMG-001: Hero Mockup
|
|
223
|
+
- **Purpose:** App dashboard floating on dark background for landing page hero
|
|
224
|
+
- **Route:** /
|
|
225
|
+
- **Component:** HeroSection
|
|
226
|
+
- **Aspect Ratio:** 16:9
|
|
227
|
+
- **Output Path:** `public/images/ui-redesign/hero-mockup.png`
|
|
228
|
+
- **Prompt:** "A floating tablet showing a modern AI fitness analysis dashboard on a dark charcoal background (#10131a). Copper (#D2802D) and gold (#FFB779) accent lighting creates warm rim highlights around the device edges. The screen displays body metrics with glassmorphism cards. Premium luxury aesthetic, photorealistic, cinematic lighting with a key light from the left and cool blue fill from the right."
|
|
229
|
+
- **Priority:** high
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Quality Checklist
|
|
233
|
+
|
|
234
|
+
- [ ] Colors match design system tokens (no random colors in prompts)
|
|
235
|
+
- [ ] No text artifacts or misspellings (if text in image)
|
|
236
|
+
- [ ] Correct aspect ratio for intended use
|
|
237
|
+
- [ ] Appropriate file size (< 500KB for web, < 200KB for cards)
|
|
238
|
+
- [ ] Descriptive alt text written for accessibility
|
|
239
|
+
- [ ] `<Image>` component with proper width/height dimensions
|
|
240
|
+
- [ ] `priority` prop set for above-the-fold images
|
|
241
|
+
- [ ] Build compiles after integration (`npm run build`)
|
|
242
|
+
- [ ] Image visible in dev server at correct route
|
|
243
|
+
|
|
244
|
+
*MORPH-SPEC by Polymorphism Tech*
|
|
@@ -81,6 +81,53 @@ const docs = await mcp__context7__query_docs({
|
|
|
81
81
|
// WebFetch for specific component API pages
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
+
### Gemini Image MCP — AI Image Generation
|
|
85
|
+
|
|
86
|
+
> Available in: **UI/UX** (preview generation), **Implement** (production assets)
|
|
87
|
+
|
|
88
|
+
| Tool | Use Case | Example |
|
|
89
|
+
|------|----------|---------|
|
|
90
|
+
| `generate_image` | Create new image from text prompt | `mcp__gemini-image-mcp__generate_image({ prompt, aspectRatio, outputPath })` |
|
|
91
|
+
| `edit_image` | Modify existing image | `mcp__gemini-image-mcp__edit_image({ imageSource, prompt })` |
|
|
92
|
+
| `continue_editing` | Iterate on last generated/edited image | `mcp__gemini-image-mcp__continue_editing({ prompt })` |
|
|
93
|
+
| `list_images` | Browse image generation history | `mcp__gemini-image-mcp__list_images({ type: "all" })` |
|
|
94
|
+
| `get_image_info` | Get metadata about a specific image | `mcp__gemini-image-mcp__get_image_info({ imageId })` |
|
|
95
|
+
|
|
96
|
+
```javascript
|
|
97
|
+
// Generate a hero image using design system tokens
|
|
98
|
+
// Read design-system.md first to get project colors and aesthetic direction
|
|
99
|
+
const image = await mcp__gemini_image_mcp__generate_image({
|
|
100
|
+
prompt: "A floating tablet showing a modern dashboard interface on a dark charcoal background with copper and gold accent lighting. Premium luxury aesthetic, photorealistic, cinematic lighting with warm rim light.",
|
|
101
|
+
aspectRatio: "16:9",
|
|
102
|
+
outputPath: "public/images/my-feature/hero-mockup.png"
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Edit an existing image (keep edits short and targeted)
|
|
106
|
+
await mcp__gemini_image_mcp__edit_image({
|
|
107
|
+
imageSource: "public/images/my-feature/hero-mockup.png",
|
|
108
|
+
prompt: "Make the lighting warmer and add a subtle copper glow around the tablet edges."
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Iterate with continue_editing (one change at a time)
|
|
112
|
+
await mcp__gemini_image_mcp__continue_editing({
|
|
113
|
+
prompt: "More contrast in the shadows."
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Fallback: Manual image creation, stock photos, or placeholder divs
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Use cases by phase:**
|
|
120
|
+
|
|
121
|
+
| Phase | Use Case | Tool |
|
|
122
|
+
|-------|----------|------|
|
|
123
|
+
| UI/UX (2) | Generate preview images for design approval | `generate_image` |
|
|
124
|
+
| Implement (7) | Generate production assets from assets.md specs | `generate_image` + `edit_image` |
|
|
125
|
+
| Implement (7) | Iterate on generated images for quality | `continue_editing` |
|
|
126
|
+
|
|
127
|
+
> **Standard:** See `frontend/design-system/ai-image-generation.md` for prompt engineering patterns, aspect ratio guide, and `assets.md` format.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
84
131
|
### Phase 3 (Design) — Schema & Architecture
|
|
85
132
|
|
|
86
133
|
| Tool | Use Case | Example |
|
|
@@ -246,6 +293,7 @@ await mcp__vercel__manage_env_vars({
|
|
|
246
293
|
| Container ops | Docker CLI (`docker ps`, `docker logs`, `docker compose`) | — |
|
|
247
294
|
| Cloud resources | Azure CLI (`az resource`, `az deployment`) | — |
|
|
248
295
|
| Vercel deployments | Vercel MCP | Bash `vercel` CLI |
|
|
296
|
+
| AI image generation | Gemini Image MCP | Manual creation or stock photos |
|
|
249
297
|
|
|
250
298
|
**Rule:** MCP tools provide structured data (JSON responses). Native tools require manual parsing. **Always prefer MCP when available** — fall back to native when not. **Exceptions:** Neon, Docker, and Azure always use their respective CLIs directly — never MCP.
|
|
251
299
|
|
package/package.json
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
import chalk from 'chalk';
|
|
14
14
|
import inquirer from 'inquirer';
|
|
15
15
|
import ora from 'ora';
|
|
16
|
+
import { readFileSync, existsSync } from 'fs';
|
|
17
|
+
import { join } from 'path';
|
|
16
18
|
import {
|
|
17
19
|
orchestrateMcpSetup,
|
|
18
20
|
installAutoMcps,
|
|
@@ -21,15 +23,34 @@ import {
|
|
|
21
23
|
generateSetupInstructions,
|
|
22
24
|
formatMcpStatusTable,
|
|
23
25
|
loadMcpRegistry,
|
|
24
|
-
isRemoteMcp
|
|
26
|
+
isRemoteMcp,
|
|
27
|
+
generateStitchProxy,
|
|
28
|
+
patchConfigForPlatform
|
|
25
29
|
} from '../../lib/installers/mcp-installer.js';
|
|
26
30
|
import { detectClaudeConfig } from '../../lib/detectors/claude-config-detector.js';
|
|
27
31
|
import { detectProject } from '../../lib/detectors/index.js';
|
|
32
|
+
import { installMcpServers } from '../../utils/claude-settings-manager.js';
|
|
28
33
|
|
|
29
34
|
/**
|
|
30
|
-
* Detect the current project's stack
|
|
35
|
+
* Detect the current project's stack.
|
|
36
|
+
* Prefers .morph/config/config.json (set during morph-spec init) over filesystem re-detection.
|
|
31
37
|
*/
|
|
32
38
|
async function detectStack(targetPath) {
|
|
39
|
+
// 1. Prefer the authoritative config (handles compound stacks like nextjs+dotnet)
|
|
40
|
+
try {
|
|
41
|
+
const configPath = join(targetPath, '.morph', 'config', 'config.json');
|
|
42
|
+
if (existsSync(configPath)) {
|
|
43
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
44
|
+
const configStack = config.project?.stack;
|
|
45
|
+
if (configStack && configStack !== 'unknown') {
|
|
46
|
+
return configStack;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
// Fall through to filesystem detection
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 2. Fallback: detect from filesystem
|
|
33
54
|
try {
|
|
34
55
|
const results = await detectProject(targetPath, { conversation: false, generateStandards: false });
|
|
35
56
|
return results.structure?.stack || 'unknown';
|
|
@@ -186,6 +207,60 @@ async function setupSpecificMcp(targetPath, name, stack, existingMcps) {
|
|
|
186
207
|
|
|
187
208
|
const credentials = mcpEntry.install.credentials || [];
|
|
188
209
|
|
|
210
|
+
// Stitch MCP — custom OAuth proxy setup
|
|
211
|
+
if (name === 'stitch' && !mcpEntry.install.autoInstall) {
|
|
212
|
+
console.log(chalk.cyan(`\n Setting up Stitch MCP (OAuth proxy)\n`));
|
|
213
|
+
console.log(chalk.gray(` Usage: ${mcpEntry.usage}`));
|
|
214
|
+
console.log('');
|
|
215
|
+
|
|
216
|
+
const gcpCred = credentials.find(c => c.envVar === 'STITCH_GCP_PROJECT');
|
|
217
|
+
const { gcpProject } = await inquirer.prompt([{
|
|
218
|
+
type: 'input',
|
|
219
|
+
name: 'gcpProject',
|
|
220
|
+
message: gcpCred ? `${gcpCred.name}:` : 'GCP Project ID:',
|
|
221
|
+
}]);
|
|
222
|
+
|
|
223
|
+
if (!gcpProject || !gcpProject.trim()) {
|
|
224
|
+
console.log(chalk.red(' GCP Project ID is required.'));
|
|
225
|
+
console.log('');
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const spinner = ora('Generating Stitch proxy...').start();
|
|
230
|
+
const { proxyPath } = generateStitchProxy(targetPath, gcpProject.trim());
|
|
231
|
+
spinner.succeed(`Proxy script generated: ${proxyPath}`);
|
|
232
|
+
|
|
233
|
+
const config = { ...mcpEntry.install.config };
|
|
234
|
+
const result = await installMcpServers(targetPath, { stitch: patchConfigForPlatform(config) });
|
|
235
|
+
if (result.added.length > 0) {
|
|
236
|
+
console.log(chalk.green(` ✓ Stitch MCP configured (using OAuth proxy)`));
|
|
237
|
+
} else {
|
|
238
|
+
console.log(chalk.yellow(` ⚠ Stitch MCP already configured`));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (mcpEntry.companionSkills?.installCommand) {
|
|
242
|
+
const { installSkills } = await inquirer.prompt([{
|
|
243
|
+
type: 'confirm',
|
|
244
|
+
name: 'installSkills',
|
|
245
|
+
message: 'Install Stitch companion skills (design, loop, etc.)?',
|
|
246
|
+
default: true
|
|
247
|
+
}]);
|
|
248
|
+
if (installSkills) {
|
|
249
|
+
const skillResults = installCompanionSkills({ stitch: mcpEntry });
|
|
250
|
+
for (const r of skillResults) {
|
|
251
|
+
if (r.success) {
|
|
252
|
+
console.log(chalk.green(` ✓ Companion skills installed: ${r.repo}`));
|
|
253
|
+
} else {
|
|
254
|
+
console.log(chalk.yellow(` ⚠ Failed to install companion skills: ${r.repo}`));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
console.log('');
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
189
264
|
// Auto-installable — just install
|
|
190
265
|
if (mcpEntry.install.autoInstall) {
|
|
191
266
|
const spinner = ora(`Installing ${name}...`).start();
|
|
@@ -349,6 +424,28 @@ async function interactiveWizard(targetPath, stack, existingMcps) {
|
|
|
349
424
|
}]);
|
|
350
425
|
|
|
351
426
|
if (setupNow) {
|
|
427
|
+
// Stitch — custom OAuth proxy flow
|
|
428
|
+
if (name === 'stitch' && !entry.install.autoInstall) {
|
|
429
|
+
const gcpCred = entry.install.credentials?.find(c => c.envVar === 'STITCH_GCP_PROJECT');
|
|
430
|
+
const { gcpProject } = await inquirer.prompt([{
|
|
431
|
+
type: 'input',
|
|
432
|
+
name: 'gcpProject',
|
|
433
|
+
message: gcpCred ? `${gcpCred.name}:` : 'GCP Project ID:',
|
|
434
|
+
}]);
|
|
435
|
+
if (gcpProject?.trim()) {
|
|
436
|
+
const { proxyPath } = generateStitchProxy(targetPath, gcpProject.trim());
|
|
437
|
+
console.log(chalk.green(` ✓ Proxy generated: ${proxyPath}`));
|
|
438
|
+
const config = { ...entry.install.config };
|
|
439
|
+
await installMcpServers(targetPath, { stitch: patchConfigForPlatform(config) });
|
|
440
|
+
console.log(chalk.green(` ✓ Stitch configured (OAuth proxy)`));
|
|
441
|
+
const skillResults = installCompanionSkills({ stitch: entry });
|
|
442
|
+
for (const r of skillResults) {
|
|
443
|
+
if (r.success) console.log(chalk.green(` ✓ Companion skills: ${r.repo}`));
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
|
|
352
449
|
// Remote MCPs — launch OAuth via claude mcp add
|
|
353
450
|
if (isRemoteMcp(entry.install.config)) {
|
|
354
451
|
console.log(chalk.cyan(` Launching ${capitalizedName} OAuth...`));
|
|
@@ -35,6 +35,7 @@ export const OUTPUT_SCHEMA = {
|
|
|
35
35
|
uiMockups: { filename: 'mockups.md', phaseDir: '1-ui', phase: 'uiux', protected: true, approvalGate: 'uiux' },
|
|
36
36
|
uiComponents: { filename: 'components.md', phaseDir: '1-ui', phase: 'uiux', protected: true, approvalGate: 'uiux' },
|
|
37
37
|
uiFlows: { filename: 'flows.md', phaseDir: '1-ui', phase: 'uiux', protected: true, approvalGate: 'uiux' },
|
|
38
|
+
stitchHtml: { filename: 'stitch-screen-map.json', phaseDir: '1-ui', phase: 'uiux', protected: true, approvalGate: 'uiux' },
|
|
38
39
|
decisions: { filename: 'decisions.md', phaseDir: '2-design', phase: 'design', protected: false },
|
|
39
40
|
recap: { filename: 'recap.md', phaseDir: '5-implement', phase: 'implement', protected: false },
|
|
40
41
|
};
|
|
@@ -30,7 +30,7 @@ export function detectProject(targetPath, opts = {}) {
|
|
|
30
30
|
try {
|
|
31
31
|
// ── Package.json analysis ──────────────────────────────────────────────
|
|
32
32
|
// Collect deps from root and common monorepo subfolders
|
|
33
|
-
const pkgLocations = ['.', 'src/frontend', 'frontend', 'apps/web', 'packages/app'];
|
|
33
|
+
const pkgLocations = ['.', 'src/frontend', 'src/web', 'frontend', 'apps/web', 'packages/app'];
|
|
34
34
|
const allDeps = {};
|
|
35
35
|
let rootPkg = null;
|
|
36
36
|
let scripts = [];
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { join } from 'path';
|
|
10
|
-
import { readFileSync, existsSync } from 'fs';
|
|
10
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'fs';
|
|
11
11
|
import { execSync } from 'child_process';
|
|
12
12
|
import { installMcpServers } from '../../utils/claude-settings-manager.js';
|
|
13
13
|
|
|
@@ -173,6 +173,145 @@ export function installCompanionSkills(mcpsInstalled, exec = execSync) {
|
|
|
173
173
|
return results;
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Generate the Stitch OAuth proxy template script.
|
|
178
|
+
* @param {string} gcpProject - GCP project ID for x-goog-user-project header
|
|
179
|
+
* @returns {string} CJS proxy script content
|
|
180
|
+
*/
|
|
181
|
+
export function getStitchProxyTemplate(gcpProject) {
|
|
182
|
+
return `/**
|
|
183
|
+
* Stitch MCP OAuth Proxy
|
|
184
|
+
* Generated by morph-spec mcp setup stitch
|
|
185
|
+
*
|
|
186
|
+
* Bridges stdio JSON-RPC to https://stitch.googleapis.com/mcp
|
|
187
|
+
* using gcloud application-default credentials (OAuth2 bearer token).
|
|
188
|
+
*/
|
|
189
|
+
const { execSync } = require('child_process');
|
|
190
|
+
const https = require('https');
|
|
191
|
+
|
|
192
|
+
const GCP_PROJECT = '${gcpProject}';
|
|
193
|
+
const STITCH_HOST = 'stitch.googleapis.com';
|
|
194
|
+
const STITCH_PATH = '/mcp';
|
|
195
|
+
const TOKEN_TTL_MS = 50 * 60 * 1000; // 50 minutes
|
|
196
|
+
|
|
197
|
+
let cachedToken = null;
|
|
198
|
+
let tokenExpiresAt = 0;
|
|
199
|
+
|
|
200
|
+
function getAccessToken() {
|
|
201
|
+
if (cachedToken && Date.now() < tokenExpiresAt) return cachedToken;
|
|
202
|
+
try {
|
|
203
|
+
cachedToken = execSync('gcloud auth application-default print-access-token', {
|
|
204
|
+
encoding: 'utf-8',
|
|
205
|
+
timeout: 10000,
|
|
206
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
207
|
+
}).trim();
|
|
208
|
+
tokenExpiresAt = Date.now() + TOKEN_TTL_MS;
|
|
209
|
+
return cachedToken;
|
|
210
|
+
} catch (err) {
|
|
211
|
+
process.stderr.write('[stitch-proxy] Failed to get access token. Run: gcloud auth application-default login\\n');
|
|
212
|
+
throw err;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function clearTokenCache() {
|
|
217
|
+
cachedToken = null;
|
|
218
|
+
tokenExpiresAt = 0;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function sendRequest(body) {
|
|
222
|
+
return new Promise((resolve, reject) => {
|
|
223
|
+
const token = getAccessToken();
|
|
224
|
+
const data = JSON.stringify(body);
|
|
225
|
+
const options = {
|
|
226
|
+
hostname: STITCH_HOST,
|
|
227
|
+
path: STITCH_PATH,
|
|
228
|
+
method: 'POST',
|
|
229
|
+
headers: {
|
|
230
|
+
'Content-Type': 'application/json',
|
|
231
|
+
'Content-Length': Buffer.byteLength(data),
|
|
232
|
+
'Authorization': \`Bearer \${token}\`,
|
|
233
|
+
'x-goog-user-project': GCP_PROJECT,
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const req = https.request(options, (res) => {
|
|
238
|
+
let responseData = '';
|
|
239
|
+
res.on('data', (chunk) => { responseData += chunk; });
|
|
240
|
+
res.on('end', () => {
|
|
241
|
+
if (res.statusCode === 401) {
|
|
242
|
+
// Token expired — retry once with fresh token
|
|
243
|
+
clearTokenCache();
|
|
244
|
+
const retryToken = getAccessToken();
|
|
245
|
+
options.headers['Authorization'] = \`Bearer \${retryToken}\`;
|
|
246
|
+
const retryData = JSON.stringify(body);
|
|
247
|
+
options.headers['Content-Length'] = Buffer.byteLength(retryData);
|
|
248
|
+
const retryReq = https.request(options, (retryRes) => {
|
|
249
|
+
let retryResponse = '';
|
|
250
|
+
retryRes.on('data', (chunk) => { retryResponse += chunk; });
|
|
251
|
+
retryRes.on('end', () => resolve(retryResponse));
|
|
252
|
+
});
|
|
253
|
+
retryReq.on('error', reject);
|
|
254
|
+
retryReq.write(retryData);
|
|
255
|
+
retryReq.end();
|
|
256
|
+
} else {
|
|
257
|
+
resolve(responseData);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
req.on('error', reject);
|
|
263
|
+
req.write(data);
|
|
264
|
+
req.end();
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Buffer stdin and split on newlines (JSON-RPC messages)
|
|
269
|
+
let buffer = '';
|
|
270
|
+
process.stdin.setEncoding('utf-8');
|
|
271
|
+
process.stdin.on('data', async (chunk) => {
|
|
272
|
+
buffer += chunk;
|
|
273
|
+
const lines = buffer.split('\\n');
|
|
274
|
+
buffer = lines.pop(); // keep incomplete line in buffer
|
|
275
|
+
|
|
276
|
+
for (const line of lines) {
|
|
277
|
+
if (!line.trim()) continue;
|
|
278
|
+
try {
|
|
279
|
+
const request = JSON.parse(line);
|
|
280
|
+
const response = await sendRequest(request);
|
|
281
|
+
process.stdout.write(response + '\\n');
|
|
282
|
+
} catch (err) {
|
|
283
|
+
const errorResponse = JSON.stringify({
|
|
284
|
+
jsonrpc: '2.0',
|
|
285
|
+
id: null,
|
|
286
|
+
error: { code: -32603, message: err.message || 'Proxy error' }
|
|
287
|
+
});
|
|
288
|
+
process.stdout.write(errorResponse + '\\n');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
process.stderr.write(\`[stitch-proxy] Ready — GCP project: \${GCP_PROJECT}\\n\`);
|
|
294
|
+
`;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Generate and write the Stitch OAuth proxy script to the target project.
|
|
299
|
+
* @param {string} targetPath - Project root directory
|
|
300
|
+
* @param {string} gcpProject - GCP project ID
|
|
301
|
+
* @returns {{ proxyPath: string }} Path to the generated proxy script
|
|
302
|
+
*/
|
|
303
|
+
export function generateStitchProxy(targetPath, gcpProject) {
|
|
304
|
+
const claudeDir = join(targetPath, '.claude');
|
|
305
|
+
if (!existsSync(claudeDir)) {
|
|
306
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const proxyPath = join(claudeDir, 'stitch-proxy.cjs');
|
|
310
|
+
writeFileSync(proxyPath, getStitchProxyTemplate(gcpProject), 'utf-8');
|
|
311
|
+
|
|
312
|
+
return { proxyPath };
|
|
313
|
+
}
|
|
314
|
+
|
|
176
315
|
/**
|
|
177
316
|
* Install a single MCP with provided credentials
|
|
178
317
|
* @param {string} targetPath - Project root directory
|