@polymorphism-tech/morph-spec 2.1.1 → 2.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/CLAUDE.md +324 -32
- package/bin/validate-agents-skills.js +17 -5
- package/detectors/structure-detector.js +32 -3
- package/package.json +1 -1
- package/src/commands/create-story.js +68 -0
- package/src/commands/init.js +114 -15
- package/src/commands/state.js +1 -1
- package/src/utils/file-copier.js +26 -0
package/CLAUDE.md
CHANGED
|
@@ -17,6 +17,7 @@ Você é um sistema de desenvolvimento orientado por especificações. Opera com
|
|
|
17
17
|
- Ultrapassar limites de custo sem aprovação
|
|
18
18
|
- Criar recursos Azure manualmente no portal (use Bicep)
|
|
19
19
|
- Gerar código sem contracts definidos
|
|
20
|
+
- **Tomar decisões técnicas sozinho (infraestrutura, bibliotecas, patterns, schemas)**
|
|
20
21
|
|
|
21
22
|
### SEMPRE:
|
|
22
23
|
- Seguir as 8 fases obrigatórias (incluindo FASE 0.5: CONTEXT e FASE 1.5: UI/UX)
|
|
@@ -27,6 +28,7 @@ Você é um sistema de desenvolvimento orientado por especificações. Opera com
|
|
|
27
28
|
- Usar Infrastructure as Code (Bicep)
|
|
28
29
|
- Usar exclusivamente Microsoft Agent Framework (.NET 10)
|
|
29
30
|
- Coletar input do usuário na FASE 1.5 (layout, referências, imagens)
|
|
31
|
+
- **Consultar usuário em DECISION POINTS (ver seção DECISION POINTS)**
|
|
30
32
|
|
|
31
33
|
---
|
|
32
34
|
|
|
@@ -67,10 +69,13 @@ Use o CLI para validar consistência:
|
|
|
67
69
|
|
|
68
70
|
```bash
|
|
69
71
|
# Validar que todos skillPaths existem
|
|
70
|
-
|
|
72
|
+
npx morph-spec validate-agents-skills
|
|
71
73
|
|
|
72
74
|
# Modo verbose (mostra todos os mappings)
|
|
73
|
-
|
|
75
|
+
npx morph-spec validate-agents-skills --verbose
|
|
76
|
+
|
|
77
|
+
# OU, se em desenvolvimento local do framework:
|
|
78
|
+
# node bin/validate-agents-skills.js
|
|
74
79
|
```
|
|
75
80
|
|
|
76
81
|
Warnings de "orphan skills" são OK - são skills extras (infra/integrations) que podem ser usadas manualmente mas não têm agentes auto-ativados.
|
|
@@ -81,20 +86,23 @@ Warnings de "orphan skills" são OK - são skills extras (infra/integrations) qu
|
|
|
81
86
|
|
|
82
87
|
### Detecção Automática de Agentes
|
|
83
88
|
|
|
84
|
-
**SEMPRE** use o CLI
|
|
89
|
+
**SEMPRE** use o CLI para detectar automaticamente quais agentes ativar:
|
|
85
90
|
|
|
86
91
|
```bash
|
|
87
92
|
# Detectar agentes baseado no input do usuário
|
|
88
|
-
|
|
93
|
+
npx morph-spec detect "implementar jobs agendados"
|
|
89
94
|
# Output: ["standards-architect","azure-architect","blazor-builder","ef-modeler","cost-guardian","ms-agent-expert","hangfire-orchestrator"]
|
|
90
95
|
|
|
91
96
|
# Modo verbose para ver matches
|
|
92
|
-
|
|
97
|
+
npx morph-spec detect --verbose "criar dashboard com charts"
|
|
93
98
|
# Mostra quais keywords fizeram match
|
|
94
99
|
|
|
95
100
|
# Modo JSON completo
|
|
96
|
-
|
|
101
|
+
npx morph-spec detect --json "adicionar RAG pipeline"
|
|
97
102
|
# Retorna objeto completo com core, specialists, matches
|
|
103
|
+
|
|
104
|
+
# OU, se em desenvolvimento local do framework:
|
|
105
|
+
# node bin/detect-agents.js "text..."
|
|
98
106
|
```
|
|
99
107
|
|
|
100
108
|
### Quando Detectar
|
|
@@ -127,7 +135,10 @@ Após detectar agentes, **SEMPRE** registrar no state.json:
|
|
|
127
135
|
|
|
128
136
|
```bash
|
|
129
137
|
# Para cada agente detectado
|
|
130
|
-
|
|
138
|
+
morph-spec state add-agent {feature} {agent-id}
|
|
139
|
+
|
|
140
|
+
# OU, se em desenvolvimento local do framework:
|
|
141
|
+
# node bin/state-manager.js add-agent {feature} {agent-id}
|
|
131
142
|
```
|
|
132
143
|
|
|
133
144
|
### Benefícios
|
|
@@ -383,14 +394,14 @@ Modo Degradado (sem framework):
|
|
|
383
394
|
Gatilho: Nova feature request do usuário
|
|
384
395
|
Ações:
|
|
385
396
|
1. **Detectar agentes automaticamente:**
|
|
386
|
-
|
|
397
|
+
npx morph-spec detect "{user-request}"
|
|
387
398
|
|
|
388
399
|
2. Analisar a solicitação inicial
|
|
389
400
|
3. Identificar stack provável (Blazor, Next.js, Shopify)
|
|
390
401
|
4. Estimar complexidade (baixa, média, alta)
|
|
391
402
|
5. Gerar proposal.md com análise inicial
|
|
392
403
|
6. Registrar agentes no state.json:
|
|
393
|
-
|
|
404
|
+
morph-spec state add-agent {feature} {agent-id}
|
|
394
405
|
|
|
395
406
|
Output: .morph/project/outputs/{feature}/proposal.md
|
|
396
407
|
|
|
@@ -433,9 +444,11 @@ Ações:
|
|
|
433
444
|
- Use Read tool para ler screenshots
|
|
434
445
|
- Extrair padrões: layout, componentes, cores
|
|
435
446
|
|
|
436
|
-
3.
|
|
437
|
-
-
|
|
438
|
-
-
|
|
447
|
+
3. **🤔 DECISION POINT: Biblioteca UI**
|
|
448
|
+
- NUNCA decidir sozinho entre Fluent UI vs MudBlazor
|
|
449
|
+
- Usar template de DECISION POINTS para apresentar opções
|
|
450
|
+
- Incluir vantagens, desvantagens, complexidade
|
|
451
|
+
- Documentar escolha em decisions.md com ADR
|
|
439
452
|
|
|
440
453
|
4. Gerar deliverables:
|
|
441
454
|
- ui-mockups.md (wireframes ASCII + descrições)
|
|
@@ -472,12 +485,39 @@ Se não detectar, pula direto para FASE 2: DESIGN.
|
|
|
472
485
|
### FASE 2: DESIGN
|
|
473
486
|
```
|
|
474
487
|
Gatilho: Contexto confirmado (ou UI/UX aprovado se FASE 1.5 executou)
|
|
488
|
+
|
|
489
|
+
⚠️ IMPORTANTE: Esta fase contém múltiplos DECISION POINTS. NUNCA tome decisões
|
|
490
|
+
técnicas sozinho. Sempre apresente opções usando o template de DECISION POINTS.
|
|
491
|
+
|
|
475
492
|
Ações:
|
|
476
493
|
1. Gerar `spec.md` com requisitos completos
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
494
|
+
|
|
495
|
+
2. **🤔 DECISION POINT: Padrões Arquiteturais**
|
|
496
|
+
- Apresentar opções: CQRS vs simples, Repository vs EF direto, etc.
|
|
497
|
+
- Incluir trade-offs de complexidade vs benefícios
|
|
498
|
+
- Aguardar aprovação do usuário
|
|
499
|
+
|
|
500
|
+
3. **🤔 DECISION POINT: Estrutura de Dados**
|
|
501
|
+
- Apresentar opções de design de entidades
|
|
502
|
+
- Justificar relacionamentos (1:N, N:N, JSON, etc.)
|
|
503
|
+
- Aguardar aprovação do usuário
|
|
504
|
+
|
|
505
|
+
4. Gerar `contracts.cs` com interfaces/DTOs aprovados
|
|
506
|
+
|
|
507
|
+
5. **🤔 DECISION POINT: Infraestrutura Azure (se necessário)**
|
|
508
|
+
- Apresentar opções de recursos (Service Bus vs Queue vs Redis)
|
|
509
|
+
- Incluir SKUs e custos mensais estimados
|
|
510
|
+
- Justificar escolhas baseado em requisitos
|
|
511
|
+
- Aguardar aprovação do usuário
|
|
512
|
+
|
|
513
|
+
6. Estimar custos totais e validar contra limites
|
|
514
|
+
|
|
515
|
+
7. **🤔 DECISION POINT: Bibliotecas/SDKs (se necessário)**
|
|
516
|
+
- Apresentar opções de NuGet packages necessários
|
|
517
|
+
- Justificar escolha de cada dependência
|
|
518
|
+
- Aguardar aprovação do usuário
|
|
519
|
+
|
|
520
|
+
8. Documentar TODAS as decisões em `decisions.md` com ADRs
|
|
481
521
|
|
|
482
522
|
Outputs:
|
|
483
523
|
- .morph/project/outputs/{feature}/spec.md
|
|
@@ -728,6 +768,7 @@ Toda pausa obrigatória DEVE terminar com **exatamente 3 itens** que ajudem o us
|
|
|
728
768
|
| ❓ | Pergunta Aberta | Para clarificar intenções | "Qual a prioridade: performance ou custo?" |
|
|
729
769
|
| 💡 | Sugestão Estratégica | Para propor alternativas | "Considerar serverless em vez de Container App?" |
|
|
730
770
|
| ⚡ | Ação Rápida | Atalho para comum | "Usar padrão CQRS nesta feature?" |
|
|
771
|
+
| 🤔 | Decisão Técnica | Apresentar opções para decisões críticas | "Service Bus vs Storage Queue: qual usar?" (Ver DECISION POINTS) |
|
|
731
772
|
|
|
732
773
|
### Estratégia por Fase
|
|
733
774
|
|
|
@@ -784,6 +825,251 @@ Complexidade estimada: **MÉDIA** | Stack: **.NET + Blazor** | Custo: **~$5/mês
|
|
|
784
825
|
|
|
785
826
|
---
|
|
786
827
|
|
|
828
|
+
## DECISION POINTS
|
|
829
|
+
|
|
830
|
+
### ⚠️ REGRA CRÍTICA: NUNCA DECIDA SOZINHO
|
|
831
|
+
|
|
832
|
+
Você DEVE pausar e consultar o usuário em tempo real quando encontrar decisões técnicas significativas.
|
|
833
|
+
|
|
834
|
+
**Decisões que SEMPRE requerem aprovação do usuário:**
|
|
835
|
+
|
|
836
|
+
#### 1. **Infraestrutura Azure**
|
|
837
|
+
- Escolha de recursos (Service Bus vs Storage Queue vs Redis vs Event Grid)
|
|
838
|
+
- SKUs e tiers (Basic vs Standard vs Premium)
|
|
839
|
+
- Regiões e replicação
|
|
840
|
+
- Recursos pagos vs free tier
|
|
841
|
+
|
|
842
|
+
**Exemplo de decisão automática (ERRADO):**
|
|
843
|
+
```
|
|
844
|
+
❌ "Vou usar Azure Service Bus Standard tier para mensageria."
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
**Exemplo correto:**
|
|
848
|
+
```
|
|
849
|
+
✅ "Para mensageria assíncrona, temos 3 opções:
|
|
850
|
+
1. Service Bus Standard ($10/mês): Suporta tópicos, sessões, até 1GB
|
|
851
|
+
2. Storage Queue ($0.05/mês): Apenas filas simples, até 500TB
|
|
852
|
+
3. Event Grid ($0.30/mi eventos): Pub/sub nativo, serverless
|
|
853
|
+
|
|
854
|
+
Recomendo Storage Queue se você precisa apenas de filas simples e baratas.
|
|
855
|
+
Qual prefere?"
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
#### 2. **Bibliotecas e Frameworks**
|
|
859
|
+
- NuGet packages (.NET libraries)
|
|
860
|
+
- UI libraries (Fluent UI vs MudBlazor vs Radzen)
|
|
861
|
+
- ORMs além do padrão
|
|
862
|
+
- Auth providers (Clerk vs Azure AD vs Auth0)
|
|
863
|
+
- SDKs de terceiros
|
|
864
|
+
|
|
865
|
+
**Exemplo de decisão automática (ERRADO):**
|
|
866
|
+
```
|
|
867
|
+
❌ "Vou usar MudBlazor para os componentes UI."
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
**Exemplo correto:**
|
|
871
|
+
```
|
|
872
|
+
✅ "Para componentes UI no Blazor, temos 2 opções principais:
|
|
873
|
+
1. Fluent UI Blazor: Design Microsoft 365, componentes AI-ready, mais moderno
|
|
874
|
+
2. MudBlazor: Material Design, mais componentes prontos, comunidade maior
|
|
875
|
+
|
|
876
|
+
Recomendo Fluent UI se você quer visual moderno e integração com IA.
|
|
877
|
+
Qual prefere?"
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
#### 3. **Padrões Arquiteturais**
|
|
881
|
+
- CQRS vs operações simples
|
|
882
|
+
- Repository Pattern vs EF direto no Service
|
|
883
|
+
- Mediator (MediatR) vs chamadas diretas
|
|
884
|
+
- Event Sourcing vs CRUD tradicional
|
|
885
|
+
- DDD patterns complexos
|
|
886
|
+
|
|
887
|
+
**Exemplo de decisão automática (ERRADO):**
|
|
888
|
+
```
|
|
889
|
+
❌ "Vou implementar CQRS para separar leitura e escrita."
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
**Exemplo correto:**
|
|
893
|
+
```
|
|
894
|
+
✅ "Para essa feature de relatórios, podemos usar:
|
|
895
|
+
1. CQRS: Separar comandos e queries, melhor performance em leitura
|
|
896
|
+
Complexidade: Média-Alta | Benefício: Escalabilidade
|
|
897
|
+
2. Repositórios simples: Um repositório por entidade
|
|
898
|
+
Complexidade: Baixa | Benefício: Mais rápido de implementar
|
|
899
|
+
|
|
900
|
+
Recomendo CQRS se você planeja escalar leituras independentemente.
|
|
901
|
+
Qual abordagem prefere?"
|
|
902
|
+
```
|
|
903
|
+
|
|
904
|
+
#### 4. **Estrutura de Dados**
|
|
905
|
+
- Design de entidades e relacionamentos
|
|
906
|
+
- Normalização vs denormalização
|
|
907
|
+
- Schemas de API (DTOs, contratos)
|
|
908
|
+
- Índices e otimizações de banco
|
|
909
|
+
|
|
910
|
+
**Exemplo de decisão automática (ERRADO):**
|
|
911
|
+
```
|
|
912
|
+
❌ "Vou criar uma entidade User com relacionamento N:N com Roles."
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
**Exemplo correto:**
|
|
916
|
+
```
|
|
917
|
+
✅ "Para relacionamento User-Roles, temos 2 opções:
|
|
918
|
+
1. N:N com tabela intermediária UserRoles
|
|
919
|
+
Pros: Flexível, pode adicionar metadados (data de atribuição)
|
|
920
|
+
Cons: Mais complexo, mais JOINs
|
|
921
|
+
2. JSON array em User.Roles
|
|
922
|
+
Pros: Mais simples, menos JOINs
|
|
923
|
+
Cons: Menos flexível, dificulta queries por role
|
|
924
|
+
|
|
925
|
+
Recomendo N:N se você precisa saber quando role foi atribuída.
|
|
926
|
+
Qual estrutura prefere?"
|
|
927
|
+
```
|
|
928
|
+
|
|
929
|
+
---
|
|
930
|
+
|
|
931
|
+
### Template de Decisão
|
|
932
|
+
|
|
933
|
+
Quando encontrar uma decisão técnica, SEMPRE use este formato:
|
|
934
|
+
|
|
935
|
+
```markdown
|
|
936
|
+
---
|
|
937
|
+
🤔 **DECISÃO TÉCNICA NECESSÁRIA**
|
|
938
|
+
|
|
939
|
+
**Contexto:** {Por que precisamos decidir isso agora}
|
|
940
|
+
|
|
941
|
+
**Opções disponíveis:**
|
|
942
|
+
|
|
943
|
+
**Opção 1: {Nome}**
|
|
944
|
+
- ✅ Vantagens: {lista concisa}
|
|
945
|
+
- ❌ Desvantagens: {lista concisa}
|
|
946
|
+
- 💰 Custo: {se Azure} ou N/A
|
|
947
|
+
- ⚡ Complexidade: {baixa/média/alta}
|
|
948
|
+
|
|
949
|
+
**Opção 2: {Nome}**
|
|
950
|
+
- ✅ Vantagens: {lista concisa}
|
|
951
|
+
- ❌ Desvantagens: {lista concisa}
|
|
952
|
+
- 💰 Custo: {se Azure} ou N/A
|
|
953
|
+
- ⚡ Complexidade: {baixa/média/alta}
|
|
954
|
+
|
|
955
|
+
**Opção 3: {Nome}** (se houver)
|
|
956
|
+
- ...
|
|
957
|
+
|
|
958
|
+
**💡 Minha recomendação:**
|
|
959
|
+
{Opção X} porque {justificativa técnica baseada no contexto do projeto}
|
|
960
|
+
|
|
961
|
+
**Trade-offs principais:**
|
|
962
|
+
- {Trade-off 1: ex: performance vs custo}
|
|
963
|
+
- {Trade-off 2: ex: simplicidade vs escalabilidade}
|
|
964
|
+
|
|
965
|
+
**Qual opção você prefere?** Ou quer ajustar alguma delas?
|
|
966
|
+
|
|
967
|
+
---
|
|
968
|
+
```
|
|
969
|
+
|
|
970
|
+
---
|
|
971
|
+
|
|
972
|
+
### Quando NÃO pausar (decisões triviais)
|
|
973
|
+
|
|
974
|
+
Decisões automáticas permitidas (não precisa consultar):
|
|
975
|
+
- ✅ Nomenclatura de variáveis/métodos (seguir standards)
|
|
976
|
+
- ✅ Estrutura de pastas (seguir Clean Architecture padrão do projeto)
|
|
977
|
+
- ✅ Imports e usings
|
|
978
|
+
- ✅ Formatação de código
|
|
979
|
+
- ✅ Tipos primitivos (string vs int, etc.)
|
|
980
|
+
- ✅ Validações básicas (Required, MaxLength, etc.)
|
|
981
|
+
- ✅ Padrões já definidos em project/standards/
|
|
982
|
+
|
|
983
|
+
**Regra de ouro:** Se a decisão afeta **custo**, **arquitetura**, **dependências externas** ou **estrutura de dados**, SEMPRE consultar.
|
|
984
|
+
|
|
985
|
+
---
|
|
986
|
+
|
|
987
|
+
### Exemplo Real Completo
|
|
988
|
+
|
|
989
|
+
**Situação:** Usuário pediu "adicionar cache para melhorar performance"
|
|
990
|
+
|
|
991
|
+
**❌ ERRADO (decisão automática):**
|
|
992
|
+
```
|
|
993
|
+
Vou implementar cache usando Redis Premium tier.
|
|
994
|
+
```
|
|
995
|
+
|
|
996
|
+
**✅ CORRETO (apresentar opções):**
|
|
997
|
+
|
|
998
|
+
```markdown
|
|
999
|
+
---
|
|
1000
|
+
🤔 **DECISÃO TÉCNICA NECESSÁRIA**
|
|
1001
|
+
|
|
1002
|
+
**Contexto:** Você pediu cache para melhorar performance das queries de produtos. Precisamos decidir qual estratégia usar.
|
|
1003
|
+
|
|
1004
|
+
**Opções disponíveis:**
|
|
1005
|
+
|
|
1006
|
+
**Opção 1: In-Memory Cache (IMemoryCache .NET)**
|
|
1007
|
+
- ✅ Vantagens: Zero custo, simples, rápido para dados pequenos
|
|
1008
|
+
- ❌ Desvantagens: Limitado à memória do servidor, perdido em restart, não compartilhado entre instâncias
|
|
1009
|
+
- 💰 Custo: $0
|
|
1010
|
+
- ⚡ Complexidade: Baixa
|
|
1011
|
+
|
|
1012
|
+
**Opção 2: Redis (Azure Cache for Redis - Basic tier)**
|
|
1013
|
+
- ✅ Vantagens: Persistente, compartilhado, suporta estruturas complexas
|
|
1014
|
+
- ❌ Desvantagens: Custo adicional, latência de rede
|
|
1015
|
+
- 💰 Custo: ~$16/mês (Basic C0)
|
|
1016
|
+
- ⚡ Complexidade: Média
|
|
1017
|
+
|
|
1018
|
+
**Opção 3: Distributed Memory Cache (SQL Server)**
|
|
1019
|
+
- ✅ Vantagens: Usa SQL existente, sem custo adicional, compartilhado
|
|
1020
|
+
- ❌ Desvantagens: Mais lento que Redis, usa DB para cache
|
|
1021
|
+
- 💰 Custo: $0 (usa SQL existente)
|
|
1022
|
+
- ⚡ Complexidade: Baixa-Média
|
|
1023
|
+
|
|
1024
|
+
**💡 Minha recomendação:**
|
|
1025
|
+
Opção 1 (IMemoryCache) se você tem apenas 1 instância do app e dados de cache < 100MB.
|
|
1026
|
+
Opção 3 (SQL Distributed Cache) se você tem múltiplas instâncias mas quer evitar custo de Redis.
|
|
1027
|
+
|
|
1028
|
+
**Trade-offs principais:**
|
|
1029
|
+
- Performance vs Custo: Redis é mais rápido mas custa $16/mês
|
|
1030
|
+
- Simplicidade vs Escalabilidade: IMemoryCache é simples mas não escala para múltiplas instâncias
|
|
1031
|
+
|
|
1032
|
+
**Qual opção você prefere?** Ou quer ajustar alguma delas?
|
|
1033
|
+
---
|
|
1034
|
+
```
|
|
1035
|
+
|
|
1036
|
+
---
|
|
1037
|
+
|
|
1038
|
+
### Integração com Fases
|
|
1039
|
+
|
|
1040
|
+
**FASE 1.5 (UI/UX):**
|
|
1041
|
+
- ❓ Perguntar: Fluent UI vs MudBlazor vs outra biblioteca
|
|
1042
|
+
- ❓ Perguntar: Preferências de layout, cores, componentes
|
|
1043
|
+
|
|
1044
|
+
**FASE 2 (DESIGN):**
|
|
1045
|
+
- ❓ Perguntar: Padrões arquiteturais (CQRS, Repository, etc.)
|
|
1046
|
+
- ❓ Perguntar: Infraestrutura Azure (recursos, SKUs)
|
|
1047
|
+
- ❓ Perguntar: Bibliotecas e SDKs necessários
|
|
1048
|
+
- ❓ Perguntar: Estrutura de dados (entidades, relacionamentos)
|
|
1049
|
+
|
|
1050
|
+
**Durante IMPLEMENTAÇÃO:**
|
|
1051
|
+
- ❓ Pausar: Se encontrar decisão não prevista no design
|
|
1052
|
+
- ❓ Pausar: Se precisar adicionar dependência nova
|
|
1053
|
+
- ❓ Pausar: Se precisar mudar estrutura de dados aprovada
|
|
1054
|
+
|
|
1055
|
+
---
|
|
1056
|
+
|
|
1057
|
+
### Validação Automática (CLI)
|
|
1058
|
+
|
|
1059
|
+
Use o comando `morph-spec validate-decisions` para verificar se decisões foram documentadas:
|
|
1060
|
+
|
|
1061
|
+
```bash
|
|
1062
|
+
# Validar decisions.md de uma feature
|
|
1063
|
+
morph-spec validate-decisions {feature-name}
|
|
1064
|
+
|
|
1065
|
+
# O comando verifica:
|
|
1066
|
+
# - Decisões de infraestrutura têm ADRs?
|
|
1067
|
+
# - Bibliotecas escolhidas estão documentadas?
|
|
1068
|
+
# - Padrões arquiteturais estão justificados?
|
|
1069
|
+
```
|
|
1070
|
+
|
|
1071
|
+
---
|
|
1072
|
+
|
|
787
1073
|
## AGENTES
|
|
788
1074
|
|
|
789
1075
|
### Core Agents (Sempre Ativos)
|
|
@@ -867,13 +1153,16 @@ Os limites de custo são definidos em `.morph/config/config.json` e podem ser cu
|
|
|
867
1153
|
|
|
868
1154
|
```bash
|
|
869
1155
|
# Calcular custos de arquivos Bicep
|
|
870
|
-
|
|
1156
|
+
morph-spec cost infra/main.bicep --verbose
|
|
871
1157
|
|
|
872
1158
|
# Múltiplos arquivos (glob pattern)
|
|
873
|
-
|
|
1159
|
+
morph-spec cost "infra/**/*.bicep" --json
|
|
874
1160
|
|
|
875
1161
|
# Usar config customizado
|
|
876
|
-
|
|
1162
|
+
morph-spec cost infra/main.bicep --config .morph/config/config.json
|
|
1163
|
+
|
|
1164
|
+
# OU, se em desenvolvimento local do framework:
|
|
1165
|
+
# node bin/calculate-costs.js infra/main.bicep --verbose
|
|
877
1166
|
```
|
|
878
1167
|
|
|
879
1168
|
**Funcionalidades:**
|
|
@@ -921,7 +1210,7 @@ ln -s ../../.morph/hooks/pre-commit-costs.sh .git/hooks/pre-commit
|
|
|
921
1210
|
|------|------|-----|
|
|
922
1211
|
| **FASE 2 (Design)** | Estimar custos de infra proposta | `/morph-design` (integrado) |
|
|
923
1212
|
| **Antes de commit** | Validar custos automaticamente | Pre-commit hook |
|
|
924
|
-
| **Revisão manual** | Verificar custos de Bicep | `
|
|
1213
|
+
| **Revisão manual** | Verificar custos de Bicep | `morph-spec cost <bicep-files> --verbose` |
|
|
925
1214
|
|
|
926
1215
|
### Exemplo de ADR de Custo
|
|
927
1216
|
|
|
@@ -1062,35 +1351,38 @@ O MORPH-SPEC rastreia automaticamente o progresso de features usando `.morph/sta
|
|
|
1062
1351
|
|
|
1063
1352
|
### CLI para State Management
|
|
1064
1353
|
|
|
1065
|
-
Use `
|
|
1354
|
+
Use o comando `morph-spec state` para manipular o state:
|
|
1066
1355
|
|
|
1067
1356
|
```bash
|
|
1068
1357
|
# Inicializar state (primeira vez)
|
|
1069
|
-
|
|
1358
|
+
morph-spec state init
|
|
1070
1359
|
|
|
1071
1360
|
# Criar/atualizar feature
|
|
1072
|
-
|
|
1073
|
-
|
|
1361
|
+
morph-spec state set feature-name phase design
|
|
1362
|
+
morph-spec state set feature-name status in_progress
|
|
1074
1363
|
|
|
1075
1364
|
# Atualizar tasks
|
|
1076
|
-
|
|
1077
|
-
|
|
1365
|
+
morph-spec state set feature-name tasks.completed 5
|
|
1366
|
+
morph-spec state set feature-name tasks.total 12
|
|
1078
1367
|
|
|
1079
1368
|
# Adicionar agentes
|
|
1080
|
-
|
|
1369
|
+
morph-spec state add-agent feature-name blazor-builder
|
|
1081
1370
|
|
|
1082
1371
|
# Marcar outputs como criados
|
|
1083
|
-
|
|
1084
|
-
|
|
1372
|
+
morph-spec state mark-output feature-name spec
|
|
1373
|
+
morph-spec state mark-output feature-name contracts
|
|
1085
1374
|
|
|
1086
1375
|
# Registrar checkpoint
|
|
1087
|
-
|
|
1376
|
+
morph-spec state checkpoint feature-name "Completadas tasks T001-T003"
|
|
1088
1377
|
|
|
1089
1378
|
# Listar todas as features
|
|
1090
|
-
|
|
1379
|
+
morph-spec state list
|
|
1091
1380
|
|
|
1092
1381
|
# Obter detalhes de uma feature (JSON)
|
|
1093
|
-
|
|
1382
|
+
morph-spec state get feature-name
|
|
1383
|
+
|
|
1384
|
+
# OU, se em desenvolvimento local do framework:
|
|
1385
|
+
# node bin/state-manager.js <command>
|
|
1094
1386
|
```
|
|
1095
1387
|
|
|
1096
1388
|
### Quando Atualizar State
|
|
@@ -118,11 +118,15 @@ function validateAgentsSkills(config, verbose = false) {
|
|
|
118
118
|
const skillPath = `.claude/skills/${skillFile.replace('.claude/skills/', '')}`;
|
|
119
119
|
|
|
120
120
|
if (!agentSkillPaths.has(skillPath)) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
121
|
+
// Orphan skills são OK - podem ser usados manualmente
|
|
122
|
+
// Apenas reportar em verbose mode
|
|
123
|
+
if (verbose) {
|
|
124
|
+
info.push({
|
|
125
|
+
type: 'manual-skill',
|
|
126
|
+
skillPath: skillPath,
|
|
127
|
+
message: `ℹ️ Manual skill: ${skillPath} (no auto-activation)`
|
|
128
|
+
});
|
|
129
|
+
}
|
|
126
130
|
}
|
|
127
131
|
}
|
|
128
132
|
|
|
@@ -194,6 +198,14 @@ function displayResults(results, verbose) {
|
|
|
194
198
|
log('║ ✅ All agents and skills are properly mapped! ║', 'green');
|
|
195
199
|
}
|
|
196
200
|
|
|
201
|
+
// Nota sobre manual skills
|
|
202
|
+
const manualSkills = info.filter(i => i.type === 'manual-skill').length;
|
|
203
|
+
if (manualSkills > 0 && verbose) {
|
|
204
|
+
log('║ ║', 'cyan');
|
|
205
|
+
log(`║ ℹ️ ${manualSkills} manual-only skills found (use --verbose to see) ║`, 'gray');
|
|
206
|
+
log('║ These are extra skills without auto-activation. ║', 'gray');
|
|
207
|
+
}
|
|
208
|
+
|
|
197
209
|
log('╚════════════════════════════════════════════════════════════════╝\n', 'cyan');
|
|
198
210
|
}
|
|
199
211
|
|
|
@@ -50,6 +50,7 @@ export async function detectStructure(projectPath) {
|
|
|
50
50
|
* Detect stack type
|
|
51
51
|
*/
|
|
52
52
|
async function detectStack(projectPath) {
|
|
53
|
+
// Order matters: more specific patterns first
|
|
53
54
|
const patterns = {
|
|
54
55
|
blazor: [
|
|
55
56
|
'**/*.razor',
|
|
@@ -57,19 +58,47 @@ async function detectStack(projectPath) {
|
|
|
57
58
|
'**/Components/**/*.razor'
|
|
58
59
|
],
|
|
59
60
|
nextjs: [
|
|
60
|
-
'pages/**/*.tsx',
|
|
61
|
-
'app/**/*.tsx',
|
|
62
61
|
'next.config.js',
|
|
63
|
-
'next.config.mjs'
|
|
62
|
+
'next.config.mjs',
|
|
63
|
+
'next.config.ts',
|
|
64
|
+
'pages/**/*.tsx',
|
|
65
|
+
'app/**/*.tsx'
|
|
64
66
|
],
|
|
65
67
|
shopify: [
|
|
66
68
|
'**/*.liquid',
|
|
67
69
|
'sections/**/*.liquid',
|
|
68
70
|
'templates/**/*.liquid'
|
|
69
71
|
],
|
|
72
|
+
react: [
|
|
73
|
+
'src/App.jsx',
|
|
74
|
+
'src/App.tsx',
|
|
75
|
+
'vite.config.js',
|
|
76
|
+
'craco.config.js'
|
|
77
|
+
],
|
|
78
|
+
vue: [
|
|
79
|
+
'vite.config.js', // Vue 3 + Vite
|
|
80
|
+
'vue.config.js', // Vue 2
|
|
81
|
+
'src/App.vue'
|
|
82
|
+
],
|
|
70
83
|
dotnet: [
|
|
71
84
|
'**/*.csproj',
|
|
72
85
|
'**/Program.cs'
|
|
86
|
+
],
|
|
87
|
+
typescript: [
|
|
88
|
+
'tsconfig.json',
|
|
89
|
+
'src/**/*.ts'
|
|
90
|
+
],
|
|
91
|
+
nodejs: [
|
|
92
|
+
'package.json' // Fallback: generic Node.js
|
|
93
|
+
],
|
|
94
|
+
python: [
|
|
95
|
+
'requirements.txt',
|
|
96
|
+
'pyproject.toml',
|
|
97
|
+
'setup.py'
|
|
98
|
+
],
|
|
99
|
+
go: [
|
|
100
|
+
'go.mod',
|
|
101
|
+
'main.go'
|
|
73
102
|
]
|
|
74
103
|
};
|
|
75
104
|
|
package/package.json
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import path from 'path';
|
|
9
|
+
import yaml from 'yaml';
|
|
9
10
|
import ora from 'ora';
|
|
10
11
|
import chalk from 'chalk';
|
|
11
12
|
import { logger } from '../utils/logger.js';
|
|
@@ -257,10 +258,77 @@ export async function createStoryCommand(feature, storyId, options) {
|
|
|
257
258
|
// Write file
|
|
258
259
|
await writeFile(outputPath, renderedStory);
|
|
259
260
|
|
|
261
|
+
// ============================================================================
|
|
262
|
+
// Update sprint-status.yaml (create if doesn't exist)
|
|
263
|
+
// ============================================================================
|
|
264
|
+
const sprintStatusPath = path.join(process.cwd(), `.morph/project/outputs/${feature}/sprint-status.yaml`);
|
|
265
|
+
let sprintStatus;
|
|
266
|
+
|
|
267
|
+
if (fs.existsSync(sprintStatusPath)) {
|
|
268
|
+
// Load existing
|
|
269
|
+
const content = fs.readFileSync(sprintStatusPath, 'utf-8');
|
|
270
|
+
sprintStatus = yaml.parse(content);
|
|
271
|
+
} else {
|
|
272
|
+
// Create new
|
|
273
|
+
sprintStatus = {
|
|
274
|
+
feature: feature,
|
|
275
|
+
epic: options.epic || toTitleCase(feature),
|
|
276
|
+
created: new Date().toISOString().split('T')[0],
|
|
277
|
+
updated: new Date().toISOString().split('T')[0],
|
|
278
|
+
stories: [],
|
|
279
|
+
metrics: {
|
|
280
|
+
total_stories: 0,
|
|
281
|
+
ready: 0,
|
|
282
|
+
in_progress: 0,
|
|
283
|
+
ready_for_qa: 0,
|
|
284
|
+
done: 0,
|
|
285
|
+
completion_percent: 0
|
|
286
|
+
},
|
|
287
|
+
current: null,
|
|
288
|
+
next: null
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Add new story to sprint-status
|
|
293
|
+
sprintStatus.stories.push({
|
|
294
|
+
id: storyId,
|
|
295
|
+
title: storyData.storyTitle,
|
|
296
|
+
file: `stories/${storyId}.md`,
|
|
297
|
+
status: 'ready',
|
|
298
|
+
created: new Date().toISOString().split('T')[0],
|
|
299
|
+
assigned: null,
|
|
300
|
+
started: null,
|
|
301
|
+
completed: null
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Update metrics
|
|
305
|
+
sprintStatus.metrics.total_stories = sprintStatus.stories.length;
|
|
306
|
+
sprintStatus.metrics.ready = sprintStatus.stories.filter(s => s.status === 'ready').length;
|
|
307
|
+
sprintStatus.metrics.in_progress = sprintStatus.stories.filter(s => s.status === 'in_progress').length;
|
|
308
|
+
sprintStatus.metrics.ready_for_qa = sprintStatus.stories.filter(s => s.status === 'ready_for_qa').length;
|
|
309
|
+
sprintStatus.metrics.done = sprintStatus.stories.filter(s => s.status === 'done').length;
|
|
310
|
+
sprintStatus.metrics.completion_percent = sprintStatus.stories.length > 0
|
|
311
|
+
? Math.round((sprintStatus.metrics.done / sprintStatus.stories.length) * 100)
|
|
312
|
+
: 0;
|
|
313
|
+
sprintStatus.updated = new Date().toISOString().split('T')[0];
|
|
314
|
+
|
|
315
|
+
// Set next story if null
|
|
316
|
+
if (!sprintStatus.next) {
|
|
317
|
+
sprintStatus.next = {
|
|
318
|
+
story_id: storyId,
|
|
319
|
+
recommendation: `Story ${storyId} is ready for development`
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Write sprint-status.yaml
|
|
324
|
+
const yamlContent = yaml.stringify(sprintStatus);
|
|
325
|
+
fs.writeFileSync(sprintStatusPath, yamlContent);
|
|
326
|
+
|
|
260
327
|
spinner.succeed('Story created!');
|
|
261
328
|
logger.blank();
|
|
262
329
|
|
|
263
330
|
logger.success(`Story file: ${chalk.cyan(outputPath)}`);
|
|
331
|
+
logger.success(`Updated: ${chalk.cyan('sprint-status.yaml')}`);
|
|
264
332
|
logger.blank();
|
|
265
333
|
|
|
266
334
|
logger.header('Dev Notes Auto-Injected:');
|
package/src/commands/init.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { join } from 'path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
2
3
|
import ora from 'ora';
|
|
3
4
|
import chalk from 'chalk';
|
|
4
5
|
import { logger } from '../utils/logger.js';
|
|
@@ -12,7 +13,8 @@ import {
|
|
|
12
13
|
ensureDir,
|
|
13
14
|
writeFile,
|
|
14
15
|
readFile,
|
|
15
|
-
updateGitignore
|
|
16
|
+
updateGitignore,
|
|
17
|
+
createSymlink
|
|
16
18
|
} from '../utils/file-copier.js';
|
|
17
19
|
import { saveProjectMorphVersion, getInstalledCLIVersion } from '../utils/version-checker.js';
|
|
18
20
|
|
|
@@ -52,16 +54,18 @@ export async function initCommand(options) {
|
|
|
52
54
|
}
|
|
53
55
|
await copyFile(claudeMdPath, claudeMdDest);
|
|
54
56
|
|
|
55
|
-
// 2. Create .morph
|
|
56
|
-
spinner.text = 'Creating .morph
|
|
57
|
+
// 2. Create .morph directory structure
|
|
58
|
+
spinner.text = 'Creating .morph structure...';
|
|
57
59
|
const projectPath = join(morphPath, 'project');
|
|
60
|
+
const configDir = join(morphPath, 'config');
|
|
58
61
|
await ensureDir(join(projectPath, 'context'));
|
|
59
62
|
await ensureDir(join(projectPath, 'standards'));
|
|
60
63
|
await ensureDir(join(projectPath, 'outputs'));
|
|
64
|
+
await ensureDir(configDir);
|
|
61
65
|
|
|
62
66
|
// 3. Create config.json
|
|
63
67
|
spinner.text = 'Generating config.json...';
|
|
64
|
-
const configPath = join(
|
|
68
|
+
const configPath = join(configDir, 'config.json');
|
|
65
69
|
const dirName = targetPath.split(/[\\/]/).pop();
|
|
66
70
|
|
|
67
71
|
const config = {
|
|
@@ -99,12 +103,102 @@ Run \`morph-spec detect\` to analyze your project.
|
|
|
99
103
|
`;
|
|
100
104
|
await writeFile(contextReadme, readmeContent);
|
|
101
105
|
|
|
102
|
-
// 5.
|
|
106
|
+
// 5. Copy templates from content
|
|
107
|
+
spinner.text = 'Copying templates...';
|
|
108
|
+
const contentDir = getContentDir();
|
|
109
|
+
const templatesSrc = join(contentDir, '.morph', 'templates');
|
|
110
|
+
const templatesDest = join(morphPath, 'templates');
|
|
111
|
+
if (await pathExists(templatesSrc)) {
|
|
112
|
+
await copyDirectory(templatesSrc, templatesDest);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 6. Copy standards from content
|
|
116
|
+
spinner.text = 'Copying standards...';
|
|
117
|
+
const standardsSrc = join(contentDir, '.morph', 'standards');
|
|
118
|
+
const standardsDest = join(morphPath, 'standards');
|
|
119
|
+
if (await pathExists(standardsSrc)) {
|
|
120
|
+
await copyDirectory(standardsSrc, standardsDest);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 7. Copy agents.json
|
|
124
|
+
spinner.text = 'Copying agents configuration...';
|
|
125
|
+
const agentsSrc = join(contentDir, '.morph', 'config', 'agents.json');
|
|
126
|
+
const agentsDest = join(configDir, 'agents.json');
|
|
127
|
+
if (await pathExists(agentsSrc)) {
|
|
128
|
+
await copyFile(agentsSrc, agentsDest);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 8. Copy azure-pricing files
|
|
132
|
+
spinner.text = 'Copying Azure pricing data...';
|
|
133
|
+
const pricingSrc = join(contentDir, '.morph', 'config', 'azure-pricing.json');
|
|
134
|
+
const pricingDest = join(configDir, 'azure-pricing.json');
|
|
135
|
+
if (await pathExists(pricingSrc)) {
|
|
136
|
+
await copyFile(pricingSrc, pricingDest);
|
|
137
|
+
}
|
|
138
|
+
const pricingSchemaSrc = join(contentDir, '.morph', 'config', 'azure-pricing.schema.json');
|
|
139
|
+
const pricingSchemaDest = join(configDir, 'azure-pricing.schema.json');
|
|
140
|
+
if (await pathExists(pricingSchemaSrc)) {
|
|
141
|
+
await copyFile(pricingSchemaSrc, pricingSchemaDest);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 9. Copy .claude commands and create symlinks for skills
|
|
145
|
+
spinner.text = 'Setting up Claude Code integration...';
|
|
146
|
+
const claudeSrc = join(contentDir, '.claude');
|
|
147
|
+
const claudeDest = join(targetPath, '.claude');
|
|
148
|
+
|
|
149
|
+
let symlinkCount = 0;
|
|
150
|
+
let copyCount = 0;
|
|
151
|
+
|
|
152
|
+
if (await pathExists(claudeSrc)) {
|
|
153
|
+
// Copy commands directory (slash commands)
|
|
154
|
+
const commandsSrc = join(claudeSrc, 'commands');
|
|
155
|
+
const commandsDest = join(claudeDest, 'commands');
|
|
156
|
+
if (await pathExists(commandsSrc)) {
|
|
157
|
+
await copyDirectory(commandsSrc, commandsDest);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Create symlinks for skills (or copy if symlink fails)
|
|
161
|
+
const skillsSrc = join(claudeSrc, 'skills');
|
|
162
|
+
const skillsDest = join(claudeDest, 'skills');
|
|
163
|
+
|
|
164
|
+
if (await pathExists(skillsSrc)) {
|
|
165
|
+
// Ensure skills destination directory exists
|
|
166
|
+
await ensureDir(skillsDest);
|
|
167
|
+
|
|
168
|
+
// Recursively walk through skills directory
|
|
169
|
+
const walkDir = async (dir, basePath) => {
|
|
170
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
171
|
+
|
|
172
|
+
for (const entry of entries) {
|
|
173
|
+
const srcPath = join(dir, entry.name);
|
|
174
|
+
const relPath = join(basePath, entry.name);
|
|
175
|
+
const destPath = join(skillsDest, relPath);
|
|
176
|
+
|
|
177
|
+
if (entry.isDirectory()) {
|
|
178
|
+
await ensureDir(destPath);
|
|
179
|
+
await walkDir(srcPath, relPath);
|
|
180
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
181
|
+
// Create symlink for skill files
|
|
182
|
+
const result = await createSymlink(srcPath, destPath, 'file');
|
|
183
|
+
if (result === 'symlink') {
|
|
184
|
+
symlinkCount++;
|
|
185
|
+
} else {
|
|
186
|
+
copyCount++;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
await walkDir(skillsSrc, '');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 10. Save version info
|
|
103
197
|
spinner.text = 'Saving version info...';
|
|
104
198
|
const cliVersion = getInstalledCLIVersion();
|
|
105
199
|
await saveProjectMorphVersion(targetPath, cliVersion);
|
|
106
200
|
|
|
107
|
-
//
|
|
201
|
+
// 11. Update .gitignore
|
|
108
202
|
spinner.text = 'Updating .gitignore...';
|
|
109
203
|
await updateGitignore(targetPath);
|
|
110
204
|
|
|
@@ -126,16 +220,21 @@ Run \`morph-spec detect\` to analyze your project.
|
|
|
126
220
|
]);
|
|
127
221
|
logger.blank();
|
|
128
222
|
|
|
129
|
-
logger.info('
|
|
130
|
-
logger.dim(`
|
|
131
|
-
logger.dim(
|
|
132
|
-
logger.dim(
|
|
133
|
-
logger.dim(
|
|
134
|
-
logger.dim(
|
|
135
|
-
logger.
|
|
223
|
+
logger.info('Files installed:');
|
|
224
|
+
logger.dim(` ✓ CLAUDE.md`);
|
|
225
|
+
logger.dim(` ✓ .morph/config/ (config.json, agents.json, azure-pricing.json)`);
|
|
226
|
+
logger.dim(` ✓ .morph/standards/ (coding.md, architecture.md, azure.md, ...)`);
|
|
227
|
+
logger.dim(` ✓ .morph/templates/ (Bicep, integrations, saas, ...)`);
|
|
228
|
+
logger.dim(` ✓ .morph/project/ (context, standards, outputs)`);
|
|
229
|
+
logger.dim(` ✓ .claude/commands/ (slash commands)`);
|
|
230
|
+
|
|
231
|
+
if (symlinkCount > 0) {
|
|
232
|
+
logger.dim(` ✓ .claude/skills/ (${symlinkCount} symlinked)`);
|
|
233
|
+
} else if (copyCount > 0) {
|
|
234
|
+
logger.dim(` ✓ .claude/skills/ (${copyCount} copied)`);
|
|
235
|
+
logger.warn(` ⚠ Symlinks not supported (copied instead). Skills won't auto-update.`);
|
|
236
|
+
}
|
|
136
237
|
|
|
137
|
-
logger.info('Content (via npm package):');
|
|
138
|
-
logger.dim(`Templates and standards available via morph-spec update`);
|
|
139
238
|
logger.blank();
|
|
140
239
|
|
|
141
240
|
} catch (error) {
|
package/src/commands/state.js
CHANGED
|
@@ -236,7 +236,7 @@ async function listCommand(options) {
|
|
|
236
236
|
? `${feature.tasks.completed}/${feature.tasks.total}`
|
|
237
237
|
: '0/0';
|
|
238
238
|
|
|
239
|
-
logger.info(`${statusEmoji} ${chalk.
|
|
239
|
+
logger.info(`${statusEmoji} ${chalk.bold(name)}`);
|
|
240
240
|
logger.dim(` Phase: ${feature.phase.padEnd(16)} │ Tasks: ${progress}`);
|
|
241
241
|
logger.dim(` Agents: ${feature.activeAgents.slice(0, 5).join(', ') || 'None'}`);
|
|
242
242
|
logger.blank();
|
package/src/utils/file-copier.js
CHANGED
|
@@ -53,6 +53,32 @@ export async function readFile(path) {
|
|
|
53
53
|
return fs.readFile(path, 'utf8');
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Create symlink with fallback to copy if symlink fails
|
|
58
|
+
* @param {string} target - Path to the original file/folder
|
|
59
|
+
* @param {string} link - Path where symlink should be created
|
|
60
|
+
* @param {string} type - 'file' or 'dir'
|
|
61
|
+
* @returns {Promise<'symlink' | 'copy'>} - Returns how the link was created
|
|
62
|
+
*/
|
|
63
|
+
export async function createSymlink(target, link, type = 'file') {
|
|
64
|
+
await fs.ensureDir(dirname(link));
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
// Try to create symlink
|
|
68
|
+
await fs.ensureSymlink(target, link, type);
|
|
69
|
+
return 'symlink';
|
|
70
|
+
} catch (error) {
|
|
71
|
+
// Fallback: copy the file/directory if symlink fails
|
|
72
|
+
// (Windows may require admin permissions for symlinks)
|
|
73
|
+
if (type === 'file') {
|
|
74
|
+
await copyFile(target, link);
|
|
75
|
+
} else {
|
|
76
|
+
await copyDirectory(target, link);
|
|
77
|
+
}
|
|
78
|
+
return 'copy';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
56
82
|
export async function updateGitignore(projectPath) {
|
|
57
83
|
const gitignorePath = join(projectPath, '.gitignore');
|
|
58
84
|
|