create-sdd-ai-stack 0.1.17

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.
Files changed (72) hide show
  1. package/AGENTS.md +164 -0
  2. package/APP-STACK.md +31 -0
  3. package/APP.md +46 -0
  4. package/ARCHITECTURE.md +115 -0
  5. package/DESIGN.md +219 -0
  6. package/LICENSE +21 -0
  7. package/NEXT.md +345 -0
  8. package/NODE.md +107 -0
  9. package/REACT.md +92 -0
  10. package/README.md +260 -0
  11. package/SKILLS/check-docs/SKILL.md +24 -0
  12. package/SKILLS/check-docs/check-docs.mjs +56 -0
  13. package/SKILLS/create-feature/SKILL.md +40 -0
  14. package/SKILLS/create-feature/create-feature.mjs +128 -0
  15. package/SKILLS/create-feature.sh +112 -0
  16. package/SKILLS/install-submodule/SKILL.md +42 -0
  17. package/SKILLS/install-submodule/install-submodule.mjs +119 -0
  18. package/bin/create-sdd-ai-stack.mjs +93 -0
  19. package/docs/CHANGELOG.md +209 -0
  20. package/docs/PLANNING.md +68 -0
  21. package/docs/PRODUCT.md +76 -0
  22. package/docs/RELEASE.md +332 -0
  23. package/lib/check-links.mjs +42 -0
  24. package/lib/constants.mjs +60 -0
  25. package/lib/scaffold.mjs +253 -0
  26. package/package.json +80 -0
  27. package/specs/BACKLOG.md +24 -0
  28. package/specs/PLAN.md +57 -0
  29. package/specs/ROADMAP.md +35 -0
  30. package/specs/history/phases/phase-0-bootstrap.md +67 -0
  31. package/specs/tasks/TASK_TEMPLATE.md +46 -0
  32. package/src/cli.mjs +119 -0
  33. package/stacks/README.md +33 -0
  34. package/stacks/ai.md +52 -0
  35. package/stacks/ci.md +59 -0
  36. package/stacks/database.md +56 -0
  37. package/stacks/git.md +51 -0
  38. package/stacks/shadcn.md +52 -0
  39. package/stacks/tailwind.md +65 -0
  40. package/stacks/testing.md +74 -0
  41. package/stacks/typescript.md +58 -0
  42. package/template/next/.env.example +4 -0
  43. package/template/next/README.md +47 -0
  44. package/template/next/biome.json +36 -0
  45. package/template/next/gitignore +32 -0
  46. package/template/next/next.config.ts +12 -0
  47. package/template/next/package.json +47 -0
  48. package/template/next/playwright.config.ts +21 -0
  49. package/template/next/postcss.config.mjs +7 -0
  50. package/template/next/src/app/(app)/app/page.tsx +18 -0
  51. package/template/next/src/app/(app)/error.tsx +30 -0
  52. package/template/next/src/app/(app)/layout.tsx +25 -0
  53. package/template/next/src/app/(marketing)/page.tsx +73 -0
  54. package/template/next/src/app/globals.css +437 -0
  55. package/template/next/src/app/layout.tsx +41 -0
  56. package/template/next/src/app/not-found.tsx +11 -0
  57. package/template/next/src/features/example/actions.ts +43 -0
  58. package/template/next/src/features/example/application/create-example.usecase.ts +26 -0
  59. package/template/next/src/features/example/container.ts +16 -0
  60. package/template/next/src/features/example/domain/IExampleRepository.ts +11 -0
  61. package/template/next/src/features/example/domain/example.schema.ts +18 -0
  62. package/template/next/src/features/example/infrastructure/example.repository.ts +26 -0
  63. package/template/next/src/features/example/ui/create-example-form.tsx +56 -0
  64. package/template/next/src/proxy.ts +23 -0
  65. package/template/next/src/shared/lib/cn.ts +6 -0
  66. package/template/next/src/shared/server/auth.ts +32 -0
  67. package/template/next/src/shared/server/env.ts +14 -0
  68. package/template/next/src/shared/ui/index.ts +3 -0
  69. package/template/next/tests/e2e/routes.spec.ts +34 -0
  70. package/template/next/tests/unit/create-example.test.ts +55 -0
  71. package/template/next/tsconfig.json +37 -0
  72. package/template/next/vitest.config.ts +19 -0
