@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.
- package/LICENSE +21 -0
- package/README.md +539 -0
- package/af +2 -0
- package/bun-upgrade.ts +130 -0
- package/commands/bun.ts +55 -0
- package/commands/changes.ts +35 -0
- package/commands/e2e.ts +12 -0
- package/commands/help.ts +236 -0
- package/commands/install-extension.ts +133 -0
- package/commands/jira.ts +577 -0
- package/commands/licenses.ts +32 -0
- package/commands/npm.ts +55 -0
- package/commands/scaffold.ts +105 -0
- package/commands/setup.tsx +156 -0
- package/commands/spec.ts +405 -0
- package/commands/stop-hook.ts +90 -0
- package/commands/todo.ts +208 -0
- package/commands/versions.ts +150 -0
- package/commands/watch.ts +344 -0
- package/commands/worktree.ts +424 -0
- package/components/change-select.tsx +71 -0
- package/components/confirm.tsx +41 -0
- package/components/file-conflict.tsx +52 -0
- package/components/input.tsx +53 -0
- package/components/layout.tsx +70 -0
- package/components/messages.tsx +48 -0
- package/components/progress.tsx +71 -0
- package/components/select.tsx +90 -0
- package/components/status-display.tsx +74 -0
- package/components/table.tsx +79 -0
- package/generated/setup-manifest.ts +67 -0
- package/git-worktree.ts +184 -0
- package/main.ts +12 -0
- package/npm-upgrade.ts +117 -0
- package/package.json +83 -0
- package/resources/copy-prompt-reporter.ts +443 -0
- package/router.ts +220 -0
- package/setup/.claude/commands/commit-work.md +47 -0
- package/setup/.claude/commands/complete-work.md +34 -0
- package/setup/.claude/commands/e2e.md +29 -0
- package/setup/.claude/commands/start-work.md +51 -0
- package/setup/.claude/skills/pm/SKILL.md +294 -0
- package/setup/.claude/skills/pm/templates/api-endpoint.md +69 -0
- package/setup/.claude/skills/pm/templates/bug-fix.md +77 -0
- package/setup/.claude/skills/pm/templates/feature.md +87 -0
- package/setup/.claude/skills/pm/templates/ui-component.md +78 -0
- package/utils/change-select-render.tsx +44 -0
- package/utils/claude.ts +9 -0
- package/utils/config.ts +58 -0
- package/utils/env.ts +53 -0
- package/utils/git.ts +120 -0
- package/utils/ink-render.tsx +50 -0
- package/utils/openspec.ts +54 -0
- package/utils/output.ts +104 -0
- package/utils/proposal.ts +160 -0
- package/utils/resources.ts +64 -0
- 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;
|