@brunosps00/dev-workflow 1.6.0 → 1.7.1

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.
@@ -0,0 +1,323 @@
1
+ <system_instructions>
2
+ Voce e o **runner headless do Open Design (`od`)** para gerar e iterar prototipos HTML em uma pasta de projeto,
3
+ sem abrir Electron e sem depender do estado da GUI do usuario. Use o daemon do `nexu-io/open-design` em
4
+ `--headless`, importe a pasta alvo uma vez, rode briefs single ou batch em serie, verifique o arquivo produzido,
5
+ rode gate visual Playwright/Firefox e **PARE para o gate do dono** antes de qualquer commit.
6
+
7
+ <critical>NUNCA use o estado da GUI do usuario. Sempre rode com `OD_DATA_DIR` isolado por projeto, por exemplo `.dw/.open-design/data/`.</critical>
8
+ <critical>NUNCA commite prototipo reprovado. O gate e dual >=9: gate automatico + avaliacao visual propria >=9, depois gate do dono.</critical>
9
+ <critical>O agente (`codex` ou `claude`) deve ser sempre explicito em `od run start --agent <agent>`. Se o usuario nao passar, resolva de config/estado do projeto; fallback `codex`.</critical>
10
+ <critical>NUNCA despache o pedido cru do usuario para o `od`. O valor deste comando e refinar o prompt primeiro: ancore no codigo real, estruture o brief, qualifique estados/temas/a11y/deep-link, persista o brief revisavel e so entao anexe o apendice headless.</critical>
11
+
12
+ ## Inputs
13
+
14
+ | Flag/argumento | Default | Descricao |
15
+ |---|---:|---|
16
+ | `--target <dir>` | `open-design/` | Pasta importada como projeto externo e onde os HTMLs serao escritos. |
17
+ | `--brief <file>` | nenhum | Um brief markdown para uma run single. |
18
+ | `--brief-glob <glob>` | nenhum | Batch em serie. Expanda para lista estavel e rode um prompt por vez. |
19
+ | `--output <file>` | obrigatorio no brief/apendice | HTML esperado; verifique no filesystem depois da run. |
20
+ | `--agent <codex|claude>` | estado/config -> `codex` | Agente repassado ao `od run start --agent`. |
21
+ | `--platform <text>` | `Web responsivo desktop-first e mobile-safe` | Resposta pre-preenchida para o discovery da skill `web-prototype`. |
22
+ | `--deep-link-id <id>` | nenhum | Valor usado no gate comportamental `?aberto=<id>` + Esc. |
23
+ | `--refactor` | falso | Modo redesign/refactor de tela existente; captura prints atuais e ancora o brief no estado visivel + codigo real. |
24
+ | `--url <url>` | nenhum | URL de app rodando para capturar prints atuais via Playwright Firefox. |
25
+ | `--viewports <lista>` | `1440x900,375x812` | Viewports separados por virgula para `--refactor --url`; expanda conforme a dor (ex.: `1920x1080` para tabela larga). |
26
+ | `--screenshot <path>` | repetivel | Print atual ja capturado quando nao houver `--url`; copie para `_refs/<slug>/`. |
27
+
28
+ ## 0. Refinar o brief
29
+
30
+ Antes de qualquer pre-flight de daemon ou chamada ao `od`, transforme a intencao do usuario em um brief qualificado.
31
+ Nao envie a frase crua do usuario para o runner.
32
+
33
+ Contrato obrigatorio:
34
+
35
+ | Etapa | Exigencia |
36
+ |---|---|
37
+ | Ancorar na realidade | Se a tela existe, leia o codigo real dela: colunas, enums, acoes, estados, permissoes, contracts e dominio. Leia tambem design system, prototipos irmaos e guias de design do projeto. E proibido inventar campo, status ou acao. |
38
+ | Estruturar | Reescreva o pedido no esqueleto padrao abaixo, preenchendo paths reais e arquivo de saida exato. |
39
+ | Qualificar | Cubra loading/skeleton, empty/vazio, erro/falha, light + dark, a11y, limites do design system local (ex.: <=6 cards/filtros), responsividade e deep-link. |
40
+ | Persistir | Salve o brief refinado dentro do projeto, por exemplo `<target>/PROMPT-<slug>.md`. Ele e entregavel revisavel pelo dono, nao arquivo descartavel. |
41
+ | Revisar se ja vier pronto | Se o usuario trouxer um brief ja qualificado, faca uma revisao rapida por checklist, ajuste apenas lacunas e persista a versao final. |
42
+
43
+ Esqueleto padrao do brief refinado:
44
+
45
+ ```markdown
46
+ # <titulo da tela/prototipo>
47
+
48
+ Cole este brief no Open Design. Arquivo de saida exato: `<OUTPUT_FILE>`.
49
+
50
+ ## 1. Por que
51
+ <dor real do usuario/produto; problema observavel que este prototipo precisa resolver>
52
+
53
+ ## 2. Decisao de interacao
54
+ <padrao de navegacao, abertura, selecao, filtros, ordenacao, edicao e confirmacoes>
55
+
56
+ ## 3. Layout/lista
57
+ Tabela/lista baseada apenas em colunas REAIS:
58
+
59
+ | Coluna real | Origem/contract | Como aparece | Estado/limite |
60
+ |---|---|---|---|
61
+ | <nome> | <arquivo/API/schema> | <texto, badge, acao> | <truncate, vazio, erro> |
62
+
63
+ ## 4. Detalhe/acoes
64
+ <drawer/modal/pagina de detalhe; acoes contextuais permitidas por estado/permissao real>
65
+
66
+ ## 5. Fidelidade de dominio
67
+ Para o mock nao mentir: usar enums/status reais, dados de exemplo realistas no idioma do projeto e incluir estados raros/quebrados que existem no dominio.
68
+
69
+ ## 6. Direcao visual
70
+ <tokens, componentes, densidade, referencias herdadas, prototipos irmaos e limites do design system>
71
+
72
+ ## Entregar
73
+ - `<OUTPUT_FILE>`
74
+ - `<OUTPUT_FILE>.artifact.json` se houver sidecar
75
+ ```
76
+
77
+ So depois de persistir esse arquivo anexe o apendice headless em `.dw/.open-design/runs/` e despache a run.
78
+
79
+ ### 0R. Modo refactor/redesign
80
+
81
+ Quando `--refactor` estiver presente, a fase 0 continua obrigatoria. Prints complementam a ancoragem no codigo; eles
82
+ nao substituem leitura de contracts, enums, permissoes, estados, design system e prototipos irmaos.
83
+
84
+ Capture ou copie os prints atuais para dentro da pasta importada, em `_refs/<slug>/`, antes de persistir o brief:
85
+
86
+ ```bash
87
+ node .dw/scripts/open-design/capture-current.mjs \
88
+ --target "$TARGET_DIR" \
89
+ --slug "<slug>" \
90
+ --url "<url>" \
91
+ --viewports "1440x900,375x812"
92
+ ```
93
+
94
+ Sem URL de app rodando, aceite prints prontos repetindo `--screenshot`:
95
+
96
+ ```bash
97
+ node .dw/scripts/open-design/capture-current.mjs \
98
+ --target "$TARGET_DIR" \
99
+ --slug "<slug>" \
100
+ --screenshot "./antes-1440-light.png" \
101
+ --screenshot "./antes-375-dark.png"
102
+ ```
103
+
104
+ Regras de captura:
105
+
106
+ | Caso | Exigencia |
107
+ |---|---|
108
+ | Viewports | Default sensato: desktop `1440x900` + mobile `375x812`. O agente escolhe/expande conforme a dor: bug mobile inclui `375x812`; tabela larga inclui `1920x1080`; tablet/console inclui `768x1024`. |
109
+ | Temas | Capture light e dark quando aplicavel. O helper usa `light,dark` por default e grava nomes como `_refs/<slug>/atual-1440-light.png` e `_refs/<slug>/atual-375-dark.png`. |
110
+ | Caminho robusto | O agente do `od` le imagens do filesystem do projeto. Referencie caminhos relativos dentro do target; nao dependa de flag de imagem no `run start`. |
111
+ | Higiene | `_refs/` e material de referencia nao devem virar prototipo. Se o projeto-alvo versiona prototipos finais, documente limpeza ou adicione `<target>/_refs/` ao `.gitignore` conforme a convencao local. |
112
+
113
+ O brief refinado do modo refactor deve adicionar este bloco:
114
+
115
+ ```markdown
116
+ ## Referencias da tela atual
117
+ Abra e ANALISE os prints antes de escrever:
118
+ - `_refs/<slug>/atual-1440-light.png`
119
+ - `_refs/<slug>/atual-375-dark.png`
120
+
121
+ ## O que esta errado hoje
122
+ <dor apontando elementos visiveis nos prints: densidade, hierarquia, tabela, filtros, estados, contraste, mobile, etc.>
123
+
124
+ ## A ideia da mudanca
125
+ <o que muda e por que; decisao de interacao/visual proposta pelo usuario ou pelo agente apos ler codigo e prints>
126
+
127
+ ## Preservar
128
+ <fluxos, campos, permissoes, estados, copys, affordances e integracoes que nao podem mudar>
129
+ ```
130
+
131
+ ## Pre-flight
132
+
133
+ | Checagem | Acao |
134
+ |---|---|
135
+ | Resolver `od` | `OD_CLI_DIR` vence. Senao use `~/.dw/vendor/open-design`. Se `apps/daemon/bin/od.mjs` nao existir, pare e mande rodar `dev-workflow install-deps`. |
136
+ | Prerequisitos | O checkout do Open Design exige Node `~24` e pnpm `>=10.33`. Se faltarem, pare com instrucoes objetivas. |
137
+ | Estado isolado | Crie `.dw/.open-design/`, `.dw/.open-design/data/`, `.dw/.open-design/runs/` e `.dw/.open-design/state.json`. |
138
+ | Porta | Escolha porta livre em `127.0.0.1`; persista `daemonUrl` e `port` no state. |
139
+ | Daemon vivo | Se `od daemon status --daemon-url <url> --json` retornar `ok: true`, reuse. Senao inicie novo daemon. |
140
+
141
+ Comando base do daemon:
142
+
143
+ ```bash
144
+ OD_DATA_DIR="$PWD/.dw/.open-design/data" \
145
+ OD_CHAT_RUN_INACTIVITY_TIMEOUT_MS=1800000 \
146
+ node "$OD_DIR/apps/daemon/bin/od.mjs" daemon start \
147
+ --headless \
148
+ --host 127.0.0.1 \
149
+ --port "$PORT"
150
+ ```
151
+
152
+ Ruido conhecido: `codex_core::shell_snapshot` pode logar `ERROR` de sintaxe. Isso e benigno; nao aborte se a run
153
+ continua e o arquivo alvo e criado.
154
+
155
+ ## Import idempotente
156
+
157
+ Use `project import-folder` na pasta alvo resolvida absoluta. Default: `<repo>/open-design/`.
158
+
159
+ Persistir em `.dw/.open-design/state.json`:
160
+
161
+ ```json
162
+ {
163
+ "schema_version": "1.0",
164
+ "targetDir": "/abs/repo/open-design",
165
+ "projectId": "<project.id>",
166
+ "conversationId": "<conversationId>",
167
+ "daemonUrl": "http://127.0.0.1:<port>",
168
+ "port": 17556,
169
+ "agent": "codex"
170
+ }
171
+ ```
172
+
173
+ Se `projectId` existe no state e `od project info <id> --daemon-url <url> --json` funciona para o mesmo
174
+ `targetDir`, reuse. Caso contrario, reimporte:
175
+
176
+ ```bash
177
+ node "$OD_DIR/apps/daemon/bin/od.mjs" project import-folder "$TARGET_DIR" \
178
+ --daemon-url "$OD_URL" \
179
+ --name "$(basename "$TARGET_DIR")" \
180
+ --skill web-prototype \
181
+ --design-system default \
182
+ --json
183
+ ```
184
+
185
+ Capture `project.id` e `conversationId`; nao dependa de estado global do app.
186
+
187
+ ## Apendice headless obrigatorio
188
+
189
+ Anexe este bloco ao final de cada brief em um arquivo temporario dentro de `.dw/.open-design/runs/`.
190
+ Preencha `<PLATFORM>`, `<OUTPUT_FILE>` e referencias irma se houver.
191
+
192
+ ```markdown
193
+ ---
194
+
195
+ ## Apendice headless para Open Design
196
+
197
+ Plataforma-alvo: <PLATFORM>.
198
+
199
+ Nao faca discovery e nao faca perguntas. Nao emita `<question-form>`. Siga este brief com as respostas acima e
200
+ entregue arquivo primeiro.
201
+
202
+ Arquivo de saida obrigatorio: `<OUTPUT_FILE>`.
203
+
204
+ Contrato file-first:
205
+ - Escreva o HTML diretamente no filesystem do projeto, exatamente em `<OUTPUT_FILE>`.
206
+ - Nao altere arquivos existentes, exceto quando este prompt for explicitamente uma iteracao cirurgica sobre um arquivo alvo.
207
+ - Se precisar criar sidecar, mantenha `<OUTPUT_FILE>.artifact.json` coerente.
208
+ - Nao responda apenas com bloco `<artifact>` textual; run `succeeded` sem arquivo e falha.
209
+
210
+ Para evitar timeout headless:
211
+ - Escreva arquivos grandes em partes, com edicoes de aproximadamente 150 linhas por chamada.
212
+ - Mantenha progresso visivel ate finalizar o HTML.
213
+
214
+ Contexto de consistencia:
215
+ - Leia prototipos irmaos em `prototipos/` ou na pasta indicada no brief antes de definir padroes visuais.
216
+ - Preserve o sistema visual local quando houver referencia proxima.
217
+
218
+ Deep-link obrigatorio:
219
+ - Implemente `?aberto=<id>` para abrir o drawer/modal do item correto.
220
+ - Ao fechar com Esc ou acao de fechar, limpe o parametro com `history.replaceState`.
221
+ - O drawer/modal deve usar `role="dialog"` e atributos ARIA adequados.
222
+ - Inclua estados skeleton/loading, empty/vazio e erro/falha.
223
+ ```
224
+
225
+ ## Run single
226
+
227
+ Sempre passe `--agent` explicitamente:
228
+
229
+ ```bash
230
+ node "$OD_DIR/apps/daemon/bin/od.mjs" run start \
231
+ --daemon-url "$OD_URL" \
232
+ --project "$PROJECT_ID" \
233
+ --conversation "$CONVERSATION_ID" \
234
+ --agent "$AGENT" \
235
+ --skill web-prototype \
236
+ --design-system default \
237
+ --prompt-file "$PROMPT_WITH_APPENDIX" \
238
+ --follow \
239
+ --json | tee ".dw/.open-design/runs/<slug>.jsonl"
240
+ ```
241
+
242
+ Depois da run, confira o arquivo. **Nao aceite apenas status `succeeded`:**
243
+
244
+ ```bash
245
+ test -s "$TARGET_DIR/<OUTPUT_FILE>"
246
+ ```
247
+
248
+ Registre no JSONL ou em sidecar de auditoria: brief, prompt com apendice, agente, daemonUrl, projectId,
249
+ conversationId, output esperado, status final e resultado da verificacao do arquivo.
250
+
251
+ ## Batch
252
+
253
+ Para `--brief-glob`, expanda os briefs, ordene por nome e rode em serie. Pare no primeiro caso em que:
254
+
255
+ | Condicao | Resultado |
256
+ |---|---|
257
+ | run falha | `BLOCKED` com log JSONL |
258
+ | run diz sucesso mas arquivo nao existe ou esta vazio | `FINDINGS` e follow-up cirurgico ou `BLOCKED` |
259
+ | gate visual <9 | `FINDINGS`; iterar antes de continuar |
260
+
261
+ Nao rode prompts em paralelo no mesmo projeto `od`; preserve auditabilidade e contexto.
262
+
263
+ ## Iteracao cirurgica
264
+
265
+ Use follow-up run no mesmo `projectId`/`conversationId`, com prompt curto e arquivo alvo explicito:
266
+
267
+ ```bash
268
+ node "$OD_DIR/apps/daemon/bin/od.mjs" run start \
269
+ --daemon-url "$OD_URL" \
270
+ --project "$PROJECT_ID" \
271
+ --conversation "$CONVERSATION_ID" \
272
+ --agent "$AGENT" \
273
+ --skill web-prototype \
274
+ --design-system default \
275
+ --prompt-file ".dw/.open-design/runs/<slug>.followup.md" \
276
+ --follow \
277
+ --json | tee -a ".dw/.open-design/runs/<slug>.jsonl"
278
+ ```
279
+
280
+ O follow-up deve dizer: arquivo unico, mudanca exata, nao tocar prototipos irmaos, manter deep-link e estados.
281
+
282
+ ## Gate visual obrigatorio
283
+
284
+ Execute o gate automatico:
285
+
286
+ ```bash
287
+ node .dw/scripts/open-design/gate-prototype.mjs \
288
+ --file "$TARGET_DIR/<OUTPUT_FILE>" \
289
+ --deep-link-id "<id-real>" \
290
+ --expected-text "<texto-do-item>" \
291
+ --out ".dw/.open-design/gate/<slug>"
292
+ ```
293
+
294
+ O gate cobre:
295
+
296
+ | Camada | Exigencia |
297
+ |---|---|
298
+ | Estrutural | `<title>`, `aberto`, `replaceState`, `role=dialog`/ARIA, skeleton/loading, empty/vazio, erro/falha. |
299
+ | Screenshot | Firefox headless em light e dark, com `colorScheme` e `data-theme`. |
300
+ | Comportamental | `?aberto=<id>` abre o item certo; Esc fecha e limpa o parametro. |
301
+ | Avaliacao | Automatico PASS + sua nota visual >=9. Se abaixo de 9, faca follow-up cirurgico. |
302
+
303
+ Depois de passar, **pare para o gate do dono**. Nao commite ate o dono aprovar.
304
+
305
+ ## Armadilhas validadas
306
+
307
+ | Armadilha | Sintoma | Contorno obrigatorio |
308
+ |---|---|---|
309
+ | Discovery da `web-prototype` | Abre com `<question-form>` e encerra o turno; headless nao gera GenUI (`od ui list` vazio). | Anexar o apendice headless respondendo plataforma e proibindo perguntas/discovery. |
310
+ | Timeout de inatividade | HTML grande fica silencioso e o run morre perto de 10min. | `OD_CHAT_RUN_INACTIVITY_TIMEOUT_MS=1800000` no daemon + instruir escrita em partes de ~150 linhas. |
311
+ | `codex_core::shell_snapshot` | Loga `ERROR` de sintaxe no stream. | Tratar como ruido se a run prossegue e o arquivo final existe. |
312
+ | Estado da GUI | Config/agente/projetos vazam do app do usuario. | `OD_DATA_DIR` isolado sempre; `--agent`, `--skill` e `--design-system` explicitos. |
313
+
314
+ ## Structured Return
315
+
316
+ **Status:** `PASS` | `FINDINGS` | `BLOCKED` | `NOT_APPLICABLE`
317
+ **Scope:** pasta alvo, briefs executados, agente usado, projectId/conversationId.
318
+ **Evidence:** logs JSONL, arquivo(s) HTML verificados, gate JSON, screenshots light/dark.
319
+ **Artifacts:** paths dos prototipos e sidecars.
320
+ **Decisions:** defaults/overrides aplicados (`agent`, `target`, plataforma, porta).
321
+ **Risks:** qualquer gate abaixo de 9, arquivo nao gerado apesar de run succeeded, dependencia ausente.
322
+ **Next Step:** gate do dono, follow-up cirurgico necessario, ou comando de resume exato.
323
+ </system_instructions>
@@ -23,20 +23,21 @@ services: []
23
23
  |--------|---------|---------------|