@@ -0,0 +1,332 @@
1
+ # πŸš€ RELEASE β€” publicar no npm
2
+
3
+ > **Fluxo ΓΊnico:** vocΓͺ versiona e cria a tag localmente; o **GitHub Actions publica**.
4
+ > NΓ£o existe `npm publish` manual neste projeto β€” Γ© proposital, para nΓ£o ter dois caminhos.
5
+
6
+ ```text
7
+ npm run version:minor β†’ 0.1.17 β†’ 0.2.0 (commita + cria tag v0.2.0)
8
+ git push origin main --tags β†’ dispara o workflow Release β†’ publica no npm
9
+ ```
10
+
11
+ ---
12
+
13
+ ## 0. Como versionar (estamos em `0.x`)
14
+
15
+ O projeto estΓ‘ em **`0.1.x`** de propΓ³sito: as regras e o template ainda vΓ£o mudar com base
16
+ no uso real. Leitura das versΓ΅es:
17
+
18
+ | MudanΓ§a | Bump | Exemplo | Version bump |
19
+ | --- | --- | --- | --- |
20
+ | CorreΓ§Γ£o de bug, ajuste de doc, nova regra pontual | **patch** | `0.1.17 β†’ 0.1.18` | `npm run version:patch` |
21
+ | Regra nova, feature nova no template, Breaking em config | **minor** | `0.1.17 β†’ 0.2.0` | `npm run version:minor` |
22
+ | Reescritura de regra estrutural (ex.: Next 16 β†’ 17) | **major** | `0.1.17 β†’ 1.0.0` | `npm run version:major` |
23
+
24
+ > Enquanto em `0.x`, **o minor Γ© o breaking change**. Mudar a estrutura das regras ou
25
+ > do template sobe o minor, nΓ£o o patch. O `major` fica reservado para quando a interface
26
+ > estabilizar e a gente for para `1.0.0`.
27
+
28
+ ---
29
+
30
+ ## 1. AutenticaΓ§Γ£o β€” leia isto antes de criar qualquer token
31
+
32
+ > 🚨 **Com 2FA ligado na conta npm, um token comum NΓƒO publica.**
33
+ > O CI nΓ£o tem como digitar o cΓ³digo do autenticador, entΓ£o o publish falha com:
34
+ >
35
+ > ```
36
+ > npm error code EOTP
37
+ > npm error This operation requires a one-time password from your authenticator.
38
+ > ```
39
+ >
40
+ > Isso **nΓ£o Γ© bug do workflow** β€” Γ© o npm exigindo presenΓ§a humana. Existem trΓͺs
41
+ > saΓ­das, e sΓ³ uma Γ© boa.
42
+
43
+ | Caminho | EsforΓ§o | SituaΓ§Γ£o |
44
+ | --- | --- | --- |
45
+ | **A. Bootstrap local + OIDC** | ~5 min, uma vez | βœ… **Recomendado.** Sem token para sempre |
46
+ | **B. Token com "Bypass 2FA"** | ~2 min | ⚠️ Funciona hoje, **deprecado em jan/2027** |
47
+ | **C. Token stage-only** | ~10 min | πŸ›‘οΈ Mais seguro, exige aprovar cada release |
48
+
49
+ ### 1.1 O `404 Not Found` na primeira publicaΓ§Γ£o Γ© enganoso
50
+
51
+ > ```
52
+ > npm error code E404
53
+ > npm error 404 Not Found - PUT https://registry.npmjs.org/create-sdd-ai-stack - Not found
54
+ > ```
55
+
56
+ **NΓ£o Γ© preciso "criar" o pacote antes.** O `npm publish` cria o pacote sozinho na
57
+ primeira publicaΓ§Γ£o. O 404 nesse PUT significa que **o registry nΓ£o reconheceu vocΓͺ
58
+ como autorizado** β€” e o npm nΓ£o quer confirmar se o nome existe para quem nΓ£o tem
59
+ acesso.
60
+
61
+ A causa mais comum (e a que aconteceu aqui) Γ© o **`.npmrc` do projeto** declarar um
62
+ `_authToken` que sobrescreve o seu:
63
+
64
+ ```text
65
+ projeto > ~/.npmrc (usuΓ‘rio) > npm global
66
+ ```
67
+
68
+ Com `//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}` no `.npmrc` do projeto e a
69
+ variΓ‘vel vazia, o token efetivo fica **vazio** e o sintoma Γ© dourado:
70
+
71
+ | Comando | Com token vΓ‘lido | Com token zerado |
72
+ | --- | --- | --- |
73
+ | `npm whoami` | `seu-usuario` | `401 Unauthorized` |
74
+ | `npm publish` (1Βͺ vez) | cria o pacote | `404 Not Found` no PUT |
75
+
76
+ **DiagnΓ³stico em 5 segundos:**
77
+
78
+ ```bash
79
+ npm whoami # tem que imprimir seu usuΓ‘rio, nΓ£o 401
80
+ npm config get "//registry.npmjs.org/:_authToken" # tem que ter tamanho > 1
81
+ ```
82
+
83
+ A correΓ§Γ£o Γ© **nΓ£o declarar `_authToken` no `.npmrc` do projeto** β€” sΓ³ `registry=`.
84
+ A auth vem do `~/.npmrc` (local) ou da variΓ‘vel `NODE_AUTH_TOKEN` (CI). HΓ‘ um teste
85
+ que trava isso: `publish: o .npmrc do projeto NÃO pode declarar _authToken`.
86
+
87
+ ### πŸ† Caminho A β€” bootstrap local + Trusted Publishing (OIDC)
88
+
89
+ O OIDC Γ© a soluΓ§Γ£o definitiva: credencial de vida curta, assinada pelo GitHub,
90
+ **sem token nenhum**. Mas tem uma limitaΓ§Γ£o: **OIDC nΓ£o publica a primeira versΓ£o** de um
91
+ pacote β€” o pacote precisa existir no npm antes de vocΓͺ configurar o publisher.
92
+
93
+ Por isso o fluxo Γ© "publica uma vez local, depois nunca mais":
94
+
95
+ **Passo 1 β€” publique a primeira versΓ£o da sua mΓ‘quina** (vocΓͺ tem o autenticador):
96
+
97
+ ```bash
98
+ npm publish --access public --provenance=false --otp=123456
99
+ # ↑ cΓ³digo do seu app autenticador
100
+ ```
101
+
102
+ > 🚨 **Não coloque `provenance` no `publishConfig` do `package.json`.**
103
+ > O npm lΓͺ `publishConfig` **com prioridade sobre flag de CLI e sobre variΓ‘vel de
104
+ > ambiente**. Com `provenance: true` lΓ‘, *qualquer* publish fora de um CI com OIDC
105
+ > falha com:
106
+ >
107
+ > ```
108
+ > npm error code EUSAGE
109
+ > npm error Automatic provenance generation not supported for provider: null
110
+ > ```
111
+ >
112
+ > NΓ£o adianta passar `--provenance=false` nem `NPM_CONFIG_PROVENANCE=false`: o
113
+ > `publishConfig` vence os dois. O provenance Γ© controlado **por invocaΓ§Γ£o** β€” o
114
+ > workflow de release passa `--provenance` explicitamente, o publish local nΓ£o passa.
115
+ >
116
+ > O `--provenance=false` acima Γ© por clarity e por seguranΓ§a de quem roda o comando
117
+ > num repo onde alguΓ©m reinseriu a flag β€” mas o que resolve Γ© o `package.json` estar limpo.
118
+
119
+ **Passo 2 β€” configure o publisher confiΓ‘vel** em
120
+ <https://www.npmjs.com/package/create-sdd-ai-stack/settings/trusted-publishers>:
121
+
122
+ | Campo | Valor |
123
+ | --- | --- |
124
+ | Provider | GitHub Actions |
125
+ | Organization or user | `marcelinosandroni` |
126
+ | Repository | `sdd-ai-stack` |
127
+ | Workflow filename | `release.yml` (sΓ³ o nome, com `.yml`) |
128
+ | Allowed actions | `npm publish` |
129
+
130
+ > ⚠️ O npm **não valida** essa configuração ao salvar. Errou o nome do workflow ou do
131
+ > repo, o erro sΓ³ aparece na hora de publicar. Tudo Γ© **case-sensitive**.
132
+
133
+ **Passo 3 β€” apague o secret** (deixe o OIDC Assumir):
134
+
135
+ ```bash
136
+ gh secret delete NPM_TOKEN --repo marcelinosandroni/sdd-ai-stack
137
+ ```
138
+
139
+ Pronto. Da prΓ³xima vez em diante:
140
+
141
+ ```bash
142
+ npm run version:patch && git push origin main && git push origin --tags
143
+ ```
144
+
145
+ Publica sozinho, com provenance, **sem token nenhum**. Pode atΓ© desligar
146
+ "Require two-factor authentication" do pacote depois β€” o OIDC nΓ£o depende dele.
147
+
148
+ ### ⚠️ Caminho B β€” token com "Bypass 2FA" (temporΓ‘rio)
149
+
150
+ Se quiser o CI funcionando **hoje** sem mexer na mΓ‘quina:
151
+
152
+ Em <https://www.npmjs.com/settings/access-tokens>, crie um token granular com:
153
+
154
+ | Campo | Valor |
155
+ | --- | --- |
156
+ | Permissions | **Read and write** |
157
+ | Bypass 2FA | βœ… **marcado** |
158
+ | Package | `create-sdd-ai-stack` |
159
+
160
+ ```bash
161
+ gh secret set NPM_TOKEN --repo marcelinosandroni/sdd-ai-stack
162
+ ```
163
+
164
+ > **SΓ³ como ponte.** O npm avisa: *"a publicaΓ§Γ£o direta com token granular serΓ‘ removida
165
+ > em janeiro de 2027"*. AlΓ©m disso, hΓ‘ bug aberto ([npm/cli#9268](https://github.com/npm/cli/issues/9268))
166
+ > onde "Bypass 2FA" Γ© ignorado pelo npm 11.x. Trate como prazo, nΓ£o como soluΓ§Γ£o.
167
+
168
+ ### πŸ›‘οΈ Caminho C β€” stage-only (mais seguro, mais atrito)
169
+
170
+ Token **Read and write (stage only)**: o CI sobe a versΓ£o, mas ela **nΓ£o vai ao ar**.
171
+ Um maintainer precisa aprovar com 2FA:
172
+
173
+ ```bash
174
+ npm stage publish # no CI, com o token stage-only
175
+ npm stage list # ver o que estΓ‘ pendente
176
+ npm stage approve --otp=123456
177
+ ```
178
+
179
+ Combine com `Require two-factor authentication and disallow tokens` no
180
+ [package settings](https://www.npmjs.com/package/create-sdd-ai-stack/settings): token
181
+ vazado nΓ£o consegue publicar nada sozinho. Γ‰ a postura mΓ‘xima de seguranΓ§a β€” ao preΓ§o
182
+ de uma aprovaΓ§Γ£o manual por release.
183
+
184
+ ---
185
+
186
+ ## 2. Como o workflow decide o modo
187
+
188
+ Ele nΓ£o precisa saber: **o npm escolhe sozinho**.
189
+
190
+ | `NPM_TOKEN` no repo | O que o workflow faz | Como o npm publica |
191
+ | --- | --- | --- |
192
+ | **definido** | escreve a linha de token no `~/.npmrc` | modo token |
193
+ | **ausente** | nΓ£o escreve **nada** no `.npmrc` | OIDC |
194
+
195
+ > ⚠️ **O passo `Publica` não define `NODE_AUTH_TOKEN` de propósito.** O npm só engata o
196
+ > OIDC quando o auth estΓ‘ **ausente**. Se `NODE_AUTH_TOKEN` estiver no ambiente, ele
197
+ > ignora o OIDC e tenta token β€” e volta a falhar.
198
+
199
+ > ⚠️ **Requisito de versΓ£o:** OIDC precisa de **npm β‰₯ 11.5.1** e **Node β‰₯ 22.14.0**.
200
+ > O Node 22 do runner do GitHub vem com npm 10.x, entΓ£o o workflow roda
201
+ > `npm install -g npm@latest` antes de publicar. Sem isso o OIDC nunca engata.
202
+
203
+ > ⚠️ **Nunca** cole o token num arquivo, no `.npmrc` commitado, ou num commit.
204
+ > Se vazar: revogue em <https://www.npmjs.com/settings/access-tokens> imediatamente.
205
+
206
+ Verificar que estΓ‘ lΓ‘:
207
+
208
+ ```bash
209
+ gh secret list --repo marcelinosandroni/sdd-ai-stack
210
+ # deve listar: NPM_TOKEN Updated: <data>
211
+ ```
212
+
213
+ No caminho A (OIDC), a lista deve estar **vazia** β€” e Γ© isso que faz o npm usar OIDC.
214
+
215
+ ---
216
+
217
+ ## 3. Testar sem publicar (recomendado na primeira vez)
218
+
219
+ Depois que o workflow estiver na `main`, valide sem queimar versΓ£o:
220
+
221
+ ```bash
222
+ gh workflow run release.yml -f version=9.9.9 -f dry_run=true
223
+ gh run watch
224
+ ```
225
+
226
+ Isso roda testes, checa links, confere o conteΓΊdo do tarball, e faz
227
+ `npm publish --dry-run`. **Nada Γ© publicado.**
228
+
229
+ ---
230
+
231
+ ## 4. Publicar de verdade
232
+
233
+ ```bash
234
+ # 1. main atualizada e CI verde
235
+ git checkout main && git pull
236
+
237
+ # 2. bump de versΓ£o (commita o package.json e cria a tag)
238
+ npm run version:patch # ou version:minor / version:major
239
+
240
+ # 3. manda cΓ³digo e tag
241
+ git push origin main
242
+ git push origin --tags
243
+ ```
244
+
245
+ O workflow dispara com o push da tag. Acompanhe:
246
+
247
+ ```bash
248
+ gh run list
249
+ gh run watch
250
+ ```
251
+
252
+ Se tudo der certo: <https://www.npmjs.com/package/create-sdd-ai-stack>
253
+
254
+ ---
255
+
256
+ ## 5. O que o workflow checa antes de publicar
257
+
258
+ | Guarda | Motivo |
259
+ | --- | --- |
260
+ | `npm test` (22 testes) | nunca publica com teste vermelho |
261
+ | `node SKILLS/check-docs/check-docs.mjs` | nenhuma regra apontando pra arquivo morto |
262
+ | tag `vX.Y.Z` == `version` do `package.json` | evita publicar 0.1.18 quando a tag Γ© 0.1.17 |
263
+ | 10 arquivos essenciais presentes no tarball | pega `files` mal configurado no `package.json` |
264
+ | `concurrency: release-npm` | dois publishes simultΓ’neos nΓ£o correm em paralelo |
265
+ | `npm >= 11.5.1` no job de publish | sem isso o OIDC nΓ£o engata (Node 22 do runner traz npm 10.x) |
266
+ | `publishConfig.provenance` | o pacote Γ© assinado pelo GitHub β€” prova de que saiu deste repo |
267
+
268
+ ---
269
+
270
+ ## 6. Problemas comuns
271
+
272
+ | Sintoma | Causa | SoluΓ§Γ£o |
273
+ | --- | --- | --- |
274
+ | `E404` / "PUT ... Not Found" **na primeira publicaΓ§Γ£o** | **nΓ£o Γ© o pacote ausente** β€” Γ© o `.npmrc` do projeto sombreando o seu token, deixando a auth vazia | veja Β§1.1 |
275
+ | `EUSAGE` / "Automatic provenance generation not supported for provider: null" | `publishConfig.provenance: true` e o publish nΓ£o saiu de um CI com OIDC | tire `provenance` do `publishConfig`; no local use `--provenance=false`, no CI `--provenance` |
276
+ | `EOTP` / "requires a one-time password" | **2FA ligado** e o token nΓ£o tem "Bypass 2FA" | seΓ§Γ£o 1 β€” caminho A (OIDC) ou B (bypass) |
277
+ | `ENEEDAUTH` / "need auth" com OIDC configurado | `NODE_AUTH_TOKEN` presente no ambiente derruba o OIDC, **ou** o workflow nΓ£o Γ© o `release.yml`, **ou** o repo/owner estΓ‘ errado | confira os 3 campos no npmjs.com; eles sΓ£o case-sensitive |
278
+ | `ENOENT` / OIDC nΓ£o engata | npm < 11.5.1 ou Node < 22.14.0 | o workflow jΓ‘ sobe o npm; se persistir, atualize o `node-version` |
279
+ | `E403 Forbidden` | token sem permissΓ£o de escrita no pacote | regere com Read and write em `create-sdd-ai-stack` |
280
+ | `E_STAGE_REQUIRED` | token Γ© stage-only e vocΓͺ chamou `npm publish` | use `npm stage publish` e aprove com `npm stage approve --otp` |
281
+ | `401 Unauthorized` | token expirado ou revogado | regere no npm e grave de novo |
282
+ | `cannot publish over previously published version` | jΓ‘ existe essa versΓ£o | bump a versΓ£o (`npm run version:patch`) |
283
+ | `tag 'v0.1.17' nΓ£o bate com package.json '0.1.18'` | esqueceu de commitar o bump | `git add package.json && git commit -m "chore: v0.1.18"` |
284
+ | `faltando no pacote: X` | `files` do `package.json` incompleto | adicione o caminho em `files` |
285
+
286
+ > **Bug conhecido do npm:** token granular com "Bypass 2FA" sendo ignorado pelo npm 11.x
287
+ > ([npm/cli#9268](https://github.com/npm/cli/issues/9268)). Se o bypass "nΓ£o funcionar",
288
+ > o caminho A (OIDC) Γ© a saΓ­da β€” ele nΓ£o depende de token nenhum.
289
+
290
+ ---
291
+
292
+ ## 7. Publicar localmente (escape, nΓ£o Γ© o caminho padrΓ£o)
293
+
294
+ Se precisar publicar da sua mΓ‘quina:
295
+
296
+ ```powershell
297
+ # PowerShell β€” o caminho de bootstrap (com 2FA ligado)
298
+ npm publish --access public --provenance=false --otp=123456
299
+ ```
300
+
301
+ Sem 2FA, ou com token de bypass:
302
+
303
+ ```powershell
304
+ $env:NODE_AUTH_TOKEN = "<seu token>"
305
+ npm publish --access public --provenance=false
306
+ ```
307
+
308
+ > `--provenance=false` Γ© obrigatΓ³rio em publish local. O provenance sΓ³ pode ser gerado
309
+ > dentro de um CI com OIDC (GitHub Actions, GitLab CI, CircleCI).
310
+
311
+ > O `.npmrc` da raiz usa `${NODE_AUTH_TOKEN}` justamente para que o token
312
+ > **nunca** fique gravado em arquivo.
313
+
314
+ E confira antes:
315
+
316
+ ```bash
317
+ npm run check:pack # mostra exatamente o que vai subir
318
+ ```
319
+
320
+ ---
321
+
322
+ ## 8. Checklist antes de apertar o botΓ£o
323
+
324
+ - [ ] `npm test` verde
325
+ - [ ] `main` sincronizada com `origin`
326
+ - [ ] `NPM_TOKEN` existe e nΓ£o expirou
327
+ - [ ] `npm run check:pack` mostra os arquivos esperados
328
+ - [ ] `version:patch|minor|major` escolhido conscientemente
329
+ - [ ] `git push origin main` e `git push origin --tags` feitos
330
+
331
+ > **Publicar Γ© irreversΓ­vel.** VersΓ£o publicada nΓ£o pode ser removida (sΓ³ despublicada,
332
+ > e o npm nunca reutiliza o nΓΊmero). A tag tambΓ©m nΓ£o deve ser deletada.
@@ -0,0 +1,42 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ /** Remove blocos de cΓ³digo cercados por ``` para nΓ£o checar links ilustrativos. */
5
+ function stripCodeFences(md) {
6
+ return md.replace(/```[\s\S]*?```/g, "").replace(/~~~[\s\S]*?~~~/g, "");
7
+ }
8
+
9
+ /** Coleta todos os .md a partir das raΓ­zes informadas (caminhos em formato posix). */
10
+ export function collectMarkdown(roots) {
11
+ const files = [];
12
+ const walk = (p) => {
13
+ if (!fs.existsSync(p)) return;
14
+ const stat = fs.statSync(p);
15
+ if (stat.isDirectory()) {
16
+ if (/node_modules|\.next|test-results|playwright-report/.test(p)) return;
17
+ for (const entry of fs.readdirSync(p)) walk(path.join(p, entry));
18
+ } else if (/\.md$/.test(p)) {
19
+ files.push(p.split(path.sep).join("/"));
20
+ }
21
+ };
22
+ roots.forEach(walk);
23
+ return files;
24
+ }
25
+
26
+ /**
27
+ * Valida todos os links relativos entre documentos markdown.
28
+ * @returns {Array<{file, link}>} lista de links quebrados
29
+ */
30
+ export function checkLinks(roots, { cwd = process.cwd() } = {}) {
31
+ const broken = [];
32
+ const re = /\]\((\.{0,2}\/[^)#\s]+)(?:#[^)]*)?\)/g;
33
+
34
+ for (const file of collectMarkdown(roots)) {
35
+ const abs = path.resolve(cwd, file);
36
+ const txt = stripCodeFences(fs.readFileSync(abs, "utf8"));
37
+ for (const m of txt.matchAll(re)) {
38
+ const target = path.resolve(path.dirname(abs), m[1]);
39
+ if (!fs.existsSync(target)) broken.push({ file, link: m[1] });
40
+ }
41
+ } return broken;
42
+ }
@@ -0,0 +1,60 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import path from "node:path";
3
+
4
+ export const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
5
+
6
+ /** Documentos de regra que vΓ£o para <projeto>/SDD/ */
7
+ export const RULE_FILES = [
8
+ "AGENTS.md",
9
+ "APP.md",
10
+ "APP-STACK.md",
11
+ "ARCHITECTURE.md",
12
+ "DESIGN.md",
13
+ "NEXT.md",
14
+ "NODE.md",
15
+ "REACT.md",
16
+ "README.md",
17
+ ];
18
+
19
+ /** Pastas de regras que vΓ£o para <projeto>/SDD/ */
20
+ export const RULE_DIRS = ["stacks", "specs", "docs", "SKILLS"];
21
+
22
+ /** Nome da pasta onde as regras sΓ£o instaladas dentro do projeto consumidor. */
23
+ export const SDD_DIR = "SDD";
24
+
25
+ /** Templates de projeto disponΓ­veis. */
26
+ export const TEMPLATES = ["next"];
27
+
28
+ export const DEFAULT_TEMPLATE = "next";
29
+
30
+ /** Atalhos na raiz do projeto que apontam para ./SDD/AGENTS.md */
31
+ export const SHORTCUTS = [
32
+ "AGENTS.md",
33
+ "CLAUDE.md",
34
+ "GEMINI.md",
35
+ ".cursorrules",
36
+ ".windsurfrules",
37
+ ".github/copilot-instructions.md",
38
+ ".clinerules",
39
+ ];
40
+
41
+ /** RepositΓ³rio usado quando o usuΓ‘rio pede para instalar como submodule. */
42
+ export const DEFAULT_SUBMODULE_URL = "https://github.com/marcelinosandroni/sdd-ai-stack.git";
43
+
44
+ /** Ignorado ao copiar regras (evita embutir a prΓ³pria lib no SDD/). */
45
+ export const COPY_IGNORE = new Set([
46
+ ".git",
47
+ ".github",
48
+ "node_modules",
49
+ ".next",
50
+ "bin",
51
+ "src",
52
+ "lib",
53
+ "tests",
54
+ "template",
55
+ "package.json",
56
+ "package-lock.json",
57
+ "coverage",
58
+ "test-results",
59
+ "playwright-report",
60
+ ]);
@@ -0,0 +1,253 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { execFileSync } from "node:child_process";
4
+ import {
5
+ COPY_IGNORE,
6
+ PKG_ROOT,
7
+ RULE_DIRS,
8
+ RULE_FILES,
9
+ SDD_DIR,
10
+ SHORTCUTS,
11
+ } from "./constants.mjs";
12
+
13
+ /* ────────────────────────────────────────────────────────────
14
+ Helpers
15
+ ──────────────────────────────────────────────────────────── */
16
+
17
+ export function ensureDir(dir) {
18
+ fs.mkdirSync(dir, { recursive: true });
19
+ }
20
+
21
+ export function copyDir(from, to, { ignore = new Set() } = {}) {
22
+ ensureDir(to);
23
+ for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
24
+ if (ignore.has(entry.name)) continue;
25
+ const src = path.join(from, entry.name);
26
+ const dest = path.join(to, entry.name);
27
+ if (entry.isDirectory()) {
28
+ copyDir(src, dest, { ignore });
29
+ } else if (entry.isFile()) {
30
+ ensureDir(path.dirname(dest));
31
+ fs.copyFileSync(src, dest);
32
+ }
33
+ }
34
+ }
35
+
36
+ /** Cria symlink relativo. Retorna "symlink" ou null se o SO nΓ£o permitir. */
37
+ function trySymlink(targetAbs, linkPath) {
38
+ const rel = path.relative(path.dirname(linkPath), targetAbs).split(path.sep).join("/");
39
+ try {
40
+ fs.symlinkSync(rel, linkPath, "file");
41
+ return "symlink";
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ const STUB_BODY = `# πŸ€– AGENTS.md (ponteiro)
48
+
49
+ > Este arquivo Γ© um **atalho** para as regras do projeto.
50
+ > A fonte da verdade vive em **[SDD/AGENTS.md](./SDD/AGENTS.md)**.
51
+
52
+ ## 🚨 LEIA AGORA
53
+
54
+ \`\`\`bash
55
+ cat SDD/AGENTS.md
56
+ \`\`\`
57
+
58
+ **Leitura mΓ­nima obrigatΓ³ria para comeΓ§ar qualquer tarefa:**
59
+
60
+ 1. \`SDD/AGENTS.md\` β€” leis do agente e fluxo de entrega
61
+ 2. \`SDD/specs/PLAN.md\` β€” a task que estΓ‘ sendo feita AGORA
62
+ 3. \`SDD/APP.md\` + \`SDD/APP-STACK.md\` β€” o que Γ© este app e qual a stack
63
+ 4. O doc de regra da stack: \`SDD/NEXT.md\` (padrΓ£o), \`SDD/NODE.md\`, \`SDD/REACT.md\`
64
+ 5. \`SDD/DESIGN.md\` β€” sΓ³ se for mexer em UI
65
+ 6. \`SDD/stacks/README.md\` β€” Γ­ndice das regras por ferramenta
66
+
67
+ > **NΓ£o edite os arquivos em \`SDD/\`** sem pedir ao humano. Eles sΓ£o a lei do template.
68
+ > Para atualizar: \`git submodule update --remote --merge SDD\` (se submodule) ou \`npm run sdd:sync\`.
69
+ `;
70
+
71
+ function writeShortcut(root, relPath, mode) {
72
+ const linkPath = path.join(root, relPath);
73
+ // caminho ABSOLUTO do destino β€” path.relative() exige isso,
74
+ // senΓ£o o segundo argumento Γ© resolvido contra process.cwd() e o link nasce quebrado
75
+ const targetAbs = path.join(root, SDD_DIR, "AGENTS.md");
76
+ ensureDir(path.dirname(linkPath));
77
+
78
+ if (fs.existsSync(linkPath)) return "skipped";
79
+ if (mode === "stub") return writeStub(linkPath);
80
+
81
+ return trySymlink(targetAbs, linkPath) ?? writeStub(linkPath);
82
+ }
83
+
84
+ function writeStub(linkPath) {
85
+ fs.writeFileSync(linkPath, STUB_BODY, "utf8");
86
+ return "stub";
87
+ }
88
+
89
+ function run(cmd, args, cwd) {
90
+ execFileSync(cmd, args, { cwd, stdio: "inherit", shell: process.platform === "win32" });
91
+ }
92
+
93
+ function has(cmd) {
94
+ try {
95
+ execFileSync(cmd, ["--version"], { stdio: "ignore", shell: process.platform === "win32" });
96
+ return true;
97
+ } catch {
98
+ return false;
99
+ }
100
+ }
101
+
102
+ /* ────────────────────────────────────────────────────────────
103
+ Installers
104
+ ──────────────────────────────────────────────────────────── */
105
+
106
+ /** Copia as regras do template para <target>/SDD/ */
107
+ export function installRules(target, { log = () => {} } = {}) {
108
+ const dest = path.join(target, SDD_DIR);
109
+ ensureDir(dest);
110
+
111
+ for (const file of RULE_FILES) {
112
+ const src = path.join(PKG_ROOT, file);
113
+ if (fs.existsSync(src)) {
114
+ fs.copyFileSync(src, path.join(dest, file));
115
+ log(` βœ“ SDD/${file}`);
116
+ }
117
+ }
118
+
119
+ for (const dir of RULE_DIRS) {
120
+ const src = path.join(PKG_ROOT, dir);
121
+ if (!fs.existsSync(src)) continue;
122
+ copyDir(src, path.join(dest, dir), { ignore: COPY_IGNORE });
123
+ log(` βœ“ SDD/${dir}/`);
124
+ }
125
+
126
+ return dest;
127
+ }
128
+
129
+ /** Instala a pasta SDD/ como git submodule apontando para o repo remoto. */
130
+ export function installSubmodule(target, url, { log = () => {} } = {}) {
131
+ const dest = path.join(target, SDD_DIR);
132
+ if (!has("git")) {
133
+ log(" ! git nΓ£o encontrado. Copiando as regras localmente.");
134
+ return installRules(target, { log });
135
+ }
136
+ ensureDir(target);
137
+ run("git", ["submodule", "add", url, SDD_DIR], target);
138
+ installShortcuts(target, { log });
139
+ return dest;
140
+ }
141
+
142
+ /** Cria os atalhos na raiz do projeto apontando para ./SDD/AGENTS.md */
143
+ export function installShortcuts(target, { log = () => {}, mode = "auto" } = {}) {
144
+ const results = SHORTCUTS.map((rel) => ({ rel, kind: writeShortcut(target, rel, mode) }));
145
+ for (const { rel, kind } of results) {
146
+ log(` βœ“ ${rel} (${kind})`);
147
+ }
148
+ return results;
149
+ }
150
+
151
+ /** Copia o template de projeto (Next.js) para o alvo. */
152
+ export function installTemplate(target, template, { log = () => {} } = {}) {
153
+ const src = path.join(PKG_ROOT, "template", template);
154
+ if (!fs.existsSync(src)) {
155
+ throw new Error(`Template "${template}" nΓ£o encontrado em ${src}`);
156
+ }
157
+ copyDir(src, target, { ignore: new Set([".git"]) });
158
+ restoreGitignore(target);
159
+ log(` βœ“ template/${template} β†’ ${target}`);
160
+ return target;
161
+ }
162
+
163
+ /**
164
+ * O npm NUNCA empacota arquivo chamado `.gitignore` (Γ© um default-ignore dele).
165
+ * Guardar como `gitignore` e renomear aqui garante que o app gerado SEMPRE
166
+ * tenha .gitignore β€” sem ele, `.env.local` e `.next` iam pro git.
167
+ */
168
+ function restoreGitignore(target) {
169
+ const from = path.join(target, "gitignore");
170
+ const to = path.join(target, ".gitignore");
171
+ if (fs.existsSync(from)) {
172
+ fs.renameSync(from, to);
173
+ return;
174
+ }
175
+ if (fs.existsSync(to)) return;
176
+ throw new Error("Template sem gitignore β€” o app gerado ficaria sem .gitignore.");
177
+ }
178
+
179
+ /* ────────────────────────────────────────────────────────────
180
+ OrquestraΓ§Γ£o
181
+ ──────────────────────────────────────────────────────────── */
182
+
183
+ /**
184
+ * Cria um projeto completo: template + regras + atalhos.
185
+ * @returns {object} resumo do que foi feito
186
+ */
187
+ export function scaffold(options) {
188
+ const {
189
+ target,
190
+ template = "next",
191
+ install = false,
192
+ git = false,
193
+ submodule = null,
194
+ shortcutMode = "auto",
195
+ log = console.log,
196
+ } = options;
197
+
198
+ const abs = path.resolve(target);
199
+ const summary = { target: abs, template, rules: false, shortcuts: [], git: false, install: false };
200
+
201
+ if (fs.existsSync(abs) && fs.readdirSync(abs).length > 0) {
202
+ throw new Error(`Pasta "${abs}" jΓ‘ existe e nΓ£o estΓ‘ vazia. Escolha outro nome.`);
203
+ }
204
+ ensureDir(abs);
205
+
206
+ // 1. Template do app (primeiro, para nΓ£o colidir com SDD/)
207
+ if (template && template !== "none") {
208
+ log(`\nπŸ“¦ Template: ${template}`);
209
+ installTemplate(abs, template, { log });
210
+ }
211
+
212
+ // 2. Regras
213
+ log("\n🧠 Regras SDD:");
214
+ if (submodule) {
215
+ log(` β†’ instalando como submodule: ${submodule}`);
216
+ installSubmodule(abs, submodule, { log });
217
+ } else {
218
+ installRules(abs, { log });
219
+ installShortcuts(abs, { log, mode: shortcutMode });
220
+ }
221
+ summary.rules = true;
222
+
223
+ // 3. Git
224
+ if (git) {
225
+ if (has("git")) {
226
+ log("\n🌿 Git:");
227
+ run("git", ["init"], abs);
228
+ run("git", ["add", "-A"], abs);
229
+ run(
230
+ "git",
231
+ ["-c", "user.name=Marcelino Sandroni", "-c", "user.email=marcelino.sandroni@gmail.com",
232
+ "commit", "-m", `chore: scaffold inicial com SDD AI Stack. (Agent: create-sdd-ai-stack)`],
233
+ abs,
234
+ );
235
+ summary.git = true;
236
+ } else {
237
+ log("\n! git nΓ£o encontrado. Pulando init.");
238
+ }
239
+ }
240
+
241
+ // 4. DependΓͺncias
242
+ if (install) {
243
+ if (fs.existsSync(path.join(abs, "package.json"))) {
244
+ log("\nπŸ“₯ Instalando dependΓͺncias…");
245
+ run("npm", ["install"], abs);
246
+ summary.install = true;
247
+ } else {
248
+ log("\n! Sem package.json β€” pulando instalaΓ§Γ£o.");
249
+ }
250
+ }
251
+
252
+ return summary;
253
+ }