@polymorphism-tech/morph-spec 4.8.14 → 4.8.16
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/bin/morph-spec.js +23 -2
- package/bin/task-manager.js +202 -14
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/agents.json +113 -116
- package/framework/hooks/claude-code/post-tool-use/dispatch.js +48 -2
- package/framework/hooks/claude-code/post-tool-use/validator-feedback.js +151 -0
- package/framework/hooks/claude-code/pre-tool-use/enforce-phase-writes.js +6 -0
- package/framework/hooks/claude-code/pre-tool-use/protect-spec-files.js +6 -0
- package/framework/hooks/claude-code/session-start/inject-morph-context.js +27 -0
- package/framework/hooks/claude-code/stop/validate-completion.js +17 -2
- package/framework/hooks/claude-code/teammate-idle/teammate-idle.js +87 -0
- package/framework/hooks/claude-code/user-prompt/set-terminal-title.js +58 -0
- package/framework/hooks/shared/phase-utils.js +1 -1
- package/framework/hooks/shared/state-reader.js +1 -0
- package/framework/skills/README.md +1 -0
- package/framework/skills/level-0-meta/brainstorming/SKILL.md +2 -0
- package/framework/skills/level-0-meta/code-review/SKILL.md +16 -0
- package/framework/skills/level-0-meta/code-review/references/review-guidelines.md +100 -0
- package/framework/skills/level-0-meta/code-review/scripts/scan-csharp.mjs +36 -6
- package/framework/skills/level-0-meta/code-review-nextjs/SKILL.md +16 -0
- package/framework/skills/level-0-meta/code-review-nextjs/scripts/scan-nextjs.mjs +189 -0
- package/framework/skills/level-0-meta/frontend-review/SKILL.md +359 -0
- package/framework/skills/level-0-meta/frontend-review/scripts/scan-accessibility.mjs +376 -0
- package/framework/skills/level-0-meta/morph-checklist/SKILL.md +1 -1
- package/framework/skills/level-0-meta/morph-init/SKILL.md +3 -2
- package/framework/skills/level-0-meta/morph-replicate/SKILL.md +10 -8
- package/framework/skills/level-0-meta/morph-replicate/references/blazor-html-mapping.md +70 -0
- package/framework/skills/level-0-meta/post-implementation/SKILL.md +315 -0
- package/framework/skills/level-0-meta/post-implementation/scripts/detect-dev-server.mjs +153 -0
- package/framework/skills/level-0-meta/post-implementation/scripts/detect-stack.mjs +234 -0
- package/framework/skills/level-0-meta/terminal-title/SKILL.md +61 -0
- package/framework/skills/level-0-meta/terminal-title/scripts/set_title.sh +65 -0
- package/framework/skills/level-0-meta/tool-usage-guide/SKILL.md +13 -206
- package/framework/skills/level-0-meta/tool-usage-guide/references/tools-per-phase.md +213 -0
- package/framework/skills/level-0-meta/verification-before-completion/SKILL.md +2 -0
- package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +4 -7
- package/framework/skills/level-1-workflows/phase-codebase-analysis/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/phase-design/SKILL.md +16 -110
- package/framework/skills/level-1-workflows/phase-design/references/architecture-analysis-guide.md +89 -0
- package/framework/skills/level-1-workflows/phase-design/references/spec-authoring-guide.md +55 -0
- package/framework/skills/level-1-workflows/phase-implement/SKILL.md +153 -118
- package/framework/skills/level-1-workflows/phase-implement/references/vsa-implementation-guide.md +92 -0
- package/framework/skills/level-1-workflows/phase-setup/SKILL.md +1 -2
- package/framework/skills/level-1-workflows/phase-tasks/SKILL.md +11 -158
- package/framework/skills/level-1-workflows/phase-tasks/references/task-planning-patterns.md +172 -0
- package/framework/skills/level-1-workflows/phase-uiux/SKILL.md +42 -3
- package/framework/squad-templates/backend-only.json +14 -1
- package/framework/squad-templates/frontend-only.json +14 -1
- package/framework/squad-templates/full-stack.json +25 -8
- package/framework/standards/STANDARDS.json +631 -86
- package/framework/standards/frontend/design-system/aesthetic-direction.md +213 -0
- package/framework/templates/project/validate.js +122 -0
- package/framework/workflows/configs/zero-touch.json +7 -0
- package/package.json +1 -1
- package/src/commands/agents/dispatch-agents.js +53 -10
- package/src/commands/state/advance-phase.js +56 -0
- package/src/commands/state/index.js +2 -1
- package/src/commands/state/phase-runner.js +215 -0
- package/src/commands/tasks/task.js +23 -2
- package/src/core/paths/output-schema.js +1 -1
- package/src/lib/generators/recap-generator.js +16 -0
- package/src/lib/orchestration/team-orchestrator.js +171 -89
- package/src/lib/phase-chain/eligibility-checker.js +243 -0
- package/src/lib/standards/digest-builder.js +231 -0
- package/src/lib/validators/blazor/blazor-concurrency-analyzer.js +39 -0
- package/src/lib/validators/nextjs/next-component-validator.js +2 -0
- package/src/lib/validators/validation-runner.js +2 -2
- package/src/utils/file-copier.js +2 -0
- package/src/utils/hooks-installer.js +31 -7
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: post-implementation
|
|
3
|
+
description: Orquestra o fluxo completo pós-implementação: detecção de stack,
|
|
4
|
+
scans automáticos (C#/Next.js), testes, validate-feature, smoke test via
|
|
5
|
+
Playwright (obrigatório se dev server ativo), checklist de code review por
|
|
6
|
+
stack, checkpoint e geração de recap. Use após concluir todas as tasks.
|
|
7
|
+
argument-hint: "[feature-name]"
|
|
8
|
+
user-invocable: true
|
|
9
|
+
allowed-tools: Read, Write, Edit, Bash, Glob, Grep, Task,
|
|
10
|
+
mcp__playwright__browser_navigate, mcp__playwright__browser_snapshot,
|
|
11
|
+
mcp__playwright__browser_take_screenshot, mcp__playwright__browser_console_messages
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Post-Implementation Review
|
|
15
|
+
|
|
16
|
+
> Orquestrador pós-implementação: executa todos os checks em sequência lógica e bloqueia em caso de falha.
|
|
17
|
+
> Use após completar todas as tasks de implementação, antes de criar o PR.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Pré-requisitos
|
|
22
|
+
|
|
23
|
+
- Todas as tasks marcadas como done (`morph-spec task done`)
|
|
24
|
+
- Build do projeto compilando sem erros
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Step 1 — Detectar Stack
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
node .claude/skills/post-implementation/scripts/detect-stack.mjs
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Output JSON: `{ stack: "DOTNET" | "NEXTJS" | "FULLSTACK" | "UNKNOWN", frontendPath, backendPath, startCommand }`
|
|
35
|
+
|
|
36
|
+
Ordem de prioridade (mais confiável primeiro):
|
|
37
|
+
1. `.morph/config/config.json` → `config.project.stack` + `frontendPath`/`backendPath`
|
|
38
|
+
2. `.morph/context/README.md` → seção `## Tech Stack`
|
|
39
|
+
3. Fallback: Glob por `*.csproj` e `package.json` com dep `"next"`
|
|
40
|
+
|
|
41
|
+
Guarde o resultado — todos os passos seguintes dependem do `stack` detectado.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Step 2 — Automated Scans
|
|
46
|
+
|
|
47
|
+
Execute os scans automáticos conforme o stack detectado.
|
|
48
|
+
|
|
49
|
+
### Se DOTNET ou FULLSTACK:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
node .claude/skills/code-review/scripts/scan-csharp.mjs --diff
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Se NEXTJS ou FULLSTACK:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
node .claude/skills/code-review-nextjs/scripts/scan-nextjs.mjs --diff
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**🚫 BLOCK se qualquer finding CRITICAL encontrado.**
|
|
62
|
+
|
|
63
|
+
Corrija todos os CRITICALs antes de continuar. Para revisar a lista completa de checks:
|
|
64
|
+
- .NET: `/code-review`
|
|
65
|
+
- Next.js: `/code-review-nextjs`
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Step 3 — Test Suite
|
|
70
|
+
|
|
71
|
+
Execute os testes conforme o stack.
|
|
72
|
+
|
|
73
|
+
### DOTNET:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
dotnet test --verbosity minimal
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### NEXTJS:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npm test -- --watchAll=false
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### FULLSTACK:
|
|
86
|
+
|
|
87
|
+
Execute ambos em paralelo (use Task tool com dois subagents independentes).
|
|
88
|
+
|
|
89
|
+
**🚫 BLOCK se testes falharem.**
|
|
90
|
+
|
|
91
|
+
Não prossiga enquanto houver testes falhando. Corrija as falhas e re-execute antes de continuar.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Step 4 — Framework Validation
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npx morph-spec validate-feature $ARGUMENTS --phase implement
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**🚫 BLOCK se falhar.**
|
|
102
|
+
|
|
103
|
+
Leia o output de validação, corrija os issues reportados e re-execute até passar.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Step 5 — Smoke Test (Playwright MCP)
|
|
108
|
+
|
|
109
|
+
### 5a. Detectar Dev Server
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
node .claude/skills/post-implementation/scripts/detect-dev-server.mjs "<startCommand>"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Passe o `startCommand` retornado pelo detect-stack no Step 1.
|
|
116
|
+
|
|
117
|
+
O script:
|
|
118
|
+
1. Varre portas `[3000, 3001, 4200, 5000, 5001, 7000, 8000, 8080]` com `fetch()` timeout 500ms
|
|
119
|
+
2. Se encontrar um servidor ativo → retorna `{ found: true, url }`
|
|
120
|
+
3. Se não encontrar e `startCommand` fornecido → tenta iniciar automaticamente e aguarda 30s
|
|
121
|
+
4. Retorna exit code 0 (server disponível) ou 1 (não disponível após tentativa)
|
|
122
|
+
|
|
123
|
+
### 5b. Se dev server disponível (exit 0):
|
|
124
|
+
|
|
125
|
+
**O smoke test é OBRIGATÓRIO.**
|
|
126
|
+
|
|
127
|
+
1. Leia `spec.md` da feature para identificar os happy paths críticos (máximo 3 flows, seção Functional Requirements)
|
|
128
|
+
2. Execute o smoke test via Playwright MCP:
|
|
129
|
+
|
|
130
|
+
```javascript
|
|
131
|
+
// Navegar para a feature
|
|
132
|
+
await mcp__playwright__browser_navigate({ url: '<url-detectada>' });
|
|
133
|
+
|
|
134
|
+
// Capturar estado da página
|
|
135
|
+
await mcp__playwright__browser_snapshot();
|
|
136
|
+
|
|
137
|
+
// Verificar erros críticos de console
|
|
138
|
+
await mcp__playwright__browser_console_messages({ level: 'error' });
|
|
139
|
+
|
|
140
|
+
// Screenshot para documentação
|
|
141
|
+
await mcp__playwright__browser_take_screenshot({
|
|
142
|
+
type: 'png',
|
|
143
|
+
filename: '.morph/features/$ARGUMENTS/4-implement/smoke-screenshots/smoke-<timestamp>.png'
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Para cada happy path crítico da spec:
|
|
148
|
+
- Navegue até o flow
|
|
149
|
+
- Verifique elementos-chave visíveis (`browser_snapshot`)
|
|
150
|
+
- Confirme ausência de erros de console críticos
|
|
151
|
+
- Capture screenshot do estado final
|
|
152
|
+
|
|
153
|
+
**Verificações obrigatórias:**
|
|
154
|
+
- [ ] Página carrega sem erro 404/500
|
|
155
|
+
- [ ] Elementos principais da feature visíveis (conforme spec)
|
|
156
|
+
- [ ] Console sem erros críticos (level: error)
|
|
157
|
+
- [ ] Happy path principal funcional
|
|
158
|
+
|
|
159
|
+
**🚫 BLOCK se qualquer verificação falhar.** Não crie PR com smoke test falhando.
|
|
160
|
+
|
|
161
|
+
### 5c. Se dev server NÃO disponível (exit 1):
|
|
162
|
+
|
|
163
|
+
**⚠️ ATENÇÃO: Dev server não encontrado após tentativa de iniciar.**
|
|
164
|
+
|
|
165
|
+
Solicite confirmação explícita ao usuário antes de pular o smoke test:
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
Dev server não detectado na porta esperada. O smoke test via Playwright é obrigatório
|
|
169
|
+
para garantir que o código funciona no browser antes de criar o PR.
|
|
170
|
+
|
|
171
|
+
Opções:
|
|
172
|
+
1. Inicie manualmente o servidor (`npm run dev` / `dotnet run`) e re-execute /post-implementation
|
|
173
|
+
2. Confirme explicitamente que deseja pular o smoke test e por quê
|
|
174
|
+
|
|
175
|
+
Não é possível prosseguir para criação de PR sem smoke test ou confirmação explícita.
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Aguarde resposta antes de continuar.**
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Step 6 — Code Review Checklist (CRITICAL + HIGH)
|
|
183
|
+
|
|
184
|
+
> Antes de revisar, leia: `.claude/skills/code-review/references/review-guidelines.md` — aplique confidence ≥ 75, ignore violações pré-existentes, inclua file:line em cada finding.
|
|
185
|
+
|
|
186
|
+
Revise os itens mais importantes por stack. Para review completo, use os skills dedicados.
|
|
187
|
+
|
|
188
|
+
### Se FULLSTACK:
|
|
189
|
+
|
|
190
|
+
Dispatch dois subagents em paralelo (Task tool):
|
|
191
|
+
|
|
192
|
+
**Subagent 1 — Backend Review:**
|
|
193
|
+
> Revisor .NET focado. Escopo: APENAS arquivos .cs alterados (git diff main...HEAD).
|
|
194
|
+
> Run: `node .claude/skills/code-review/scripts/scan-csharp.mjs --diff`
|
|
195
|
+
> Depois revisar manualmente itens CRITICAL+HIGH do checklist .NET.
|
|
196
|
+
> Aplicar review-guidelines.md: confidence ≥ 75, skip pré-existentes.
|
|
197
|
+
> Output: findings com file:line, ou "✅ Sem CRITICAL/HIGH em backend alterado."
|
|
198
|
+
|
|
199
|
+
**Subagent 2 — Frontend Review:**
|
|
200
|
+
> Revisor Next.js focado. Escopo: APENAS arquivos .tsx/.ts alterados (git diff main...HEAD).
|
|
201
|
+
> Run: `node .claude/skills/code-review-nextjs/scripts/scan-nextjs.mjs --diff`
|
|
202
|
+
> Depois revisar manualmente itens CRITICAL+HIGH do checklist Next.js.
|
|
203
|
+
> Aplicar review-guidelines.md: confidence ≥ 75, skip pré-existentes.
|
|
204
|
+
> Output: findings com file:line, ou "✅ Sem CRITICAL/HIGH em frontend alterado."
|
|
205
|
+
|
|
206
|
+
Aguardar ambos. **🚫 BLOCK se algum retornar CRITICAL não resolvido.**
|
|
207
|
+
|
|
208
|
+
### Se DOTNET:
|
|
209
|
+
|
|
210
|
+
**Itens CRITICAL e HIGH — .NET:**
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
[ ] CancellationToken propagado em toda a chain async
|
|
214
|
+
[ ] Sem .Result / .Wait() (deadlock risk)
|
|
215
|
+
[ ] Domain tem zero refs para Infrastructure ou Web
|
|
216
|
+
[ ] Sem circular dependencies
|
|
217
|
+
[ ] Sem empty catch blocks
|
|
218
|
+
[ ] Background ops usam IDbContextFactory + await using
|
|
219
|
+
[ ] Métodos async têm sufixo Async
|
|
220
|
+
[ ] Interfaces prefixadas com I
|
|
221
|
+
[ ] Sem classes > 300 linhas
|
|
222
|
+
[ ] DTOs com nomes descritivos + tipos corretos
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
> Para lista completa: `/code-review`
|
|
226
|
+
|
|
227
|
+
### Se NEXTJS:
|
|
228
|
+
|
|
229
|
+
**Itens CRITICAL e HIGH — Next.js:**
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
[ ] node .claude/skills/code-review-nextjs/scripts/scan-nextjs.mjs --diff → 0 CRITICAL
|
|
233
|
+
[ ] File names em kebab-case (user-card.tsx, não UserCard.tsx)
|
|
234
|
+
[ ] 'use client' apenas em componentes com hooks/event handlers
|
|
235
|
+
[ ] Sem useEffect para data fetching → Server Component ou useQuery
|
|
236
|
+
[ ] Zod schema definido primeiro, type derivado com z.infer<>
|
|
237
|
+
[ ] zodResolver conectado ao useForm, useMutation para submit
|
|
238
|
+
[ ] Query key factory usado (userKeys.lists(), não ['users'])
|
|
239
|
+
[ ] Feature public API via features/{name}/index.ts
|
|
240
|
+
[ ] components/ui/ intocados (shadcn CLI only)
|
|
241
|
+
[ ] Sem any type annotation
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
> Para lista completa: `/code-review-nextjs`
|
|
245
|
+
|
|
246
|
+
**🚫 BLOCK se qualquer item CRITICAL não estiver resolvido.**
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Step 7 — Checkpoint + Recap
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
# Salvar checkpoint pós-review
|
|
254
|
+
npx morph-spec checkpoint-save $ARGUMENTS --note "post-implementation review"
|
|
255
|
+
|
|
256
|
+
# Gerar recap final
|
|
257
|
+
npx morph-spec generate recap $ARGUMENTS
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## Step 8 — PR Suggestion
|
|
263
|
+
|
|
264
|
+
Quando todos os steps passarem, sugira o PR:
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
gh pr create \
|
|
268
|
+
--title "feat($ARGUMENTS): <descrição concisa da feature>" \
|
|
269
|
+
--body "$(cat <<'EOF'
|
|
270
|
+
## Summary
|
|
271
|
+
|
|
272
|
+
- <principais mudanças implementadas>
|
|
273
|
+
- <tasks concluídas>
|
|
274
|
+
|
|
275
|
+
## Test Results
|
|
276
|
+
|
|
277
|
+
- Build: ✅ passou
|
|
278
|
+
- Tests: ✅ X testes passando
|
|
279
|
+
- Smoke Test: ✅ happy path verificado via Playwright
|
|
280
|
+
|
|
281
|
+
## Screenshots
|
|
282
|
+
|
|
283
|
+
<!-- Cole os paths dos screenshots salvos em smoke-screenshots/ -->
|
|
284
|
+
|
|
285
|
+
## Checklist
|
|
286
|
+
|
|
287
|
+
- [ ] Automated scans: 0 CRITICAL findings
|
|
288
|
+
- [ ] All tests passing
|
|
289
|
+
- [ ] Smoke test via Playwright: ✅
|
|
290
|
+
- [ ] Code review checklist: CRITICAL + HIGH itens verificados
|
|
291
|
+
- [ ] validate-feature: ✅ passou
|
|
292
|
+
- [ ] Checkpoint salvo
|
|
293
|
+
- [ ] recap.md gerado
|
|
294
|
+
EOF
|
|
295
|
+
)"
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Resumo dos BLOCKs
|
|
301
|
+
|
|
302
|
+
| Step | Condição de BLOCK |
|
|
303
|
+
|------|-------------------|
|
|
304
|
+
| Step 2 | Qualquer finding CRITICAL nos scans automáticos |
|
|
305
|
+
| Step 3 | Qualquer teste falhando |
|
|
306
|
+
| Step 4 | `validate-feature` falhando |
|
|
307
|
+
| Step 5 | Dev server ativo + smoke test com falha |
|
|
308
|
+
| Step 5 | Dev server não detectado sem confirmação explícita do usuário |
|
|
309
|
+
| Step 6 | Qualquer item CRITICAL não resolvido no checklist manual |
|
|
310
|
+
|
|
311
|
+
**Todos os BLOCKs devem ser resolvidos antes de criar o PR.**
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
*MORPH-SPEC by Polymorphism Tech*
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* detect-dev-server.mjs
|
|
4
|
+
*
|
|
5
|
+
* Detects an active dev server on common localhost ports,
|
|
6
|
+
* optionally starting one if not found.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node detect-dev-server.mjs [startCommand]
|
|
10
|
+
*
|
|
11
|
+
* Examples:
|
|
12
|
+
* node detect-dev-server.mjs
|
|
13
|
+
* node detect-dev-server.mjs "npm run dev"
|
|
14
|
+
* node detect-dev-server.mjs "dotnet run --project src/Api/Api.csproj"
|
|
15
|
+
*
|
|
16
|
+
* Output JSON (stdout):
|
|
17
|
+
* { found: true, url: "http://localhost:3000", started?: true } → exit 0
|
|
18
|
+
* { found: false, error?: "..." } → exit 1
|
|
19
|
+
*
|
|
20
|
+
* Behavior:
|
|
21
|
+
* 1. Scan ports [3000, 3001, 4200, 5000, 5001, 7000, 8000, 8080] with 500ms timeout
|
|
22
|
+
* 2. If found → output { found: true, url } and exit 0
|
|
23
|
+
* 3. If not found and startCommand provided:
|
|
24
|
+
* - Spawn startCommand as background process
|
|
25
|
+
* - Poll ports every 2s for up to 30s
|
|
26
|
+
* - If server starts → output { found: true, url, started: true } and exit 0
|
|
27
|
+
* - If timeout → output { found: false, error: "timeout after 30s" } and exit 1
|
|
28
|
+
* 4. If not found and no startCommand → output { found: false } and exit 1
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { spawn } from 'child_process';
|
|
32
|
+
|
|
33
|
+
const PORTS = [3000, 3001, 4200, 5000, 5001, 7000, 8000, 8080];
|
|
34
|
+
const FETCH_TIMEOUT_MS = 500;
|
|
35
|
+
const START_TIMEOUT_MS = 30_000;
|
|
36
|
+
const POLL_INTERVAL_MS = 2_000;
|
|
37
|
+
|
|
38
|
+
const startCommand = process.argv[2] ?? null;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Try to fetch a URL with a timeout.
|
|
42
|
+
* Returns true if response is received (any status code), false on network error/timeout.
|
|
43
|
+
*/
|
|
44
|
+
async function isPortOpen(url) {
|
|
45
|
+
const controller = new AbortController();
|
|
46
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
47
|
+
try {
|
|
48
|
+
await fetch(url, { signal: controller.signal });
|
|
49
|
+
return true;
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
} finally {
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Scan all known ports and return the first responsive URL, or null.
|
|
59
|
+
*/
|
|
60
|
+
async function scanPorts() {
|
|
61
|
+
for (const port of PORTS) {
|
|
62
|
+
const url = `http://localhost:${port}`;
|
|
63
|
+
if (await isPortOpen(url)) {
|
|
64
|
+
return url;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─── Step 1: Scan for existing server ────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
const existing = await scanPorts();
|
|
73
|
+
|
|
74
|
+
if (existing) {
|
|
75
|
+
console.log(JSON.stringify({ found: true, url: existing }, null, 2));
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── Step 2: No server found, try to start one ───────────────────────────────
|
|
80
|
+
|
|
81
|
+
if (!startCommand) {
|
|
82
|
+
console.log(JSON.stringify({ found: false }, null, 2));
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Spawn the start command as a background process
|
|
87
|
+
// Support both simple commands and commands with "cd X && Y" syntax
|
|
88
|
+
let child;
|
|
89
|
+
try {
|
|
90
|
+
// Use shell=true to handle complex commands like "cd frontend && npm run dev"
|
|
91
|
+
child = spawn(startCommand, [], {
|
|
92
|
+
shell: true,
|
|
93
|
+
detached: false,
|
|
94
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Suppress child output (we only care if it starts)
|
|
98
|
+
child.stdout?.resume();
|
|
99
|
+
child.stderr?.resume();
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.log(JSON.stringify({ found: false, error: `Failed to spawn: ${err.message}` }, null, 2));
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Poll for the server to become available
|
|
106
|
+
const startedAt = Date.now();
|
|
107
|
+
|
|
108
|
+
const result = await new Promise((resolve) => {
|
|
109
|
+
const poll = async () => {
|
|
110
|
+
const elapsed = Date.now() - startedAt;
|
|
111
|
+
|
|
112
|
+
if (elapsed >= START_TIMEOUT_MS) {
|
|
113
|
+
resolve({ found: false, error: 'timeout after 30s — server did not start in time' });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const url = await scanPorts();
|
|
118
|
+
if (url) {
|
|
119
|
+
resolve({ found: true, url, started: true });
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
setTimeout(poll, POLL_INTERVAL_MS);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// Handle child process errors / early exit
|
|
127
|
+
child.on('error', (err) => {
|
|
128
|
+
resolve({ found: false, error: `Process error: ${err.message}` });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
child.on('exit', (code) => {
|
|
132
|
+
// Only treat as error if it exited quickly (< 3s) with non-zero
|
|
133
|
+
if (Date.now() - startedAt < 3_000 && code !== 0 && code !== null) {
|
|
134
|
+
resolve({ found: false, error: `Process exited with code ${code}` });
|
|
135
|
+
}
|
|
136
|
+
// Otherwise it may have crashed after starting — let the poll timeout handle it
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Start polling after a short initial delay
|
|
140
|
+
setTimeout(poll, POLL_INTERVAL_MS);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Clean up the child process if still running
|
|
144
|
+
if (child && !child.killed) {
|
|
145
|
+
try {
|
|
146
|
+
child.kill('SIGTERM');
|
|
147
|
+
} catch {
|
|
148
|
+
// ignore cleanup errors
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
console.log(JSON.stringify(result, null, 2));
|
|
153
|
+
process.exit(result.found ? 0 : 1);
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* detect-stack.mjs
|
|
4
|
+
*
|
|
5
|
+
* Detects the project tech stack from config, context, or file scan.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node detect-stack.mjs
|
|
9
|
+
*
|
|
10
|
+
* Output JSON:
|
|
11
|
+
* {
|
|
12
|
+
* stack: "DOTNET" | "NEXTJS" | "FULLSTACK" | "UNKNOWN",
|
|
13
|
+
* frontendPath: string | null,
|
|
14
|
+
* backendPath: string | null,
|
|
15
|
+
* startCommand: string | null
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* Detection order (most reliable first):
|
|
19
|
+
* 1. .morph/config/config.json → config.project.stack + frontendPath/backendPath
|
|
20
|
+
* 2. .morph/context/README.md → "## Tech Stack" section
|
|
21
|
+
* 3. Fallback: glob *.csproj (DOTNET) + package.json with dep "next" (NEXTJS)
|
|
22
|
+
*
|
|
23
|
+
* Exit code: 0 always
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
|
|
27
|
+
import { join, relative, dirname } from 'path';
|
|
28
|
+
|
|
29
|
+
const cwd = process.cwd();
|
|
30
|
+
|
|
31
|
+
/** Try to read a JSON file safely. Returns null on failure. */
|
|
32
|
+
function readJson(filePath) {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Try to read a text file safely. Returns null on failure. */
|
|
41
|
+
function readText(filePath) {
|
|
42
|
+
try {
|
|
43
|
+
return readFileSync(filePath, 'utf-8');
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Find files matching a pattern recursively.
|
|
51
|
+
* Returns relative paths from cwd.
|
|
52
|
+
*/
|
|
53
|
+
function findFiles(dir, predicate, maxDepth = 4, _depth = 0) {
|
|
54
|
+
if (_depth > maxDepth) return [];
|
|
55
|
+
if (!existsSync(dir)) return [];
|
|
56
|
+
const results = [];
|
|
57
|
+
const ignored = ['node_modules', '.git', '.next', 'dist', 'build', '.turbo', 'bin', 'obj', '.morph'];
|
|
58
|
+
try {
|
|
59
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
60
|
+
if (entry.isDirectory()) {
|
|
61
|
+
if (!ignored.includes(entry.name)) {
|
|
62
|
+
results.push(...findFiles(join(dir, entry.name), predicate, maxDepth, _depth + 1));
|
|
63
|
+
}
|
|
64
|
+
} else if (entry.isFile() && predicate(entry.name)) {
|
|
65
|
+
results.push(join(dir, entry.name));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
// ignore permission errors
|
|
70
|
+
}
|
|
71
|
+
return results;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Normalize a stack string from config to canonical form.
|
|
76
|
+
* Handles values like "nextjs+dotnet", "dotnet", "next.js", "nextjs", etc.
|
|
77
|
+
*/
|
|
78
|
+
function normalizeStack(raw) {
|
|
79
|
+
if (!raw || typeof raw !== 'string') return null;
|
|
80
|
+
const s = raw.toLowerCase();
|
|
81
|
+
const hasDotnet = s.includes('dotnet') || s.includes('.net');
|
|
82
|
+
const hasNext = s.includes('next') || s.includes('nextjs');
|
|
83
|
+
if (hasDotnet && hasNext) return 'FULLSTACK';
|
|
84
|
+
if (hasDotnet) return 'DOTNET';
|
|
85
|
+
if (hasNext) return 'NEXTJS';
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Parse "## Tech Stack" section from a README-style markdown file.
|
|
91
|
+
* Returns normalized stack or null.
|
|
92
|
+
*/
|
|
93
|
+
function parseReadmeStack(content) {
|
|
94
|
+
if (!content) return null;
|
|
95
|
+
// Find "## Tech Stack" section and read next ~10 lines
|
|
96
|
+
const lines = content.split('\n');
|
|
97
|
+
let inSection = false;
|
|
98
|
+
const sectionLines = [];
|
|
99
|
+
for (const line of lines) {
|
|
100
|
+
if (/^##\s+tech\s+stack/i.test(line)) {
|
|
101
|
+
inSection = true;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (inSection) {
|
|
105
|
+
if (/^##/.test(line)) break; // next section
|
|
106
|
+
sectionLines.push(line.toLowerCase());
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (sectionLines.length === 0) return null;
|
|
110
|
+
const text = sectionLines.join(' ');
|
|
111
|
+
const hasDotnet = /dotnet|\.net|c#|asp\.net/.test(text);
|
|
112
|
+
const hasNext = /next\.?js|nextjs/.test(text);
|
|
113
|
+
if (hasDotnet && hasNext) return 'FULLSTACK';
|
|
114
|
+
if (hasDotnet) return 'DOTNET';
|
|
115
|
+
if (hasNext) return 'NEXTJS';
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Build a startCommand from stack + paths.
|
|
121
|
+
*/
|
|
122
|
+
function buildStartCommand(stack, frontendPath, backendPath) {
|
|
123
|
+
if (stack === 'NEXTJS') {
|
|
124
|
+
const base = frontendPath ? join(cwd, frontendPath) : cwd;
|
|
125
|
+
// Check for custom dev script
|
|
126
|
+
const pkgPath = join(base, 'package.json');
|
|
127
|
+
const pkg = readJson(pkgPath);
|
|
128
|
+
if (pkg?.scripts?.dev) {
|
|
129
|
+
return frontendPath ? `cd ${frontendPath} && npm run dev` : 'npm run dev';
|
|
130
|
+
}
|
|
131
|
+
return frontendPath ? `cd ${frontendPath} && npm run dev` : 'npm run dev';
|
|
132
|
+
}
|
|
133
|
+
if (stack === 'DOTNET') {
|
|
134
|
+
if (backendPath) {
|
|
135
|
+
// Try to find .csproj inside backendPath
|
|
136
|
+
const csprojFiles = findFiles(join(cwd, backendPath), (n) => n.endsWith('.csproj'), 2);
|
|
137
|
+
if (csprojFiles.length > 0) {
|
|
138
|
+
const rel = relative(cwd, csprojFiles[0]).replace(/\\/g, '/');
|
|
139
|
+
return `dotnet run --project ${rel}`;
|
|
140
|
+
}
|
|
141
|
+
return `dotnet run --project ${backendPath}`;
|
|
142
|
+
}
|
|
143
|
+
// Find any .csproj
|
|
144
|
+
const csprojFiles = findFiles(cwd, (n) => n.endsWith('.csproj'), 4);
|
|
145
|
+
if (csprojFiles.length > 0) {
|
|
146
|
+
const rel = relative(cwd, csprojFiles[0]).replace(/\\/g, '/');
|
|
147
|
+
return `dotnet run --project ${rel}`;
|
|
148
|
+
}
|
|
149
|
+
return 'dotnet run';
|
|
150
|
+
}
|
|
151
|
+
if (stack === 'FULLSTACK') {
|
|
152
|
+
// Prefer Next.js start for smoke testing
|
|
153
|
+
const fe = frontendPath ? join(cwd, frontendPath) : cwd;
|
|
154
|
+
return frontendPath ? `cd ${frontendPath} && npm run dev` : 'npm run dev';
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ─── Detection Strategy 1: .morph/config/config.json ─────────────────────────
|
|
160
|
+
|
|
161
|
+
const configPath = join(cwd, '.morph', 'config', 'config.json');
|
|
162
|
+
const config = readJson(configPath);
|
|
163
|
+
|
|
164
|
+
if (config?.config?.project?.stack) {
|
|
165
|
+
const stack = normalizeStack(config.config.project.stack);
|
|
166
|
+
if (stack) {
|
|
167
|
+
const frontendPath = config.config.project.frontendPath ?? null;
|
|
168
|
+
const backendPath = config.config.project.backendPath ?? null;
|
|
169
|
+
const startCommand = buildStartCommand(stack, frontendPath, backendPath);
|
|
170
|
+
console.log(JSON.stringify({ stack, frontendPath, backendPath, startCommand }, null, 2));
|
|
171
|
+
process.exit(0);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ─── Detection Strategy 2: .morph/context/README.md ──────────────────────────
|
|
176
|
+
|
|
177
|
+
const readmePath = join(cwd, '.morph', 'context', 'README.md');
|
|
178
|
+
const readmeContent = readText(readmePath);
|
|
179
|
+
const readmeStack = parseReadmeStack(readmeContent);
|
|
180
|
+
|
|
181
|
+
if (readmeStack) {
|
|
182
|
+
const startCommand = buildStartCommand(readmeStack, null, null);
|
|
183
|
+
console.log(JSON.stringify({
|
|
184
|
+
stack: readmeStack,
|
|
185
|
+
frontendPath: null,
|
|
186
|
+
backendPath: null,
|
|
187
|
+
startCommand,
|
|
188
|
+
}, null, 2));
|
|
189
|
+
process.exit(0);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ─── Detection Strategy 3: File scan ─────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
const csprojFiles = findFiles(cwd, (n) => n.endsWith('.csproj'), 4);
|
|
195
|
+
const hasDotnet = csprojFiles.length > 0;
|
|
196
|
+
|
|
197
|
+
// Find package.json files with "next" dependency (excluding node_modules already filtered)
|
|
198
|
+
const packageJsonFiles = findFiles(cwd, (n) => n === 'package.json', 4);
|
|
199
|
+
let hasNext = false;
|
|
200
|
+
let nextPackageDir = null;
|
|
201
|
+
|
|
202
|
+
for (const pkgFile of packageJsonFiles) {
|
|
203
|
+
const pkg = readJson(pkgFile);
|
|
204
|
+
if (pkg?.dependencies?.next || pkg?.devDependencies?.next) {
|
|
205
|
+
hasNext = true;
|
|
206
|
+
nextPackageDir = relative(cwd, dirname(pkgFile)).replace(/\\/g, '/') || null;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let stack = 'UNKNOWN';
|
|
212
|
+
let frontendPath = null;
|
|
213
|
+
let backendPath = null;
|
|
214
|
+
|
|
215
|
+
if (hasDotnet && hasNext) {
|
|
216
|
+
stack = 'FULLSTACK';
|
|
217
|
+
frontendPath = nextPackageDir;
|
|
218
|
+
backendPath = csprojFiles.length > 0
|
|
219
|
+
? relative(cwd, dirname(csprojFiles[0])).replace(/\\/g, '/') || null
|
|
220
|
+
: null;
|
|
221
|
+
} else if (hasDotnet) {
|
|
222
|
+
stack = 'DOTNET';
|
|
223
|
+
backendPath = csprojFiles.length > 0
|
|
224
|
+
? relative(cwd, dirname(csprojFiles[0])).replace(/\\/g, '/') || null
|
|
225
|
+
: null;
|
|
226
|
+
} else if (hasNext) {
|
|
227
|
+
stack = 'NEXTJS';
|
|
228
|
+
frontendPath = nextPackageDir;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const startCommand = buildStartCommand(stack, frontendPath, backendPath);
|
|
232
|
+
|
|
233
|
+
console.log(JSON.stringify({ stack, frontendPath, backendPath, startCommand }, null, 2));
|
|
234
|
+
process.exit(0);
|