@ai-qa/workflow 2.0.16 → 2.0.18

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-qa/workflow",
3
- "version": "2.0.16",
3
+ "version": "2.0.18",
4
4
  "description": "AI QA Workflow Template — transforms any AI agent into an autonomous QA engineer. AI explores, plans, generates tests, and heals. Scripts execute and report.",
5
5
  "keywords": [
6
6
  "qa",
@@ -33,7 +33,9 @@
33
33
  "opencode.json",
34
34
  ".qa-workflow.json",
35
35
  "router.md",
36
+ "tsconfig.json",
36
37
  ".github/",
38
+ "tsconfig.json",
37
39
  ".env"
38
40
  ],
39
41
  "license": "MIT",
@@ -57,15 +59,15 @@
57
59
  "qa:report:allure": "node ai-qa-workflow.js report:allure",
58
60
  "qa:status": "node ai-qa-workflow.js status",
59
61
  "qa:list": "node ai-qa-workflow.js list",
62
+ "qa:update": "node install.js . --update --yes",
60
63
  "dashboard": "cd qa-dashboard && npm start",
61
64
  "dashboard:dev": "cd qa-dashboard && npx nodemon app.js",
62
- "dashboard:stop": "npx kill-port 4000"
65
+ "dashboard:stop": "npx kill-port 4000",
66
+ "allure:generate": "allure generate allure-results -o allure-report --clean",
67
+ "allure:open": "allure open allure-report",
68
+ "allure:serve": "allure serve allure-results"
63
69
  },
64
70
  "devDependencies": {
65
- "@playwright/test": "^1.52.0",
66
- "allure-playwright": "^3.2.1",
67
- "allure-commandline": "^2.33.0",
68
- "@applitools/eyes-playwright": "^1.42.0",
69
- "@applitools/mcp": "^1.5.0"
71
+ "@types/node": "^25.9.2"
70
72
  }
71
73
  }