24
24
  | Forma | frontend / backend / fullstack | [por que essa forma — superficie unica, API para parceiros, etc.] |
25
25
  | Frontend | Next.js / Vite+React / n/a | [por que esse framework — SSR, simplicidade SPA, etc.] |
26
- | Backend | FastAPI / ASP.NET Core minimal / Axum / Fastify / n/a | [por que — expertise do time, ecosystem, alvo de latencia] |
27
- | Database | Postgres / MySQL / SQLite / MongoDB / nenhum | [por que esse DB — relacional, JSON-heavy, transacional, etc.] |
26
+ | Backend | NestJS / Fastify / FastAPI / ASP.NET Core minimal / Axum / n/a | [por que — expertise do time, ecosystem, alvo de latencia] |
27
+ | Database | Postgres / Postgres+pgvector / MySQL / SQLite / MongoDB / nenhum | [por que esse DB — relacional, vetorial, transacional, etc.] |
28
28
  | Cache | Redis / Memcached / nenhum | [por que ou por que nao] |
29
- | Fila | BullMQ / Celery / RabbitMQ / LocalStack SQS / nenhum | [por que ou nao + sync vs async workers] |
30
- | Email — dev | MailHog (default) / Mailpit / smtp4dev / nenhum | [normalmente MailHog — captura only, nunca envia real] |
29
+ | Fila | pg-boss / BullMQ / Celery / RabbitMQ / LocalStack SQS / nenhum | [por que ou nao + sync vs async workers] |
30
+ | Email — dev | Mailpit (default) / MailHog / smtp4dev / nenhum | [normalmente Mailpit — captura only, nunca envia real] |
31
31
  | Email — prod | SMTP / SendGrid / Resend / Postmark / SES / nenhum | [por que esse provider — volume, deliverability, custo] |
