@avantmedia/af 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +539 -0
  3. package/af +2 -0
  4. package/bun-upgrade.ts +130 -0
  5. package/commands/bun.ts +55 -0
  6. package/commands/changes.ts +35 -0
  7. package/commands/e2e.ts +12 -0
  8. package/commands/help.ts +236 -0
  9. package/commands/install-extension.ts +133 -0
  10. package/commands/jira.ts +577 -0
  11. package/commands/licenses.ts +32 -0
  12. package/commands/npm.ts +55 -0
  13. package/commands/scaffold.ts +105 -0
  14. package/commands/setup.tsx +156 -0
  15. package/commands/spec.ts +405 -0
  16. package/commands/stop-hook.ts +90 -0
  17. package/commands/todo.ts +208 -0
  18. package/commands/versions.ts +150 -0
  19. package/commands/watch.ts +344 -0
  20. package/commands/worktree.ts +424 -0
  21. package/components/change-select.tsx +71 -0
  22. package/components/confirm.tsx +41 -0
  23. package/components/file-conflict.tsx +52 -0
  24. package/components/input.tsx +53 -0
  25. package/components/layout.tsx +70 -0
  26. package/components/messages.tsx +48 -0
  27. package/components/progress.tsx +71 -0
  28. package/components/select.tsx +90 -0
  29. package/components/status-display.tsx +74 -0
  30. package/components/table.tsx +79 -0
  31. package/generated/setup-manifest.ts +67 -0
  32. package/git-worktree.ts +184 -0
  33. package/main.ts +12 -0
  34. package/npm-upgrade.ts +117 -0
  35. package/package.json +83 -0
  36. package/resources/copy-prompt-reporter.ts +443 -0
  37. package/router.ts +220 -0
  38. package/setup/.claude/commands/commit-work.md +47 -0
  39. package/setup/.claude/commands/complete-work.md +34 -0
  40. package/setup/.claude/commands/e2e.md +29 -0
  41. package/setup/.claude/commands/start-work.md +51 -0
  42. package/setup/.claude/skills/pm/SKILL.md +294 -0
  43. package/setup/.claude/skills/pm/templates/api-endpoint.md +69 -0
  44. package/setup/.claude/skills/pm/templates/bug-fix.md +77 -0
  45. package/setup/.claude/skills/pm/templates/feature.md +87 -0
  46. package/setup/.claude/skills/pm/templates/ui-component.md +78 -0
  47. package/utils/change-select-render.tsx +44 -0
  48. package/utils/claude.ts +9 -0
  49. package/utils/config.ts +58 -0
  50. package/utils/env.ts +53 -0
  51. package/utils/git.ts +120 -0
  52. package/utils/ink-render.tsx +50 -0
  53. package/utils/openspec.ts +54 -0
  54. package/utils/output.ts +104 -0
  55. package/utils/proposal.ts +160 -0
  56. package/utils/resources.ts +64 -0
  57. package/utils/setup-files.ts +230 -0