@@ -6,6 +6,16 @@ export default defineConfig({
6
6
  forbidOnly: !!process.env.CI,
7
7
  retries: process.env.CI ? 2 : 0,
8
8
  workers: process.env.CI ? 1 : undefined,
9
+ reporter: [
10
+ ['list'],
11
+ ['json', { outputFile: 'test-results/playwright-report.json' }],
12
+ ['html', { outputFolder: 'playwright-report' }],
13
+ ['allure-playwright', {
14
+ outputFolder: 'allure-results',
15
+ detail: true,
16
+ }],
17
+ ],
18
+
9
19
  use: {
10
20
  baseURL: process.env.APP_URL || 'http://localhost:3000',
11
21
  trace: 'on-first-retry',
@@ -1,283 +1,52 @@
1
- # Prompting Template — How to Talk to the AI QA Agent
1
+ # Prompting Template — AI QA Agent
2
2
 
3
- This guide covers every prompt you need — from installation to final report. Use it as your conversation script.
3
+ ## Quick Start
4
4
 
5
- ---
6
-
7
- ## Quick Start (Copy-Paste Ready)
8
-
9
- After installing the template and writing a user story, open your AI editor and send:
10
-
11
- > **"Read router.md and follow the QA workflow for my-story.md"**
12
-
13
- That's it. The AI handles everything from there, stopping at each phase for your approval.
14
-
15
- ---
16
-
17
- ## Phase-by-Phase Prompts
18
-
19
- ### Phase 0: First Contact (Environment Check)
20
-
21
- When you open the project for the **first time**, the AI automatically runs an environment check. If it doesn't, or if you want to re-check:
22
-
23
- > **"Run the environment check and tell me what's ready and what's missing"**
24
-
25
- The AI will report:
26
- - ✅ What's installed and configured
27
- - ✅ Pipeline state from `.qa-context/pipeline.json` (phases completed, last run)
28
- - ✅ Auth status from `.auth/credentials.json` (credentials found or missing)
29
- - ❌ What's missing (with commands to fix)
30
- - 📋 What it needs from you (user story, credentials, etc.)
31
-
32
- The AI **automatically reads `.qa-context/pipeline.json`, `.qa-context/selectors.json`, and `.qa-context/heal-history.json`** to understand the current state before starting any phase.
33
-
34
- ---
35
-
36
- ### Phase 1: Test Planning
37
-
38
- You have a user story. You want the AI to explore the app and write a test plan.
39
-
40
- > **"Read router.md and plan tests for my-story.md"**
41
-
42
- The AI will:
43
- 1. Read `router.md` → routes to `playwright-test-planner.agent.md`
44
- 2. Open your app with Playwright MCP
45
- 3. Explore navigation, flows, and UI components
46
- 4. Map critical paths and edge cases
47
- 5. Write a test plan to `specs/my-story-test-plan.md`
48
- 6. **STOP and present the plan to you**
49
-
50
- #### What you should review in the plan:
51
- - Are all acceptance criteria covered?
52
- - Are edge cases and negative scenarios included?
53
- - Are the preconditions accurate?
54
- - Does the environment URL match your running app?
55
-
56
- #### Approval responses:
57
- | You say | Result |
58
- |---------|--------|
59
- | "Looks good, continue" | AI proceeds to test generation |
60
- | "Approved" | Same |
61
- | "Add a scenario for X" | AI updates the plan, then waits again |
62
- | "I'll review later, proceed" | AI proceeds (use with caution) |
63
-
64
- ---
65
-
66
- ### Phase 2: Test Generation
67
-
68
- The plan is approved. Now you want the AI to write real Playwright tests.
69
-
70
- > **"Generate tests from the plan in specs/my-story-test-plan.md"**
71
-
72
- Or simply (if already in context):
73
-
74
- > **"Generate the tests"**
75
-
76
- The AI will:
77
- 1. Read `playwright-test-generator.agent.md`
78
- 2. Read `prompts/QAe2eprompt.md` for conventions
79
- 3. Use Playwright MCP to manually execute each scenario
80
- 4. Capture real selectors from the actual page
81
- 5. Add visual checkpoints via Applitools on critical pages (if `APPLITOOLS_API_KEY` is set)
82
- 6. Write complete Playwright `.spec.ts` files to `tests/`
83
- 7. **STOP and present the code to you**
84
-
85
- #### What you should review in the test code:
86
- - Are selectors using real elements? (no hallucinated CSS classes)
87
- - Are assertions meaningful?
88
- - Are visual checkpoints added on critical pages? (login, dashboard, checkout)
89
- - Does the test handle auth/state correctly?
90
- - Are there timeouts or waits that might flake?
91
-
92
- #### Approval responses:
93
- | You say | Result |
94
- |---------|--------|
95
- | "Code looks good, execute" | User runs tests, or AI suggests running |
96
- | "Approved" | Same |
97
- | "Fix the selector on line 23" | AI corrects it, presents again |
98
- | "Add a test for X edge case" | AI adds the scenario, presents again |
99
-
100
- ---
101
-
102
- ### Phase 3: Execution
103
-
104
- Tests are written and approved. Time to run them.
105
-
106
- > **User runs:** `npm run qa:execute [test-name]`
107
-
108
- Or, if you want the AI to trigger execution (if it has shell access):
109
-
110
- > **"Execute the tests and report back"**
111
-
112
- The AI runs Playwright and reports:
113
- - ✅ Tests passed → ready for report
114
- - ❌ Tests failed → moves to healing phase
115
-
116
- ---
117
-
118
- ### Phase 4: Healing (Debugging Failures)
119
-
120
- Tests failed. You want the AI to fix them.
121
-
122
- > **"Debug and fix the failing tests"**
123
-
124
- Or specifically:
125
-
126
- > **"Read playwright-test-healer.agent.md and debug the failures in test-results/latest-run"**
127
-
128
- The AI will:
129
- 1. Read `playwright-test-healer.agent.md`
130
- 2. Run the failing tests with `test_run`
131
- 3. Debug with `test_debug` — examine the actual UI state
132
- 4. Classify the failure:
133
- - **Selector broken** → proposes 1-3 line fix
134
- - **Timing issue** → proposes adding a wait
135
- - **App bug** → marks `test.fixme()`, logs defect
136
- 5. **STOP and present diagnosis + proposed fix to you**
137
-
138
- #### What you should review in the diagnosis:
139
- - What exactly failed and why
140
- - What the AI wants to change (which file, which lines)
141
- - Is it a test issue or a real bug in your app?
142
-
143
- #### Approval responses:
144
- | You say | Result |
145
- |---------|--------|
146
- | "Fix it" | AI applies the 1-3 line fix and re-runs |
147
- | "Approved, apply the fix" | Same |
148
- | "That's a real bug, file it" | AI marks as defect, moves on |
149
- | "Try a different approach" | AI suggests alternative fix |
150
- | "Skip it, I'll investigate later" | AI marks as `test.fixme()` |
151
-
152
- ---
153
-
154
- ### Phase 5: Report
155
-
156
- Tests are done. You want a summary.
157
-
158
- > **"Generate the execution report"**
159
-
160
- Or manually:
161
-
162
- > **User runs:** `npm run qa:report [run-id]`
163
-
164
- The AI will:
165
- 1. Run `npm run qa:report` (or read the results directly)
166
- 2. Present a summary: passed, failed, healed, defects found
167
- 3. Update `docs/application-context.md` with learned selectors and flaky areas
168
-
169
- ---
170
-
171
- ## Quick Reference — All Prompts in One Table
172
-
173
- | Phase | Your prompt | AI does | You do |
174
- |-------|------------|---------|--------|
175
- | **0** | *(automatic on first open)* | Environment check | Read status report |
176
- | **0** | "Run the environment check" | Re-check setup | Fix any missing items |
177
- | **1** | "Read router.md and plan tests for my-story.md" | Explore app, write plan | **Review + approve** |
178
- | **2** | "Generate tests from the plan" | Write Playwright code | **Review + approve** |
179
- | **3** | `npm run qa:execute` | Run tests | Check results |
180
- | **4** | "Debug and fix the failing tests" | Diagnose, propose fix | **Review + approve** |
181
- | **5** | "Generate the report" | Summarize results | Review final report |
5
+ ```text
6
+ "Read router.md and follow the QA workflow for my-story.md"
7
+ ```
182
8
 
183
9
  ---
184
10
 
185
- ## Best Practices
186
-
187
- ### For Best Results
188
-
189
- 1. **Write structured user stories** — Use the format in the README. Clear acceptance criteria produce better test plans.
190
-
191
- 2. **Review the plan first** — The AI's understanding of your app is only as good as its exploration. Catch incorrect assumptions early.
192
-
193
- 3. **Don't skip the approval gates** — Each gate is a chance to redirect the AI before it wastes time on the wrong approach.
194
-
195
- 4. **Be specific in your feedback** — Instead of "this looks wrong", say "the selector on line 23 should use `data-testid` instead of class name".
196
-
197
- 5. **Provide context upfront** — Populate `docs/application-context.md` with stable selectors, auth details, and tech stack notes before starting. The AI also reads `.qa-context/` automatically — you don't need to tell it.
198
-
199
- 6. **Memory is persistent** — The AI stores its learning in `.qa-context/` (selectors, healing history, pipeline state). This means each session starts smarter than the last. Don't delete this folder unless you want to reset the AI's memory.
11
+ ## Commandes essentielles
200
12
 
201
- 7. **Credentials are stored once** — The AI saves your login to `.auth/credentials.json` after first use. If your password changes, just update that file or delete it and the AI will ask again.
202
-
203
- 6. **Use `npm run qa:retry` for simple flakes** — If a test fails intermittently, try the mechanical retry first before asking the AI to debug.
204
-
205
- 7. **Token efficiency works both ways** Keep your feedback concise. The AI is designed for 1-3 line fixes and targeted edits.
206
-
207
- ### Common Mistakes to Avoid
208
-
209
- | Mistake | Better approach |
210
- |---------|----------------|
211
- | Sending a vague "test my app" prompt | "Read router.md and plan tests for user-story/login.md" |
212
- | Approving without reviewing the plan | Always check: does it cover the acceptance criteria? |
213
- | Letting the AI generate tests without a plan | Always do Phase 1 before Phase 2 |
214
- | Running tests before reviewing the code | AI can hallucinate selectors — verify them first |
215
- | Asking the AI to fix everything at once | One test at a time: "Debug the login test failure" |
216
- | Skipping the approval gate | You're the supervisor for a reason |
13
+ | Phase | Prompt / Commande |
14
+ |---|---|
15
+ | Environment check | `"Run the environment check"` |
16
+ | Planifier les tests | `"Read router.md and plan tests for user-story/my-story.md"` |
17
+ | Générer les tests | `"Generate tests from specs/my-story-test-plan.md"` |
18
+ | Exécuter | `npm run qa:execute` |
19
+ | Auto-guérir | `"Debug and fix the failing tests"` |
20
+ | Rapport | `npm run qa:report` ou `"Generate the report"` |
217
21
 
218
22
  ---
219
23
 
220
- ## Example Full Session
221
-
222
- ```
223
- You: "Read router.md and plan tests for login-story.md"
224
-
225
- AI: [Explores app, writes plan to specs/login-story-test-plan.md]
226
- "Plan is ready. Here's what I found:
227
- - 3 scenarios: login success, wrong password, locked account
228
- - 1 concern: no visible error toast for locked accounts
229
- Do you approve?"
230
-
231
- You: "Looks good, but add a scenario for empty email field."
232
-
233
- AI: [Updates plan]
234
- "Added. 4 scenarios now. Approve?"
235
-
236
- You: "Approved, generate the tests."
237
-
238
- AI: [Writes tests/login-story.spec.ts]
239
- "Tests generated. I used getByRole for buttons and getByLabel for inputs.
240
- Do you approve the code?"
241
-
242
- You: "Looks good, execute."
24
+ ## Exemple de session complète
243
25
 
244
- AI: "Run: npm run qa:execute login-story"
26
+ ```text
27
+ 1. "Read router.md and plan tests for user-story/my-story.md"
28
+ → L'IA explore l'app, écrit specs/my-story-test-plan.md
29
+ → STOP — vous validez
245
30
 
246
- You: [Runs it] "Two tests failed — wrong password and locked account."
31
+ 2. "Approved, generate the tests"
32
+ → L'IA écrit tests/my-story.spec.ts
33
+ → STOP — vous validez
247
34
 
248
- AI: [Debugs]
249
- "Diagnosis:
250
- - Wrong password: error message class changed, needs new selector
251
- - Locked account: the lock icon isn't rendering (app bug)
252
- Fix the selector on line 42? Approve?"
35
+ 3. npm run qa:execute
36
+ → Tests exécutés
253
37
 
254
- You: "Fix the selector, mark the lock icon as a bug."
255
-
256
- AI: [Applies fix, re-runs, marks test.fixme()]
257
- "Fixed: wrong password now passes. Locked account marked as defect.
258
- Generate the report?"
38
+ 4. "Debug and fix the failing tests"
39
+ → L'IA diagnostique, corrige, réessaie (max 2x)
40
+ STOP vous validez
259
41
 
260
- You: "Yes, generate it."
42
+ 5. npm run qa:report:allure
43
+ → Rapport Allure généré
261
44
  ```
262
45
 
263
46
  ---
264
47
 
265
- ## Quick Copy-Paste
266
-
267
- ### Minimal workflow (5 prompts total)
48
+ ## Règles
268
49
 
269
- ```
270
- 1. "Read router.md and plan tests for my-story.md"
271
- 2. "Approved" (after reviewing plan)
272
- 3. "Approved" (after reviewing tests)
273
- (run npm run qa:execute)
274
- 4. "Debug and fix the failing tests"
275
- 5. "Approved, apply the fix" (after reviewing diagnosis)
276
- (run npm run qa:report)
277
- ```
278
-
279
- ### Single command to start everything
280
-
281
- ```
282
- "Read router.md and follow the QA workflow for my-story.md"
283
- ```
50
+ - L'IA propose → vous approuvez → L'IA exécute
51
+ - Ne jamais sauter la validation (Phase 1 → plan, Phase 2 → code)
52
+ - 1 erreur à la fois pour le debug
@@ -89,11 +89,15 @@ app.use('/runs', require('./routes/runs'));
89
89
  app.use('/review', require('./routes/review'));
90
90
  app.use('/analytics', require('./routes/analytics'));
91
91
  app.use('/export', require('./routes/export'));
92
+ app.use('/terminal', require('./routes/terminal'));
92
93
  app.use('/data', require('./routes/test-data'));
93
94
 
94
95
  app.use((req, res) => { res.status(404).render('error', { message: 'Page not found' }); });
95
96
 
96
97
  app.listen(PORT, () => {
97
- console.log(`QA Dashboard running at http://localhost:${PORT}`);
98
- console.log(`Close the browser tab to stop the server.`);
98
+ console.log(`\n ┌─────────────────────────────────────┐`);
99
+ console.log(` │ Orchestrator QA Dashboard │`);
100
+ console.log(` │ http://localhost:${PORT}${' '.repeat(9 - String(PORT).length)}│`);
101
+ console.log(` └─────────────────────────────────────┘`);
102
+ console.log(` Close the browser tab to stop the server.\n`);
99
103
  });
@@ -5,7 +5,7 @@
5
5
  "main": "app.js",
6
6
  "scripts": {
7
7
  "start": "node app.js",
8
- "dev": "node app.js"
8
+ "dev": "nodemon app.js"
9
9
  },
10
10
  "dependencies": {
11
11
  "chart.js": "^4.4.0",
@@ -14,5 +14,8 @@
14
14
  "express": "^4.18.2",
15
15
  "express-ejs-layouts": "^2.5.1",
16
16
  "morgan": "^1.10.0"
17
+ },
18
+ "devDependencies": {
19
+ "nodemon": "^3.1.0"
17
20
  }
18
21
  }