32
32
  | Object storage | S3 / MinIO (dev) / GCS / nenhum | [por que ou nao] |
33
33
  | Search | Meilisearch / Typesense / Elasticsearch / nenhum | [por que esse engine — features, escala, simplicidade] |
34
34
  | Observability | Sentry / OTel + Jaeger / nenhum | [por que essa abordagem — so error tracking, tracing completo, etc.] |
35
35
  | Reverse proxy | Traefik / Caddy / nenhum | [normalmente so multi-host dev ou prod] |
36
- | Auth | NextAuth / Lucia / Clerk / fastapi-users / dotnet Identity / JWT custom / nenhum | [por que — social login, B2B, etc.] |
36
+ | Auth | Auth.js / OIDC generico / Clerk / fastapi-users / dotnet Identity / JWT custom / nenhum | [por que — social login, B2B, etc.] |
37
37
  | Linter / formatter | Biome / ESLint+Prettier / Ruff+Black / dotnet format / cargo fmt+clippy | [preferencia do time] |
38
38
  | Package manager | pnpm / npm / yarn / poetry / uv / cargo / dotnet | [preferencia do time] |
39
- | Monorepo orchestrator | pnpm workspaces / npm workspaces / Turborepo / Nx / n/a | [so para fullstack — caching/build] |
39
+ | Workspace + task runner | pnpm workspaces + Turborepo / npm workspaces / Nx / n/a | [so para fullstack — caching/build] |
40
+ | Topologia de dev | apps no host / tudo no Compose | [trade-off de hot reload e paridade] |
40
41
  | CI | GitHub Actions / nenhum | [normalmente GitHub Actions; pular so para repos nao-publicos] |
