@opice/harness 0.0.4 → 0.1.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/README.md +80 -44
- package/dist/accessible.d.ts +28 -0
- package/dist/accessible.d.ts.map +1 -0
- package/dist/accessible.js +31 -0
- package/dist/accessible.js.map +1 -0
- package/dist/command.d.ts +65 -0
- package/dist/command.d.ts.map +1 -0
- package/dist/command.js +88 -0
- package/dist/command.js.map +1 -0
- package/dist/context.d.ts +10 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +50 -0
- package/dist/context.js.map +1 -0
- package/dist/dsn.d.ts +17 -0
- package/dist/dsn.d.ts.map +1 -0
- package/dist/dsn.js +17 -0
- package/dist/dsn.js.map +1 -0
- package/dist/element.d.ts +50 -0
- package/dist/element.d.ts.map +1 -0
- package/dist/element.js +82 -0
- package/dist/element.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/navigation.d.ts +22 -0
- package/dist/navigation.d.ts.map +1 -0
- package/dist/navigation.js +35 -0
- package/dist/navigation.js.map +1 -0
- package/dist/reporter.d.ts +61 -0
- package/dist/reporter.d.ts.map +1 -0
- package/dist/reporter.js +210 -0
- package/dist/reporter.js.map +1 -0
- package/dist/scenario.d.ts +30 -0
- package/dist/scenario.d.ts.map +1 -0
- package/dist/scenario.js +162 -0
- package/dist/scenario.js.map +1 -0
- package/package.json +11 -4
- package/src/accessible.ts +21 -187
- package/src/command.ts +134 -0
- package/src/context.ts +55 -0
- package/src/element.ts +56 -99
- package/src/index.ts +12 -1
- package/src/navigation.ts +16 -28
- package/src/scenario.ts +29 -22
- package/src/agent-browser.ts +0 -30
package/src/element.ts
CHANGED
|
@@ -1,134 +1,91 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Locator, Page } from 'playwright'
|
|
2
|
+
import { getPage } from './context.js'
|
|
2
3
|
|
|
3
4
|
const POLL_INTERVAL = 200
|
|
4
5
|
const POLL_TIMEOUT = 10_000
|
|
5
|
-
const ACTION_SETTLE_MS = 500
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* Resolve a selector into a `Locator` on an explicit page — the shared core
|
|
9
|
+
* behind `el()` and the command-registry context. Bare identifiers become
|
|
10
|
+
* test-ids (`getByTestId`, matching `data-testid`); anything with CSS-flavoured
|
|
11
|
+
* characters (`[ ] . # : > ` or a space) is a raw CSS selector.
|
|
12
12
|
*/
|
|
13
|
-
function
|
|
13
|
+
export function locatorOn(page: Page, selectorOrTestId: string): Locator {
|
|
14
14
|
if (/[\[\].#:> ]/.test(selectorOrTestId)) {
|
|
15
|
-
return selectorOrTestId
|
|
15
|
+
return page.locator(selectorOrTestId)
|
|
16
16
|
}
|
|
17
|
-
return
|
|
17
|
+
return page.getByTestId(selectorOrTestId)
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
21
|
+
* Resolve a selector into a Playwright `Locator`.
|
|
22
|
+
*
|
|
23
|
+
* Bare identifiers are auto-wrapped as test-ids (`page.getByTestId`, which
|
|
24
|
+
* matches `data-testid` by default); anything with CSS-flavoured characters
|
|
25
|
+
* (`[ ] . # : > ` or a space) is treated as a raw CSS selector. Heuristic — if
|
|
26
|
+
* you need a plain-tag selector (e.g. `h1`), give it structure (`main h1`).
|
|
27
|
+
*
|
|
28
|
+
* The returned value is a real Playwright `Locator`, so the full Locator API
|
|
29
|
+
* (`.click()`, `.fill()`, `.textContent()`, `.first()`, `.nth()`, …) and the
|
|
30
|
+
* web-first `expect(locator)` assertions are available. All actions auto-wait
|
|
31
|
+
* for actionability and fire real user gestures.
|
|
23
32
|
*/
|
|
24
|
-
export function
|
|
25
|
-
|
|
33
|
+
export function el(selectorOrTestId: string): Locator {
|
|
34
|
+
return locatorOn(getPage(), selectorOrTestId)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build a `[data-testid="..."]` selector string, for composing into a larger
|
|
39
|
+
* CSS selector: `el(`${tid('row')} button`)`. For a plain test-id, prefer
|
|
40
|
+
* `el('row')` directly.
|
|
41
|
+
*/
|
|
42
|
+
export function tid(testId: string): string {
|
|
43
|
+
return `[data-testid="${testId}"]`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Poll a (possibly async) condition until it returns true or times out.
|
|
48
|
+
*
|
|
49
|
+
* Prefer Playwright's retrying assertions — `await expect(el('x')).toBeVisible()`,
|
|
50
|
+
* `.toHaveText(...)` — which auto-wait and give better failure messages. Keep
|
|
51
|
+
* `waitFor` for arbitrary predicates that don't map to a locator assertion.
|
|
52
|
+
*/
|
|
53
|
+
export async function waitFor(
|
|
54
|
+
condition: () => boolean | Promise<boolean>,
|
|
26
55
|
{ timeout = POLL_TIMEOUT, interval = POLL_INTERVAL, message }: { timeout?: number; interval?: number; message?: string } = {},
|
|
27
|
-
): void {
|
|
56
|
+
): Promise<void> {
|
|
28
57
|
const start = Date.now()
|
|
29
58
|
while (Date.now() - start < timeout) {
|
|
30
59
|
try {
|
|
31
|
-
if (condition()) return
|
|
60
|
+
if (await condition()) return
|
|
32
61
|
} catch {
|
|
33
62
|
// condition threw — treat as not yet ready
|
|
34
63
|
}
|
|
35
|
-
|
|
64
|
+
await new Promise((resolve) => setTimeout(resolve, interval))
|
|
36
65
|
}
|
|
37
|
-
if (!condition()) {
|
|
66
|
+
if (!(await condition())) {
|
|
38
67
|
const elapsed = Date.now() - start
|
|
39
68
|
const hint = message ?? condition.toString().slice(0, 120)
|
|
40
69
|
throw new Error(`waitFor timed out after ${elapsed}ms: ${hint}`)
|
|
41
70
|
}
|
|
42
71
|
}
|
|
43
72
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
readonly value: string
|
|
48
|
-
readonly isDisabled: boolean
|
|
49
|
-
attr(name: string): string
|
|
50
|
-
count(): number
|
|
51
|
-
click(): void
|
|
52
|
-
fill(value: string): void
|
|
53
|
-
select(optionText: string): void
|
|
54
|
-
focus(): void
|
|
55
|
-
hover(): void
|
|
56
|
-
/** Focus the element, then send a key (e.g. `Enter`, `Escape`, `ArrowDown`). */
|
|
57
|
-
press(key: string): void
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function el(selector: string): ElementHandle {
|
|
61
|
-
const sel = resolveSelector(selector)
|
|
62
|
-
const quoted = q(sel)
|
|
63
|
-
return {
|
|
64
|
-
get exists(): boolean {
|
|
65
|
-
return parseInt(exec(`agent-browser get count ${quoted}`), 10) > 0
|
|
66
|
-
},
|
|
67
|
-
get text(): string {
|
|
68
|
-
return exec(`agent-browser get text ${quoted}`)
|
|
69
|
-
},
|
|
70
|
-
get value(): string {
|
|
71
|
-
return exec(`agent-browser get value ${quoted}`)
|
|
72
|
-
},
|
|
73
|
-
get isDisabled(): boolean {
|
|
74
|
-
return exec(`agent-browser is enabled ${quoted}`) !== 'true'
|
|
75
|
-
},
|
|
76
|
-
attr(name: string): string {
|
|
77
|
-
return exec(`agent-browser get attr ${name} ${quoted}`)
|
|
78
|
-
},
|
|
79
|
-
count(): number {
|
|
80
|
-
return parseInt(exec(`agent-browser get count ${quoted}`), 10) || 0
|
|
81
|
-
},
|
|
82
|
-
click(): void {
|
|
83
|
-
exec(`agent-browser scrollintoview ${quoted}`)
|
|
84
|
-
exec(`agent-browser click ${quoted}`)
|
|
85
|
-
Bun.sleepSync(ACTION_SETTLE_MS)
|
|
86
|
-
},
|
|
87
|
-
fill(value: string): void {
|
|
88
|
-
exec(`agent-browser scrollintoview ${quoted}`)
|
|
89
|
-
exec(`agent-browser fill ${quoted} ${q(value)}`)
|
|
90
|
-
Bun.sleepSync(ACTION_SETTLE_MS)
|
|
91
|
-
},
|
|
92
|
-
select(optionText: string): void {
|
|
93
|
-
exec(`agent-browser scrollintoview ${quoted}`)
|
|
94
|
-
exec(`agent-browser select ${quoted} ${q(optionText)}`)
|
|
95
|
-
Bun.sleepSync(ACTION_SETTLE_MS)
|
|
96
|
-
},
|
|
97
|
-
focus(): void {
|
|
98
|
-
exec(`agent-browser scrollintoview ${quoted}`)
|
|
99
|
-
exec(`agent-browser focus ${quoted}`)
|
|
100
|
-
},
|
|
101
|
-
hover(): void {
|
|
102
|
-
exec(`agent-browser scrollintoview ${quoted}`)
|
|
103
|
-
exec(`agent-browser hover ${quoted}`)
|
|
104
|
-
Bun.sleepSync(ACTION_SETTLE_MS)
|
|
105
|
-
},
|
|
106
|
-
press(key: string): void {
|
|
107
|
-
exec(`agent-browser focus ${quoted}`)
|
|
108
|
-
exec(`agent-browser press ${key}`)
|
|
109
|
-
Bun.sleepSync(ACTION_SETTLE_MS)
|
|
110
|
-
},
|
|
111
|
-
}
|
|
73
|
+
/** Fixed sleep. Avoid when possible — prefer `waitFor` or retrying assertions. */
|
|
74
|
+
export async function wait(ms: number): Promise<void> {
|
|
75
|
+
await new Promise((resolve) => setTimeout(resolve, ms))
|
|
112
76
|
}
|
|
113
77
|
|
|
114
78
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
79
|
+
* Evaluate JavaScript in the page and return its result. Thin wrapper over
|
|
80
|
+
* `page.evaluate`; the value is the real JS value (not a JSON string).
|
|
117
81
|
*/
|
|
118
|
-
export function
|
|
119
|
-
return
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export function wait(ms: number): void {
|
|
123
|
-
Bun.sleepSync(ms)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export function evalJs(js: string): string {
|
|
127
|
-
return exec(`agent-browser eval ${q(js)}`)
|
|
82
|
+
export function evalJs<T = unknown>(js: string): Promise<T> {
|
|
83
|
+
return getPage().evaluate(js) as Promise<T>
|
|
128
84
|
}
|
|
129
85
|
|
|
130
|
-
|
|
86
|
+
/** Capture a screenshot to `path` (or a temp file) and return the path. */
|
|
87
|
+
export async function screenshot(path?: string): Promise<string> {
|
|
131
88
|
const target = path ?? `/tmp/opice-screenshot-${Date.now()}.png`
|
|
132
|
-
|
|
89
|
+
await getPage().screenshot({ path: target })
|
|
133
90
|
return target
|
|
134
91
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
export { el, tid, waitFor, wait, evalJs, screenshot } from './element.js'
|
|
2
|
-
export type { ElementHandle } from './element.js'
|
|
3
2
|
|
|
4
3
|
export { byLabel, byRole, byText } from './accessible.js'
|
|
5
4
|
|
|
6
5
|
export { back, currentPath, currentUrl, forward, open, reload } from './navigation.js'
|
|
7
6
|
|
|
7
|
+
export { getPage, getContext } from './context.js'
|
|
8
|
+
|
|
8
9
|
export { browserTest, step } from './scenario.js'
|
|
9
10
|
export type { BrowserTestOptions } from './scenario.js'
|
|
10
11
|
|
|
@@ -13,3 +14,13 @@ export type { Reporter, ReporterConfig, StepEvent, ScenarioStart, ScenarioFinish
|
|
|
13
14
|
|
|
14
15
|
export { parseOpiceDsn } from './dsn.js'
|
|
15
16
|
export type { OpiceDsn } from './dsn.js'
|
|
17
|
+
|
|
18
|
+
export { command, call, runCommand, makeCtx, loadUserCommands, findUserCommandsFile, z } from './command.js'
|
|
19
|
+
export type { Command, CommandCtx } from './command.js'
|
|
20
|
+
|
|
21
|
+
// Playwright's web-first `expect` (retrying locator matchers + generic matchers)
|
|
22
|
+
// works under `bun:test`; re-export it so tests use a single `expect`.
|
|
23
|
+
export { expect } from '@playwright/test'
|
|
24
|
+
|
|
25
|
+
// The DSL returns Playwright Locators directly — re-export the type.
|
|
26
|
+
export type { Locator } from 'playwright'
|
package/src/navigation.ts
CHANGED
|
@@ -1,53 +1,41 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { evalJs } from './element.js'
|
|
1
|
+
import { getPage } from './context.js'
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* Page navigation primitives. `browserTest` opens the scenario URL for you in
|
|
6
5
|
* `beforeAll`; these are for mid-scenario navigation — following a hard link,
|
|
7
6
|
* reloading after mutating storage/cookies, or going back/forward.
|
|
8
7
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* after writing auth tokens to localStorage/cookies so the app re-reads them.
|
|
8
|
+
* Each navigating call waits for the `load` event (Playwright's default), so
|
|
9
|
+
* the old agent-browser reload caveat (a reload from inside `eval` getting
|
|
10
|
+
* dropped) no longer applies — `reload()` drives the page directly.
|
|
13
11
|
*/
|
|
14
12
|
|
|
15
|
-
/** Navigate to a URL in the current
|
|
16
|
-
export function open(url: string): void {
|
|
17
|
-
|
|
13
|
+
/** Navigate to a URL in the current page. */
|
|
14
|
+
export async function open(url: string): Promise<void> {
|
|
15
|
+
await getPage().goto(url)
|
|
18
16
|
}
|
|
19
17
|
|
|
20
|
-
/** Reload the current page
|
|
21
|
-
export function reload(): void {
|
|
22
|
-
|
|
18
|
+
/** Reload the current page. */
|
|
19
|
+
export async function reload(): Promise<void> {
|
|
20
|
+
await getPage().reload()
|
|
23
21
|
}
|
|
24
22
|
|
|
25
23
|
/** Go back in history. */
|
|
26
|
-
export function back(): void {
|
|
27
|
-
|
|
24
|
+
export async function back(): Promise<void> {
|
|
25
|
+
await getPage().goBack()
|
|
28
26
|
}
|
|
29
27
|
|
|
30
28
|
/** Go forward in history. */
|
|
31
|
-
export function forward(): void {
|
|
32
|
-
|
|
29
|
+
export async function forward(): Promise<void> {
|
|
30
|
+
await getPage().goForward()
|
|
33
31
|
}
|
|
34
32
|
|
|
35
33
|
/** The current full URL (`location.href`). */
|
|
36
34
|
export function currentUrl(): string {
|
|
37
|
-
return
|
|
35
|
+
return getPage().url()
|
|
38
36
|
}
|
|
39
37
|
|
|
40
38
|
/** The current path (`location.pathname`). */
|
|
41
39
|
export function currentPath(): string {
|
|
42
|
-
return
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function readLocation(prop: 'href' | 'pathname'): string {
|
|
46
|
-
const raw = evalJs(`location.${prop}`)
|
|
47
|
-
try {
|
|
48
|
-
const value: unknown = JSON.parse(raw)
|
|
49
|
-
return typeof value === 'string' ? value : raw
|
|
50
|
-
} catch {
|
|
51
|
-
return raw
|
|
52
|
-
}
|
|
40
|
+
return new URL(getPage().url()).pathname
|
|
53
41
|
}
|
package/src/scenario.ts
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import crypto from 'node:crypto'
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
3
2
|
import path from 'node:path'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { closePage, launchPage } from './context.js'
|
|
4
|
+
import { screenshot } from './element.js'
|
|
6
5
|
import { getReporter } from './reporter.js'
|
|
7
6
|
|
|
7
|
+
/**
|
|
8
|
+
* `bun:test` is resolved lazily, at the moment `browserTest` registers a
|
|
9
|
+
* scenario — never at module load. That keeps `@opice/harness` importable
|
|
10
|
+
* under plain Node (the `opice-browser` authoring daemon imports the command
|
|
11
|
+
* registry from this package and runs on Node, where `bun:test` doesn't
|
|
12
|
+
* exist). Tests still register synchronously: `require` is sync under Bun.
|
|
13
|
+
*/
|
|
14
|
+
const require = createRequire(import.meta.url)
|
|
15
|
+
function bunTest(): typeof import('bun:test') {
|
|
16
|
+
return require('bun:test') as typeof import('bun:test')
|
|
17
|
+
}
|
|
18
|
+
|
|
8
19
|
const PLAYGROUND_URL = process.env['PLAYGROUND_URL'] ?? 'http://localhost:15180'
|
|
9
20
|
|
|
10
21
|
export interface BrowserTestOptions {
|
|
@@ -60,20 +71,20 @@ let currentScenarioStepSeq = 0
|
|
|
60
71
|
/**
|
|
61
72
|
* Register a top-level browser test scenario.
|
|
62
73
|
*
|
|
63
|
-
* Each `browserTest(name, fn)`
|
|
64
|
-
* to the playground URL, runs the given `fn` (which
|
|
65
|
-
* `describe`/`test` blocks), and
|
|
74
|
+
* Each `browserTest(name, fn)` launches its own isolated Playwright browser +
|
|
75
|
+
* context + page, navigates to the playground URL, runs the given `fn` (which
|
|
76
|
+
* typically contains nested `describe`/`test` blocks), and tears the browser
|
|
77
|
+
* down in `afterAll`.
|
|
66
78
|
*/
|
|
67
79
|
export function browserTest(name: string, fn: () => void, options: BrowserTestOptions | string = {}): void {
|
|
68
80
|
const opts: BrowserTestOptions = typeof options === 'string' ? { hash: options } : options
|
|
69
81
|
const reporter = getReporter()
|
|
70
82
|
const testFile = captureTestFile()
|
|
71
83
|
const scenarioFile = opts.scenarioFile ?? defaultScenarioFile(testFile)
|
|
84
|
+
const { describe, beforeAll, afterAll } = bunTest()
|
|
72
85
|
|
|
73
86
|
describe(name, () => {
|
|
74
87
|
beforeAll(async () => {
|
|
75
|
-
const session = `opice-${crypto.randomUUID().slice(0, 8)}`
|
|
76
|
-
setSession(session)
|
|
77
88
|
currentScenarioStart = Date.now()
|
|
78
89
|
currentScenarioFailures = 0
|
|
79
90
|
currentScenarioStepSeq = 0
|
|
@@ -82,25 +93,18 @@ export function browserTest(name: string, fn: () => void, options: BrowserTestOp
|
|
|
82
93
|
} catch {
|
|
83
94
|
currentScenarioId = null
|
|
84
95
|
}
|
|
96
|
+
const page = await launchPage()
|
|
85
97
|
const base = opts.url ?? PLAYGROUND_URL
|
|
86
98
|
const url = opts.hash ? `${base}#${opts.hash}` : base
|
|
87
|
-
|
|
88
|
-
waitFor(() => {
|
|
89
|
-
try {
|
|
90
|
-
return exec('agent-browser get title').length > 0
|
|
91
|
-
} catch {
|
|
92
|
-
return false
|
|
93
|
-
}
|
|
94
|
-
}, { timeout: 15_000 })
|
|
99
|
+
await page.goto(url)
|
|
95
100
|
}, 30_000)
|
|
96
101
|
|
|
97
102
|
afterAll(async () => {
|
|
98
103
|
try {
|
|
99
|
-
|
|
104
|
+
await closePage()
|
|
100
105
|
} catch {
|
|
101
106
|
// ignore close errors
|
|
102
107
|
}
|
|
103
|
-
setSession(null)
|
|
104
108
|
if (currentScenarioId) {
|
|
105
109
|
// Drain pending step records (incl. their screenshot uploads)
|
|
106
110
|
// before marking the scenario done. step() fires recordStep
|
|
@@ -129,8 +133,11 @@ export function browserTest(name: string, fn: () => void, options: BrowserTestOp
|
|
|
129
133
|
/**
|
|
130
134
|
* A reportable step inside a scenario. Captures duration + screenshot on
|
|
131
135
|
* finish, forwards to the active reporter (no-op unless configured via env).
|
|
136
|
+
*
|
|
137
|
+
* The body may be sync or async; `step` always returns a promise, so call it
|
|
138
|
+
* with `await step('…', async () => { … })`.
|
|
132
139
|
*/
|
|
133
|
-
export function step(name: string, fn: () => void): void {
|
|
140
|
+
export async function step(name: string, fn: () => void | Promise<void>): Promise<void> {
|
|
134
141
|
const reporter = getReporter()
|
|
135
142
|
// Capture order at call time, before the fire-and-forget record below.
|
|
136
143
|
const sequence = currentScenarioStepSeq++
|
|
@@ -138,7 +145,7 @@ export function step(name: string, fn: () => void): void {
|
|
|
138
145
|
let status: 'passed' | 'failed' = 'passed'
|
|
139
146
|
let error: string | undefined
|
|
140
147
|
try {
|
|
141
|
-
fn()
|
|
148
|
+
await fn()
|
|
142
149
|
} catch (e) {
|
|
143
150
|
status = 'failed'
|
|
144
151
|
error = e instanceof Error ? e.message : String(e)
|
|
@@ -148,7 +155,7 @@ export function step(name: string, fn: () => void): void {
|
|
|
148
155
|
const durationMs = Date.now() - start
|
|
149
156
|
let screenshotPath: string | undefined
|
|
150
157
|
try {
|
|
151
|
-
screenshotPath = screenshot()
|
|
158
|
+
screenshotPath = await screenshot()
|
|
152
159
|
} catch {
|
|
153
160
|
// screenshot failure shouldn't fail the test
|
|
154
161
|
}
|
package/src/agent-browser.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { execSync } from 'node:child_process'
|
|
2
|
-
|
|
3
|
-
const EXEC_TIMEOUT = 30_000
|
|
4
|
-
|
|
5
|
-
let currentSession: string | null = null
|
|
6
|
-
|
|
7
|
-
export function setSession(session: string | null): void {
|
|
8
|
-
currentSession = session
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function getSession(): string | null {
|
|
12
|
-
return currentSession
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function exec(cmd: string): string {
|
|
16
|
-
const sessionFlag = currentSession ? `--session ${currentSession} ` : ''
|
|
17
|
-
const fullCmd = cmd.replace(/^agent-browser /, `agent-browser ${sessionFlag}`)
|
|
18
|
-
try {
|
|
19
|
-
const raw = execSync(fullCmd, { encoding: 'utf-8', timeout: EXEC_TIMEOUT, stdio: ['pipe', 'pipe', 'pipe'] }).trim()
|
|
20
|
-
return raw.replace(/\x1B\[[0-9;]*m/g, '')
|
|
21
|
-
} catch (e: unknown) {
|
|
22
|
-
const err = e as { stdout?: string; stderr?: string; message?: string }
|
|
23
|
-
const output = err.stdout?.trim() ?? err.stderr?.trim() ?? err.message ?? 'unknown error'
|
|
24
|
-
throw new Error(`agent-browser command failed: ${fullCmd}\n${output}`)
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function q(s: string): string {
|
|
29
|
-
return `'${s.replace(/'/g, "'\\''")}'`
|
|
30
|
-
}
|