@@ -5,7 +5,6 @@ const pm = require('../services/project-manager');
5
5
  router.get('/', (req, res) => {
6
6
  const projects = pm.getAll();
7
7
 
8
- // Single-project mode: always use the first (and only) registered project
9
8
  const project = projects.length > 0 ? projects[0] : null;
10
9
 
11
10
  let stats = {
@@ -15,7 +14,10 @@ router.get('/', (req, res) => {
15
14
  runs: 0,
16
15
  passedRuns: 0,
17
16
  passRate: 0,
18
- lastRunStatus: null, // 'passed' | 'failed' | null
17
+ lastRunStatus: null,
18
+ storiesList: [],
19
+ latestRun: null,
20
+ hasAllure: false,
19
21
  };
20
22
 
21
23
  if (project) {
@@ -27,6 +29,18 @@ router.get('/', (req, res) => {
27
29
  stats.plans = list.plans.length;
28
30
  stats.specs = list.specs.length;
29
31
  stats.runs = runDirs.length;
32
+ stats.hasAllure = bridge.hasAllureReport();
33
+
34
+ stats.storiesList = list.stories.map(s => {
35
+ const baseName = s.replace(/\.md$/i, '');
36
+ return {
37
+ name: s,
38
+ hasPlan: list.plans.some(p => p.toLowerCase().includes(baseName.toLowerCase())),
39
+ planName: list.plans.find(p => p.toLowerCase().includes(baseName.toLowerCase())),
40
+ hasSpec: list.specs.some(sp => sp.toLowerCase().includes(baseName.toLowerCase())),
41
+ specName: list.specs.find(sp => sp.toLowerCase().includes(baseName.toLowerCase())),
42
+ };
43
+ });
30
44
 
31
45
  if (runDirs.length > 0) {
32
46
  const results = runDirs.map(d => bridge.getRunResult(d.name)).filter(Boolean);
@@ -35,9 +49,17 @@ router.get('/', (req, res) => {
35
49
  ? Math.round((stats.passedRuns / results.length) * 100)
36
50
  : 0;
37
51
 
38
- // Most recent run result
39
52
  const latest = bridge.getRunResult(runDirs[0].name);
40
- if (latest) stats.lastRunStatus = latest.success ? 'passed' : 'failed';
53
+ if (latest) {
54
+ stats.lastRunStatus = latest.success ? 'passed' : 'failed';
55
+ stats.latestRun = {
56
+ id: runDirs[0].name,
57
+ duration: latest.duration,
58
+ passedCount: latest.passedTests ? latest.passedTests.length : 0,
59
+ failedCount: latest.failedTests ? latest.failedTests.length : 0,
60
+ testName: latest.test,
61
+ };
62
+ }
41
63
  }
42
64
  }
43
65
 
@@ -29,7 +29,31 @@ router.get('/:id', (req, res) => {
29
29
  const statusResult = bridge.status();
30
30
  const list = bridge.listAll();
31
31
 
32
- res.render('project', { project, status: statusResult, list });
32
+ const storiesWithStatus = list.stories.map(s => {
33
+ const baseName = s.replace(/\.md$/i, '');
34
+ const planName = list.plans.find(p => p.toLowerCase().includes(baseName.toLowerCase()));
35
+ const specName = list.specs.find(sp => sp.toLowerCase().includes(baseName.toLowerCase()));
36
+ const runDirs = bridge.getRunDirs();
37
+ const hasRun = runDirs.some(d => {
38
+ const result = bridge.getRunResult(d.name);
39
+ return result && result.test && result.test.toLowerCase().includes(baseName.toLowerCase());
40
+ });
41
+ return {
42
+ name: s,
43
+ baseName,
44
+ hasPlan: !!planName,
45
+ planName,
46
+ hasSpec: !!specName,
47
+ specName,
48
+ hasRun,
49
+ };
50
+ });
51
+
52
+ const hasAllure = bridge.hasAllureReport();
53
+ const runDirs = bridge.getRunDirs();
54
+ const latestRun = runDirs.length > 0 ? runDirs[0].name : null;
55
+
56
+ res.render('project', { project, status: statusResult, list, storiesWithStatus, hasAllure, latestRun });
33
57
  });
34
58
 
35
59
  router.post('/:id/remove', (req, res) => {
@@ -115,4 +115,42 @@ router.get('/:runId/compare/:otherRunId', (req, res) => {
115
115
  ] });
116
116
  });
117
117
 
118
+ // Rerun a specific test from a previous run
119
+ router.post('/:runId/rerun', (req, res) => {
120
+ const projectId = req.query.project;
121
+ const project = projectId ? pm.get(projectId) : pm.getAll()[0];
122
+ if (!project) return res.status(404).json({ error: 'Project not found' });
123
+
124
+ const bridge = pm.getBridge(project.id);
125
+ const result = bridge.getRunResult(req.params.runId);
126
+ if (!result) return res.status(404).json({ error: 'Run not found' });
127
+
128
+ const testName = result.test || '';
129
+ if (!testName || testName === 'all') {
130
+ // Rerun all tests
131
+ const runResult = bridge.execute('');
132
+ return res.json({ success: runResult.success, output: runResult.output, error: runResult.error });
133
+ }
134
+
135
+ const runResult = bridge.execute(testName);
136
+ res.json({ success: runResult.success, output: runResult.output, error: runResult.error, runId: extractRunId(runResult.output) });
137
+ });
138
+
139
+ // Regenerate Allure report
140
+ router.post('/allure-regenerate', (req, res) => {
141
+ const projectId = req.query.project;
142
+ const project = projectId ? pm.get(projectId) : pm.getAll()[0];
143
+ if (!project) return res.status(404).json({ error: 'Project not found' });
144
+
145
+ const bridge = pm.getBridge(project.id);
146
+ const result = bridge.runCommand('npx allure generate allure-results --clean -o allure-report');
147
+ res.json({ success: result.success, output: result.output, error: result.error });
148
+ });
149
+
150
+ function extractRunId(output) {
151
+ if (!output) return null;
152
+ const match = output.match(/run-[\d-T-]+/);
153
+ return match ? match[0] : null;
154
+ }
155
+
118
156
  module.exports = router;
@@ -44,7 +44,24 @@ router.get('/:storyName', (req, res) => {
44
44
  const plan = planName ? bridge.getPlanContent(planName) : null;
45
45
  const spec = specName ? bridge.getSpecContent(specName) : null;
46
46
 
47
- res.render('story', { project, story: { name: req.params.storyName, content, plan, planName, spec, specName } });
47
+ const runDirs = bridge.getRunDirs();
48
+ const results = runDirs.map(d => {
49
+ const r = bridge.getRunResult(d.name);
50
+ if (!r) return null;
51
+ const testMatch = specName ? r.test && r.test.toLowerCase().includes(baseName.toLowerCase()) : true;
52
+ return testMatch ? {
53
+ id: d.name,
54
+ success: r.success,
55
+ duration: r.duration,
56
+ passedCount: r.passedTests ? r.passedTests.length : 0,
57
+ failedCount: r.failedTests ? r.failedTests.length : 0,
58
+ } : null;
59
+ }).filter(Boolean);
60
+
61
+ const hasRun = results.length > 0;
62
+ const hasAllure = bridge.hasAllureReport();
63
+
64
+ res.render('story', { project, story: { name: req.params.storyName, content, plan, planName, spec, specName, results, hasRun, hasAllure } });
48
65
  });
49
66
 
50
67
  // Helper to stream CLI command output asynchronously to client
@@ -0,0 +1,51 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const pm = require('../services/project-manager');
4
+ const { spawn } = require('child_process');
5
+
6
+ router.get('/', (req, res) => {
7
+ const projects = pm.getAll();
8
+ const projectId = req.query.project || (projects.length > 0 ? projects[0].id : null);
9
+ const project = projects.find(p => p.id === projectId);
10
+ res.render('terminal', { project, projects });
11
+ });
12
+
13
+ // Execute a command asynchronously with streaming output
14
+ router.post('/run', (req, res) => {
15
+ const projects = pm.getAll();
16
+ const projectId = req.query.project || (projects.length > 0 ? projects[0].id : null);
17
+ const project = projects.find(p => p.id === projectId);
18
+ if (!project) return res.status(404).send('Project not found');
19
+
20
+ const { command } = req.body;
21
+ if (!command) return res.status(400).send('No command provided');
22
+
23
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
24
+ res.setHeader('Transfer-Encoding', 'chunked');
25
+ res.setHeader('X-Content-Type-Options', 'nosniff');
26
+
27
+ const child = spawn(command, [], {
28
+ cwd: project.path,
29
+ shell: true,
30
+ env: { ...process.env, FORCE_COLOR: '0' },
31
+ });
32
+
33
+ child.stdout.on('data', (data) => res.write(data.toString()));
34
+ child.stderr.on('data', (data) => res.write(data.toString()));
35
+
36
+ child.on('close', (code) => {
37
+ res.write(`\n\n--- [FINISHED: ${code === 0 ? 'SUCCESS' : 'FAILED'}] (exit code: ${code}) ---\n`);
38
+ res.end();
39
+ });
40
+
41
+ child.on('error', (err) => {
42
+ res.write(`\nError: ${err.message}\n`);
43
+ res.end();
44
+ });
45
+
46
+ req.on('close', () => {
47
+ if (child && !child.killed) child.kill('SIGINT');
48
+ });
49
+ });
50
+
51
+ module.exports = router;