@equinor/fusion-framework-cli-plugin-copilot 2.0.2 → 2.0.3
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/CHANGELOG.md +10 -0
- package/dist/esm/commands/app/{session-logger.js → attach-session-logger.js} +16 -2
- package/dist/esm/commands/app/attach-session-logger.js.map +1 -0
- package/dist/esm/commands/app/{command.js → copilot-command.js} +22 -11
- package/dist/esm/commands/app/copilot-command.js.map +1 -0
- package/dist/esm/commands/app/{eval.js → create-session.js} +7 -5
- package/dist/esm/commands/app/create-session.js.map +1 -0
- package/dist/esm/commands/app/format-plan.js +24 -0
- package/dist/esm/commands/app/format-plan.js.map +1 -0
- package/dist/esm/commands/app/format-step-result.js +14 -0
- package/dist/esm/commands/app/format-step-result.js.map +1 -0
- package/dist/esm/commands/app/format-verdict.js +38 -0
- package/dist/esm/commands/app/format-verdict.js.map +1 -0
- package/dist/esm/commands/app/prompts/{system.dev-server.prompt.js → create-dev-server-context.js} +1 -1
- package/dist/esm/commands/app/prompts/create-dev-server-context.js.map +1 -0
- package/dist/esm/commands/app/prompts/{execute-step.prompt.js → create-execute-step-prompt.js} +1 -1
- package/dist/esm/commands/app/prompts/create-execute-step-prompt.js.map +1 -0
- package/dist/esm/commands/app/prompts/{judge.prompt.js → create-judge-prompt.js} +5 -1
- package/dist/esm/commands/app/prompts/create-judge-prompt.js.map +1 -0
- package/dist/esm/commands/app/prompts/{plan.prompt.js → create-plan-prompt.js} +1 -1
- package/dist/esm/commands/app/prompts/create-plan-prompt.js.map +1 -0
- package/dist/esm/commands/app/prompts/{step.prompt.js → create-step-prompt.js} +3 -2
- package/dist/esm/commands/app/prompts/create-step-prompt.js.map +1 -0
- package/dist/esm/commands/app/prompts/{system.prompt.js → create-system-prompt.js} +1 -1
- package/dist/esm/commands/app/prompts/create-system-prompt.js.map +1 -0
- package/dist/esm/commands/app/{login.js → run-login.js} +5 -1
- package/dist/esm/commands/app/run-login.js.map +1 -0
- package/dist/esm/commands/app/{server.js → start-app-server.js} +7 -1
- package/dist/esm/commands/app/start-app-server.js.map +1 -0
- package/dist/esm/commands/app/status-icon.js +18 -0
- package/dist/esm/commands/app/status-icon.js.map +1 -0
- package/dist/esm/commands/app/tools/{registry.js → AgentBrowserToolRegistry.js} +1 -1
- package/dist/esm/commands/app/tools/AgentBrowserToolRegistry.js.map +1 -0
- package/dist/esm/commands/app/tools/{write-file.js → create-append-file-tool.js} +5 -38
- package/dist/esm/commands/app/tools/create-append-file-tool.js.map +1 -0
- package/dist/esm/commands/app/tools/{click.js → create-click-tool.js} +1 -1
- package/dist/esm/commands/app/tools/create-click-tool.js.map +1 -0
- package/dist/esm/commands/app/tools/{errors.js → create-errors-tool.js} +1 -1
- package/dist/esm/commands/app/tools/create-errors-tool.js.map +1 -0
- package/dist/esm/commands/app/tools/{eval-js.js → create-eval-js-tool.js} +1 -1
- package/dist/esm/commands/app/tools/create-eval-js-tool.js.map +1 -0
- package/dist/esm/commands/app/tools/{fill.js → create-fill-tool.js} +1 -1
- package/dist/esm/commands/app/tools/create-fill-tool.js.map +1 -0
- package/dist/esm/commands/app/tools/{find.js → create-find-tool.js} +4 -1
- package/dist/esm/commands/app/tools/create-find-tool.js.map +1 -0
- package/dist/esm/commands/app/tools/{get-styles.js → create-get-styles-tool.js} +1 -1
- package/dist/esm/commands/app/tools/create-get-styles-tool.js.map +1 -0
- package/dist/esm/commands/app/tools/{get-url.js → create-get-url-tool.js} +1 -1
- package/dist/esm/commands/app/tools/create-get-url-tool.js.map +1 -0
- package/dist/esm/commands/app/tools/{go-back.js → create-go-back-tool.js} +1 -1
- package/dist/esm/commands/app/tools/create-go-back-tool.js.map +1 -0
- package/dist/esm/commands/app/tools/{hover.js → create-hover-tool.js} +1 -1
- package/dist/esm/commands/app/tools/create-hover-tool.js.map +1 -0
- package/dist/esm/commands/app/tools/{navigate.js → create-navigate-tool.js} +1 -1
- package/dist/esm/commands/app/tools/create-navigate-tool.js.map +1 -0
- package/dist/esm/commands/app/tools/{press-key.js → create-press-key-tool.js} +1 -1
- package/dist/esm/commands/app/tools/create-press-key-tool.js.map +1 -0
- package/dist/esm/commands/app/tools/{read-file.js → create-read-file-tool.js} +4 -2
- package/dist/esm/commands/app/tools/create-read-file-tool.js.map +1 -0
- package/dist/esm/commands/app/tools/{reload.js → create-reload-tool.js} +1 -1
- package/dist/esm/commands/app/tools/create-reload-tool.js.map +1 -0
- package/dist/esm/commands/app/tools/{screenshot.js → create-screenshot-tool.js} +7 -1
- package/dist/esm/commands/app/tools/create-screenshot-tool.js.map +1 -0
- package/dist/esm/commands/app/tools/{scroll.js → create-scroll-tool.js} +3 -1
- package/dist/esm/commands/app/tools/create-scroll-tool.js.map +1 -0
- package/dist/esm/commands/app/tools/{select.js → create-select-tool.js} +1 -1
- package/dist/esm/commands/app/tools/create-select-tool.js.map +1 -0
- package/dist/esm/commands/app/tools/{snapshot.js → create-snapshot-tool.js} +1 -1
- package/dist/esm/commands/app/tools/create-snapshot-tool.js.map +1 -0
- package/dist/esm/commands/app/tools/{type-text.js → create-type-text-tool.js} +1 -1
- package/dist/esm/commands/app/tools/create-type-text-tool.js.map +1 -0
- package/dist/esm/commands/app/tools/{wait.js → create-wait-tool.js} +5 -1
- package/dist/esm/commands/app/tools/create-wait-tool.js.map +1 -0
- package/dist/esm/commands/app/tools/create-write-file-tool.js +65 -0
- package/dist/esm/commands/app/tools/create-write-file-tool.js.map +1 -0
- package/dist/esm/commands/app/tools/index.js +19 -19
- package/dist/esm/commands/app/tools/index.js.map +1 -1
- package/dist/esm/commands/app/truncate.js +11 -0
- package/dist/esm/commands/app/truncate.js.map +1 -0
- package/dist/esm/commands/app/try-format-message.js +39 -0
- package/dist/esm/commands/app/try-format-message.js.map +1 -0
- package/dist/esm/exit-with-resolution-error.js +16 -0
- package/dist/esm/exit-with-resolution-error.js.map +1 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/resolve-eval-command-input.js +37 -0
- package/dist/esm/resolve-eval-command-input.js.map +1 -0
- package/dist/esm/resolve-eval-files.js +49 -0
- package/dist/esm/resolve-eval-files.js.map +1 -0
- package/dist/esm/utils/ab-error-message.js +34 -0
- package/dist/esm/utils/ab-error-message.js.map +1 -0
- package/dist/esm/utils/{agent-browser.js → ab.js} +3 -32
- package/dist/esm/utils/ab.js.map +1 -0
- package/dist/esm/utils/create-run-dir.js +24 -0
- package/dist/esm/utils/create-run-dir.js.map +1 -0
- package/dist/esm/utils/{process.js → helpers.js} +28 -11
- package/dist/esm/utils/helpers.js.map +1 -0
- package/dist/esm/utils/index.js +8 -5
- package/dist/esm/utils/index.js.map +1 -1
- package/dist/esm/utils/{browser-poll.js → poll-console.js} +5 -3
- package/dist/esm/utils/poll-console.js.map +1 -0
- package/dist/esm/utils/{daemon.js → reset-daemon.js} +5 -14
- package/dist/esm/utils/reset-daemon.js.map +1 -0
- package/dist/esm/utils/resolve-app-key.js +20 -0
- package/dist/esm/utils/resolve-app-key.js.map +1 -0
- package/dist/esm/utils/wait-for-server.js +26 -0
- package/dist/esm/utils/wait-for-server.js.map +1 -0
- package/dist/esm/version.js +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/commands/app/format-plan.d.ts +8 -0
- package/dist/types/commands/app/format-step-result.d.ts +8 -0
- package/dist/types/commands/app/format-verdict.d.ts +8 -0
- package/dist/types/commands/app/prompts/{step.prompt.d.ts → create-step-prompt.d.ts} +1 -1
- package/dist/types/commands/app/status-icon.d.ts +8 -0
- package/dist/types/commands/app/tools/create-append-file-tool.d.ts +10 -0
- package/dist/types/commands/app/tools/{write-file.d.ts → create-write-file-tool.d.ts} +0 -9
- package/dist/types/commands/app/truncate.d.ts +8 -0
- package/dist/types/commands/app/try-format-message.d.ts +10 -0
- package/dist/types/exit-with-resolution-error.d.ts +8 -0
- package/dist/types/{eval-resolve.d.ts → resolve-eval-command-input.d.ts} +0 -8
- package/dist/types/resolve-eval-files.d.ts +8 -0
- package/dist/types/utils/ab-error-message.d.ts +11 -0
- package/dist/types/utils/{agent-browser.d.ts → ab.d.ts} +0 -13
- package/dist/types/utils/create-run-dir.d.ts +8 -0
- package/dist/types/utils/{process.d.ts → helpers.d.ts} +13 -6
- package/dist/types/utils/index.d.ts +8 -5
- package/dist/types/utils/{daemon.d.ts → reset-daemon.d.ts} +0 -5
- package/dist/types/utils/resolve-app-key.d.ts +9 -0
- package/dist/types/utils/wait-for-server.d.ts +9 -0
- package/dist/types/version.d.ts +1 -1
- package/package.json +4 -4
- package/src/commands/app/{session-logger.ts → attach-session-logger.ts} +15 -1
- package/src/commands/app/{command.ts → copilot-command.ts} +21 -12
- package/src/commands/app/{eval.ts → create-session.ts} +6 -4
- package/src/commands/app/format-plan.ts +26 -0
- package/src/commands/app/format-step-result.ts +16 -0
- package/src/commands/app/format-verdict.ts +43 -0
- package/src/commands/app/prompts/{judge.prompt.ts → create-judge-prompt.ts} +4 -0
- package/src/commands/app/prompts/{step.prompt.ts → create-step-prompt.ts} +2 -1
- package/src/commands/app/{login.ts → run-login.ts} +4 -0
- package/src/commands/app/{server.ts → start-app-server.ts} +6 -0
- package/src/commands/app/status-icon.ts +16 -0
- package/src/commands/app/tools/{write-file.ts → create-append-file-tool.ts} +4 -39
- package/src/commands/app/tools/{find.ts → create-find-tool.ts} +3 -0
- package/src/commands/app/tools/{read-file.ts → create-read-file-tool.ts} +3 -1
- package/src/commands/app/tools/{screenshot.ts → create-screenshot-tool.ts} +6 -0
- package/src/commands/app/tools/{scroll.ts → create-scroll-tool.ts} +2 -0
- package/src/commands/app/tools/{wait.ts → create-wait-tool.ts} +4 -0
- package/src/commands/app/tools/create-write-file-tool.ts +69 -0
- package/src/commands/app/tools/index.ts +19 -19
- package/src/commands/app/truncate.ts +10 -0
- package/src/commands/app/try-format-message.ts +42 -0
- package/src/exit-with-resolution-error.ts +15 -0
- package/src/index.ts +1 -1
- package/src/resolve-eval-command-input.ts +59 -0
- package/src/resolve-eval-files.ts +57 -0
- package/src/utils/ab-error-message.ts +31 -0
- package/src/utils/{agent-browser.ts → ab.ts} +2 -30
- package/src/utils/create-run-dir.ts +24 -0
- package/src/utils/{process.ts → helpers.ts} +28 -10
- package/src/utils/index.ts +8 -5
- package/src/utils/{browser-poll.ts → poll-console.ts} +4 -2
- package/src/utils/{daemon.ts → reset-daemon.ts} +4 -13
- package/src/utils/resolve-app-key.ts +20 -0
- package/src/utils/wait-for-server.ts +24 -0
- package/src/version.ts +1 -1
- package/dist/esm/commands/app/command.js.map +0 -1
- package/dist/esm/commands/app/eval.js.map +0 -1
- package/dist/esm/commands/app/format.js +0 -93
- package/dist/esm/commands/app/format.js.map +0 -1
- package/dist/esm/commands/app/login.js.map +0 -1
- package/dist/esm/commands/app/prompts/execute-step.prompt.js.map +0 -1
- package/dist/esm/commands/app/prompts/judge.prompt.js.map +0 -1
- package/dist/esm/commands/app/prompts/plan.prompt.js.map +0 -1
- package/dist/esm/commands/app/prompts/step.prompt.js.map +0 -1
- package/dist/esm/commands/app/prompts/system.dev-server.prompt.js.map +0 -1
- package/dist/esm/commands/app/prompts/system.prompt.js.map +0 -1
- package/dist/esm/commands/app/server.js.map +0 -1
- package/dist/esm/commands/app/session-logger.js.map +0 -1
- package/dist/esm/commands/app/tools/click.js.map +0 -1
- package/dist/esm/commands/app/tools/errors.js.map +0 -1
- package/dist/esm/commands/app/tools/eval-js.js.map +0 -1
- package/dist/esm/commands/app/tools/fill.js.map +0 -1
- package/dist/esm/commands/app/tools/find.js.map +0 -1
- package/dist/esm/commands/app/tools/get-styles.js.map +0 -1
- package/dist/esm/commands/app/tools/get-url.js.map +0 -1
- package/dist/esm/commands/app/tools/go-back.js.map +0 -1
- package/dist/esm/commands/app/tools/hover.js.map +0 -1
- package/dist/esm/commands/app/tools/navigate.js.map +0 -1
- package/dist/esm/commands/app/tools/press-key.js.map +0 -1
- package/dist/esm/commands/app/tools/read-file.js.map +0 -1
- package/dist/esm/commands/app/tools/registry.js.map +0 -1
- package/dist/esm/commands/app/tools/reload.js.map +0 -1
- package/dist/esm/commands/app/tools/screenshot.js.map +0 -1
- package/dist/esm/commands/app/tools/scroll.js.map +0 -1
- package/dist/esm/commands/app/tools/select.js.map +0 -1
- package/dist/esm/commands/app/tools/snapshot.js.map +0 -1
- package/dist/esm/commands/app/tools/type-text.js.map +0 -1
- package/dist/esm/commands/app/tools/wait.js.map +0 -1
- package/dist/esm/commands/app/tools/write-file.js.map +0 -1
- package/dist/esm/eval-resolve.js +0 -83
- package/dist/esm/eval-resolve.js.map +0 -1
- package/dist/esm/utils/agent-browser.js.map +0 -1
- package/dist/esm/utils/browser-poll.js.map +0 -1
- package/dist/esm/utils/daemon.js.map +0 -1
- package/dist/esm/utils/process.js.map +0 -1
- package/dist/esm/utils/server.js +0 -65
- package/dist/esm/utils/server.js.map +0 -1
- package/dist/types/commands/app/format.d.ts +0 -14
- package/dist/types/utils/server.d.ts +0 -26
- package/src/commands/app/format.ts +0 -110
- package/src/eval-resolve.ts +0 -113
- package/src/utils/server.ts +0 -66
- /package/dist/types/commands/app/{session-logger.d.ts → attach-session-logger.d.ts} +0 -0
- /package/dist/types/commands/app/{command.d.ts → copilot-command.d.ts} +0 -0
- /package/dist/types/commands/app/{eval.d.ts → create-session.d.ts} +0 -0
- /package/dist/types/commands/app/prompts/{system.dev-server.prompt.d.ts → create-dev-server-context.d.ts} +0 -0
- /package/dist/types/commands/app/prompts/{execute-step.prompt.d.ts → create-execute-step-prompt.d.ts} +0 -0
- /package/dist/types/commands/app/prompts/{judge.prompt.d.ts → create-judge-prompt.d.ts} +0 -0
- /package/dist/types/commands/app/prompts/{plan.prompt.d.ts → create-plan-prompt.d.ts} +0 -0
- /package/dist/types/commands/app/prompts/{system.prompt.d.ts → create-system-prompt.d.ts} +0 -0
- /package/dist/types/commands/app/{login.d.ts → run-login.d.ts} +0 -0
- /package/dist/types/commands/app/{server.d.ts → start-app-server.d.ts} +0 -0
- /package/dist/types/commands/app/tools/{registry.d.ts → AgentBrowserToolRegistry.d.ts} +0 -0
- /package/dist/types/commands/app/tools/{click.d.ts → create-click-tool.d.ts} +0 -0
- /package/dist/types/commands/app/tools/{errors.d.ts → create-errors-tool.d.ts} +0 -0
- /package/dist/types/commands/app/tools/{eval-js.d.ts → create-eval-js-tool.d.ts} +0 -0
- /package/dist/types/commands/app/tools/{fill.d.ts → create-fill-tool.d.ts} +0 -0
- /package/dist/types/commands/app/tools/{find.d.ts → create-find-tool.d.ts} +0 -0
- /package/dist/types/commands/app/tools/{get-styles.d.ts → create-get-styles-tool.d.ts} +0 -0
- /package/dist/types/commands/app/tools/{get-url.d.ts → create-get-url-tool.d.ts} +0 -0
- /package/dist/types/commands/app/tools/{go-back.d.ts → create-go-back-tool.d.ts} +0 -0
- /package/dist/types/commands/app/tools/{hover.d.ts → create-hover-tool.d.ts} +0 -0
- /package/dist/types/commands/app/tools/{navigate.d.ts → create-navigate-tool.d.ts} +0 -0
- /package/dist/types/commands/app/tools/{press-key.d.ts → create-press-key-tool.d.ts} +0 -0
- /package/dist/types/commands/app/tools/{read-file.d.ts → create-read-file-tool.d.ts} +0 -0
- /package/dist/types/commands/app/tools/{reload.d.ts → create-reload-tool.d.ts} +0 -0
- /package/dist/types/commands/app/tools/{screenshot.d.ts → create-screenshot-tool.d.ts} +0 -0
- /package/dist/types/commands/app/tools/{scroll.d.ts → create-scroll-tool.d.ts} +0 -0
- /package/dist/types/commands/app/tools/{select.d.ts → create-select-tool.d.ts} +0 -0
- /package/dist/types/commands/app/tools/{snapshot.d.ts → create-snapshot-tool.d.ts} +0 -0
- /package/dist/types/commands/app/tools/{type-text.d.ts → create-type-text-tool.d.ts} +0 -0
- /package/dist/types/commands/app/tools/{wait.d.ts → create-wait-tool.d.ts} +0 -0
- /package/dist/types/utils/{browser-poll.d.ts → poll-console.d.ts} +0 -0
- /package/src/commands/app/prompts/{system.dev-server.prompt.ts → create-dev-server-context.ts} +0 -0
- /package/src/commands/app/prompts/{execute-step.prompt.ts → create-execute-step-prompt.ts} +0 -0
- /package/src/commands/app/prompts/{plan.prompt.ts → create-plan-prompt.ts} +0 -0
- /package/src/commands/app/prompts/{system.prompt.ts → create-system-prompt.ts} +0 -0
- /package/src/commands/app/tools/{registry.ts → AgentBrowserToolRegistry.ts} +0 -0
- /package/src/commands/app/tools/{click.ts → create-click-tool.ts} +0 -0
- /package/src/commands/app/tools/{errors.ts → create-errors-tool.ts} +0 -0
- /package/src/commands/app/tools/{eval-js.ts → create-eval-js-tool.ts} +0 -0
- /package/src/commands/app/tools/{fill.ts → create-fill-tool.ts} +0 -0
- /package/src/commands/app/tools/{get-styles.ts → create-get-styles-tool.ts} +0 -0
- /package/src/commands/app/tools/{get-url.ts → create-get-url-tool.ts} +0 -0
- /package/src/commands/app/tools/{go-back.ts → create-go-back-tool.ts} +0 -0
- /package/src/commands/app/tools/{hover.ts → create-hover-tool.ts} +0 -0
- /package/src/commands/app/tools/{navigate.ts → create-navigate-tool.ts} +0 -0
- /package/src/commands/app/tools/{press-key.ts → create-press-key-tool.ts} +0 -0
- /package/src/commands/app/tools/{reload.ts → create-reload-tool.ts} +0 -0
- /package/src/commands/app/tools/{select.ts → create-select-tool.ts} +0 -0
- /package/src/commands/app/tools/{snapshot.ts → create-snapshot-tool.ts} +0 -0
- /package/src/commands/app/tools/{type-text.ts → create-type-text-tool.ts} +0 -0
|
@@ -28,11 +28,13 @@ import {
|
|
|
28
28
|
*/
|
|
29
29
|
export async function runLogin(absAppPath: string, port: number, host: string): Promise<void> {
|
|
30
30
|
const pkgJsonPath = join(absAppPath, 'package.json');
|
|
31
|
+
// Login needs a valid Fusion app directory; bail out clearly if this isn't one
|
|
31
32
|
if (!existsSync(pkgJsonPath)) {
|
|
32
33
|
console.error(`❌ No package.json found at ${absAppPath}`);
|
|
33
34
|
process.exit(1);
|
|
34
35
|
}
|
|
35
36
|
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8')) as { name?: string };
|
|
37
|
+
// The app name is required to resolve the appKey later
|
|
36
38
|
if (!pkgJson.name) {
|
|
37
39
|
console.error(`❌ package.json at ${absAppPath} has no "name" field`);
|
|
38
40
|
process.exit(1);
|
|
@@ -51,6 +53,7 @@ export async function runLogin(absAppPath: string, port: number, host: string):
|
|
|
51
53
|
|
|
52
54
|
const serverUrl = `http://localhost:${port}`;
|
|
53
55
|
const ready = await waitForServer(serverUrl, 60);
|
|
56
|
+
// Abort login when the dev server never comes up within the timeout
|
|
54
57
|
if (!ready) {
|
|
55
58
|
console.error('❌ Server failed to start within 60s');
|
|
56
59
|
cleanup(serverProcess);
|
|
@@ -79,6 +82,7 @@ export async function runLogin(absAppPath: string, port: number, host: string):
|
|
|
79
82
|
300_000,
|
|
80
83
|
);
|
|
81
84
|
|
|
85
|
+
// Report success or a soft timeout without failing the login flow
|
|
82
86
|
if (initialized) {
|
|
83
87
|
await sleep(2000);
|
|
84
88
|
console.log('✅ Login complete — session saved.');
|
|
@@ -22,10 +22,12 @@ export interface AppServerResult {
|
|
|
22
22
|
*/
|
|
23
23
|
function freePort(port: number): void {
|
|
24
24
|
try {
|
|
25
|
+
// Drop the empty string lsof emits when no matching process was found
|
|
25
26
|
const pids = execSync(`lsof -ti tcp:${port}`, { encoding: 'utf-8' })
|
|
26
27
|
.trim()
|
|
27
28
|
.split('\n')
|
|
28
29
|
.filter(Boolean);
|
|
30
|
+
// Terminate each stale process bound to the port
|
|
29
31
|
for (const pid of pids) {
|
|
30
32
|
try {
|
|
31
33
|
process.kill(parseInt(pid, 10), 'SIGTERM');
|
|
@@ -33,6 +35,7 @@ function freePort(port: number): void {
|
|
|
33
35
|
// already dead
|
|
34
36
|
}
|
|
35
37
|
}
|
|
38
|
+
// Only log when we actually killed something
|
|
36
39
|
if (pids.length) {
|
|
37
40
|
console.log(`🔄 Killed stale process(es) on port ${port}: ${pids.join(', ')}`);
|
|
38
41
|
}
|
|
@@ -63,11 +66,13 @@ export async function startAppServer(
|
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
const pkgJsonPath = join(absAppPath, 'package.json');
|
|
69
|
+
// Startup needs a valid Fusion app directory; bail out clearly if this isn't one
|
|
66
70
|
if (!existsSync(pkgJsonPath)) {
|
|
67
71
|
console.error(`❌ No package.json found at ${absAppPath}`);
|
|
68
72
|
process.exit(1);
|
|
69
73
|
}
|
|
70
74
|
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8')) as { name?: string };
|
|
75
|
+
// The app name is required to resolve the appKey later
|
|
71
76
|
if (!pkgJson.name) {
|
|
72
77
|
console.error(`❌ package.json at ${absAppPath} has no "name" field`);
|
|
73
78
|
process.exit(1);
|
|
@@ -92,6 +97,7 @@ export async function startAppServer(
|
|
|
92
97
|
|
|
93
98
|
const serverUrl = `http://localhost:${options.port}`;
|
|
94
99
|
const ready = await waitForServer(serverUrl, 60);
|
|
100
|
+
// Abort startup when the dev server never comes up within the timeout
|
|
95
101
|
if (!ready) {
|
|
96
102
|
console.error('❌ Server failed to start within 60s');
|
|
97
103
|
cleanup(serverProcess);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Renders a status glyph for a step or verdict outcome.
|
|
5
|
+
*
|
|
6
|
+
* @param ok - Outcome value: `true` (pass), `false` (fail), or a string label
|
|
7
|
+
* such as `'blocked'` or `'flaky'` for anything in between.
|
|
8
|
+
* @returns A single coloured glyph representing the outcome.
|
|
9
|
+
*/
|
|
10
|
+
export function statusIcon(ok: boolean | string): string {
|
|
11
|
+
// A literal boolean outcome maps directly to pass/fail glyphs
|
|
12
|
+
if (ok === true) return chalk.green('✔');
|
|
13
|
+
// An explicit failure gets its own glyph, distinct from unknown/other states
|
|
14
|
+
if (ok === false) return chalk.red('✖');
|
|
15
|
+
return chalk.yellow('⚠'); // "blocked", "flaky", etc.
|
|
16
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { appendFileSync, mkdirSync
|
|
1
|
+
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
2
2
|
import { dirname, isAbsolute, join } from 'node:path';
|
|
3
3
|
|
|
4
4
|
import type { DefineTool } from './types.js';
|
|
@@ -18,55 +18,20 @@ import type { DefineTool } from './types.js';
|
|
|
18
18
|
function resolveSafe(baseDir: string, input: string): string {
|
|
19
19
|
// If the model echoed the full absolute path from the prompt, strip the base prefix.
|
|
20
20
|
const relative = isAbsolute(input)
|
|
21
|
-
? input.startsWith(baseDir
|
|
21
|
+
? input.startsWith(`${baseDir}/`) || input.startsWith(`${baseDir}\\`)
|
|
22
22
|
? input.slice(baseDir.length + 1)
|
|
23
23
|
: (() => {
|
|
24
24
|
throw new Error(`Absolute path outside output directory: ${input}`);
|
|
25
25
|
})()
|
|
26
26
|
: input;
|
|
27
27
|
const resolved = join(baseDir, relative);
|
|
28
|
+
// Reject any resolved path that escapes the allowed base directory
|
|
28
29
|
if (!resolved.startsWith(baseDir)) {
|
|
29
30
|
throw new Error(`Path escapes the output directory: ${input}`);
|
|
30
31
|
}
|
|
31
32
|
return resolved;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
/**
|
|
35
|
-
* Creates the write-file tool so the agent can save artifacts
|
|
36
|
-
* (plan.json, verdict.json, etc.) to the run output directory.
|
|
37
|
-
*
|
|
38
|
-
* @param baseDir - The run output directory that scopes all writes
|
|
39
|
-
* @param defineTool - Copilot SDK helper used to declare tools
|
|
40
|
-
* @returns Copilot tool definition for writing a file
|
|
41
|
-
*/
|
|
42
|
-
export function createWriteFileTool(baseDir: string, defineTool: DefineTool) {
|
|
43
|
-
return defineTool('write_file', {
|
|
44
|
-
description:
|
|
45
|
-
'Write content to a file in the output directory. Creates parent directories as needed.',
|
|
46
|
-
parameters: {
|
|
47
|
-
type: 'object' as const,
|
|
48
|
-
properties: {
|
|
49
|
-
path: {
|
|
50
|
-
type: 'string',
|
|
51
|
-
description: 'File path relative to the output directory (e.g. "plan.json")',
|
|
52
|
-
},
|
|
53
|
-
content: {
|
|
54
|
-
type: 'string',
|
|
55
|
-
description: 'The full file content to write',
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
required: ['path', 'content'],
|
|
59
|
-
},
|
|
60
|
-
handler: async (args) => {
|
|
61
|
-
const { path, content } = args as { path: string; content: string };
|
|
62
|
-
const resolved = resolveSafe(baseDir, path);
|
|
63
|
-
mkdirSync(dirname(resolved), { recursive: true });
|
|
64
|
-
writeFileSync(resolved, content);
|
|
65
|
-
return `Wrote ${content.length} bytes to ${path}`;
|
|
66
|
-
},
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
35
|
/**
|
|
71
36
|
* Creates the append-file tool so the agent can append lines
|
|
72
37
|
* to artifacts like executions.jsonl.
|
|
@@ -97,7 +62,7 @@ export function createAppendFileTool(baseDir: string, defineTool: DefineTool) {
|
|
|
97
62
|
const { path, content } = args as { path: string; content: string };
|
|
98
63
|
const resolved = resolveSafe(baseDir, path);
|
|
99
64
|
mkdirSync(dirname(resolved), { recursive: true });
|
|
100
|
-
appendFileSync(resolved, content.endsWith('\n') ? content : content
|
|
65
|
+
appendFileSync(resolved, content.endsWith('\n') ? content : `${content}\n`);
|
|
101
66
|
return `Appended to ${path}`;
|
|
102
67
|
},
|
|
103
68
|
});
|
|
@@ -40,8 +40,11 @@ export function createFindTool(context: AgentBrowserToolContext, defineTool: Def
|
|
|
40
40
|
fillValue?: string;
|
|
41
41
|
};
|
|
42
42
|
const command = ['find', by, value];
|
|
43
|
+
// Append optional refinements only when the caller actually provided them
|
|
43
44
|
if (action) command.push(action);
|
|
45
|
+
// Attach the accessible-name filter when the caller supplied one
|
|
44
46
|
if (name) command.push('--name', name);
|
|
47
|
+
// Attach the fill value when the action is 'fill'
|
|
45
48
|
if (fillValue) command.push(fillValue);
|
|
46
49
|
return context.invoke(command);
|
|
47
50
|
},
|
|
@@ -27,13 +27,14 @@ const IMAGE_MIME: Record<string, string> = {
|
|
|
27
27
|
*/
|
|
28
28
|
function resolveSafe(baseDir: string, input: string): string {
|
|
29
29
|
const relative = isAbsolute(input)
|
|
30
|
-
? input.startsWith(baseDir
|
|
30
|
+
? input.startsWith(`${baseDir}/`) || input.startsWith(`${baseDir}\\`)
|
|
31
31
|
? input.slice(baseDir.length + 1)
|
|
32
32
|
: (() => {
|
|
33
33
|
throw new Error(`Absolute path outside output directory: ${input}`);
|
|
34
34
|
})()
|
|
35
35
|
: input;
|
|
36
36
|
const resolved = join(baseDir, relative);
|
|
37
|
+
// Reject any resolved path that escapes the allowed base directory
|
|
37
38
|
if (!resolved.startsWith(baseDir)) {
|
|
38
39
|
throw new Error(`Path escapes the output directory: ${input}`);
|
|
39
40
|
}
|
|
@@ -72,6 +73,7 @@ export function createReadFileTool(baseDir: string, defineTool: DefineTool) {
|
|
|
72
73
|
const { path } = args as { path: string };
|
|
73
74
|
const resolved = resolveSafe(baseDir, path);
|
|
74
75
|
|
|
76
|
+
// Report a friendly message instead of throwing when the path doesn't exist
|
|
75
77
|
if (!existsSync(resolved)) {
|
|
76
78
|
return `File not found: ${path}`;
|
|
77
79
|
}
|
|
@@ -26,15 +26,18 @@ function resolveScreenshotArtifactName(
|
|
|
26
26
|
requestedPath: string | undefined,
|
|
27
27
|
format: 'jpeg' | 'png',
|
|
28
28
|
): string | undefined {
|
|
29
|
+
// No requested path means the caller doesn't need a specific artifact name
|
|
29
30
|
if (!requestedPath) return undefined;
|
|
30
31
|
const fileName = basename(requestedPath);
|
|
31
32
|
const currentExtension = extname(fileName).toLowerCase();
|
|
32
33
|
const expectedExtension = format === 'jpeg' ? '.jpg' : '.png';
|
|
33
34
|
|
|
35
|
+
// The extension already matches the selected format; reuse it verbatim
|
|
34
36
|
if (currentExtension === expectedExtension) {
|
|
35
37
|
return fileName;
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
// Swap a mismatched extension for the one that matches the selected format
|
|
38
41
|
if (currentExtension.length > 0) {
|
|
39
42
|
return `${fileName.slice(0, -currentExtension.length)}${expectedExtension}`;
|
|
40
43
|
}
|
|
@@ -53,6 +56,7 @@ function resolveScreenshotDirectory(
|
|
|
53
56
|
evidenceDir: string,
|
|
54
57
|
requestedDirectory: string | undefined,
|
|
55
58
|
): string {
|
|
59
|
+
// No requested directory means the default evidence directory is used
|
|
56
60
|
if (!requestedDirectory) {
|
|
57
61
|
return evidenceDir;
|
|
58
62
|
}
|
|
@@ -150,10 +154,12 @@ export function createScreenshotTool(context: AgentBrowserToolContext, defineToo
|
|
|
150
154
|
screenshotDirectory,
|
|
151
155
|
];
|
|
152
156
|
|
|
157
|
+
// Overlay numbered element labels only when the model explicitly asked for it
|
|
153
158
|
if (annotate) {
|
|
154
159
|
command.push('--annotate');
|
|
155
160
|
}
|
|
156
161
|
|
|
162
|
+
// JPEG quality only applies when the selected format is actually JPEG
|
|
157
163
|
if (screenshotFormat === 'jpeg') {
|
|
158
164
|
const screenshotQuality = Math.min(100, Math.max(0, quality ?? 60));
|
|
159
165
|
command.push('--screenshot-quality', String(screenshotQuality));
|
|
@@ -33,11 +33,13 @@ export function createScrollTool(context: AgentBrowserToolContext, defineTool: D
|
|
|
33
33
|
amount?: number;
|
|
34
34
|
target?: string;
|
|
35
35
|
};
|
|
36
|
+
// A target ref overrides plain direction/amount scrolling
|
|
36
37
|
if (target) {
|
|
37
38
|
return context.invoke(['scrollintoview', target]);
|
|
38
39
|
}
|
|
39
40
|
const scrollDirection = direction ?? 'down';
|
|
40
41
|
const command = ['scroll', scrollDirection];
|
|
42
|
+
// Only pass an explicit pixel amount when the caller provided one
|
|
41
43
|
if (amount) command.push(String(amount));
|
|
42
44
|
return context.invoke(command);
|
|
43
45
|
},
|
|
@@ -30,9 +30,13 @@ export function createWaitTool(context: AgentBrowserToolContext, defineTool: Def
|
|
|
30
30
|
load?: string;
|
|
31
31
|
timeout?: number;
|
|
32
32
|
};
|
|
33
|
+
// Prefer whichever single wait condition the caller specified, in priority order
|
|
33
34
|
if (text) return context.invoke(['wait', '--text', text], 60_000);
|
|
35
|
+
// Fall back to waiting on a page load state
|
|
34
36
|
if (load) return context.invoke(['wait', '--load', load], 60_000);
|
|
37
|
+
// Fall back to waiting on a specific element/selector
|
|
35
38
|
if (selector) return context.invoke(['wait', selector], 60_000);
|
|
39
|
+
// A plain timeout falls back to a bare delay instead of an invoke command
|
|
36
40
|
if (timeout) {
|
|
37
41
|
await new Promise((resolve) => setTimeout(resolve, timeout));
|
|
38
42
|
return `Waited ${timeout}ms`;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, isAbsolute, join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import type { DefineTool } from './types.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Resolves a path within a base directory, rejecting escapes.
|
|
8
|
+
*
|
|
9
|
+
* Accepts both relative paths and absolute paths that fall within
|
|
10
|
+
* the base directory. Strips the base prefix from absolute paths
|
|
11
|
+
* before resolving to handle models that echo full paths from prompts.
|
|
12
|
+
*
|
|
13
|
+
* @param baseDir - The allowed root directory
|
|
14
|
+
* @param input - The user-provided path (relative or absolute within baseDir)
|
|
15
|
+
* @returns The resolved absolute path
|
|
16
|
+
* @throws {Error} When the path escapes the base directory
|
|
17
|
+
*/
|
|
18
|
+
function resolveSafe(baseDir: string, input: string): string {
|
|
19
|
+
// If the model echoed the full absolute path from the prompt, strip the base prefix.
|
|
20
|
+
const relative = isAbsolute(input)
|
|
21
|
+
? input.startsWith(`${baseDir}/`) || input.startsWith(`${baseDir}\\`)
|
|
22
|
+
? input.slice(baseDir.length + 1)
|
|
23
|
+
: (() => {
|
|
24
|
+
throw new Error(`Absolute path outside output directory: ${input}`);
|
|
25
|
+
})()
|
|
26
|
+
: input;
|
|
27
|
+
const resolved = join(baseDir, relative);
|
|
28
|
+
// Reject any resolved path that escapes the allowed base directory
|
|
29
|
+
if (!resolved.startsWith(baseDir)) {
|
|
30
|
+
throw new Error(`Path escapes the output directory: ${input}`);
|
|
31
|
+
}
|
|
32
|
+
return resolved;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Creates the write-file tool so the agent can save artifacts
|
|
37
|
+
* (plan.json, verdict.json, etc.) to the run output directory.
|
|
38
|
+
*
|
|
39
|
+
* @param baseDir - The run output directory that scopes all writes
|
|
40
|
+
* @param defineTool - Copilot SDK helper used to declare tools
|
|
41
|
+
* @returns Copilot tool definition for writing a file
|
|
42
|
+
*/
|
|
43
|
+
export function createWriteFileTool(baseDir: string, defineTool: DefineTool) {
|
|
44
|
+
return defineTool('write_file', {
|
|
45
|
+
description:
|
|
46
|
+
'Write content to a file in the output directory. Creates parent directories as needed.',
|
|
47
|
+
parameters: {
|
|
48
|
+
type: 'object' as const,
|
|
49
|
+
properties: {
|
|
50
|
+
path: {
|
|
51
|
+
type: 'string',
|
|
52
|
+
description: 'File path relative to the output directory (e.g. "plan.json")',
|
|
53
|
+
},
|
|
54
|
+
content: {
|
|
55
|
+
type: 'string',
|
|
56
|
+
description: 'The full file content to write',
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
required: ['path', 'content'],
|
|
60
|
+
},
|
|
61
|
+
handler: async (args) => {
|
|
62
|
+
const { path, content } = args as { path: string; content: string };
|
|
63
|
+
const resolved = resolveSafe(baseDir, path);
|
|
64
|
+
mkdirSync(dirname(resolved), { recursive: true });
|
|
65
|
+
writeFileSync(resolved, content);
|
|
66
|
+
return `Wrote ${content.length} bytes to ${path}`;
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import { createClickTool } from './click.js';
|
|
2
|
-
import { createErrorsTool } from './errors.js';
|
|
3
|
-
import { createFillTool } from './fill.js';
|
|
4
|
-
import { createFindTool } from './find.js';
|
|
5
|
-
import { createGetUrlTool } from './get-url.js';
|
|
6
|
-
import { createGoBackTool } from './go-back.js';
|
|
7
|
-
import { createHoverTool } from './hover.js';
|
|
8
|
-
import { createNavigateTool } from './navigate.js';
|
|
9
|
-
import { createPressKeyTool } from './press-key.js';
|
|
10
|
-
import { createReloadTool } from './reload.js';
|
|
11
|
-
import { createEvalJsTool } from './eval-js.js';
|
|
12
|
-
import { createGetStylesTool } from './get-styles.js';
|
|
13
|
-
import { AgentBrowserToolRegistry } from './
|
|
14
|
-
import { createScreenshotTool } from './screenshot.js';
|
|
15
|
-
import { createScrollTool } from './scroll.js';
|
|
16
|
-
import { createSelectTool } from './select.js';
|
|
17
|
-
import { createSnapshotTool } from './snapshot.js';
|
|
18
|
-
import { createTypeTextTool } from './type-text.js';
|
|
1
|
+
import { createClickTool } from './create-click-tool.js';
|
|
2
|
+
import { createErrorsTool } from './create-errors-tool.js';
|
|
3
|
+
import { createFillTool } from './create-fill-tool.js';
|
|
4
|
+
import { createFindTool } from './create-find-tool.js';
|
|
5
|
+
import { createGetUrlTool } from './create-get-url-tool.js';
|
|
6
|
+
import { createGoBackTool } from './create-go-back-tool.js';
|
|
7
|
+
import { createHoverTool } from './create-hover-tool.js';
|
|
8
|
+
import { createNavigateTool } from './create-navigate-tool.js';
|
|
9
|
+
import { createPressKeyTool } from './create-press-key-tool.js';
|
|
10
|
+
import { createReloadTool } from './create-reload-tool.js';
|
|
11
|
+
import { createEvalJsTool } from './create-eval-js-tool.js';
|
|
12
|
+
import { createGetStylesTool } from './create-get-styles-tool.js';
|
|
13
|
+
import { AgentBrowserToolRegistry } from './AgentBrowserToolRegistry.js';
|
|
14
|
+
import { createScreenshotTool } from './create-screenshot-tool.js';
|
|
15
|
+
import { createScrollTool } from './create-scroll-tool.js';
|
|
16
|
+
import { createSelectTool } from './create-select-tool.js';
|
|
17
|
+
import { createSnapshotTool } from './create-snapshot-tool.js';
|
|
18
|
+
import { createTypeTextTool } from './create-type-text-tool.js';
|
|
19
19
|
import type { AgentBrowserToolContext, AgentBrowserToolList, DefineTool } from './types.js';
|
|
20
|
-
import { createWaitTool } from './wait.js';
|
|
20
|
+
import { createWaitTool } from './create-wait-tool.js';
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Creates the full set of Copilot browser tools for an eval session.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Truncates a string to a maximum length, appending an ellipsis when cut.
|
|
3
|
+
*
|
|
4
|
+
* @param str - The string to truncate.
|
|
5
|
+
* @param max - Maximum length of the returned string, including the ellipsis.
|
|
6
|
+
* @returns The original string, or a shortened version ending in `…`.
|
|
7
|
+
*/
|
|
8
|
+
export function truncate(str: string, max: number): string {
|
|
9
|
+
return str.length > max ? `${str.slice(0, max - 1)}…` : str;
|
|
10
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Plan, StepResult, Verdict } from './types.js';
|
|
2
|
+
import { formatPlan } from './format-plan.js';
|
|
3
|
+
import { formatStepResult } from './format-step-result.js';
|
|
4
|
+
import { formatVerdict } from './format-verdict.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Attempts to detect the JSON shape of an assistant message and format it.
|
|
8
|
+
*
|
|
9
|
+
* Returns `null` when the content is not a recognised structure, so the
|
|
10
|
+
* caller can fall back to plain-text rendering.
|
|
11
|
+
*
|
|
12
|
+
* @param content - Raw assistant message content, expected to be a JSON string.
|
|
13
|
+
* @returns A formatted string for a recognised plan, step result, or verdict, or `null` otherwise.
|
|
14
|
+
*/
|
|
15
|
+
export function tryFormatMessage(content: string): string | null {
|
|
16
|
+
let parsed: unknown;
|
|
17
|
+
try {
|
|
18
|
+
parsed = JSON.parse(content.trim());
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
// Only object shapes can be matched against the recognised message formats
|
|
23
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
24
|
+
|
|
25
|
+
// Verdict: { pass, reasoning, steps[] }
|
|
26
|
+
if ('pass' in parsed && 'steps' in parsed && Array.isArray((parsed as Verdict).steps)) {
|
|
27
|
+
return formatVerdict(parsed as Verdict);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Plan: { summary, steps[] }
|
|
31
|
+
if ('summary' in parsed && 'steps' in parsed && Array.isArray((parsed as Plan).steps)) {
|
|
32
|
+
return formatPlan(parsed as Plan);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Step result: { criterion, ok, note }
|
|
36
|
+
if ('criterion' in parsed && 'ok' in parsed && 'note' in parsed) {
|
|
37
|
+
return formatStepResult(parsed as StepResult);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// No recognised shape matched; caller should fall back to plain-text rendering
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prints a CLI resolution error and exits the current process.
|
|
3
|
+
*
|
|
4
|
+
* @param message - Primary error message.
|
|
5
|
+
* @param detail - Optional follow-up detail shown on the next line.
|
|
6
|
+
* @returns This function never returns because the process exits.
|
|
7
|
+
*/
|
|
8
|
+
export function exitWithResolutionError(message: string, detail?: string): never {
|
|
9
|
+
console.error(`❌ ${message}`);
|
|
10
|
+
// Only print the detail line when one was provided
|
|
11
|
+
if (detail) {
|
|
12
|
+
console.error(` ${detail}`);
|
|
13
|
+
}
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { basename, dirname, extname, resolve } from 'node:path';
|
|
2
|
+
import { existsSync, statSync } from 'node:fs';
|
|
3
|
+
|
|
4
|
+
import { exitWithResolutionError } from './exit-with-resolution-error.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Normalized positional input for the `copilot app eval` command.
|
|
8
|
+
*
|
|
9
|
+
* When the user passes an application directory, `evalFile` is undefined.
|
|
10
|
+
* When the user passes a direct `eval/*.md` file path, `appPath` is derived
|
|
11
|
+
* from the file location and `evalFile` points to the selected markdown file.
|
|
12
|
+
*/
|
|
13
|
+
export interface EvalCommandInput {
|
|
14
|
+
/** Absolute path to the Fusion application directory. */
|
|
15
|
+
appPath: string;
|
|
16
|
+
/** Optional absolute path to the directly selected eval markdown file. */
|
|
17
|
+
evalFile?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolves the positional `copilot app eval` argument.
|
|
22
|
+
*
|
|
23
|
+
* Supports both application directory paths and direct paths to markdown files
|
|
24
|
+
* inside an application's `eval/` directory.
|
|
25
|
+
*
|
|
26
|
+
* @param inputPath - Positional CLI argument passed to the eval command.
|
|
27
|
+
* @returns Normalized application path and optional direct eval file path.
|
|
28
|
+
*/
|
|
29
|
+
export function resolveEvalCommandInput(inputPath: string): EvalCommandInput {
|
|
30
|
+
const absInput = resolve(inputPath);
|
|
31
|
+
|
|
32
|
+
// A non-markdown path is treated as the application directory itself
|
|
33
|
+
if (extname(absInput) !== '.md') {
|
|
34
|
+
return { appPath: absInput };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// The direct eval file path must actually exist on disk
|
|
38
|
+
if (!existsSync(absInput)) {
|
|
39
|
+
exitWithResolutionError(`Eval file not found: ${absInput}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Reject directories passed off as a direct eval file path
|
|
43
|
+
if (!statSync(absInput).isFile()) {
|
|
44
|
+
exitWithResolutionError(`Eval path is not a file: ${absInput}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const evalDir = dirname(absInput);
|
|
48
|
+
// Direct eval files must live inside an `eval/` directory so appPath can be derived
|
|
49
|
+
if (basename(evalDir) !== 'eval') {
|
|
50
|
+
exitWithResolutionError(
|
|
51
|
+
`Direct eval file path must point to a markdown file inside an eval directory: ${absInput}`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
appPath: dirname(evalDir),
|
|
57
|
+
evalFile: absInput,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { extname, isAbsolute, join, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { exitWithResolutionError } from './exit-with-resolution-error.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Resolves eval markdown files for a Fusion application.
|
|
8
|
+
*
|
|
9
|
+
* @param appPath - Path to the Fusion application directory
|
|
10
|
+
* @param evalOpt - Value of the `--eval` CLI option (name, path, or `undefined`)
|
|
11
|
+
* @returns Array of absolute paths to eval markdown files
|
|
12
|
+
*/
|
|
13
|
+
export function resolveEvalFiles(appPath: string, evalOpt?: string): string[] {
|
|
14
|
+
const absApp = resolve(appPath);
|
|
15
|
+
|
|
16
|
+
// No --eval option: run every eval file found in the app's eval/ directory
|
|
17
|
+
if (!evalOpt) {
|
|
18
|
+
const evalDir = join(absApp, 'eval');
|
|
19
|
+
// Fail fast when the application has no eval directory to scan
|
|
20
|
+
if (!existsSync(evalDir)) {
|
|
21
|
+
exitWithResolutionError(
|
|
22
|
+
`No eval directory found at ${evalDir}`,
|
|
23
|
+
'Create eval/*.md files or use --eval <file>',
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
const files = readdirSync(evalDir)
|
|
27
|
+
// Keep only markdown files, sorted for stable ordering, resolved to absolute paths
|
|
28
|
+
.filter((f) => f.endsWith('.md'))
|
|
29
|
+
.sort()
|
|
30
|
+
// Resolve each filename to its absolute path within the eval directory
|
|
31
|
+
.map((f) => join(evalDir, f));
|
|
32
|
+
|
|
33
|
+
// Surface a clear error instead of silently running zero eval files
|
|
34
|
+
if (files.length === 0) {
|
|
35
|
+
exitWithResolutionError(`No .md files found in ${evalDir}`);
|
|
36
|
+
}
|
|
37
|
+
return files;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// A path-like --eval value (absolute or containing separators) is a direct file reference
|
|
41
|
+
if (isAbsolute(evalOpt) || evalOpt.includes('/') || evalOpt.includes('\\')) {
|
|
42
|
+
const absPath = isAbsolute(evalOpt) ? evalOpt : resolve(evalOpt);
|
|
43
|
+
// Validate the direct file path exists before returning it
|
|
44
|
+
if (!existsSync(absPath)) {
|
|
45
|
+
exitWithResolutionError(`Eval file not found: ${absPath}`);
|
|
46
|
+
}
|
|
47
|
+
return [absPath];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const evalFileName = extname(evalOpt) === '.md' ? evalOpt : `${evalOpt}.md`;
|
|
51
|
+
const evalFile = join(absApp, 'eval', evalFileName);
|
|
52
|
+
// Otherwise treat --eval as a bare name inside the app's eval/ directory
|
|
53
|
+
if (!existsSync(evalFile)) {
|
|
54
|
+
exitWithResolutionError(`Eval file not found: ${evalFile}`);
|
|
55
|
+
}
|
|
56
|
+
return [evalFile];
|
|
57
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts the user-facing error message from an agent-browser failure.
|
|
3
|
+
*
|
|
4
|
+
* Strips ANSI escape codes from stderr and returns only the diagnostic
|
|
5
|
+
* lines prefixed with `✗` or `⚠`. Falls back to the first 200 characters
|
|
6
|
+
* of the error message when no diagnostic lines are found.
|
|
7
|
+
*
|
|
8
|
+
* @param err - The caught error (typically from {@link ab})
|
|
9
|
+
* @returns A single-line summary suitable for console output
|
|
10
|
+
*/
|
|
11
|
+
export function abErrorMessage(err: unknown): string {
|
|
12
|
+
// Non-Error throws have no structured message to extract
|
|
13
|
+
if (!(err instanceof Error)) return String(err);
|
|
14
|
+
const e = err as Error & { stderr?: string };
|
|
15
|
+
// Prefer the raw stderr stream when it was captured on the error
|
|
16
|
+
if (e.stderr) {
|
|
17
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: stripping ANSI escape sequences
|
|
18
|
+
const ansiPattern = /\x1B\[[0-9;]*m/g;
|
|
19
|
+
const lines = e.stderr
|
|
20
|
+
.replace(ansiPattern, '')
|
|
21
|
+
.split('\n')
|
|
22
|
+
// Keep only the diagnostic lines agent-browser prefixes with a status glyph
|
|
23
|
+
.map((l) => l.trim())
|
|
24
|
+
// Filter down to lines that look like agent-browser's own status output
|
|
25
|
+
.filter((l) => l.startsWith('✗') || l.startsWith('⚠'));
|
|
26
|
+
// Only report an assembled diagnostic summary when matching lines were found
|
|
27
|
+
if (lines.length) return lines.join(' | ');
|
|
28
|
+
}
|
|
29
|
+
const match = e.message.match(/[✗⚠].+/);
|
|
30
|
+
return match ? match[0] : e.message.slice(0, 200);
|
|
31
|
+
}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import { existsSync, unlinkSync } from 'node:fs';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
3
|
import { join } from 'node:path';
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
export const PROFILE_DIR = `${homedir()}/.fusion-smoke-profile`;
|
|
5
|
+
import { PROFILE_DIR } from './helpers.js';
|
|
8
6
|
|
|
9
7
|
/**
|
|
10
8
|
* Runs an agent-browser command with the persistent Chrome profile.
|
|
@@ -22,6 +20,7 @@ export function ab(args: string[], timeoutMs = 30_000): string {
|
|
|
22
20
|
// Remove stale SingletonLock left by a previous Chrome crash to prevent
|
|
23
21
|
// "Failed to create SingletonLock: File exists" errors.
|
|
24
22
|
const lockPath = join(PROFILE_DIR, 'SingletonLock');
|
|
23
|
+
// Only attempt removal when a stale lock file is actually present
|
|
25
24
|
if (existsSync(lockPath)) {
|
|
26
25
|
try {
|
|
27
26
|
unlinkSync(lockPath);
|
|
@@ -37,30 +36,3 @@ export function ab(args: string[], timeoutMs = 30_000): string {
|
|
|
37
36
|
cwd: process.cwd(),
|
|
38
37
|
}).trim();
|
|
39
38
|
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Extracts the user-facing error message from an agent-browser failure.
|
|
43
|
-
*
|
|
44
|
-
* Strips ANSI escape codes from stderr and returns only the diagnostic
|
|
45
|
-
* lines prefixed with `✗` or `⚠`. Falls back to the first 200 characters
|
|
46
|
-
* of the error message when no diagnostic lines are found.
|
|
47
|
-
*
|
|
48
|
-
* @param err - The caught error (typically from {@link ab})
|
|
49
|
-
* @returns A single-line summary suitable for console output
|
|
50
|
-
*/
|
|
51
|
-
export function abErrorMessage(err: unknown): string {
|
|
52
|
-
if (!(err instanceof Error)) return String(err);
|
|
53
|
-
const e = err as Error & { stderr?: string };
|
|
54
|
-
if (e.stderr) {
|
|
55
|
-
// biome-ignore lint/suspicious/noControlCharactersInRegex: stripping ANSI escape sequences
|
|
56
|
-
const ansiPattern = /\x1B\[[0-9;]*m/g;
|
|
57
|
-
const lines = e.stderr
|
|
58
|
-
.replace(ansiPattern, '')
|
|
59
|
-
.split('\n')
|
|
60
|
-
.map((l) => l.trim())
|
|
61
|
-
.filter((l) => l.startsWith('✗') || l.startsWith('⚠'));
|
|
62
|
-
if (lines.length) return lines.join(' | ');
|
|
63
|
-
}
|
|
64
|
-
const match = e.message.match(/[✗⚠].+/);
|
|
65
|
-
return match ? match[0] : e.message.slice(0, 200);
|
|
66
|
-
}
|