41
42
 
42
43
  ## Servicos & Infra
@@ -47,7 +48,7 @@ services: []
47
48
  |---------|--------------|----|--------------------|
48
49
  | postgres | 5432 | — | POSTGRES_USER=app, POSTGRES_PASSWORD=app, POSTGRES_DB=app |
49
50
  | redis | 6379 | — | (sem auth em dev) |
50
- | mailhog | 1025 (smtp), 8025 (UI) | http://localhost:8025 | (sem auth) |
51
+ | mailpit | 1025 (smtp), 8025 (UI) | http://localhost:8025 | (sem auth) |
51
52
  | ... | ... | ... | ... |
52
53
 
53
54
  ## Diagrama da Arquitetura
@@ -59,12 +60,12 @@ services: []
59
60
  [ Browser ] -> [ Next.js (apps/web) ]
60
61
 
61
62
  # Fullstack
62
- [ Browser ] -> [ Next.js (apps/web) ] -> [ FastAPI (apps/api) ] -> [ Postgres ]
63
+ [ Browser ] -> [ Next.js (apps/web) ] -> [ NestJS (apps/api) ] -> [ Postgres ]
63
64
  |-> [ Redis ]
64
- |-> [ MailHog ]
65
+ |-> [ Mailpit ]
65
66
 
