@brunosps00/dev-workflow 1.6.0 → 1.7.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-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/lib/constants.js +2 -0
- package/lib/install-deps.js +22 -1
- package/lib/install-open-design.js +89 -0
- package/package.json +1 -1
- package/scaffold/en/commands/dw-help.md +3 -1
- package/scaffold/en/commands/dw-open-design.md +324 -0
- package/scaffold/pt-br/commands/dw-help.md +3 -1
- package/scaffold/pt-br/commands/dw-open-design.md +323 -0
- package/scaffold/scripts/open-design/capture-current.mjs +170 -0
- package/scaffold/scripts/open-design/gate-prototype.mjs +223 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dev-workflow",
|
|
3
3
|
"description": "Standalone, à-la-carte skills from the dev-workflow pipeline: minimalism, search-first, simplification, verification, security review, and more.",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.7.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Bruno Santos"
|
|
7
7
|
},
|
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ Exportable skills (no `.dw/` pipeline required): `dw-minimalism`, `dw-search-fir
|
|
|
42
42
|
|
|
43
43
|
## Commands
|
|
44
44
|
|
|
45
|
-
dev-workflow v1.
|
|
45
|
+
dev-workflow v1.7.0 ships **38 commands** organized into four tiers. Most users only invoke Tier 1 + Tier 2.
|
|
46
46
|
|
|
47
47
|
### Tier 1 — Gateway (3)
|
|
48
48
|
|
package/lib/constants.js
CHANGED
|
@@ -22,6 +22,7 @@ const COMMANDS = {
|
|
|
22
22
|
{ name: 'dw-intel', description: 'Codebase intelligence: query mode (default) answers questions citing .dw/intel/ + .dw/rules/; --build mode (re)builds the index.' },
|
|
23
23
|
{ name: 'dw-learn', description: 'Synthesizes durable decisions, bugfixes, deviations, and git history into atomic confidence-weighted instincts you approve; can propose constitution principles. On-demand and human-in-the-loop — no always-on observer.' },
|
|
24
24
|
{ name: 'dw-new-project', description: 'Interviews you about stack and infra, then scaffolds a working monorepo with docker-compose for dev, .env, scripts, CI, and seeded rules.' },
|
|
25
|
+
{ name: 'dw-open-design', userInvoked: true, description: 'Trigger when user asks to generate, iterate, or refactor/redesign headless Open Design HTML prototypes via the od CLI. Imports a target folder with isolated OD_DATA_DIR, refines briefs before dispatch, supports --refactor screenshots via --url/--viewports or --screenshot, runs web-prototype briefs with explicit --agent codex|claude, verifies files, captures JSONL audit logs, and stops for a dual >=9 visual gate.' },
|
|
25
26
|
{ name: 'dw-opportunities', description: 'Scans the installed project for product, UX, automation, refactor, and security opportunities, then routes each idea to the right next dw-* command.' },
|
|
26
27
|
{ name: 'dw-pause', description: 'Trigger when user says "pause work", "end session", or "save where we are". Consolidates open loops, decisions, blockers, and todos into .dw/STATE.md so the next session can resume.' },
|
|
27
28
|
{ name: 'dw-plan', description: 'Trigger when user has a feature idea and needs spec + architecture + tasks. Runs PRD → TechSpec → Tasks sequentially. Stages: prd / techspec / tasks; --from techspec; --council.' },
|
|
@@ -61,6 +62,7 @@ const COMMANDS = {
|
|
|
61
62
|
{ name: 'dw-intel', description: 'Inteligencia do codebase: modo query (default) responde citando .dw/intel/ + .dw/rules/; modo --build (re)constroi indice.' },
|
|
62
63
|
{ name: 'dw-learn', description: 'Sintetiza decisoes duraveis, bugfixes, deviations e historico git em instincts atomicos ponderados por confianca que voce aprova; pode propor principios de constitution. On-demand e human-in-the-loop — sem observador sempre-ligado.' },
|
|
63
64
|
{ name: 'dw-new-project', description: 'Entrevista voce sobre stack e infra, depois faz scaffold de um monorepo com docker-compose para dev, .env, scripts, CI e rules seed.' },
|
|
65
|
+
{ name: 'dw-open-design', userInvoked: true, description: 'Trigger quando usuario pede para gerar, iterar ou refatorar/redesenhar prototipos HTML headless do Open Design via od CLI. Importa pasta alvo com OD_DATA_DIR isolado, refina briefs antes de despachar, suporta prints --refactor via --url/--viewports ou --screenshot, roda briefs web-prototype com --agent codex|claude explicito, verifica arquivos, captura logs JSONL e para para gate visual dual >=9.' },
|
|
64
66
|
{ name: 'dw-opportunities', description: 'Escaneia o projeto instalado por oportunidades de produto, UX, automacao, refactor e seguranca, e roteia cada ideia para o proximo comando dw-* correto.' },
|
|
65
67
|
{ name: 'dw-pause', description: 'Trigger quando usuario diz "pausa o trabalho", "encerra a sessao" ou "salva onde paramos". Consolida pontas soltas, decisoes, bloqueios e todos em .dw/STATE.md para a proxima sessao retomar.' },
|
|
66
68
|
{ name: 'dw-plan', description: 'Trigger quando usuario tem ideia de feature e precisa spec + arquitetura + tasks. Roda PRD → TechSpec → Tasks sequencial. Stages: prd / techspec / tasks; --from techspec; --council.' },
|
package/lib/install-deps.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { execSync } = require('child_process');
|
|
2
|
+
const openDesign = require('./install-open-design');
|
|
2
3
|
|
|
3
4
|
function run() {
|
|
4
5
|
console.log('\n dev-workflow install-deps');
|
|
@@ -84,6 +85,12 @@ function run() {
|
|
|
84
85
|
'Verify after install: docker --version && docker compose version',
|
|
85
86
|
],
|
|
86
87
|
},
|
|
88
|
+
{
|
|
89
|
+
name: 'Open Design CLI (od)',
|
|
90
|
+
check: openDesign.check,
|
|
91
|
+
install: openDesign.install,
|
|
92
|
+
instructions: openDesign.instructions(),
|
|
93
|
+
},
|
|
87
94
|
];
|
|
88
95
|
|
|
89
96
|
let installed = 0;
|
|
@@ -113,12 +120,26 @@ function run() {
|
|
|
113
120
|
|
|
114
121
|
process.stdout.write(` Installing ${dep.name}...`);
|
|
115
122
|
try {
|
|
116
|
-
|
|
123
|
+
if (typeof dep.check === 'function' && dep.check()) {
|
|
124
|
+
console.log(' \x1b[32m✓ already installed\x1b[0m');
|
|
125
|
+
installed++;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (typeof dep.install === 'function') {
|
|
129
|
+
dep.install();
|
|
130
|
+
} else {
|
|
131
|
+
execSync(dep.install, { stdio: 'pipe', timeout: 300000 });
|
|
132
|
+
}
|
|
117
133
|
console.log(' \x1b[32m✓\x1b[0m');
|
|
118
134
|
installed++;
|
|
119
135
|
} catch (err) {
|
|
120
136
|
console.log(' \x1b[31m✗\x1b[0m');
|
|
121
137
|
console.log(` Error: ${err.message.split('\n')[0]}`);
|
|
138
|
+
if (Array.isArray(dep.instructions)) {
|
|
139
|
+
for (const line of dep.instructions) {
|
|
140
|
+
console.log(` ${line}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
122
143
|
failed++;
|
|
123
144
|
}
|
|
124
145
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { execFileSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
const OPEN_DESIGN_REPO = 'https://github.com/nexu-io/open-design';
|
|
7
|
+
const OPEN_DESIGN_REF = '1cb7eae';
|
|
8
|
+
const OPEN_DESIGN_VENDOR_DIR = path.join(os.homedir(), '.dw', 'vendor', 'open-design');
|
|
9
|
+
|
|
10
|
+
function odBin(dir = OPEN_DESIGN_VENDOR_DIR) {
|
|
11
|
+
return path.join(dir, 'apps', 'daemon', 'bin', 'od.mjs');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function check() {
|
|
15
|
+
return fs.existsSync(odBin());
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function install() {
|
|
19
|
+
checkPrerequisites();
|
|
20
|
+
|
|
21
|
+
fs.mkdirSync(path.dirname(OPEN_DESIGN_VENDOR_DIR), { recursive: true });
|
|
22
|
+
if (!fs.existsSync(path.join(OPEN_DESIGN_VENDOR_DIR, '.git'))) {
|
|
23
|
+
if (fs.existsSync(OPEN_DESIGN_VENDOR_DIR)) {
|
|
24
|
+
throw new Error(`${OPEN_DESIGN_VENDOR_DIR} exists but is not a git checkout; move it aside and retry.`);
|
|
25
|
+
}
|
|
26
|
+
execFileSync('git', ['clone', OPEN_DESIGN_REPO, OPEN_DESIGN_VENDOR_DIR], { stdio: 'pipe', timeout: 600000 });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
execFileSync('git', ['fetch', '--tags', '--force', 'origin'], { cwd: OPEN_DESIGN_VENDOR_DIR, stdio: 'pipe', timeout: 600000 });
|
|
30
|
+
execFileSync('git', ['checkout', OPEN_DESIGN_REF], { cwd: OPEN_DESIGN_VENDOR_DIR, stdio: 'pipe', timeout: 120000 });
|
|
31
|
+
execFileSync('pnpm', ['install', '--frozen-lockfile'], { cwd: OPEN_DESIGN_VENDOR_DIR, stdio: 'pipe', timeout: 1200000 });
|
|
32
|
+
|
|
33
|
+
if (!check()) {
|
|
34
|
+
throw new Error(`Open Design install finished but ${odBin()} was not found.`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function instructions() {
|
|
39
|
+
return [
|
|
40
|
+
'Open Design CLI is provisioned by dev-workflow into a managed checkout:',
|
|
41
|
+
` ${OPEN_DESIGN_VENDOR_DIR}`,
|
|
42
|
+
` repo: ${OPEN_DESIGN_REPO}`,
|
|
43
|
+
` pinned ref: ${OPEN_DESIGN_REF}`,
|
|
44
|
+
'',
|
|
45
|
+
'Prerequisites:',
|
|
46
|
+
' Node.js ~24: install with fnm/nvm/asdf, then verify: node -v',
|
|
47
|
+
' pnpm >=10.33: corepack enable && corepack prepare pnpm@10.33.2 --activate',
|
|
48
|
+
' git: required to clone the upstream monorepo',
|
|
49
|
+
'',
|
|
50
|
+
'Then run: dev-workflow install-deps',
|
|
51
|
+
'To use your own checkout instead of the managed one, set OD_CLI_DIR=/path/to/open-design.',
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function checkPrerequisites() {
|
|
56
|
+
const nodeVersion = process.versions.node;
|
|
57
|
+
const nodeMajor = Number(nodeVersion.split('.')[0]);
|
|
58
|
+
if (nodeMajor !== 24) {
|
|
59
|
+
throw new Error(`Open Design requires Node.js ~24; detected ${nodeVersion}.`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const pnpmRaw = execFileSync('pnpm', ['-v'], { stdio: 'pipe', timeout: 10000 }).toString().trim();
|
|
63
|
+
if (!isAtLeast(pnpmRaw, '10.33.0')) {
|
|
64
|
+
throw new Error(`Open Design requires pnpm >=10.33; detected ${pnpmRaw}.`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
execFileSync('git', ['--version'], { stdio: 'pipe', timeout: 10000 });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isAtLeast(actual, minimum) {
|
|
71
|
+
const a = actual.split('.').map((part) => Number(part));
|
|
72
|
+
const b = minimum.split('.').map((part) => Number(part));
|
|
73
|
+
for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
|
|
74
|
+
const av = a[index] || 0;
|
|
75
|
+
const bv = b[index] || 0;
|
|
76
|
+
if (av > bv) return true;
|
|
77
|
+
if (av < bv) return false;
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = {
|
|
83
|
+
OPEN_DESIGN_REF,
|
|
84
|
+
OPEN_DESIGN_VENDOR_DIR,
|
|
85
|
+
check,
|
|
86
|
+
install,
|
|
87
|
+
instructions,
|
|
88
|
+
odBin,
|
|
89
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brunosps00/dev-workflow",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "AI-driven development workflow commands for any project. Scaffolds a complete PRD-to-PR pipeline with multi-platform AI assistant support.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"dev-workflow": "./bin/dev-workflow.js"
|
|
@@ -38,12 +38,13 @@ Use `/dw-autopilot "wish"` as the gateway for most feature work. The granular co
|
|
|
38
38
|
| `/dw-commit` | Atomic Conventional Commits for pending work. |
|
|
39
39
|
| `/dw-generate-pr [target]` | Push branch, draft PR body, open browser. |
|
|
40
40
|
|
|
41
|
-
## Tier 3 — Specialty (
|
|
41
|
+
## Tier 3 — Specialty (12)
|
|
42
42
|
|
|
43
43
|
| Command | What |
|
|
44
44
|
|---------|------|
|
|
45
45
|
| `/dw-analyze-project` | Scan the repo, write `.dw/rules/`, offer `.dw/constitution.md`, generate `.dw/rules/concerns.md`, and synthesize frontend `DESIGN.md` when tokens exist. |
|
|
46
46
|
| `/dw-redesign-ui "target"` | Audit, propose 2-3 design directions, ship. Enforces UI grounding + WCAG; can run the deterministic impeccable slop detector. |
|
|
47
|
+
| `/dw-open-design --brief <file>` | Generate, iterate, or `--refactor` headless Open Design HTML prototypes via `od`; refines briefs first, can capture current screenshots with `--url --viewports` or `--screenshot`, then verifies file + visual gate. |
|
|
47
48
|
| `/dw-refactor "target"` | Audit code-health and tech debt with Fowler smells, deep-modules analysis, and behavior-preserving test gates. |
|
|
48
49
|
| `/dw-functional-doc` | Map screens + flows into a functional doc validated with Playwright; uses WSL-resilient browser resolution. |
|
|
49
50
|
| `/dw-context-budget` | Audit context overhead from commands, skills, agents, instructions, and MCPs. |
|
|
@@ -112,6 +113,7 @@ Match the keyword and suggest:
|
|
|
112
113
|
| `research`, `compare`, `state of the art` | `/dw-brainstorm --research` |
|
|
113
114
|
| `refactor`, `smell`, `code health`, `tech debt` | `/dw-refactor` |
|
|
114
115
|
| `ui`, `design`, `redesign` | `/dw-redesign-ui` |
|
|
116
|
+
| `open design`, `prototype`, `prototipo`, `od cli`, `html prototype` | `/dw-open-design --brief <file>`, `/dw-open-design --brief-glob "<glob>" --agent claude`, or `/dw-open-design --refactor --url <url> --viewports "1440x900,375x812"` |
|
|
115
117
|
| `intel`, `where is`, `what uses` | `/dw-intel` (or `--build` to (re)create the index) |
|
|
116
118
|
| `context`, `tokens`, `slow agent` | `/dw-context-budget` |
|
|
117
119
|
| `harness`, `install`, `wrappers`, `agents` | `/dw-harness-audit` |
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
<system_instructions>
|
|
2
|
+
You are the **headless Open Design (`od`) runner** for generating and iterating HTML prototypes in a project
|
|
3
|
+
folder, without Electron and without depending on the user's GUI state. Start the `nexu-io/open-design` daemon in
|
|
4
|
+
`--headless`, import the target folder once, run single briefs or serial batches, verify the output file, run the
|
|
5
|
+
Playwright/Firefox visual gate, and **STOP for the owner's gate** before any commit.
|
|
6
|
+
|
|
7
|
+
<critical>NEVER use the user's GUI state. Always run with an isolated per-project `OD_DATA_DIR`, for example `.dw/.open-design/data/`.</critical>
|
|
8
|
+
<critical>NEVER commit a rejected prototype. The gate is dual >=9: automated gate + your own visual score >=9, then owner gate.</critical>
|
|
9
|
+
<critical>The agent (`codex` or `claude`) must always be explicit in `od run start --agent <agent>`. If the user does not pass one, resolve from project config/state; fallback `codex`.</critical>
|
|
10
|
+
<critical>NEVER dispatch the user's raw request to `od`. This command's value is refining the prompt first: ground it in real code, structure the brief, qualify states/themes/a11y/deep-link, persist the reviewable brief, and only then append the headless appendix.</critical>
|
|
11
|
+
|
|
12
|
+
## Inputs
|
|
13
|
+
|
|
14
|
+
| Flag/argument | Default | Description |
|
|
15
|
+
|---|---:|---|
|
|
16
|
+
| `--target <dir>` | `open-design/` | Folder imported as the external project and where HTML files are written. |
|
|
17
|
+
| `--brief <file>` | none | One markdown brief for a single run. |
|
|
18
|
+
| `--brief-glob <glob>` | none | Serial batch. Expand to a stable list and run one prompt at a time. |
|
|
19
|
+
| `--output <file>` | required in the brief/appendix | Expected HTML file; verify it on disk after the run. |
|
|
20
|
+
| `--agent <codex|claude>` | state/config -> `codex` | Agent forwarded to `od run start --agent`. |
|
|
21
|
+
| `--platform <text>` | `Responsive web, desktop-first and mobile-safe` | Pre-filled discovery answer for the `web-prototype` skill. |
|
|
22
|
+
| `--deep-link-id <id>` | none | Value used by the behavioral gate for `?aberto=<id>` + Esc. |
|
|
23
|
+
| `--refactor` | false | Existing-screen redesign/refactor mode; captures current screenshots and grounds the brief in visible state + real code. |
|
|
24
|
+
| `--url <url>` | none | Running app URL for current screenshot capture via Playwright Firefox. |
|
|
25
|
+
| `--viewports <list>` | `1440x900,375x812` | Comma-separated viewports for `--refactor --url`; expand as the pain requires (for example `1920x1080` for a wide table). |
|
|
26
|
+
| `--screenshot <path>` | repeatable | Already-captured current screenshot when there is no `--url`; copy it into `_refs/<slug>/`. |
|
|
27
|
+
|
|
28
|
+
## 0. Refine The Brief
|
|
29
|
+
|
|
30
|
+
Before any daemon pre-flight or `od` call, turn the user's intent into a qualified brief. Do not send the user's raw
|
|
31
|
+
sentence to the runner.
|
|
32
|
+
|
|
33
|
+
Required contract:
|
|
34
|
+
|
|
35
|
+
| Step | Requirement |
|
|
36
|
+
|---|---|
|
|
37
|
+
| Ground in reality | If the screen exists, read its real code: columns, enums, actions, states, permissions, contracts, and domain. Also read the project's design system, sibling prototypes, and design guides. Inventing a field, status, or action is forbidden. |
|
|
38
|
+
| Structure | Rewrite the request into the standard skeleton below, filling real paths and the exact output file. |
|
|
39
|
+
| Qualify | Cover loading/skeleton, empty, error/failure, light + dark, a11y, local design-system limits (for example <=6 cards/filters), responsiveness, and deep-link. |
|
|
40
|
+
| Persist | Save the refined brief inside the project, for example `<target>/PROMPT-<slug>.md`. It is a reviewable owner deliverable, not a disposable scratch file. |
|
|
41
|
+
| Review if already ready | If the user already provides a qualified brief, do a quick checklist review, patch only gaps, and persist the final version. |
|
|
42
|
+
|
|
43
|
+
Standard refined brief skeleton:
|
|
44
|
+
|
|
45
|
+
```markdown
|
|
46
|
+
# <screen/prototype title>
|
|
47
|
+
|
|
48
|
+
Paste this brief into Open Design. Exact output file: `<OUTPUT_FILE>`.
|
|
49
|
+
|
|
50
|
+
## 1. Why
|
|
51
|
+
<real user/product pain; observable problem this prototype must solve>
|
|
52
|
+
|
|
53
|
+
## 2. Interaction Decision
|
|
54
|
+
<navigation, opening, selection, filters, sorting, editing, and confirmation pattern>
|
|
55
|
+
|
|
56
|
+
## 3. Layout/List
|
|
57
|
+
Table/list based only on REAL columns:
|
|
58
|
+
|
|
59
|
+
| Real column | Source/contract | Presentation | State/limit |
|
|
60
|
+
|---|---|---|---|
|
|
61
|
+
| <name> | <file/API/schema> | <text, badge, action> | <truncate, empty, error> |
|
|
62
|
+
|
|
63
|
+
## 4. Detail/Actions
|
|
64
|
+
<drawer/modal/detail page; contextual actions allowed by real state/permission>
|
|
65
|
+
|
|
66
|
+
## 5. Domain Fidelity
|
|
67
|
+
So the mock does not lie: use real enums/statuses, realistic sample data in the project's language, and include rare/broken states that exist in the domain.
|
|
68
|
+
|
|
69
|
+
## 6. Visual Direction
|
|
70
|
+
<tokens, components, density, inherited references, sibling prototypes, and design-system limits>
|
|
71
|
+
|
|
72
|
+
## Deliver
|
|
73
|
+
- `<OUTPUT_FILE>`
|
|
74
|
+
- `<OUTPUT_FILE>.artifact.json` when there is a sidecar
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Only after persisting this file should you append the headless appendix under `.dw/.open-design/runs/` and dispatch
|
|
78
|
+
the run.
|
|
79
|
+
|
|
80
|
+
### 0R. Refactor/Redesign Mode
|
|
81
|
+
|
|
82
|
+
When `--refactor` is present, phase 0 remains mandatory. Screenshots complement grounding in code; they do not
|
|
83
|
+
replace reading contracts, enums, permissions, states, the design system, and sibling prototypes.
|
|
84
|
+
|
|
85
|
+
Capture or copy current screenshots into the imported folder, under `_refs/<slug>/`, before persisting the brief:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
node .dw/scripts/open-design/capture-current.mjs \
|
|
89
|
+
--target "$TARGET_DIR" \
|
|
90
|
+
--slug "<slug>" \
|
|
91
|
+
--url "<url>" \
|
|
92
|
+
--viewports "1440x900,375x812"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Without a running app URL, accept existing screenshots by repeating `--screenshot`:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
node .dw/scripts/open-design/capture-current.mjs \
|
|
99
|
+
--target "$TARGET_DIR" \
|
|
100
|
+
--slug "<slug>" \
|
|
101
|
+
--screenshot "./before-1440-light.png" \
|
|
102
|
+
--screenshot "./before-375-dark.png"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Capture rules:
|
|
106
|
+
|
|
107
|
+
| Case | Requirement |
|
|
108
|
+
|---|---|
|
|
109
|
+
| Viewports | Sensible default: desktop `1440x900` + mobile `375x812`. The agent chooses/expands as the pain requires: mobile bug includes `375x812`; wide table includes `1920x1080`; tablet/console includes `768x1024`. |
|
|
110
|
+
| Themes | Capture light and dark when applicable. The helper uses `light,dark` by default and writes names like `_refs/<slug>/atual-1440-light.png` and `_refs/<slug>/atual-375-dark.png`. |
|
|
111
|
+
| Robust path | The `od` agent reads images from the project filesystem. Reference relative paths inside the target; do not depend on an image flag in `run start`. |
|
|
112
|
+
| Hygiene | `_refs/` and reference material must not become prototypes. If the target project tracks final prototypes, document cleanup or add `<target>/_refs/` to `.gitignore` according to local convention. |
|
|
113
|
+
|
|
114
|
+
The refined refactor brief must add this block:
|
|
115
|
+
|
|
116
|
+
```markdown
|
|
117
|
+
## Current Screen References
|
|
118
|
+
Open and ANALYZE the screenshots before writing:
|
|
119
|
+
- `_refs/<slug>/atual-1440-light.png`
|
|
120
|
+
- `_refs/<slug>/atual-375-dark.png`
|
|
121
|
+
|
|
122
|
+
## What Is Wrong Today
|
|
123
|
+
<pain pointing to visible elements in the screenshots: density, hierarchy, table, filters, states, contrast, mobile, etc.>
|
|
124
|
+
|
|
125
|
+
## The Change Idea
|
|
126
|
+
<what changes and why; interaction/visual decision proposed by the user or by the agent after reading code and screenshots>
|
|
127
|
+
|
|
128
|
+
## Preserve
|
|
129
|
+
<flows, fields, permissions, states, copy, affordances, and integrations that must not change>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Pre-flight
|
|
133
|
+
|
|
134
|
+
| Check | Action |
|
|
135
|
+
|---|---|
|
|
136
|
+
| Resolve `od` | `OD_CLI_DIR` wins. Otherwise use `~/.dw/vendor/open-design`. If `apps/daemon/bin/od.mjs` is missing, stop and tell the user to run `dev-workflow install-deps`. |
|
|
137
|
+
| Prerequisites | The Open Design checkout requires Node `~24` and pnpm `>=10.33`. If missing, stop with concrete instructions. |
|
|
138
|
+
| Isolated state | Create `.dw/.open-design/`, `.dw/.open-design/data/`, `.dw/.open-design/runs/`, and `.dw/.open-design/state.json`. |
|
|
139
|
+
| Port | Pick a free port on `127.0.0.1`; persist `daemonUrl` and `port` in state. |
|
|
140
|
+
| Live daemon | If `od daemon status --daemon-url <url> --json` returns `ok: true`, reuse it. Otherwise start a new daemon. |
|
|
141
|
+
|
|
142
|
+
Daemon command shape:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
OD_DATA_DIR="$PWD/.dw/.open-design/data" \
|
|
146
|
+
OD_CHAT_RUN_INACTIVITY_TIMEOUT_MS=1800000 \
|
|
147
|
+
node "$OD_DIR/apps/daemon/bin/od.mjs" daemon start \
|
|
148
|
+
--headless \
|
|
149
|
+
--host 127.0.0.1 \
|
|
150
|
+
--port "$PORT"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Known noise: `codex_core::shell_snapshot` may log a syntax `ERROR`. Treat it as benign when the run continues and
|
|
154
|
+
the target file is created.
|
|
155
|
+
|
|
156
|
+
## Idempotent Import
|
|
157
|
+
|
|
158
|
+
Use `project import-folder` on the resolved absolute target folder. Default: `<repo>/open-design/`.
|
|
159
|
+
|
|
160
|
+
Persist in `.dw/.open-design/state.json`:
|
|
161
|
+
|
|
162
|
+
```json
|
|
163
|
+
{
|
|
164
|
+
"schema_version": "1.0",
|
|
165
|
+
"targetDir": "/abs/repo/open-design",
|
|
166
|
+
"projectId": "<project.id>",
|
|
167
|
+
"conversationId": "<conversationId>",
|
|
168
|
+
"daemonUrl": "http://127.0.0.1:<port>",
|
|
169
|
+
"port": 17556,
|
|
170
|
+
"agent": "codex"
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
If `projectId` exists in state and `od project info <id> --daemon-url <url> --json` works for the same `targetDir`,
|
|
175
|
+
reuse it. Otherwise, reimport:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
node "$OD_DIR/apps/daemon/bin/od.mjs" project import-folder "$TARGET_DIR" \
|
|
179
|
+
--daemon-url "$OD_URL" \
|
|
180
|
+
--name "$(basename "$TARGET_DIR")" \
|
|
181
|
+
--skill web-prototype \
|
|
182
|
+
--design-system default \
|
|
183
|
+
--json
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Capture `project.id` and `conversationId`; do not depend on global app state.
|
|
187
|
+
|
|
188
|
+
## Required Headless Appendix
|
|
189
|
+
|
|
190
|
+
Append this block to every brief in a temporary file under `.dw/.open-design/runs/`.
|
|
191
|
+
Fill `<PLATFORM>`, `<OUTPUT_FILE>`, and sibling references when applicable.
|
|
192
|
+
|
|
193
|
+
```markdown
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Headless appendix for Open Design
|
|
197
|
+
|
|
198
|
+
Target platform: <PLATFORM>.
|
|
199
|
+
|
|
200
|
+
Do not run discovery and do not ask questions. Do not emit `<question-form>`. Follow this brief with the answers
|
|
201
|
+
above and deliver file-first.
|
|
202
|
+
|
|
203
|
+
Required output file: `<OUTPUT_FILE>`.
|
|
204
|
+
|
|
205
|
+
File-first contract:
|
|
206
|
+
- Write the HTML directly to the project filesystem, exactly at `<OUTPUT_FILE>`.
|
|
207
|
+
- Do not alter existing files, except when this prompt is explicitly a surgical iteration on a target file.
|
|
208
|
+
- If you need to create a sidecar, keep `<OUTPUT_FILE>.artifact.json` coherent.
|
|
209
|
+
- Do not answer only with a textual `<artifact>` block; run `succeeded` without the file is a failure.
|
|
210
|
+
|
|
211
|
+
To avoid headless timeout:
|
|
212
|
+
- Write large files in parts, with edits of about 150 lines per call.
|
|
213
|
+
- Keep visible progress until the HTML is complete.
|
|
214
|
+
|
|
215
|
+
Consistency context:
|
|
216
|
+
- Read sibling prototypes in `prototipos/` or the folder named by the brief before defining visual patterns.
|
|
217
|
+
- Preserve the local visual system when a nearby reference exists.
|
|
218
|
+
|
|
219
|
+
Required deep link:
|
|
220
|
+
- Implement `?aberto=<id>` to open the correct item's drawer/modal.
|
|
221
|
+
- On Esc or close action, clear the parameter with `history.replaceState`.
|
|
222
|
+
- The drawer/modal must use `role="dialog"` and appropriate ARIA attributes.
|
|
223
|
+
- Include skeleton/loading, empty, and error states.
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Single Run
|
|
227
|
+
|
|
228
|
+
Always pass `--agent` explicitly:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
node "$OD_DIR/apps/daemon/bin/od.mjs" run start \
|
|
232
|
+
--daemon-url "$OD_URL" \
|
|
233
|
+
--project "$PROJECT_ID" \
|
|
234
|
+
--conversation "$CONVERSATION_ID" \
|
|
235
|
+
--agent "$AGENT" \
|
|
236
|
+
--skill web-prototype \
|
|
237
|
+
--design-system default \
|
|
238
|
+
--prompt-file "$PROMPT_WITH_APPENDIX" \
|
|
239
|
+
--follow \
|
|
240
|
+
--json | tee ".dw/.open-design/runs/<slug>.jsonl"
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
After the run, check the file. **Do not accept only `succeeded`:**
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
test -s "$TARGET_DIR/<OUTPUT_FILE>"
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Record in JSONL or an audit sidecar: brief, prompt with appendix, agent, daemonUrl, projectId, conversationId,
|
|
250
|
+
expected output, final status, and file verification result.
|
|
251
|
+
|
|
252
|
+
## Batch
|
|
253
|
+
|
|
254
|
+
For `--brief-glob`, expand the briefs, sort by name, and run serially. Stop at the first case where:
|
|
255
|
+
|
|
256
|
+
| Condition | Result |
|
|
257
|
+
|---|---|
|
|
258
|
+
| run fails | `BLOCKED` with JSONL log |
|
|
259
|
+
| run says success but file is missing or empty | `FINDINGS` and surgical follow-up or `BLOCKED` |
|
|
260
|
+
| visual gate <9 | `FINDINGS`; iterate before continuing |
|
|
261
|
+
|
|
262
|
+
Do not run prompts in parallel in the same `od` project; preserve auditability and context.
|
|
263
|
+
|
|
264
|
+
## Surgical Iteration
|
|
265
|
+
|
|
266
|
+
Use a follow-up run on the same `projectId`/`conversationId`, with a short prompt and explicit target file:
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
node "$OD_DIR/apps/daemon/bin/od.mjs" run start \
|
|
270
|
+
--daemon-url "$OD_URL" \
|
|
271
|
+
--project "$PROJECT_ID" \
|
|
272
|
+
--conversation "$CONVERSATION_ID" \
|
|
273
|
+
--agent "$AGENT" \
|
|
274
|
+
--skill web-prototype \
|
|
275
|
+
--design-system default \
|
|
276
|
+
--prompt-file ".dw/.open-design/runs/<slug>.followup.md" \
|
|
277
|
+
--follow \
|
|
278
|
+
--json | tee -a ".dw/.open-design/runs/<slug>.jsonl"
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
The follow-up must say: single file, exact change, do not touch sibling prototypes, preserve deep-link and states.
|
|
282
|
+
|
|
283
|
+
## Required Visual Gate
|
|
284
|
+
|
|
285
|
+
Run the automated gate:
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
node .dw/scripts/open-design/gate-prototype.mjs \
|
|
289
|
+
--file "$TARGET_DIR/<OUTPUT_FILE>" \
|
|
290
|
+
--deep-link-id "<real-id>" \
|
|
291
|
+
--expected-text "<item-text>" \
|
|
292
|
+
--out ".dw/.open-design/gate/<slug>"
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
The gate covers:
|
|
296
|
+
|
|
297
|
+
| Layer | Requirement |
|
|
298
|
+
|---|---|
|
|
299
|
+
| Structural | `<title>`, `aberto`, `replaceState`, `role=dialog`/ARIA, skeleton/loading, empty, error. |
|
|
300
|
+
| Screenshot | Headless Firefox in light and dark, with `colorScheme` and `data-theme`. |
|
|
301
|
+
| Behavioral | `?aberto=<id>` opens the correct item; Esc closes and clears the parameter. |
|
|
302
|
+
| Evaluation | Automated PASS + your visual score >=9. If below 9, run a surgical follow-up. |
|
|
303
|
+
|
|
304
|
+
After it passes, **stop for the owner's gate**. Do not commit until the owner approves.
|
|
305
|
+
|
|
306
|
+
## Validated Pitfalls
|
|
307
|
+
|
|
308
|
+
| Pitfall | Symptom | Required workaround |
|
|
309
|
+
|---|---|---|
|
|
310
|
+
| `web-prototype` discovery | Opens with `<question-form>` and ends the turn; headless does not create GenUI (`od ui list` empty). | Append the headless appendix answering platform and forbidding questions/discovery. |
|
|
311
|
+
| Inactivity timeout | Large HTML goes silent and the run dies near 10 minutes. | `OD_CHAT_RUN_INACTIVITY_TIMEOUT_MS=1800000` on the daemon + instruct ~150-line chunked writes. |
|
|
312
|
+
| `codex_core::shell_snapshot` | Logs a syntax `ERROR` in the stream. | Treat as noise if the run continues and the final file exists. |
|
|
313
|
+
| GUI state | App config/agent/projects leak from the user's GUI. | Isolated `OD_DATA_DIR` always; explicit `--agent`, `--skill`, and `--design-system`. |
|
|
314
|
+
|
|
315
|
+
## Structured Return
|
|
316
|
+
|
|
317
|
+
**Status:** `PASS` | `FINDINGS` | `BLOCKED` | `NOT_APPLICABLE`
|
|
318
|
+
**Scope:** target folder, briefs run, agent used, projectId/conversationId.
|
|
319
|
+
**Evidence:** JSONL logs, verified HTML file(s), gate JSON, light/dark screenshots.
|
|
320
|
+
**Artifacts:** prototype and sidecar paths.
|
|
321
|
+
**Decisions:** defaults/overrides applied (`agent`, `target`, platform, port).
|
|
322
|
+
**Risks:** any gate below 9, missing file despite run succeeded, missing dependency.
|
|
323
|
+
**Next Step:** owner gate, required surgical follow-up, or exact resume command.
|
|
324
|
+
</system_instructions>
|
|
@@ -36,12 +36,13 @@ Use `/dw-autopilot "desejo"` como gateway pra maior parte do trabalho. Comandos
|
|
|
36
36
|
| `/dw-commit` | Commits atômicos Conventional pra trabalho pendente. |
|
|
37
37
|
| `/dw-generate-pr [target]` | Push branch, draft do PR body, abre browser. |
|
|
38
38
|
|
|
39
|
-
## Tier 3 — Especialidade (
|
|
39
|
+
## Tier 3 — Especialidade (12)
|
|
40
40
|
|
|
41
41
|
| Comando | O que |
|
|
42
42
|
|---------|-------|
|
|
43
43
|
| `/dw-analyze-project` | Scan do repo, escreve `.dw/rules/`, oferece `.dw/constitution.md`, gera `.dw/rules/concerns.md` e sintetiza `DESIGN.md` em frontend quando há tokens. |
|
|
44
44
|
| `/dw-redesign-ui "target"` | Audit, propõe 2-3 direções, entrega. Enforça UI grounding + WCAG; pode rodar o detector determinístico do impeccable. |
|
|
45
|
+
| `/dw-open-design --brief <file>` | Gera, itera ou faz `--refactor` de prototipos HTML headless via Open Design `od`; refina briefs primeiro, pode capturar prints atuais com `--url --viewports` ou `--screenshot`, depois verifica arquivo + gate visual. |
|
|
45
46
|
| `/dw-refactor "target"` | Audita code health e tech debt com smells Fowler, deep-modules e gates de teste preservando comportamento. |
|
|
46
47
|
| `/dw-functional-doc` | Mapeia screens + flows em doc funcional validado com Playwright; usa resolução de browser resiliente no WSL. |
|
|
47
48
|
| `/dw-context-budget` | Audita contexto de commands, skills, agentes, instrucoes e MCPs. |
|
|
@@ -108,6 +109,7 @@ Auto-invocados por comandos primários mas disponíveis standalone.
|
|
|
108
109
|
| `research`, `compara`, `estado da arte` | `/dw-brainstorm --research` |
|
|
109
110
|
| `refactor`, `smell`, `code health`, `tech debt` | `/dw-refactor` |
|
|
110
111
|
| `ui`, `design`, `redesign` | `/dw-redesign-ui` |
|
|
112
|
+
| `open design`, `prototype`, `prototipo`, `od cli`, `html prototype` | `/dw-open-design --brief <file>`, `/dw-open-design --brief-glob "<glob>" --agent claude` ou `/dw-open-design --refactor --url <url> --viewports "1440x900,375x812"` |
|
|
111
113
|
| `intel`, `onde está`, `o que usa` | `/dw-intel` (ou `--build`) |
|
|
112
114
|
| `contexto`, `tokens`, `agente lento` | `/dw-context-budget` |
|
|
113
115
|
| `harness`, `install`, `wrappers`, `agentes` | `/dw-harness-audit` |
|
|
@@ -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>
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
|
|
7
|
+
function parseArgs(argv) {
|
|
8
|
+
const result = {};
|
|
9
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
10
|
+
const token = argv[index];
|
|
11
|
+
if (!token.startsWith("--")) continue;
|
|
12
|
+
const [rawKey, inlineValue] = token.slice(2).split("=", 2);
|
|
13
|
+
const key = rawKey.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
14
|
+
if (inlineValue !== undefined) {
|
|
15
|
+
result[key] = inlineValue;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const next = argv[index + 1];
|
|
19
|
+
if (!next || next.startsWith("--")) {
|
|
20
|
+
result[key] = true;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
result[key] = next;
|
|
24
|
+
index += 1;
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function importFirefox() {
|
|
30
|
+
try {
|
|
31
|
+
return (await import("@playwright/test")).firefox;
|
|
32
|
+
} catch {
|
|
33
|
+
try {
|
|
34
|
+
return (await import("playwright")).firefox;
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function slugify(value) {
|
|
42
|
+
return (
|
|
43
|
+
String(value)
|
|
44
|
+
.toLowerCase()
|
|
45
|
+
.replace(/\.html?$/, "")
|
|
46
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
47
|
+
.replace(/^-+|-+$/g, "")
|
|
48
|
+
.slice(0, 70) || "prototype"
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function addResult(results, name, pass, details = "") {
|
|
53
|
+
results.push({ name, pass: Boolean(pass), details });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function structuralChecks(page, html, results) {
|
|
57
|
+
const data = await page.evaluate(() => {
|
|
58
|
+
const visibleText = document.body ? document.body.innerText : "";
|
|
59
|
+
const hasDialogRole = Boolean(document.querySelector('[role="dialog"], [aria-modal="true"], [aria-labelledby]'));
|
|
60
|
+
const title = document.title.trim();
|
|
61
|
+
return { visibleText, hasDialogRole, title };
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
addResult(results, "title", data.title.length > 0, data.title || "missing document title");
|
|
65
|
+
addResult(results, "aberto-param", /\baberto\b/.test(html), "source contains aberto");
|
|
66
|
+
addResult(results, "replace-state", /replaceState/.test(html), "source contains replaceState");
|
|
67
|
+
addResult(results, "dialog-aria", data.hasDialogRole, "role=dialog, aria-modal, or aria-labelledby present");
|
|
68
|
+
addResult(results, "skeleton-state", /skeleton|carregando|loading/i.test(html + "\n" + data.visibleText));
|
|
69
|
+
addResult(results, "empty-state", /empty|vazio|sem\s+(resultado|dados|itens|registros)/i.test(html + "\n" + data.visibleText));
|
|
70
|
+
addResult(results, "error-state", /error|erro|falha|failed|retry|tentar novamente/i.test(html + "\n" + data.visibleText));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function visibleDialog(page, selector) {
|
|
74
|
+
const selected = selector ? page.locator(selector).first() : page.locator('[role="dialog"], [aria-modal="true"]').first();
|
|
75
|
+
try {
|
|
76
|
+
return (await selected.count()) > 0 && (await selected.isVisible());
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function clickFirstOpenable(page, rowSelector) {
|
|
83
|
+
const selector =
|
|
84
|
+
rowSelector ||
|
|
85
|
+
'[data-open], [data-id], tbody tr, [role="row"], [role="button"], article, li button, li, button';
|
|
86
|
+
const candidates = page.locator(selector);
|
|
87
|
+
const count = Math.min(await candidates.count(), 20);
|
|
88
|
+
for (let index = 0; index < count; index += 1) {
|
|
89
|
+
const candidate = candidates.nth(index);
|
|
90
|
+
try {
|
|
91
|
+
if (!(await candidate.isVisible())) continue;
|
|
92
|
+
await candidate.click({ timeout: 1500 });
|
|
93
|
+
return true;
|
|
94
|
+
} catch {
|
|
95
|
+
// Try the next plausible opener.
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function behavioralChecks(page, baseUrl, args, results) {
|
|
102
|
+
const dialogSelector = args.dialogSelector ? String(args.dialogSelector) : "";
|
|
103
|
+
if (!(await visibleDialog(page, dialogSelector))) {
|
|
104
|
+
const clicked = await clickFirstOpenable(page, args.rowSelector ? String(args.rowSelector) : "");
|
|
105
|
+
await page.waitForTimeout(300);
|
|
106
|
+
addResult(results, "open-drawer-click", clicked && (await visibleDialog(page, dialogSelector)), "first row/openable opens dialog or drawer");
|
|
107
|
+
} else {
|
|
108
|
+
addResult(results, "open-drawer-click", true, "dialog already visible");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const deepLinkId = args.deepLinkId ? String(args.deepLinkId) : "";
|
|
112
|
+
if (!deepLinkId) {
|
|
113
|
+
addResult(results, "deep-link-opens", false, "missing --deep-link-id");
|
|
114
|
+
addResult(results, "esc-clears-param", false, "missing --deep-link-id");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const url = new URL(baseUrl);
|
|
119
|
+
url.searchParams.set("aberto", deepLinkId);
|
|
120
|
+
await page.goto(url.toString(), { waitUntil: "load" });
|
|
121
|
+
await page.waitForTimeout(500);
|
|
122
|
+
|
|
123
|
+
const expectedText = String(args.expectedText || deepLinkId);
|
|
124
|
+
const deepLinkData = await page.evaluate((text) => {
|
|
125
|
+
const bodyText = document.body ? document.body.innerText : "";
|
|
126
|
+
const hasDialog = Boolean(document.querySelector('[role="dialog"], [aria-modal="true"]'));
|
|
127
|
+
return {
|
|
128
|
+
hasDialog,
|
|
129
|
+
hasText: bodyText.toLowerCase().includes(text.toLowerCase()),
|
|
130
|
+
search: window.location.search,
|
|
131
|
+
};
|
|
132
|
+
}, expectedText);
|
|
133
|
+
addResult(
|
|
134
|
+
results,
|
|
135
|
+
"deep-link-opens",
|
|
136
|
+
deepLinkData.search.includes(`aberto=${encodeURIComponent(deepLinkId)}`) && deepLinkData.hasDialog && deepLinkData.hasText,
|
|
137
|
+
`expected visible text: ${expectedText}`,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
await page.keyboard.press("Escape");
|
|
141
|
+
await page.waitForTimeout(500);
|
|
142
|
+
const searchAfterEsc = await page.evaluate(() => window.location.search);
|
|
143
|
+
addResult(results, "esc-clears-param", !new URLSearchParams(searchAfterEsc).has("aberto"), searchAfterEsc || "empty search");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function captureScreenshots(browser, fileUrl, outDir, slug) {
|
|
147
|
+
const shots = [];
|
|
148
|
+
for (const scheme of ["light", "dark"]) {
|
|
149
|
+
const context = await browser.newContext({
|
|
150
|
+
colorScheme: scheme,
|
|
151
|
+
viewport: { width: 1440, height: 1000 },
|
|
152
|
+
});
|
|
153
|
+
const page = await context.newPage();
|
|
154
|
+
await page.goto(fileUrl, { waitUntil: "load" });
|
|
155
|
+
await page.evaluate((theme) => {
|
|
156
|
+
document.documentElement.setAttribute("data-theme", theme);
|
|
157
|
+
}, scheme);
|
|
158
|
+
await page.waitForTimeout(300);
|
|
159
|
+
const shotPath = path.join(outDir, `${slug}-${scheme}.png`);
|
|
160
|
+
await page.screenshot({ path: shotPath, fullPage: true });
|
|
161
|
+
shots.push({ colorScheme: scheme, path: shotPath });
|
|
162
|
+
await context.close();
|
|
163
|
+
}
|
|
164
|
+
return shots;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function main() {
|
|
168
|
+
const args = parseArgs(process.argv.slice(2));
|
|
169
|
+
if (!args.file) {
|
|
170
|
+
throw new Error("Missing required argument: --file <prototype.html>");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const filePath = path.resolve(String(args.file));
|
|
174
|
+
if (!fs.existsSync(filePath)) {
|
|
175
|
+
throw new Error(`Prototype file not found: ${filePath}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const firefox = await importFirefox();
|
|
179
|
+
if (!firefox) {
|
|
180
|
+
throw new Error("Playwright is not installed. Run: npm i -D @playwright/test && npx playwright install firefox");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const html = fs.readFileSync(filePath, "utf8");
|
|
184
|
+
const outDir = path.resolve(String(args.out || path.join(".dw", ".open-design", "gate", slugify(path.basename(filePath)))));
|
|
185
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
186
|
+
const baseUrl = pathToFileURL(filePath).toString();
|
|
187
|
+
const slug = slugify(path.basename(filePath));
|
|
188
|
+
|
|
189
|
+
const browser = await firefox.launch({ headless: true });
|
|
190
|
+
const results = [];
|
|
191
|
+
let screenshots = [];
|
|
192
|
+
try {
|
|
193
|
+
const context = await browser.newContext({ colorScheme: "light", viewport: { width: 1440, height: 1000 } });
|
|
194
|
+
const page = await context.newPage();
|
|
195
|
+
await page.goto(baseUrl, { waitUntil: "load" });
|
|
196
|
+
await structuralChecks(page, html, results);
|
|
197
|
+
await behavioralChecks(page, baseUrl, args, results);
|
|
198
|
+
await context.close();
|
|
199
|
+
screenshots = await captureScreenshots(browser, baseUrl, outDir, slug);
|
|
200
|
+
} finally {
|
|
201
|
+
await browser.close();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const failed = results.filter((result) => !result.pass);
|
|
205
|
+
const summary = {
|
|
206
|
+
file: filePath,
|
|
207
|
+
score: Number(((results.length - failed.length) / results.length * 10).toFixed(1)),
|
|
208
|
+
pass: failed.length === 0,
|
|
209
|
+
results,
|
|
210
|
+
screenshots,
|
|
211
|
+
};
|
|
212
|
+
fs.writeFileSync(path.join(outDir, `${slug}-gate.json`), JSON.stringify(summary, null, 2) + "\n", "utf8");
|
|
213
|
+
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
|
|
214
|
+
|
|
215
|
+
if (failed.length > 0) {
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
main().catch((error) => {
|
|
221
|
+
process.stderr.write(`gate-prototype: ${error.message}\n`);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
});
|