pi-browser-harness 0.12.0 → 0.13.0
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 +36 -0
- package/README.md +86 -0
- package/package.json +12 -6
- package/skills/pi-browser-harness/SKILL.md +57 -2
- package/src/cdp/commands.ts +200 -14
- package/src/cdp/console-buffer.ts +37 -23
- package/src/cdp/discovery.ts +57 -25
- package/src/cdp/events.ts +8 -3
- package/src/cdp/network-buffer.ts +38 -32
- package/src/cdp/ownership.ts +22 -4
- package/src/cdp/record-store.ts +2 -2
- package/src/cdp/session.ts +320 -217
- package/src/cdp/target-factory.ts +34 -15
- package/src/cdp/types.ts +17 -1
- package/src/client.ts +76 -43
- package/src/daemon/bridge.ts +110 -349
- package/src/daemon/connection.ts +323 -0
- package/src/daemon/index.ts +77 -48
- package/src/daemon/lines.ts +40 -0
- package/src/daemon/main.ts +12 -0
- package/src/daemon/protocol.ts +55 -25
- package/src/daemon/server.ts +100 -63
- package/src/daemon/socket.ts +10 -0
- package/src/daemon/spawn.ts +21 -22
- package/src/daemon/transport.ts +80 -49
- package/src/domains/ax-tree.ts +8 -8
- package/src/domains/cdp-call.ts +14 -7
- package/src/domains/click.ts +2 -2
- package/src/domains/concurrency.ts +50 -0
- package/src/domains/console.ts +20 -64
- package/src/domains/drag.ts +26 -28
- package/src/domains/element-call.ts +102 -68
- package/src/domains/files.ts +13 -9
- package/src/domains/form-batch.ts +13 -10
- package/src/domains/form.ts +78 -107
- package/src/domains/headers.ts +2 -2
- package/src/domains/history.ts +4 -4
- package/src/domains/io.ts +35 -0
- package/src/domains/isolated-tab.ts +6 -12
- package/src/domains/js.ts +65 -44
- package/src/domains/keyboard.ts +14 -11
- package/src/domains/navigate.ts +102 -66
- package/src/domains/network.ts +55 -95
- package/src/domains/page/fill.page.ts +139 -0
- package/src/domains/page/form.page.ts +61 -0
- package/src/domains/page/inject.ts +37 -0
- package/src/domains/page/keyboard.page.ts +16 -0
- package/src/domains/page/live-value.page.ts +10 -0
- package/src/domains/page/read-page.page.ts +51 -0
- package/src/domains/page/serp.page.ts +39 -0
- package/src/domains/page.ts +18 -51
- package/src/domains/poll.ts +42 -0
- package/src/domains/readpage/read-page.ts +48 -53
- package/src/domains/ref-resolve.ts +43 -29
- package/src/domains/ref-signature.ts +11 -0
- package/src/domains/render.ts +12 -23
- package/src/domains/screenshot-capture.ts +4 -2
- package/src/domains/screenshot.ts +13 -10
- package/src/domains/scroll.ts +4 -4
- package/src/domains/search/google-serp.ts +1 -1
- package/src/domains/search/web-search.ts +38 -33
- package/src/domains/snapshot.ts +71 -73
- package/src/domains/tab-resolve.ts +36 -0
- package/src/domains/tabs.ts +18 -69
- package/src/domains/target.ts +1 -1
- package/src/index.ts +90 -33
- package/src/profile/bind.ts +45 -14
- package/src/profile/browser-process.ts +50 -12
- package/src/profile/command.ts +20 -12
- package/src/profile/launch.ts +13 -9
- package/src/profile/list.ts +9 -13
- package/src/profile/store.ts +2 -1
- package/src/prompt.ts +32 -7
- package/src/registry.ts +65 -4
- package/src/schemas/parse.ts +2 -2
- package/src/setup.ts +28 -19
- package/src/state.ts +2 -1
- package/src/util/debug.ts +7 -1
- package/src/util/guards.ts +3 -0
- package/src/util/mutex.ts +1 -1
- package/src/util/paths.ts +3 -2
- package/src/util/result.ts +8 -0
- package/src/util/sharp-shim.ts +18 -11
- package/src/util/sleep.ts +18 -0
- package/src/util/tool.ts +163 -114
- package/src/util/truncate.ts +1 -1
- package/tsconfig.json +2 -1
- package/src/domains/fill-engine.ts +0 -80
package/src/util/tool.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { isRecord } from "./guards";
|
|
2
|
+
import { Type, type TSchema, type Static } from "typebox";
|
|
3
|
+
import { Compile } from "typebox/compile";
|
|
2
4
|
import type {
|
|
3
5
|
AgentToolResult,
|
|
4
6
|
ExtensionAPI,
|
|
@@ -7,14 +9,44 @@ import type {
|
|
|
7
9
|
ToolDefinition,
|
|
8
10
|
ToolRenderResultOptions,
|
|
9
11
|
} from "@mariozechner/pi-coding-agent";
|
|
10
|
-
import type
|
|
12
|
+
import { type Component, Text } from "@mariozechner/pi-tui";
|
|
11
13
|
import { daemonSocketMayExist } from "../daemon/spawn";
|
|
12
14
|
import type { BrowserClient } from "../client";
|
|
13
|
-
import type
|
|
15
|
+
import { type Result, err } from "./result";
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
// Details are stored by pi as JSON and handed back on replay, so a renderer only ever reads them.
|
|
18
|
+
export type DeepReadonly<T> = T extends ReadonlyArray<infer U>
|
|
19
|
+
? ReadonlyArray<DeepReadonly<U>>
|
|
20
|
+
: T extends object
|
|
21
|
+
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
|
|
22
|
+
: T;
|
|
23
|
+
|
|
24
|
+
// pi serializes details to JSON, so a Map, a function or an undefined-valued key would be silently lost or
|
|
25
|
+
// mangled on replay; the untyped contract admits only what survives the round trip.
|
|
26
|
+
export type JsonValue = string | number | boolean | null | ReadonlyArray<JsonValue> | JsonObject;
|
|
27
|
+
export type JsonObject = { readonly [key: string]: JsonValue };
|
|
28
|
+
|
|
29
|
+
const isJsonValue = (v: unknown): v is JsonValue => {
|
|
30
|
+
if (v === null || typeof v === "string" || typeof v === "number" || typeof v === "boolean") return true;
|
|
31
|
+
if (Array.isArray(v)) return v.every(isJsonValue);
|
|
32
|
+
if (typeof v !== "object" || Object.getPrototypeOf(v) !== Object.prototype) return false;
|
|
33
|
+
return Object.values(v).every(isJsonValue);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const isJsonObject = (v: unknown): v is JsonObject => isRecord(v) && isJsonValue(v);
|
|
37
|
+
|
|
38
|
+
// A tool that declares no `details` schema may still attach any JSON object for logs; its renderer (if any)
|
|
39
|
+
// sees that object untyped, or undefined when the handler attached nothing.
|
|
40
|
+
export const UntypedDetailsSchema = Type.Union([Type.Record(Type.String(), Type.Unknown()), Type.Undefined()]);
|
|
41
|
+
export type UntypedDetailsSchema = typeof UntypedDetailsSchema;
|
|
42
|
+
export type UntypedDetails = JsonObject | undefined;
|
|
43
|
+
|
|
44
|
+
// The value type a details schema describes, as the handler produces it and the renderer reads it back.
|
|
45
|
+
export type DetailsOf<D extends TSchema> = D extends UntypedDetailsSchema ? UntypedDetails : DeepReadonly<Static<D>>;
|
|
46
|
+
|
|
47
|
+
export type ToolOk<T = UntypedDetails> = {
|
|
16
48
|
readonly text: string;
|
|
17
|
-
readonly details?:
|
|
49
|
+
readonly details?: T;
|
|
18
50
|
};
|
|
19
51
|
|
|
20
52
|
export type ToolErrKind =
|
|
@@ -22,6 +54,7 @@ export type ToolErrKind =
|
|
|
22
54
|
| "cdp_error"
|
|
23
55
|
| "timeout"
|
|
24
56
|
| "invalid_state"
|
|
57
|
+
| "invalid_args"
|
|
25
58
|
| "io_error"
|
|
26
59
|
| "internal";
|
|
27
60
|
|
|
@@ -31,80 +64,131 @@ export type ToolErr = {
|
|
|
31
64
|
readonly details?: Readonly<Record<string, unknown>>;
|
|
32
65
|
};
|
|
33
66
|
|
|
34
|
-
export type HandlerContext = {
|
|
67
|
+
export type HandlerContext<T = UntypedDetails> = {
|
|
35
68
|
readonly client: BrowserClient;
|
|
36
69
|
readonly signal: AbortSignal | undefined;
|
|
37
|
-
readonly onUpdate: (update: ToolOk) => void;
|
|
70
|
+
readonly onUpdate: (update: ToolOk<T>) => void;
|
|
38
71
|
readonly extensionCtx: ExtensionContext;
|
|
39
72
|
};
|
|
40
73
|
|
|
41
|
-
export type ToolHandler<S extends TSchema> = (
|
|
74
|
+
export type ToolHandler<S extends TSchema, T = UntypedDetails> = (
|
|
42
75
|
args: Static<S>,
|
|
43
|
-
ctx: HandlerContext
|
|
44
|
-
) => Promise<Result<ToolOk
|
|
76
|
+
ctx: HandlerContext<T>,
|
|
77
|
+
) => Promise<Result<ToolOk<T>, ToolErr>>;
|
|
45
78
|
|
|
46
|
-
export type
|
|
79
|
+
export type ToolConcurrency = "serialized" | "parallel";
|
|
80
|
+
|
|
81
|
+
// S types the arguments, D the structured details the handler attaches to a successful result. pi persists
|
|
82
|
+
// details and hands them back to renderResult on replay; declaring D lets the tool read them back typed, after
|
|
83
|
+
// the contract has validated them against the schema. A tool with no details leaves D at its default.
|
|
84
|
+
export type BrowserToolDefinition<S extends TSchema, D extends TSchema = UntypedDetailsSchema> = {
|
|
47
85
|
readonly name: string;
|
|
48
86
|
readonly label: string;
|
|
49
87
|
readonly description: string;
|
|
50
88
|
readonly promptSnippet: string;
|
|
51
89
|
readonly promptGuidelines: ReadonlyArray<string>;
|
|
52
90
|
readonly parameters: S;
|
|
53
|
-
readonly
|
|
91
|
+
readonly details?: D;
|
|
92
|
+
readonly handler: ToolHandler<S, DetailsOf<D>>;
|
|
54
93
|
readonly renderCall?: (args: Static<S>, theme: Theme) => Component;
|
|
55
|
-
readonly renderResult?: (result: AgentToolResult<
|
|
56
|
-
readonly concurrency:
|
|
94
|
+
readonly renderResult?: (result: AgentToolResult<DetailsOf<D>>, expanded: boolean, theme: Theme) => Component;
|
|
95
|
+
readonly concurrency: ToolConcurrency;
|
|
57
96
|
readonly ensureAlive?: boolean;
|
|
58
97
|
};
|
|
59
98
|
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
99
|
+
// The schema-erased view of a tool. Everything that depends on S or D (handler, renderers, the details
|
|
100
|
+
// validator) stays closed over inside `register`, which was built while they were still bound, so no cast is
|
|
101
|
+
// needed to hold tools of different schemas in one list.
|
|
102
|
+
export type BrowserTool = Pick<
|
|
103
|
+
BrowserToolDefinition<TSchema>,
|
|
104
|
+
"name" | "label" | "description" | "promptSnippet" | "promptGuidelines" | "concurrency" | "ensureAlive"
|
|
105
|
+
> & {
|
|
106
|
+
readonly register: (pi: Pick<ExtensionAPI, "registerTool">, client: BrowserClient) => void;
|
|
64
107
|
};
|
|
65
108
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
type
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
):
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
109
|
+
// A defined tool is both: tests reach the typed handler and parameters, the registry sees BrowserTool.
|
|
110
|
+
export type DefinedBrowserTool<S extends TSchema, D extends TSchema = UntypedDetailsSchema> =
|
|
111
|
+
BrowserToolDefinition<S, D> & BrowserTool;
|
|
112
|
+
|
|
113
|
+
// pi does not export ToolRenderContext from its index, so it is read off the renderer signature.
|
|
114
|
+
type RenderResultContext<S extends TSchema> = Parameters<NonNullable<ToolDefinition<S>["renderResult"]>>[3];
|
|
115
|
+
|
|
116
|
+
export const renderToolError = (toolName: string, e: ToolErr): string =>
|
|
117
|
+
`${toolName} failed (${e.kind}): ${e.message}`;
|
|
118
|
+
|
|
119
|
+
// Stored details are whatever the handler attached; the schema (not a marker) is what tells a renderer they
|
|
120
|
+
// are usable on replay.
|
|
121
|
+
const okResult = <T>(u: ToolOk<T>): AgentToolResult<unknown> => ({
|
|
122
|
+
content: [{ type: "text", text: u.text }],
|
|
123
|
+
details: u.details,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const storedText = (result: AgentToolResult<unknown>, fallback: string): string => {
|
|
127
|
+
const first = result.content[0];
|
|
128
|
+
return first?.type === "text" ? first.text : fallback;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Readonly-ness is a compile-time view, so a value the compiled schema accepts is a DetailsOf<D> value. This
|
|
132
|
+
// is the one place the predicate is asserted; tools get their details typed through it.
|
|
133
|
+
// Compiled once: most tools declare no schema, and compiling the shared fallback per tool is wasted codegen.
|
|
134
|
+
const UNTYPED_DETAILS_VALIDATOR = Compile(UntypedDetailsSchema);
|
|
135
|
+
|
|
136
|
+
const detailsGuard = <D extends TSchema>(schema: D | undefined): ((v: unknown) => v is DetailsOf<D>) => {
|
|
137
|
+
const validator = schema ? Compile(schema) : UNTYPED_DETAILS_VALIDATOR;
|
|
138
|
+
return (v: unknown): v is DetailsOf<D> => validator.Check(v);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const notConnected = (message: string): Result<never, ToolErr> =>
|
|
142
|
+
err({ kind: "not_connected", message });
|
|
143
|
+
|
|
144
|
+
const runHandler = async <S extends TSchema, D extends TSchema>(
|
|
145
|
+
def: BrowserToolDefinition<S, D>,
|
|
146
|
+
client: BrowserClient,
|
|
147
|
+
args: Static<S>,
|
|
148
|
+
signal: AbortSignal | undefined,
|
|
149
|
+
onUpdate: ((partial: AgentToolResult<unknown>) => void) | undefined,
|
|
150
|
+
extensionCtx: ExtensionContext,
|
|
151
|
+
): Promise<Result<ToolOk<DetailsOf<D>>, ToolErr>> => {
|
|
152
|
+
if (def.ensureAlive !== false) {
|
|
153
|
+
if (!(await daemonSocketMayExist())) {
|
|
154
|
+
return notConnected("Browser harness not initialized. Run /browser-setup first");
|
|
155
|
+
}
|
|
156
|
+
const alive = await client.ensureAlive();
|
|
157
|
+
if (!alive.success) return notConnected(alive.error.message);
|
|
158
|
+
}
|
|
159
|
+
let release: (() => void) | undefined;
|
|
160
|
+
if (def.concurrency !== "parallel") {
|
|
161
|
+
release = await client.mutationMutex().acquire();
|
|
162
|
+
if (signal?.aborted) {
|
|
163
|
+
release();
|
|
164
|
+
return err({ kind: "invalid_state", message: "Tool execution aborted before entering serialized lane" });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
return await def.handler(args, {
|
|
169
|
+
client,
|
|
170
|
+
signal,
|
|
171
|
+
onUpdate: (u) => onUpdate?.(okResult(u)),
|
|
172
|
+
extensionCtx,
|
|
173
|
+
});
|
|
174
|
+
} catch (e) {
|
|
175
|
+
// A throw is a bug in the handler, not a tool outcome; it still has to reach the model as an error.
|
|
176
|
+
return err({ kind: "internal", message: e instanceof Error ? e.message : String(e) });
|
|
177
|
+
} finally {
|
|
178
|
+
release?.();
|
|
85
179
|
}
|
|
86
|
-
const details: ErrDetails = {
|
|
87
|
-
ok: false,
|
|
88
|
-
kind: r.error.kind,
|
|
89
|
-
message: r.error.message,
|
|
90
|
-
...(r.error.details ?? {}),
|
|
91
|
-
};
|
|
92
|
-
return {
|
|
93
|
-
isError: true,
|
|
94
|
-
content: [{ type: "text", text: `${toolName} failed (${r.error.kind}): ${r.error.message}` }],
|
|
95
|
-
details,
|
|
96
|
-
};
|
|
97
180
|
};
|
|
98
181
|
|
|
99
|
-
|
|
100
|
-
|
|
182
|
+
// pi has no error field on a resolved tool result: the agent loop marks a result isError only when
|
|
183
|
+
// execute rejects, and then stores the thrown message as the result text. So a failed handler throws.
|
|
184
|
+
const toPiTool = <S extends TSchema, D extends TSchema>(
|
|
185
|
+
def: BrowserToolDefinition<S, D>,
|
|
101
186
|
client: BrowserClient,
|
|
102
|
-
|
|
103
|
-
): void => {
|
|
104
|
-
type S = TSchema;
|
|
187
|
+
): ToolDefinition<S> => {
|
|
105
188
|
const defRenderCall = def.renderCall;
|
|
106
189
|
const defRenderResult = def.renderResult;
|
|
107
|
-
const
|
|
190
|
+
const isDetails = detailsGuard(def.details);
|
|
191
|
+
return {
|
|
108
192
|
name: def.name,
|
|
109
193
|
label: def.label,
|
|
110
194
|
description: def.description,
|
|
@@ -112,11 +196,7 @@ export const registerBrowserTool = (
|
|
|
112
196
|
promptGuidelines: [...def.promptGuidelines],
|
|
113
197
|
parameters: def.parameters,
|
|
114
198
|
...(defRenderCall
|
|
115
|
-
? {
|
|
116
|
-
renderCall: (args: Static<S>, theme: Theme, _ctx: Parameters<NonNullable<ToolDefinition<S>["renderCall"]>>[2]) =>
|
|
117
|
-
// Sanctioned cast: at runtime args is always the concrete Static<S> value the tool was defined with.
|
|
118
|
-
(defRenderCall as (args: unknown, theme: Theme) => Component)(args, theme),
|
|
119
|
-
}
|
|
199
|
+
? { renderCall: (args: Static<S>, theme: Theme) => defRenderCall(args, theme) }
|
|
120
200
|
: {}),
|
|
121
201
|
...(defRenderResult
|
|
122
202
|
? {
|
|
@@ -124,64 +204,33 @@ export const registerBrowserTool = (
|
|
|
124
204
|
result: AgentToolResult<unknown>,
|
|
125
205
|
options: ToolRenderResultOptions,
|
|
126
206
|
theme: Theme,
|
|
127
|
-
|
|
128
|
-
) =>
|
|
207
|
+
ctx: RenderResultContext<S>,
|
|
208
|
+
) => {
|
|
209
|
+
// A replayed error carries only the thrown text (details are empty), so the tool's own renderer never sees it.
|
|
210
|
+
if (ctx.isError) {
|
|
211
|
+
const text = storedText(result, `${def.name} failed (internal): unknown error`);
|
|
212
|
+
return new Text(theme.fg("error", text), 0, 0);
|
|
213
|
+
}
|
|
214
|
+
// Replayed details may predate the schema (older session, changed shape); then the stored text stands in.
|
|
215
|
+
const details: unknown = result.details;
|
|
216
|
+
if (!isDetails(details)) return new Text(storedText(result, ""), 0, 0);
|
|
217
|
+
return defRenderResult({ ...result, details }, options.expanded, theme);
|
|
218
|
+
},
|
|
129
219
|
}
|
|
130
220
|
: {}),
|
|
131
221
|
async execute(_toolCallId, args, signal, onUpdate, extensionCtx) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
{
|
|
136
|
-
success: false,
|
|
137
|
-
error: {
|
|
138
|
-
kind: "not_connected",
|
|
139
|
-
message: "Browser harness not initialized. Run /browser-setup first",
|
|
140
|
-
},
|
|
141
|
-
},
|
|
142
|
-
def.name,
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
const alive = await client.ensureAlive();
|
|
146
|
-
if (!alive.success) {
|
|
147
|
-
return toToolResult(
|
|
148
|
-
{ success: false, error: { kind: "not_connected", message: alive.error.message } },
|
|
149
|
-
def.name,
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
let release: (() => void) | undefined;
|
|
154
|
-
if (def.concurrency !== "parallel") {
|
|
155
|
-
const acquire = await client.mutationMutex().acquire();
|
|
156
|
-
release = acquire;
|
|
157
|
-
if (signal?.aborted) {
|
|
158
|
-
release();
|
|
159
|
-
return toToolResult(
|
|
160
|
-
{ success: false, error: { kind: "invalid_state", message: "Tool execution aborted before entering serialized lane" } },
|
|
161
|
-
def.name,
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
try {
|
|
166
|
-
const result = await def.handler(args, {
|
|
167
|
-
client,
|
|
168
|
-
signal,
|
|
169
|
-
onUpdate: (u) => {
|
|
170
|
-
if (onUpdate) {
|
|
171
|
-
const update: AgentToolResult<OkDetails> = {
|
|
172
|
-
content: [{ type: "text", text: u.text }],
|
|
173
|
-
details: { ok: true, ...(u.details ?? {}) },
|
|
174
|
-
};
|
|
175
|
-
onUpdate(update);
|
|
176
|
-
}
|
|
177
|
-
},
|
|
178
|
-
extensionCtx,
|
|
179
|
-
});
|
|
180
|
-
return toToolResult(result, def.name);
|
|
181
|
-
} finally {
|
|
182
|
-
release?.();
|
|
183
|
-
}
|
|
222
|
+
const result = await runHandler(def, client, args, signal, onUpdate, extensionCtx);
|
|
223
|
+
if (!result.success) throw new Error(renderToolError(def.name, result.error));
|
|
224
|
+
return okResult(result.data);
|
|
184
225
|
},
|
|
185
226
|
};
|
|
186
|
-
pi.registerTool(td);
|
|
187
227
|
};
|
|
228
|
+
|
|
229
|
+
export const defineBrowserTool = <S extends TSchema, D extends TSchema = UntypedDetailsSchema>(
|
|
230
|
+
def: BrowserToolDefinition<S, D>,
|
|
231
|
+
): DefinedBrowserTool<S, D> => ({
|
|
232
|
+
...def,
|
|
233
|
+
register: (pi, client) => {
|
|
234
|
+
pi.registerTool(toPiTool(def, client));
|
|
235
|
+
},
|
|
236
|
+
});
|
package/src/util/truncate.ts
CHANGED
|
@@ -13,7 +13,7 @@ const tempDirs: string[] = [];
|
|
|
13
13
|
|
|
14
14
|
export const cleanupTempDirs = async (): Promise<void> => {
|
|
15
15
|
const dirs = tempDirs.splice(0);
|
|
16
|
-
await Promise.all(dirs.map((d) => rm(d, { recursive: true, force: true }).catch(() =>
|
|
16
|
+
await Promise.all(dirs.map((d) => rm(d, { recursive: true, force: true }).catch(() => undefined)));
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
export type TruncatedOutput = {
|
package/tsconfig.json
CHANGED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
export type FillOptions = {
|
|
2
|
-
readonly rejectSelect: boolean;
|
|
3
|
-
readonly focusFirst: boolean;
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
const ENGINE = `
|
|
7
|
-
const el = this;
|
|
8
|
-
if (!el || el.nodeType !== 1) return { ok: false, reason: "ref does not point to an element" };
|
|
9
|
-
const tagName = el.tagName || "";
|
|
10
|
-
const tag = tagName.toLowerCase();
|
|
11
|
-
const type = (el.type || "").toLowerCase();
|
|
12
|
-
|
|
13
|
-
if (__rejectSelect && tag === "select") {
|
|
14
|
-
return { ok: false, reason: "element is a <select>", kind: "select", tag: tagName };
|
|
15
|
-
}
|
|
16
|
-
if (el.disabled) return { ok: false, reason: "element is disabled", tag: tagName };
|
|
17
|
-
|
|
18
|
-
const fire = function (t) { el.dispatchEvent(new Event(t, { bubbles: true })); };
|
|
19
|
-
if (__focusFirst) { try { el.focus(); } catch (e) {} }
|
|
20
|
-
|
|
21
|
-
if (tag === "select") {
|
|
22
|
-
const want = String(value);
|
|
23
|
-
let matched = null;
|
|
24
|
-
for (let i = 0; i < el.options.length; i++) {
|
|
25
|
-
const o = el.options[i];
|
|
26
|
-
if (o.value === want || o.label === want || o.text === want) { matched = o; break; }
|
|
27
|
-
}
|
|
28
|
-
if (!matched) {
|
|
29
|
-
const opts = [];
|
|
30
|
-
for (let i = 0; i < el.options.length; i++) opts.push({ value: el.options[i].value, text: el.options[i].text });
|
|
31
|
-
return { ok: false, reason: "no matching option", kind: "select", options: opts, tag: tagName };
|
|
32
|
-
}
|
|
33
|
-
el.value = matched.value;
|
|
34
|
-
fire("input"); fire("change");
|
|
35
|
-
return { ok: true, kind: "select", value: el.value, text: matched.text, tag: tagName };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (tag === "input" && (type === "checkbox" || type === "radio")) {
|
|
39
|
-
const want = value === true || value === "true" || value === "on" || value === 1;
|
|
40
|
-
let changed = false;
|
|
41
|
-
if (el.checked !== want) {
|
|
42
|
-
try { el.click(); } catch (e) {}
|
|
43
|
-
if (el.checked !== want) { el.checked = want; fire("input"); fire("change"); }
|
|
44
|
-
changed = true;
|
|
45
|
-
}
|
|
46
|
-
return { ok: true, kind: type, checked: el.checked, changed: changed, tag: tagName };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (el.isContentEditable) {
|
|
50
|
-
try { el.focus(); } catch (e) {}
|
|
51
|
-
let done = false;
|
|
52
|
-
try {
|
|
53
|
-
const sel = window.getSelection();
|
|
54
|
-
if (sel) { sel.removeAllRanges(); const rng = document.createRange(); rng.selectNodeContents(el); sel.addRange(rng); }
|
|
55
|
-
done = document.execCommand("insertText", false, String(value));
|
|
56
|
-
} catch (e) { done = false; }
|
|
57
|
-
if (!done) {
|
|
58
|
-
el.textContent = String(value);
|
|
59
|
-
el.dispatchEvent(new InputEvent("input", { bubbles: true }));
|
|
60
|
-
}
|
|
61
|
-
return { ok: true, kind: "contenteditable", value: el.textContent || "", tag: tagName };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (tag === "input" || tag === "textarea") {
|
|
65
|
-
const proto = tag === "textarea" ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype;
|
|
66
|
-
const desc = Object.getOwnPropertyDescriptor(proto, "value");
|
|
67
|
-
if (desc && desc.set) desc.set.call(el, String(value));
|
|
68
|
-
else el.value = String(value);
|
|
69
|
-
fire("input"); fire("change");
|
|
70
|
-
return { ok: true, kind: el.type || tag, value: el.value, tag: tagName };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return { ok: false, reason: "element is not a fillable field (tag=" + tag + ")", tag: tagName };
|
|
74
|
-
`;
|
|
75
|
-
|
|
76
|
-
export const fillBody = (opts: FillOptions): string =>
|
|
77
|
-
`const __rejectSelect = ${opts.rejectSelect}; const __focusFirst = ${opts.focusFirst};\n${ENGINE}`;
|
|
78
|
-
|
|
79
|
-
export const fillDeclaration = (opts: FillOptions): string =>
|
|
80
|
-
`function (value) { ${fillBody(opts)} }`;
|