66
67
  # Com observability
67
- ... -> [ FastAPI ] -> { OTLP } -> [ Jaeger ]
68
+ ... -> [ NestJS ] -> { OTLP } -> [ Jaeger ]
68
69
  ```
69
70
 
70
71
  ## Arquivos Gerados
@@ -75,7 +76,7 @@ services: []
75
76
  {{TARGET_DIR}}/
76
77
  ├── apps/
77
78
  │ ├── web/ (criado por `pnpm create next-app`)
78
- │ └── api/ (scaffold inline — FastAPI)
79
+ │ └── api/ (criado por `pnpm dlx @nestjs/cli` ou scaffold escolhido)
79
80
  ├── packages/
80
81
  │ └── shared/ (criado pelo /dw-new-project)
81
82
  ├── docker-compose.dev.yml (composto a partir de .agents/skills/docker-compose-recipes/)
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+
6
+ function parseArgs(argv) {
7
+ const result = { screenshot: [] };
8
+ for (let index = 0; index < argv.length; index += 1) {
9
+ const token = argv[index];
10
+ if (!token.startsWith("--")) continue;
11
+ const [rawKey, inlineValue] = token.slice(2).split("=", 2);
12
+ const key = rawKey.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
13
+ if (inlineValue !== undefined) {
14
+ if (key === "screenshot") result.screenshot.push(inlineValue);
15
+ else result[key] = inlineValue;
16
+ continue;
17
+ }
18
+ const next = argv[index + 1];
19
+ if (!next || next.startsWith("--")) {
20
+ if (key === "screenshot") {
21
+ throw new Error("Missing value for --screenshot <path>.");
22
+ }
23
+ result[key] = true;
24
+ continue;
25
+ }
26
+ if (key === "screenshot") result.screenshot.push(next);
27
+ else result[key] = next;
28
+ index += 1;
29
+ }
30
+ return result;
31
+ }
32
+
33
+ async function importFirefox() {
34
+ try {
35
+ return (await import("@playwright/test")).firefox;
36
+ } catch {
37
+ try {
38
+ return (await import("playwright")).firefox;
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+ }
44
+
45
+ function slugify(value) {
46
+ return (
47
+ String(value)
48
+ .toLowerCase()
49
+ .replace(/^https?:\/\//, "")
50
+ .replace(/[^a-z0-9]+/g, "-")
51
+ .replace(/^-+|-+$/g, "")
52
+ .slice(0, 70) || "screen"
53
+ );
54
+ }
55
+
56
+ function parseViewports(value) {
57
+ return String(value || "1440x900,375x812")
58
+ .split(",")
59
+ .map((entry) => {
60
+ const match = entry.trim().match(/^(\d{2,5})x(\d{2,5})$/);
61
+ if (!match) return null;
62
+ return { width: Number(match[1]), height: Number(match[2]) };
63
+ })
64
+ .filter(Boolean);
65
+ }
66
+
67
+ function parseThemes(value) {
68
+ return String(value || "light,dark")
69
+ .split(",")
70
+ .map((entry) => entry.trim().toLowerCase())
71
+ .filter((entry) => entry === "light" || entry === "dark");
72
+ }
73
+
74
+ function inferProvidedName(source, index) {
75
+ const base = path.basename(source, path.extname(source)).toLowerCase();
76
+ const width = base.match(/(?:^|[^0-9])(\d{3,5})(?:x\d{3,5})?(?:[^0-9]|$)/)?.[1];
77
+ const theme = base.includes("dark") ? "dark" : base.includes("light") ? "light" : "";
78
+ if (width && theme) return `atual-${width}-${theme}.png`;
79
+ if (width) return `atual-${width}-${String(index + 1).padStart(2, "0")}.png`;
80
+ return `atual-ref-${String(index + 1).padStart(2, "0")}.png`;
81
+ }
82
+
83
+ async function captureFromUrl(args, outDir) {
84
+ if (!args.url) return [];
85
+ const firefox = await importFirefox();
86
+ if (!firefox) {
87
+ throw new Error("Playwright is not installed. Run: npm i -D @playwright/test && npx playwright install firefox");
88
+ }
89
+
90
+ const viewports = parseViewports(args.viewports);
91
+ if (viewports.length === 0) {
92
+ throw new Error("No valid --viewports entries. Use comma-separated WIDTHxHEIGHT values, for example 1440x900,375x812.");
93
+ }
94
+ const themes = parseThemes(args.themes);
95
+ if (themes.length === 0) {
96
+ throw new Error("No valid --themes entries. Use light,dark or one of them.");
97
+ }
98
+
99
+ const browser = await firefox.launch({ headless: true });
100
+ const shots = [];
101
+ try {
102
+ for (const viewport of viewports) {
103
+ for (const theme of themes) {
104
+ const context = await browser.newContext({
105
+ colorScheme: theme,
106
+ viewport,
107
+ });
108
+ const page = await context.newPage();
109
+ await page.goto(String(args.url), { waitUntil: "networkidle" }).catch(() => page.goto(String(args.url)));
110
+ await page.evaluate((value) => {
111
+ document.documentElement.setAttribute("data-theme", value);
112
+ }, theme);
113
+ await page.waitForTimeout(300);
114
+ const target = path.join(outDir, `atual-${viewport.width}-${theme}.png`);
115
+ await page.screenshot({ path: target, fullPage: true });
116
+ shots.push({ source: String(args.url), viewport, theme, path: target });
117
+ await context.close();
118
+ }
119
+ }
120
+ } finally {
121
+ await browser.close();
122
+ }
123
+ return shots;
124
+ }
125
+
126
+ function copyProvidedScreenshots(args, outDir) {
127
+ return args.screenshot.map((source, index) => {
128
+ const resolved = path.resolve(String(source));
129
+ if (!fs.existsSync(resolved)) {
130
+ throw new Error(`Screenshot not found: ${resolved}`);
131
+ }
132
+ const target = path.join(outDir, inferProvidedName(resolved, index));
133
+ fs.copyFileSync(resolved, target);
134
+ return { source: resolved, path: target };
135
+ });
136
+ }
137
+
138
+ async function main() {
139
+ const args = parseArgs(process.argv.slice(2));
140
+ if (!args.url && args.screenshot.length === 0) {
141
+ throw new Error("Pass --url <app-url> or one or more --screenshot <path> values.");
142
+ }
143
+
144
+ const targetDir = path.resolve(String(args.target || "open-design"));
145
+ const slug = slugify(args.slug || args.url || "screen");
146
+ const outDir = path.resolve(String(args.out || path.join(targetDir, "_refs", slug)));
147
+ fs.mkdirSync(outDir, { recursive: true });
148
+
149
+ const copied = copyProvidedScreenshots(args, outDir);
150
+ const captured = await captureFromUrl(args, outDir);
151
+ const shots = [...copied, ...captured];
152
+
153
+ process.stdout.write(
154
+ `${JSON.stringify(
155
+ {
156
+ targetDir,
157
+ refsDir: outDir,
158
+ relativeRefs: shots.map((shot) => path.relative(targetDir, shot.path).split(path.sep).join("/")),
159
+ shots,
160
+ },
161
+ null,
162
+ 2,
163
+ )}\n`,
164
+ );
165
+ }
166
+
167
+ main().catch((error) => {
168
+ process.stderr.write(`capture-current: ${error.message}\n`);
169
+ process.exit(1);
170
+ });