package/main.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { route } from './router.ts';
2
+ import { loadEnv } from './utils/env.ts';
3
+
4
+ // Load environment variables from .env in current working directory
5
+ loadEnv();
6
+
7
+ // Parse command-line arguments
8
+ const args = process.argv.slice(2);
9
+
10
+ // Route to appropriate command handler
11
+ const exitCode = await route(args);
12
+ process.exit(exitCode);
package/npm-upgrade.ts ADDED
@@ -0,0 +1,117 @@
1
+ import { spawn } from 'child_process';
2
+
3
+ interface OutdatedPackage {
4
+ current: string;
5
+ wanted: string;
6
+ latest: string;
7
+ dependent: string;
8
+ location: string;
9
+ }
10
+
11
+ interface OutdatedPackages {
12
+ [packageName: string]: OutdatedPackage;
13
+ }
14
+
15
+ interface UpgradeResult {
16
+ package: string;
17
+ success: boolean;
18
+ error?: string;
19
+ }
20
+
21
+ /**
22
+ * Execute npm outdated and return list of packages that need updating
23
+ */
24
+ export async function getOutdatedPackages(): Promise<string[]> {
25
+ return new Promise((resolve, reject) => {
26
+ const npmProcess = spawn('npm', ['outdated', '--json'], {
27
+ stdio: ['ignore', 'pipe', 'pipe'],
28
+ shell: true,
29
+ });
30
+
31
+ let stdout = '';
32
+ let stderr = '';
33
+
34
+ npmProcess.stdout.on('data', (data: Buffer) => {
35
+ stdout += data.toString();
36
+ });
37
+
38
+ npmProcess.stderr.on('data', (data: Buffer) => {
39
+ stderr += data.toString();
40
+ });
41
+
42
+ npmProcess.on('close', (code: number | null) => {
43
+ // npm outdated returns exit code 1 when there are outdated packages
44
+ // and 0 when all packages are up to date
45
+ if (code !== 0 && code !== 1) {
46
+ reject(new Error(`npm outdated failed with code ${code}: ${stderr}`));
47
+ return;
48
+ }
49
+
50
+ // If stdout is empty, no packages are outdated
51
+ if (!stdout.trim()) {
52
+ resolve([]);
53
+ return;
54
+ }
55
+
56
+ try {
57
+ const outdated: OutdatedPackages = JSON.parse(stdout);
58
+ const packageNames = Object.keys(outdated);
59
+ resolve(packageNames);
60
+ } catch (error) {
61
+ reject(new Error(`Failed to parse npm outdated output: ${error}`));
62
+ }
63
+ });
64
+
65
+ npmProcess.on('error', (error: Error) => {
66
+ reject(new Error(`Failed to execute npm outdated: ${error.message}`));
67
+ });
68
+ });
69
+ }
70
+
71
+ /**
72
+ * Upgrade a single package to its latest version
73
+ */
74
+ export async function upgradePackage(packageName: string): Promise<UpgradeResult> {
75
+ return new Promise(resolve => {
76
+ console.log(`\nUpgrading ${packageName}...`);
77
+
78
+ const npmProcess = spawn('npm', ['install', `${packageName}@latest`], {
79
+ stdio: ['ignore', 'inherit', 'inherit'],
80
+ shell: true,
81
+ });
82
+
83
+ npmProcess.on('close', (code: number | null) => {
84
+ if (code === 0) {
85
+ resolve({ package: packageName, success: true });
86
+ } else {
87
+ resolve({
88
+ package: packageName,
89
+ success: false,
90
+ error: `npm install failed with exit code ${code}`,
91
+ });
92
+ }
93
+ });
94
+
95
+ npmProcess.on('error', (error: Error) => {
96
+ resolve({
97
+ package: packageName,
98
+ success: false,
99
+ error: error.message,
100
+ });
101
+ });
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Upgrade all outdated packages sequentially
107
+ */
108
+ export async function upgradeAllPackages(packages: string[]): Promise<UpgradeResult[]> {
109
+ const results: UpgradeResult[] = [];
110
+
111
+ for (const packageName of packages) {
112
+ const result = await upgradePackage(packageName);
113
+ results.push(result);
114
+ }
115
+
116
+ return results;
117
+ }
package/package.json ADDED
@@ -0,0 +1,83 @@
1
+ {
2
+ "name": "@avantmedia/af",
3
+ "version": "0.0.1",
4
+ "description": "Development utility.",
5
+ "homepage": "https://github.com/avantmedialtd/artifex#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/avantmedialtd/artifex/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/avantmedialtd/artifex.git"
12
+ },
13
+ "license": "MIT",
14
+ "author": "István Antal <istvan@antal.xyz>",
15
+ "keywords": [
16
+ "cli",
17
+ "developer-tools",
18
+ "bun",
19
+ "openspec",
20
+ "dependency-upgrade"
21
+ ],
22
+ "bin": {
23
+ "af": "./af"
24
+ },
25
+ "engines": {
26
+ "node": ">=22.6.0"
27
+ },
28
+ "type": "module",
29
+ "files": [
30
+ "af",
31
+ "main.ts",
32
+ "router.ts",
33
+ "bun-upgrade.ts",
34
+ "npm-upgrade.ts",
35
+ "git-worktree.ts",
36
+ "commands/",
37
+ "components/",
38
+ "utils/",
39
+ "generated/",
40
+ "setup/",
41
+ "resources/",
42
+ "!**/*.test.ts"
43
+ ],
44
+ "main": "main.ts",
45
+ "scripts": {
46
+ "format": "prettier --write .",
47
+ "format:check": "prettier --check .",
48
+ "format:write": "prettier --write .",
49
+ "lint": "oxlint .",
50
+ "lint:fix": "oxlint . --fix",
51
+ "lint:check": "oxlint .",
52
+ "spell:check": "cspell \"**\"",
53
+ "pretest": "bun run generate:manifest",
54
+ "test": "vitest run",
55
+ "test:watch": "vitest",
56
+ "test:coverage": "vitest run --coverage",
57
+ "generate:manifest": "bun run scripts/generate-setup-manifest.ts",
58
+ "precompile": "bun run generate:manifest",
59
+ "compile": "bun run compile:all",
60
+ "compile:all": "bun run compile:darwin-arm64 && bun run compile:darwin-x64 && bun run compile:linux-x64 && bun run compile:linux-arm64 && bun run compile:windows-x64",
61
+ "compile:darwin-arm64": "bun build --compile --minify --target=bun-darwin-arm64 ./main.ts --outfile dist/af-darwin-arm64",
62
+ "compile:darwin-x64": "bun build --compile --minify --target=bun-darwin-x64 ./main.ts --outfile dist/af-darwin-x64",
63
+ "compile:linux-x64": "bun build --compile --minify --target=bun-linux-x64 ./main.ts --outfile dist/af-linux-x64",
64
+ "compile:linux-arm64": "bun build --compile --minify --target=bun-linux-arm64 ./main.ts --outfile dist/af-linux-arm64",
65
+ "compile:windows-x64": "bun build --compile --minify --target=bun-windows-x64 ./main.ts --outfile dist/af-windows-x64.exe",
66
+ "compile:current": "bun build --compile --minify ./main.ts --outfile dist/af"
67
+ },
68
+ "devDependencies": {
69
+ "@types/node": "^25.0.3",
70
+ "@types/react": "^19.2.7",
71
+ "@vitest/ui": "^4.0.16",
72
+ "cspell": "^9.4.0",
73
+ "oxlint": "^1.36.0",
74
+ "prettier": "^3.7.4",
75
+ "typescript": "^5.9.3",
76
+ "react-devtools-core": "^7.0.1",
77
+ "vitest": "^4.0.16"
78
+ },
79
+ "dependencies": {
80
+ "ink": "^6.6.0",
81
+ "react": "^19.2.3"
82
+ }
83
+ }
@@ -0,0 +1,443 @@
1
+ import type {
2
+ FullConfig,
3
+ FullResult,
4
+ Reporter,
5
+ Suite,
6
+ TestCase,
7
+ TestResult,
8
+ } from '@playwright/test/reporter';
9
+
10
+ interface TestInfo {
11
+ title: string;
12
+ file: string;
13
+ line: number;
14
+ column: number;
15
+ duration: number;
16
+ status: string;
17
+ error?: TestResult['error'];
18
+ stdout: TestResult['stdout'];
19
+ stderr: TestResult['stderr'];
20
+ steps: TestResult['steps'];
21
+ attachments: TestResult['attachments'];
22
+ }
23
+
24
+ class CopyPromptReporter implements Reporter {
25
+ private config: FullConfig | null = null;
26
+ private suite: Suite | null = null;
27
+ private results: {
28
+ passed: TestInfo[];
29
+ failed: TestInfo[];
30
+ skipped: TestInfo[];
31
+ timedOut: TestInfo[];
32
+ } = {
33
+ passed: [],
34
+ failed: [],
35
+ skipped: [],
36
+ timedOut: [],
37
+ };
38
+ private totalTests = 0;
39
+ private completedTests = 0;
40
+ private currentTest: string | null = null;
41
+ private startTime: number | null = null;
42
+
43
+ // Convert Docker container paths to host paths for AI agent access
44
+ private toHostPath(containerPath: string | undefined): string | undefined {
45
+ if (!containerPath) return containerPath;
46
+ return containerPath.replace(/^\/workspace\//, './');
47
+ }
48
+
49
+ onBegin(config: FullConfig, suite: Suite): void {
50
+ this.config = config;
51
+ this.suite = suite;
52
+ this.startTime = Date.now();
53
+
54
+ const { total, willRun, willSkip } = this.countTests(suite);
55
+ this.totalTests = total;
56
+
57
+ console.log('\n🎭 Playwright Test Run Started');
58
+ if (willSkip > 0) {
59
+ console.log(
60
+ `📊 Running ${willRun} test${willRun === 1 ? '' : 's'} (${willSkip} skipped)\n`,
61
+ );
62
+ } else {
63
+ console.log(`📊 Running ${total} test${total === 1 ? '' : 's'}\n`);
64
+ }
65
+ }
66
+
67
+ private countTests(suite: Suite): { total: number; willRun: number; willSkip: number } {
68
+ let total = 0;
69
+ let willSkip = 0;
70
+ for (const test of suite.allTests()) {
71
+ total++;
72
+ // Tests with expectedStatus 'skipped' won't run
73
+ if (test.expectedStatus === 'skipped') {
74
+ willSkip++;
75
+ }
76
+ }
77
+ return { total, willRun: total - willSkip, willSkip };
78
+ }
79
+
80
+ onTestBegin(test: TestCase, _result: TestResult): void {
81
+ this.currentTest = test.title;
82
+ // this.updateProgressBar();
83
+ }
84
+
85
+ onTestEnd(test: TestCase, result: TestResult): void {
86
+ const testInfo: TestInfo = {
87
+ title: test.title,
88
+ file: test.location.file,
89
+ line: test.location.line,
90
+ column: test.location.column,
91
+ duration: result.duration,
92
+ status: result.status,
93
+ error: result.error,
94
+ stdout: result.stdout,
95
+ stderr: result.stderr,
96
+ steps: result.steps,
97
+ attachments: result.attachments,
98
+ };
99
+
100
+ if (result.status === 'passed') {
101
+ this.results.passed.push(testInfo);
102
+ } else if (result.status === 'failed') {
103
+ this.results.failed.push(testInfo);
104
+ } else if (result.status === 'timedOut') {
105
+ this.results.timedOut.push(testInfo);
106
+ } else {
107
+ this.results.skipped.push(testInfo);
108
+ }
109
+
110
+ this.completedTests++;
111
+ this.currentTest = null;
112
+ // this.updateProgressBar();
113
+
114
+ // Show test result immediately after a brief pause for progress bar visibility
115
+ setTimeout(() => {
116
+ const status =
117
+ result.status === 'passed'
118
+ ? '✅'
119
+ : result.status === 'failed'
120
+ ? '❌'
121
+ : result.status === 'timedOut'
122
+ ? '⏲️'
123
+ : '⏭️';
124
+ const duration = result.duration ? `(${result.duration}ms)` : '';
125
+ const suiteName = test.parent.title ? `${test.parent.title} › ` : '';
126
+ if (status === '✅') {
127
+ console.log(`\r\x1b[K${status} ${suiteName}${test.title} ${duration}`);
128
+ } else if (status === '❌') {
129
+ console.log(`\r\x1b[K${status} ${suiteName}${test.title} ${duration}`);
130
+ } else if (status === '⏲️') {
131
+ console.log(
132
+ `\r\x1b[K\x1b[31m${status} Timeout: ${suiteName}${test.title} ${duration}\x1b[0m`,
133
+ );
134
+ } else {
135
+ console.log(`\r\x1b[K${status} ${suiteName}${test.title} ${duration}`);
136
+ }
137
+ // If test failed, show brief error info
138
+ if (result.status !== 'passed' && result.status !== 'skipped') {
139
+ const t: TestInfo = {
140
+ title: test.title,
141
+ file: test.location.file,
142
+ line: test.location.line,
143
+ column: test.location.column,
144
+ duration: result.duration,
145
+ status: result.status,
146
+ error: result.error,
147
+ stdout: result.stdout,
148
+ stderr: result.stderr,
149
+ steps: result.steps,
150
+ attachments: result.attachments,
151
+ };
152
+ const banner = result.status === 'timedOut' ? 'Timeout Details' : 'Failure Details';
153
+ console.log(
154
+ ` ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ${banner} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
155
+ );
156
+ console.log(` 📁 File: ${this.getRelativePath(t.file)}:${t.line}:${t.column}`);
157
+ if (typeof t.duration === 'number') console.log(` ⏱️ Duration: ${t.duration}ms`);
158
+
159
+ if (t.error) {
160
+ const errMsg = t.error.message
161
+ ? t.error.message.split('\n')[0]
162
+ : 'Unknown error';
163
+ console.log(` 💥 Error: ${errMsg}`);
164
+
165
+ if (t.error.stack) {
166
+ const stackLines = t.error.stack.split('\n');
167
+ const relevantStack = stackLines
168
+ .filter(line => line.includes('.spec.') || line.includes('test-'))
169
+ .slice(0, 3);
170
+
171
+ if (relevantStack.length > 0) {
172
+ console.log(' 📍 Stack trace:');
173
+ relevantStack.forEach(line => {
174
+ const cleanLine = line.trim().replace(/^\s*at\s*/, '');
175
+ console.log(` ${cleanLine}`);
176
+ });
177
+ }
178
+
179
+ // Extract specific location within the failing spec file
180
+ const rel = this.getRelativePath(t.file);
181
+ const relevantLine = stackLines.find(line => line.includes(rel));
182
+ if (relevantLine) {
183
+ const loc = relevantLine.match(/:(\d+):(\d+)/);
184
+ if (loc)
185
+ console.log(` 📌 Location: Line ${loc[1]}, Column ${loc[2]}`);
186
+ }
187
+ }
188
+ }
189
+
190
+ // Extract failed steps (if any)
191
+ if (Array.isArray(t.steps) && t.steps.length > 0) {
192
+ const failedSteps = t.steps.filter(s => s && s.error);
193
+ if (failedSteps.length > 0) {
194
+ console.log(' 🔍 Failed steps:');
195
+ failedSteps.forEach(s => {
196
+ console.log(` • ${s.title}`);
197
+ if (s.error?.message) console.log(` Error: ${s.error.message}`);
198
+ });
199
+ }
200
+
201
+ // Show the last few steps for quick context
202
+ console.log(' 🔄 Test execution flow:');
203
+ t.steps.slice(-3).forEach(s => {
204
+ const st = s.error ? '❌' : '✅';
205
+ const d = typeof s.duration === 'number' ? `${s.duration}ms` : '—';
206
+ console.log(` ${st} ${s.title} (${d})`);
207
+ if (s.error?.message) console.log(` 💥 ${s.error.message}`);
208
+ });
209
+ }
210
+
211
+ // Show stdout/stderr if available
212
+ if (Array.isArray(t.stdout) && t.stdout.length > 0) {
213
+ console.log(` 📤 Output: ${t.stdout.join(' ')}`);
214
+ }
215
+ if (Array.isArray(t.stderr) && t.stderr.length > 0) {
216
+ console.log(` ❗ Errors: ${t.stderr.join(' ')}`);
217
+ }
218
+
219
+ // Show attachments (screenshots, traces, etc.)
220
+ if (Array.isArray(t.attachments) && t.attachments.length > 0) {
221
+ console.log(' 📎 Attachments (use Read tool to view screenshots):');
222
+ t.attachments.forEach(att => {
223
+ console.log(` • ${att.name}: ${att.contentType}`);
224
+ if (att.path) console.log(` Path: ${this.toHostPath(att.path)}`);
225
+ });
226
+ }
227
+
228
+ console.log(
229
+ ' 💡 Tip: Read error-context.md for DOM snapshot, or trace.zip for full trace.',
230
+ );
231
+ console.log(
232
+ ' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
233
+ );
234
+ }
235
+ }, 100);
236
+ }
237
+
238
+ private updateProgressBar(): void {
239
+ const percentage =
240
+ this.totalTests > 0 ? Math.round((this.completedTests / this.totalTests) * 100) : 0;
241
+ const elapsed = this.startTime ? Math.round((Date.now() - this.startTime) / 1000) : 0;
242
+ const currentTestText = this.currentTest
243
+ ? ` | Running: ${this.currentTest.slice(0, 50)}${this.currentTest.length > 50 ? '...' : ''}`
244
+ : '';
245
+
246
+ // Use \r to overwrite the current line, \x1b[K to clear to end of line
247
+ process.stdout.write(
248
+ `\r\x1b[K${percentage}% (${this.completedTests}/${this.totalTests}) | ${elapsed}s${currentTestText}`,
249
+ );
250
+
251
+ if (this.completedTests === this.totalTests || this.currentTest === null) {
252
+ process.stdout.write('\n');
253
+ }
254
+ }
255
+
256
+ onEnd(_result: FullResult): void {
257
+ // Clear progress bar and add some space
258
+ process.stdout.write('\r\x1b[K');
259
+
260
+ const { passed, failed, skipped, timedOut } = this.results;
261
+ const total = passed.length + failed.length + skipped.length + timedOut.length;
262
+
263
+ console.log('\n📊 Test Results Summary');
264
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
265
+
266
+ if (failed.length > 0) {
267
+ console.log(`\n🚨 ${failed.length} test${failed.length === 1 ? '' : 's'} failed\n`);
268
+
269
+ failed.forEach((test, index) => {
270
+ console.log(`${index + 1}. ❌ ${test.title}`);
271
+ console.log(
272
+ ` 📁 File: ${this.getRelativePath(test.file)}:${test.line}:${test.column}`,
273
+ );
274
+ console.log(` ⏱️ Duration: ${test.duration}ms`);
275
+
276
+ if (test.error) {
277
+ console.log(` 💥 Error: ${test.error.message || 'Unknown error'}`);
278
+
279
+ if (test.error.stack) {
280
+ const stackLines = test.error.stack.split('\n');
281
+ const relevantStack = stackLines
282
+ .filter(line => line.includes('.spec.') || line.includes('test-'))
283
+ .slice(0, 3);
284
+
285
+ if (relevantStack.length > 0) {
286
+ console.log(` 📍 Stack trace:`);
287
+ relevantStack.forEach(line => {
288
+ const cleanLine = line.trim().replace(/^\s*at\s*/, '');
289
+ console.log(` ${cleanLine}`);
290
+ });
291
+ }
292
+ }
293
+ }
294
+
295
+ // Extract test steps and failures
296
+ if (test.steps && test.steps.length > 0) {
297
+ const failedSteps = test.steps.filter(step => step.error);
298
+ if (failedSteps.length > 0) {
299
+ console.log(` 🔍 Failed steps:`);
300
+ failedSteps.forEach(step => {
301
+ console.log(` • ${step.title}`);
302
+ if (step.error) {
303
+ console.log(` Error: ${step.error.message}`);
304
+ }
305
+ });
306
+ }
307
+ }
308
+
309
+ // Show stdout/stderr if available
310
+ if (test.stdout && test.stdout.length > 0) {
311
+ console.log(` 📤 Output: ${test.stdout.join(' ')}`);
312
+ }
313
+ if (test.stderr && test.stderr.length > 0) {
314
+ console.log(` ❗ Errors: ${test.stderr.join(' ')}`);
315
+ }
316
+
317
+ console.log('');
318
+ });
319
+
320
+ // Provide Copy Prompt-style debugging context
321
+ console.log('🤖 AI Debugging Context:');
322
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
323
+
324
+ failed.slice(0, 3).forEach((test, index) => {
325
+ console.log(`\n📋 Test ${index + 1}: ${test.title}`);
326
+ console.log(` 📁 File: ${this.getRelativePath(test.file)}:${test.line}`);
327
+
328
+ if (test.error) {
329
+ console.log(` 📝 Message: ${test.error.message || 'Unknown error'}`);
330
+
331
+ // Extract meaningful error context
332
+ if (test.error.stack) {
333
+ const relevantLine = test.error.stack
334
+ .split('\n')
335
+ .find(line => line.includes(this.getRelativePath(test.file)));
336
+ if (relevantLine) {
337
+ const location = relevantLine.match(/:(\d+):(\d+)/);
338
+ if (location) {
339
+ console.log(
340
+ ` 📍 Location: Line ${location[1]}, Column ${location[2]}`,
341
+ );
342
+ }
343
+ }
344
+ }
345
+ }
346
+
347
+ // Show test context (last few steps before failure)
348
+ if (test.steps && test.steps.length > 0) {
349
+ console.log(` 🔄 Test execution flow:`);
350
+ test.steps.slice(-3).forEach(step => {
351
+ const status = step.error ? '❌' : '✅';
352
+ console.log(` ${status} ${step.title} (${step.duration}ms)`);
353
+ if (step.error) {
354
+ console.log(` 💥 ${step.error.message}`);
355
+ }
356
+ });
357
+ }
358
+
359
+ // Show attachments (screenshots, traces, etc.)
360
+ if (test.attachments && test.attachments.length > 0) {
361
+ console.log(` 📎 Attachments (use Read tool to view screenshots):`);
362
+ test.attachments.forEach(attachment => {
363
+ console.log(` • ${attachment.name}: ${attachment.contentType}`);
364
+ if (attachment.path) {
365
+ console.log(` Path: ${this.toHostPath(attachment.path)}`);
366
+ }
367
+ });
368
+ }
369
+ });
370
+
371
+ console.log('\n💡 Complete Debugging Information:');
372
+ console.log(
373
+ ' • DOM Snapshot: Read error-context.md in ./test-results/ for page state',
374
+ );
375
+ console.log(' • HTML Report: Open playwright-report/index.html for full traces');
376
+ }
377
+
378
+ if (timedOut.length > 0) {
379
+ console.log(
380
+ `\n\x1b[31m⏲️ ${timedOut.length} test${timedOut.length === 1 ? '' : 's'} timed out\x1b[0m\n`,
381
+ );
382
+
383
+ timedOut.forEach((test, index) => {
384
+ console.log(`\x1b[31m${index + 1}. ⏲️ ${test.title}\x1b[0m`);
385
+ console.log(
386
+ ` 📁 File: ${this.getRelativePath(test.file)}:${test.line}:${test.column}`,
387
+ );
388
+ if (typeof test.duration === 'number')
389
+ console.log(` ⏱️ Duration: ${test.duration}ms`);
390
+
391
+ if (test.error) {
392
+ console.log(` 💥 Error: ${test.error.message || 'Timeout exceeded'}`);
393
+ }
394
+
395
+ // Show last steps for context
396
+ if (Array.isArray(test.steps) && test.steps.length > 0) {
397
+ console.log(' 🔄 Test execution flow:');
398
+ test.steps.slice(-3).forEach(step => {
399
+ const status = step.error ? '❌' : '✅';
400
+ const d = typeof step.duration === 'number' ? `${step.duration}ms` : '—';
401
+ console.log(` ${status} ${step.title} (${d})`);
402
+ if (step.error?.message) console.log(` 💥 ${step.error.message}`);
403
+ });
404
+ }
405
+
406
+ console.log('');
407
+ });
408
+ }
409
+
410
+ if (passed.length > 0) {
411
+ console.log(`\n✅ ${passed.length} test${passed.length === 1 ? '' : 's'} passed`);
412
+ }
413
+
414
+ if (skipped.length > 0) {
415
+ console.log(`\n⏭️ ${skipped.length} test${skipped.length === 1 ? '' : 's'} skipped`);
416
+ }
417
+
418
+ const totalElapsed = this.startTime ? Math.round((Date.now() - this.startTime) / 1000) : 0;
419
+ const failedText =
420
+ failed.length > 0 ? `\x1b[31m${failed.length} failed\x1b[0m` : '0 failed';
421
+ const timedOutText =
422
+ timedOut.length > 0 ? `\x1b[31m${timedOut.length} timed out\x1b[0m` : '0 timed out';
423
+ console.log(
424
+ `\n🎯 Summary: ${passed.length} passed, ${failedText}, ${timedOutText}, ${skipped.length} skipped (${total} total)`,
425
+ );
426
+ console.log(`⏱️ Total time: ${totalElapsed}s\n`);
427
+ }
428
+
429
+ private getRelativePath(fullPath: string): string {
430
+ // Convert absolute path to relative path for cleaner output
431
+ const projectRoot = process.cwd();
432
+ if (fullPath.startsWith(projectRoot)) {
433
+ return fullPath.substring(projectRoot.length + 1);
434
+ }
435
+ return fullPath;
436
+ }
437
+
438
+ printsToStdio(): boolean {
439
+ return true;
440
+ }
441
+ }
442
+
443
+ export default CopyPromptReporter;