@webhands/core 0.1.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.
Files changed (76) hide show
  1. package/dist/cookies-export.d.ts +56 -0
  2. package/dist/cookies-export.d.ts.map +1 -0
  3. package/dist/cookies-export.js +69 -0
  4. package/dist/cookies-export.js.map +1 -0
  5. package/dist/errors.d.ts +126 -0
  6. package/dist/errors.d.ts.map +1 -0
  7. package/dist/errors.js +135 -0
  8. package/dist/errors.js.map +1 -0
  9. package/dist/index.d.ts +16 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +15 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/playwright-attach-transport.d.ts +28 -0
  14. package/dist/playwright-attach-transport.d.ts.map +1 -0
  15. package/dist/playwright-attach-transport.js +175 -0
  16. package/dist/playwright-attach-transport.js.map +1 -0
  17. package/dist/playwright-launch-transport.d.ts +90 -0
  18. package/dist/playwright-launch-transport.d.ts.map +1 -0
  19. package/dist/playwright-launch-transport.js +305 -0
  20. package/dist/playwright-launch-transport.js.map +1 -0
  21. package/dist/profile-location.d.ts +61 -0
  22. package/dist/profile-location.d.ts.map +1 -0
  23. package/dist/profile-location.js +61 -0
  24. package/dist/profile-location.js.map +1 -0
  25. package/dist/remote-session.d.ts +22 -0
  26. package/dist/remote-session.d.ts.map +1 -0
  27. package/dist/remote-session.js +57 -0
  28. package/dist/remote-session.js.map +1 -0
  29. package/dist/seam.d.ts +212 -0
  30. package/dist/seam.d.ts.map +1 -0
  31. package/dist/seam.js +25 -0
  32. package/dist/seam.js.map +1 -0
  33. package/dist/session-endpoint.d.ts +53 -0
  34. package/dist/session-endpoint.d.ts.map +1 -0
  35. package/dist/session-endpoint.js +75 -0
  36. package/dist/session-endpoint.js.map +1 -0
  37. package/dist/session-rpc.d.ts +82 -0
  38. package/dist/session-rpc.d.ts.map +1 -0
  39. package/dist/session-rpc.js +107 -0
  40. package/dist/session-rpc.js.map +1 -0
  41. package/dist/session-server.d.ts +79 -0
  42. package/dist/session-server.d.ts.map +1 -0
  43. package/dist/session-server.js +141 -0
  44. package/dist/session-server.js.map +1 -0
  45. package/dist/setup-profile.d.ts +84 -0
  46. package/dist/setup-profile.d.ts.map +1 -0
  47. package/dist/setup-profile.js +52 -0
  48. package/dist/setup-profile.js.map +1 -0
  49. package/dist/stub-transport.d.ts +26 -0
  50. package/dist/stub-transport.d.ts.map +1 -0
  51. package/dist/stub-transport.js +76 -0
  52. package/dist/stub-transport.js.map +1 -0
  53. package/dist/test-fixtures/fixture-pages.d.ts +12 -0
  54. package/dist/test-fixtures/fixture-pages.d.ts.map +1 -0
  55. package/dist/test-fixtures/fixture-pages.js +204 -0
  56. package/dist/test-fixtures/fixture-pages.js.map +1 -0
  57. package/dist/test-fixtures/fixture-server.d.ts +19 -0
  58. package/dist/test-fixtures/fixture-server.d.ts.map +1 -0
  59. package/dist/test-fixtures/fixture-server.js +41 -0
  60. package/dist/test-fixtures/fixture-server.js.map +1 -0
  61. package/package.json +34 -0
  62. package/src/cookies-export.ts +91 -0
  63. package/src/errors.ts +185 -0
  64. package/src/index.ts +89 -0
  65. package/src/playwright-attach-transport.ts +214 -0
  66. package/src/playwright-launch-transport.ts +363 -0
  67. package/src/profile-location.ts +92 -0
  68. package/src/remote-session.ts +66 -0
  69. package/src/seam.ts +222 -0
  70. package/src/session-endpoint.ts +104 -0
  71. package/src/session-rpc.ts +143 -0
  72. package/src/session-server.ts +231 -0
  73. package/src/setup-profile.ts +134 -0
  74. package/src/stub-transport.ts +100 -0
  75. package/src/test-fixtures/fixture-pages.ts +210 -0
  76. package/src/test-fixtures/fixture-server.ts +54 -0
@@ -0,0 +1,107 @@
1
+ import { locator, } from './seam.js';
2
+ /**
3
+ * The wire protocol for driving the long-lived session over HTTP (ADR-0005).
4
+ *
5
+ * The served process holds ONE live {@link Page} in memory; a thin client verb
6
+ * cannot hold a JS reference to it across the process boundary, so each verb
7
+ * call is sent as a small JSON request to the server, which runs it against its
8
+ * live page and returns the result. This module is the SINGLE source of truth
9
+ * for that request/response shape, imported by BOTH the server handler and the
10
+ * client proxy so they cannot drift (mirrors how `serializeCookies` is shared
11
+ * by the cookies verb and its test).
12
+ *
13
+ * It is a thin transport detail, NOT a second verb surface: every request maps
14
+ * 1:1 to a {@link Page} method, and the seam's verb semantics (ADR-0003/0004)
15
+ * are unchanged. The {@link LocatorString} brand and the structured
16
+ * {@link WaitCondition} cross as plain JSON and are re-branded on the server
17
+ * with {@link locator}; no Playwright/CDP type is ever named here.
18
+ */
19
+ /** The path the session RPC is served under, below the server's base URL. */
20
+ export const SESSION_RPC_PATH = '/session/call';
21
+ /**
22
+ * Run one {@link SessionRpcRequest} against a live {@link Page}, returning the
23
+ * value the wire should carry back. The server's HTTP handler is just this plus
24
+ * JSON framing; keeping the dispatch here (not inline in the handler) means the
25
+ * verb-to-page mapping is in one place and unit-testable without HTTP.
26
+ *
27
+ * The locator string and wait condition arrive as plain JSON; we re-brand the
28
+ * locator with {@link locator} before handing it to the page so the seam's
29
+ * branded-string contract holds.
30
+ */
31
+ export async function applySessionRpc(page, request) {
32
+ switch (request.verb) {
33
+ case 'navigate':
34
+ await page.navigate(request.url);
35
+ return undefined;
36
+ case 'snapshot':
37
+ return page.snapshot({ full: request.full });
38
+ case 'click':
39
+ await page.click(locator(request.locator));
40
+ return undefined;
41
+ case 'type':
42
+ await page.type(locator(request.locator), request.text);
43
+ return undefined;
44
+ case 'eval':
45
+ return page.eval(request.expression);
46
+ case 'wait':
47
+ await page.wait(rebrandWait(request.condition));
48
+ return undefined;
49
+ case 'cookies':
50
+ return page.cookies();
51
+ case 'setCookies':
52
+ await page.setCookies(request.cookies);
53
+ return undefined;
54
+ }
55
+ }
56
+ /**
57
+ * A {@link Page} whose verbs forward to a server via the supplied transport.
58
+ *
59
+ * Used by the client-side proxy (see `remote-session.ts`): each verb builds a
60
+ * {@link SessionRpcRequest}, hands it to `send`, and shapes the reply back into
61
+ * the verb's return type. The `send` function owns the actual HTTP; this keeps
62
+ * the verb-to-request mapping (the other half of {@link applySessionRpc}) in
63
+ * one place so request and response shapes cannot drift between the two sides.
64
+ */
65
+ export function makeRpcPage(send) {
66
+ return {
67
+ async navigate(url) {
68
+ await send({ verb: 'navigate', url });
69
+ },
70
+ async snapshot(options) {
71
+ return (await send({
72
+ verb: 'snapshot',
73
+ full: options?.full,
74
+ }));
75
+ },
76
+ async click(target) {
77
+ await send({ verb: 'click', locator: target });
78
+ },
79
+ async type(target, text) {
80
+ await send({ verb: 'type', locator: target, text });
81
+ },
82
+ async eval(expression) {
83
+ return send({ verb: 'eval', expression });
84
+ },
85
+ async wait(condition) {
86
+ await send({ verb: 'wait', condition });
87
+ },
88
+ async cookies() {
89
+ return (await send({ verb: 'cookies' }));
90
+ },
91
+ async setCookies(cookies) {
92
+ await send({ verb: 'setCookies', cookies });
93
+ },
94
+ };
95
+ }
96
+ /**
97
+ * Re-brand a {@link WaitCondition} that arrived as plain JSON: only the
98
+ * `locator` form carries a branded string, which JSON flattens to a plain
99
+ * `string`, so we re-tag it before it reaches the page.
100
+ */
101
+ function rebrandWait(condition) {
102
+ if (condition.kind === 'locator') {
103
+ return { kind: 'locator', target: locator(condition.target) };
104
+ }
105
+ return condition;
106
+ }
107
+ //# sourceMappingURL=session-rpc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-rpc.js","sourceRoot":"","sources":["../src/session-rpc.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,79 @@
1
+ import { SessionAlreadyActiveError } from './errors.js';
2
+ import type { ProfileLocationOptions } from './profile-location.js';
3
+ import { type SessionEndpoint } from './session-endpoint.js';
4
+ import type { OpenTarget, Transport } from './seam.js';
5
+ /**
6
+ * The long-lived host that keeps ONE browser session alive between separate CLI
7
+ * invocations (ADR-0005; ADR-0001's control loop made concrete).
8
+ *
9
+ * This IS the controller: it opens the single live {@link Session} ONCE through
10
+ * a {@link Transport} and then serves that already-live page over HTTP, so each
11
+ * `webhands <verb>` thin-client process drives the SAME page state
12
+ * (not just the on-disk profile) and exits. The browser is launched once here,
13
+ * never per verb. It owns three things ADR-0005 calls out:
14
+ *
15
+ * 1. **Single session.** It holds exactly one session; a second {@link open}
16
+ * while one is live is a {@link SessionAlreadyActiveError}, not a second
17
+ * browser.
18
+ * 2. **Discovery.** On start it writes its endpoint (the bound URL + pid) under
19
+ * the config dir so client verbs can find it; on stop it clears that file.
20
+ * 3. **Explicit teardown.** {@link stop} closes the browser and stops the
21
+ * listener; nothing auto-spawns and nothing auto-tears-down.
22
+ *
23
+ * The HTTP surface here is the small session RPC (`/session/call`, see
24
+ * `session-rpc.ts`), deliberately SEPARATE from incur's per-verb commands: a
25
+ * verb command opens-and-closes a session per call, which is exactly what
26
+ * cross-invocation persistence must NOT do. The CLI's `serve` command wraps
27
+ * this server; the CLI's verb commands become thin clients of it.
28
+ *
29
+ * Shared-write isolation: the endpoint file lives under the controller home
30
+ * root, so a {@link SessionServer} created with a temp `root`/`env` (via
31
+ * {@link SessionServerOptions}) writes only there, and tests assert the real
32
+ * `~/.webhands` is untouched.
33
+ */
34
+ export interface SessionServerOptions extends ProfileLocationOptions {
35
+ /**
36
+ * The transport that opens the single live session (defaults to the caller's
37
+ * choice of launch/attach transport). Injectable so a test drives the server
38
+ * with any seam transport (e.g. a real Playwright launch against the local
39
+ * fixture profile) without the server hard-coding one.
40
+ */
41
+ readonly transport: Transport;
42
+ /**
43
+ * Host to bind the HTTP listener to. Defaults to loopback (`127.0.0.1`): the
44
+ * server is a LOCAL tool on the user's machine (PRD "Out of Scope": not a
45
+ * hosted service), so it never listens on a public interface.
46
+ */
47
+ readonly host?: string;
48
+ /** TCP port to bind. Defaults to `0` (an OS-assigned ephemeral port). */
49
+ readonly port?: number;
50
+ }
51
+ /** A running {@link SessionServer}: its advertised endpoint and how to stop it. */
52
+ export interface RunningSessionServer {
53
+ /** The endpoint advertised under the config dir for client discovery. */
54
+ readonly endpoint: SessionEndpoint;
55
+ /**
56
+ * Tear the session down: close the browser, stop the HTTP listener, and clear
57
+ * the endpoint file. Idempotent.
58
+ */
59
+ stop(): Promise<void>;
60
+ }
61
+ /**
62
+ * Start the long-lived session server: open the single session via the
63
+ * transport, bind the HTTP listener, advertise the endpoint, and serve the
64
+ * session RPC. Returns once the server is live and discoverable.
65
+ *
66
+ * Enforces the single-session invariant ACROSS processes via the endpoint file:
67
+ * if a live endpoint is already advertised, this refuses with
68
+ * {@link SessionAlreadyActiveError} rather than opening a second browser. (The
69
+ * caller checks discovery first; this is the last-line guard.)
70
+ */
71
+ export declare function startSessionServer(target: OpenTarget, options: SessionServerOptions): Promise<RunningSessionServer>;
72
+ /**
73
+ * Guard the single-session invariant against double-open. The mechanism a
74
+ * caller actually relies on is discovery (no endpoint file ⇒ no live server);
75
+ * this is the explicit error a caller raises when it finds one already live and
76
+ * wants to refuse rather than open a second.
77
+ */
78
+ export declare function sessionAlreadyActive(): SessionAlreadyActiveError;
79
+ //# sourceMappingURL=session-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-server.d.ts","sourceRoot":"","sources":["../src/session-server.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,yBAAyB,EAAC,MAAM,aAAa,CAAC;AACtD,OAAO,KAAK,EAAC,sBAAsB,EAAC,MAAM,uBAAuB,CAAC;AAClE,OAAO,EAGN,KAAK,eAAe,EACpB,MAAM,uBAAuB,CAAC;AAO/B,OAAO,KAAK,EAAC,UAAU,EAAW,SAAS,EAAC,MAAM,WAAW,CAAC;AAE9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,WAAW,oBAAqB,SAAQ,sBAAsB;IACnE;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,yEAAyE;IACzE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,mFAAmF;AACnF,MAAM,WAAW,oBAAoB;IACpC,yEAAyE;IACzE,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC;;;OAGG;IACH,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACtB;AAED;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,CACvC,MAAM,EAAE,UAAU,EAClB,OAAO,EAAE,oBAAoB,GAC3B,OAAO,CAAC,oBAAoB,CAAC,CAiD/B;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,IAAI,yBAAyB,CAEhE"}
@@ -0,0 +1,141 @@
1
+ import { createServer } from 'node:http';
2
+ import { SessionAlreadyActiveError } from './errors.js';
3
+ import { clearSessionEndpoint, writeSessionEndpoint, } from './session-endpoint.js';
4
+ import { applySessionRpc, SESSION_RPC_PATH, } from './session-rpc.js';
5
+ /**
6
+ * Start the long-lived session server: open the single session via the
7
+ * transport, bind the HTTP listener, advertise the endpoint, and serve the
8
+ * session RPC. Returns once the server is live and discoverable.
9
+ *
10
+ * Enforces the single-session invariant ACROSS processes via the endpoint file:
11
+ * if a live endpoint is already advertised, this refuses with
12
+ * {@link SessionAlreadyActiveError} rather than opening a second browser. (The
13
+ * caller checks discovery first; this is the last-line guard.)
14
+ */
15
+ export async function startSessionServer(target, options) {
16
+ const { transport, host = '127.0.0.1', port = 0, ...location } = options;
17
+ // Open the ONE live session up front: the browser launches here, once.
18
+ const session = await transport.open(target);
19
+ let server;
20
+ try {
21
+ server = createServer((req, res) => {
22
+ handleRequest(session, req, res);
23
+ });
24
+ await listen(server, port, host);
25
+ }
26
+ catch (cause) {
27
+ // Binding failed after we opened the browser; do not leak the session.
28
+ await session.close();
29
+ throw cause;
30
+ }
31
+ const address = server.address();
32
+ if (address === null || typeof address === 'string') {
33
+ await stopServer(server);
34
+ await session.close();
35
+ throw new Error('session server failed to bind to a TCP port');
36
+ }
37
+ const endpoint = {
38
+ url: `http://${host}:${address.port}`,
39
+ pid: process.pid,
40
+ };
41
+ try {
42
+ await writeSessionEndpoint(endpoint, location);
43
+ }
44
+ catch (cause) {
45
+ await stopServer(server);
46
+ await session.close();
47
+ throw cause;
48
+ }
49
+ let stopped = false;
50
+ return {
51
+ endpoint,
52
+ async stop() {
53
+ if (stopped)
54
+ return;
55
+ stopped = true;
56
+ await clearSessionEndpoint(location);
57
+ await stopServer(server);
58
+ await session.close();
59
+ },
60
+ };
61
+ }
62
+ /**
63
+ * Guard the single-session invariant against double-open. The mechanism a
64
+ * caller actually relies on is discovery (no endpoint file ⇒ no live server);
65
+ * this is the explicit error a caller raises when it finds one already live and
66
+ * wants to refuse rather than open a second.
67
+ */
68
+ export function sessionAlreadyActive() {
69
+ return new SessionAlreadyActiveError();
70
+ }
71
+ /** Handle one session-RPC HTTP request against the live session's page. */
72
+ function handleRequest(session, req, res) {
73
+ const url = req.url ?? '/';
74
+ const path = url.split('?')[0];
75
+ if (req.method !== 'POST' || path !== SESSION_RPC_PATH) {
76
+ writeJson(res, 404, {
77
+ ok: false,
78
+ error: `no route for ${req.method} ${path}`,
79
+ });
80
+ return;
81
+ }
82
+ collectBody(req)
83
+ .then(async (body) => {
84
+ let request;
85
+ try {
86
+ request = JSON.parse(body);
87
+ }
88
+ catch {
89
+ writeJson(res, 400, { ok: false, error: 'invalid JSON request body' });
90
+ return;
91
+ }
92
+ try {
93
+ const value = await applySessionRpc(session.page, request);
94
+ const reply = { ok: true, value };
95
+ writeJson(res, 200, reply);
96
+ }
97
+ catch (cause) {
98
+ // A verb that throws in the page (or a closed session) maps to an
99
+ // ok:false reply carrying the message; the client re-throws a faithful
100
+ // Error so the seam's "a page throw rejects" contract holds remotely.
101
+ const message = cause instanceof Error ? cause.message : String(cause);
102
+ const reply = { ok: false, error: message };
103
+ writeJson(res, 200, reply);
104
+ }
105
+ })
106
+ .catch((cause) => {
107
+ const message = cause instanceof Error ? cause.message : String(cause);
108
+ writeJson(res, 500, { ok: false, error: message });
109
+ });
110
+ }
111
+ /** Read a request body to a string. */
112
+ function collectBody(req) {
113
+ return new Promise((resolve, reject) => {
114
+ const chunks = [];
115
+ req.on('data', (chunk) => chunks.push(chunk));
116
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
117
+ req.on('error', reject);
118
+ });
119
+ }
120
+ /** Write a JSON response with a status code. */
121
+ function writeJson(res, status, body) {
122
+ res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
123
+ res.end(JSON.stringify(body));
124
+ }
125
+ /** Promisified `server.listen`. */
126
+ function listen(server, port, host) {
127
+ return new Promise((resolve, reject) => {
128
+ server.once('error', reject);
129
+ server.listen(port, host, () => {
130
+ server.removeListener('error', reject);
131
+ resolve();
132
+ });
133
+ });
134
+ }
135
+ /** Promisified `server.close`. */
136
+ function stopServer(server) {
137
+ return new Promise((resolve, reject) => {
138
+ server.close((err) => (err ? reject(err) : resolve()));
139
+ });
140
+ }
141
+ //# sourceMappingURL=session-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-server.js","sourceRoot":"","sources":["../src/session-server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,84 @@
1
+ import { type ProfileLocation, type ProfileLocationOptions } from './profile-location.js';
2
+ import type { Session, Transport } from './seam.js';
3
+ /**
4
+ * The headed one-time-login flow (PRD User Story 1; CONTEXT `setup-profile`;
5
+ * ADR-0002).
6
+ *
7
+ * `setup-profile` opens the dedicated profile in a VISIBLE (headed) browser so
8
+ * a human logs into a site and/or clears an anti-bot challenge ONCE. The
9
+ * cookies/state the human's session writes persist in the profile dir, so a
10
+ * later `launch --headless` against the SAME profile reuses that logged-in
11
+ * state without re-login.
12
+ *
13
+ * This is the orchestration layer over the launch transport, not a second
14
+ * transport: the launch transport refuses to launch a profile whose dir does
15
+ * not exist (a typed {@link MissingProfileError}, so a `launch` typo cannot
16
+ * spawn a blank profile). CREATING that dir is exactly `setup-profile`'s job,
17
+ * which is why the launch transport defers it here. So this flow:
18
+ *
19
+ * 1. resolves the dedicated profile dir (isolated to a temp root in tests,
20
+ * never the real `~/.webhands`),
21
+ * 2. CREATES it if absent (the one place a profile dir is created), so the
22
+ * profile is now "set up" for later launches,
23
+ * 3. opens it HEADED through the launch transport, and
24
+ * 4. emits a clear, actionable prompt telling the human what to do (log in /
25
+ * clear the challenge, then close the window) and WHICH profile is being set
26
+ * up.
27
+ *
28
+ * The verb only OPENS the window; it never types credentials or touches a
29
+ * credential (ADR-0002: the human does the one-time login, we never bypass it
30
+ * or solve CAPTCHAs). The caller holds the returned {@link Session} open for
31
+ * the interactive login and closes it when the human is done; on close the
32
+ * persistent context flushes the new state to the profile dir. The real
33
+ * third-party login is exercised only in the manual Kayak smoke, not here.
34
+ */
35
+ /** A function that receives the headed-login prompt (one call, the full text). */
36
+ export type PromptSink = (message: string) => void;
37
+ /** Options for {@link setupProfile}. */
38
+ export interface SetupProfileOptions extends ProfileLocationOptions {
39
+ /** Name of the dedicated profile to set up (e.g. `default`). */
40
+ readonly profile: string;
41
+ /**
42
+ * Where the actionable prompt is delivered. Defaults to STDERR
43
+ * (`console.error`), because STDOUT is reserved for the CLI's structured
44
+ * output envelope; the human-facing instruction is a side-channel message.
45
+ * Tests inject a sink to assert the prompt's content.
46
+ */
47
+ readonly onPrompt?: PromptSink;
48
+ /**
49
+ * The transport that opens the headed session. Defaults to a
50
+ * {@link PlaywrightLaunchTransport} bound to this flow's profile location.
51
+ * Injectable so the orchestration (dir creation, headed open, prompt) is
52
+ * testable without a real browser, and so the SAME launch transport the PRD
53
+ * mandates is reused rather than a parallel headed-open path.
54
+ */
55
+ readonly transport?: Transport;
56
+ }
57
+ /** The result of {@link setupProfile}: the live headed session + where it is. */
58
+ export interface SetupProfileResult {
59
+ /**
60
+ * The live headed session, held OPEN for the human's interactive login. The
61
+ * caller closes it when the human is done; closing flushes the session state
62
+ * to the profile dir for a later headless launch.
63
+ */
64
+ readonly session: Session;
65
+ /** The resolved location of the profile that was set up. */
66
+ readonly location: ProfileLocation;
67
+ }
68
+ /**
69
+ * Run the headed `setup-profile` flow for {@link SetupProfileOptions.profile}.
70
+ *
71
+ * Creates the dedicated profile dir if absent, opens it headed through the
72
+ * launch transport, emits the actionable prompt, and returns the live session
73
+ * for the caller to hold open during the interactive login (see this module's
74
+ * overview). Does NOT close the session: holding it open IS the headed-login
75
+ * window, and the caller owns its lifetime.
76
+ */
77
+ export declare function setupProfile(options: SetupProfileOptions): Promise<SetupProfileResult>;
78
+ /**
79
+ * Compose the clear, actionable headed-login prompt (PRD acceptance: tell the
80
+ * user what to do AND which profile is being set up). Kept pure so a test can
81
+ * assert its content directly.
82
+ */
83
+ export declare function buildPrompt(location: ProfileLocation): string;
84
+ //# sourceMappingURL=setup-profile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup-profile.d.ts","sourceRoot":"","sources":["../src/setup-profile.ts"],"names":[],"mappings":"AAEA,OAAO,EAEN,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC3B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAC,OAAO,EAAE,SAAS,EAAC,MAAM,WAAW,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,kFAAkF;AAClF,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;AAEnD,wCAAwC;AACxC,MAAM,WAAW,mBAAoB,SAAQ,sBAAsB;IAClE,gEAAgE;IAChE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,UAAU,CAAC;IAC/B;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;CAC/B;AAED,iFAAiF;AACjF,MAAM,WAAW,kBAAkB;IAClC;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,4DAA4D;IAC5D,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;CACnC;AAED;;;;;;;;GAQG;AACH,wBAAsB,YAAY,CACjC,OAAO,EAAE,mBAAmB,GAC1B,OAAO,CAAC,kBAAkB,CAAC,CAsB7B;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,eAAe,GAAG,MAAM,CAe7D"}
@@ -0,0 +1,52 @@
1
+ import { mkdir } from 'node:fs/promises';
2
+ import { PlaywrightLaunchTransport } from './playwright-launch-transport.js';
3
+ import { resolveProfileLocation, } from './profile-location.js';
4
+ /**
5
+ * Run the headed `setup-profile` flow for {@link SetupProfileOptions.profile}.
6
+ *
7
+ * Creates the dedicated profile dir if absent, opens it headed through the
8
+ * launch transport, emits the actionable prompt, and returns the live session
9
+ * for the caller to hold open during the interactive login (see this module's
10
+ * overview). Does NOT close the session: holding it open IS the headed-login
11
+ * window, and the caller owns its lifetime.
12
+ */
13
+ export async function setupProfile(options) {
14
+ const { profile, onPrompt, transport, ...locationOptions } = options;
15
+ const location = resolveProfileLocation(profile, locationOptions);
16
+ // Create the dedicated profile dir (idempotent). This is the ONE place a
17
+ // profile dir is created: the launch transport refuses a missing one with
18
+ // MissingProfileError precisely so `setup-profile` owns its creation.
19
+ await mkdir(location.profileDir, { recursive: true });
20
+ const driver = transport ?? new PlaywrightLaunchTransport(locationOptions);
21
+ // Open the profile HEADED (visible) so the human can interact with it.
22
+ const session = await driver.open({
23
+ mode: 'launch',
24
+ profile,
25
+ headed: true,
26
+ });
27
+ const sink = onPrompt ?? ((m) => console.error(m));
28
+ sink(buildPrompt(location));
29
+ return { session, location };
30
+ }
31
+ /**
32
+ * Compose the clear, actionable headed-login prompt (PRD acceptance: tell the
33
+ * user what to do AND which profile is being set up). Kept pure so a test can
34
+ * assert its content directly.
35
+ */
36
+ export function buildPrompt(location) {
37
+ return [
38
+ `Setting up the "${location.profile}" profile for webhands.`,
39
+ `Profile directory: ${location.profileDir}`,
40
+ '',
41
+ 'A browser window is now open. In that window:',
42
+ ' 1. Log in to the site(s) you want this profile to stay signed in to.',
43
+ ' 2. Clear any anti-bot challenge / CAPTCHA if one appears.',
44
+ ' 3. When you are done, CLOSE the browser window.',
45
+ '',
46
+ 'Your session (cookies, logins, challenge clearance) is saved into the',
47
+ 'profile directory above, so a later `launch --headless` against the same',
48
+ 'profile reuses it without re-login. Nobody but you types your credentials:',
49
+ 'this tool only opens the window.',
50
+ ].join('\n');
51
+ }
52
+ //# sourceMappingURL=setup-profile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup-profile.js","sourceRoot":"","sources":["../src/setup-profile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,KAAK,EAAC,MAAM,kBAAkB,CAAC;AACvC,OAAO,EAAC,yBAAyB,EAAC,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EACN,sBAAsB,GAGtB,MAAM,uBAAuB,CAAC;AAwE/B;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CACjC,OAA4B;IAE5B,MAAM,EAAC,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,eAAe,EAAC,GAAG,OAAO,CAAC;IACnE,MAAM,QAAQ,GAAG,sBAAsB,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC;IAElE,yEAAyE;IACzE,0EAA0E;IAC1E,sEAAsE;IACtE,MAAM,KAAK,CAAC,QAAQ,CAAC,UAAU,EAAE,EAAC,SAAS,EAAE,IAAI,EAAC,CAAC,CAAC;IAEpD,MAAM,MAAM,GAAG,SAAS,IAAI,IAAI,yBAAyB,CAAC,eAAe,CAAC,CAAC;IAE3E,uEAAuE;IACvE,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC;QACjC,IAAI,EAAE,QAAQ;QACd,OAAO;QACP,MAAM,EAAE,IAAI;KACZ,CAAC,CAAC;IAEH,MAAM,IAAI,GAAe,QAAQ,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/D,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC;IAE5B,OAAO,EAAC,OAAO,EAAE,QAAQ,EAAC,CAAC;AAC5B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,QAAyB;IACpD,OAAO;QACN,mBAAmB,QAAQ,CAAC,OAAO,yBAAyB;QAC5D,sBAAsB,QAAQ,CAAC,UAAU,EAAE;QAC3C,EAAE;QACF,+CAA+C;QAC/C,wEAAwE;QACxE,6DAA6D;QAC7D,mDAAmD;QACnD,EAAE;QACF,uEAAuE;QACvE,0EAA0E;QAC1E,4EAA4E;QAC5E,kCAAkC;KAClC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACd,CAAC"}
@@ -0,0 +1,26 @@
1
+ import type { OpenTarget, Page, Session, Transport } from './seam.js';
2
+ /**
3
+ * A record of one verb call against the stub, for assertions in seam tests.
4
+ */
5
+ export interface StubCall {
6
+ readonly verb: keyof Page;
7
+ readonly args: readonly unknown[];
8
+ }
9
+ /**
10
+ * An in-process, no-op {@link Transport} used to exercise the SEAM SHAPE
11
+ * without a real browser. It is NOT the Playwright transport (that lands in a
12
+ * later task) and implements no real verb behaviour: every verb is a no-op
13
+ * that records the call so a unit test can assert an `open` -> `Session` ->
14
+ * verb round-trip through the `core` `Driver` interface.
15
+ *
16
+ * Session lifetime: a session lives from {@link StubTransport.open} until its
17
+ * {@link Session.close} is called. After `close()` the page rejects further
18
+ * verb calls, mirroring the real "the browser is gone" contract so the seam's
19
+ * lifetime is testable.
20
+ */
21
+ export declare class StubTransport implements Transport {
22
+ /** Every verb call across every session this transport opened, in order. */
23
+ readonly calls: StubCall[];
24
+ open(target: OpenTarget): Promise<Session>;
25
+ }
26
+ //# sourceMappingURL=stub-transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stub-transport.d.ts","sourceRoot":"","sources":["../src/stub-transport.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEX,UAAU,EACV,IAAI,EACJ,OAAO,EAGP,SAAS,EAET,MAAM,WAAW,CAAC;AAEnB;;GAEG;AACH,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,IAAI,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,SAAS,OAAO,EAAE,CAAC;CAClC;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,aAAc,YAAW,SAAS;IAC9C,4EAA4E;IAC5E,QAAQ,CAAC,KAAK,EAAE,QAAQ,EAAE,CAAM;IAE1B,IAAI,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC;CAgEhD"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * An in-process, no-op {@link Transport} used to exercise the SEAM SHAPE
3
+ * without a real browser. It is NOT the Playwright transport (that lands in a
4
+ * later task) and implements no real verb behaviour: every verb is a no-op
5
+ * that records the call so a unit test can assert an `open` -> `Session` ->
6
+ * verb round-trip through the `core` `Driver` interface.
7
+ *
8
+ * Session lifetime: a session lives from {@link StubTransport.open} until its
9
+ * {@link Session.close} is called. After `close()` the page rejects further
10
+ * verb calls, mirroring the real "the browser is gone" contract so the seam's
11
+ * lifetime is testable.
12
+ */
13
+ export class StubTransport {
14
+ /** Every verb call across every session this transport opened, in order. */
15
+ calls = [];
16
+ async open(target) {
17
+ const calls = this.calls;
18
+ let closed = false;
19
+ const ensureOpen = () => {
20
+ if (closed) {
21
+ throw new Error('session is closed');
22
+ }
23
+ };
24
+ const url = target.mode === 'launch'
25
+ ? `stub://launch/${target.profile}`
26
+ : `stub://attach/${target.endpoint}`;
27
+ const page = {
28
+ async navigate(to) {
29
+ ensureOpen();
30
+ calls.push({ verb: 'navigate', args: [to] });
31
+ },
32
+ async snapshot(options) {
33
+ ensureOpen();
34
+ calls.push({ verb: 'snapshot', args: [options] });
35
+ return {
36
+ url,
37
+ view: options?.full === true ? 'full' : 'accessibility',
38
+ content: '',
39
+ };
40
+ },
41
+ async click(t) {
42
+ ensureOpen();
43
+ calls.push({ verb: 'click', args: [t] });
44
+ },
45
+ async type(t, text) {
46
+ ensureOpen();
47
+ calls.push({ verb: 'type', args: [t, text] });
48
+ },
49
+ async eval(expression) {
50
+ ensureOpen();
51
+ calls.push({ verb: 'eval', args: [expression] });
52
+ return undefined;
53
+ },
54
+ async wait(condition) {
55
+ ensureOpen();
56
+ calls.push({ verb: 'wait', args: [condition] });
57
+ },
58
+ async cookies() {
59
+ ensureOpen();
60
+ calls.push({ verb: 'cookies', args: [] });
61
+ return [];
62
+ },
63
+ async setCookies(cookies) {
64
+ ensureOpen();
65
+ calls.push({ verb: 'setCookies', args: [cookies] });
66
+ },
67
+ };
68
+ return {
69
+ page,
70
+ async close() {
71
+ closed = true;
72
+ },
73
+ };
74
+ }
75
+ }
76
+ //# sourceMappingURL=stub-transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stub-transport.js","sourceRoot":"","sources":["../src/stub-transport.ts"],"names":[],"mappings":"AAmBA;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,aAAa;IACzB,4EAA4E;IACnE,KAAK,GAAe,EAAE,CAAC;IAEhC,KAAK,CAAC,IAAI,CAAC,MAAkB;QAC5B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;QACzB,IAAI,MAAM,GAAG,KAAK,CAAC;QAEnB,MAAM,UAAU,GAAG,GAAG,EAAE;YACvB,IAAI,MAAM,EAAE,CAAC;gBACZ,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;YACtC,CAAC;QACF,CAAC,CAAC;QAEF,MAAM,GAAG,GACR,MAAM,CAAC,IAAI,KAAK,QAAQ;YACvB,CAAC,CAAC,iBAAiB,MAAM,CAAC,OAAO,EAAE;YACnC,CAAC,CAAC,iBAAiB,MAAM,CAAC,QAAQ,EAAE,CAAC;QAEvC,MAAM,IAAI,GAAS;YAClB,KAAK,CAAC,QAAQ,CAAC,EAAU;gBACxB,UAAU,EAAE,CAAC;gBACb,KAAK,CAAC,IAAI,CAAC,EAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,EAAC,CAAC,CAAC;YAC5C,CAAC;YACD,KAAK,CAAC,QAAQ,CAAC,OAAyB;gBACvC,UAAU,EAAE,CAAC;gBACb,KAAK,CAAC,IAAI,CAAC,EAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,OAAO,CAAC,EAAC,CAAC,CAAC;gBAChD,OAAO;oBACN,GAAG;oBACH,IAAI,EAAE,OAAO,EAAE,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,eAAe;oBACvD,OAAO,EAAE,EAAE;iBACX,CAAC;YACH,CAAC;YACD,KAAK,CAAC,KAAK,CAAC,CAAC;gBACZ,UAAU,EAAE,CAAC;gBACb,KAAK,CAAC,IAAI,CAAC,EAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAC,CAAC,CAAC;YACxC,CAAC;YACD,KAAK,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI;gBACjB,UAAU,EAAE,CAAC;gBACb,KAAK,CAAC,IAAI,CAAC,EAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,IAAI,CAAC,EAAC,CAAC,CAAC;YAC7C,CAAC;YACD,KAAK,CAAC,IAAI,CAAC,UAAkB;gBAC5B,UAAU,EAAE,CAAC;gBACb,KAAK,CAAC,IAAI,CAAC,EAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,UAAU,CAAC,EAAC,CAAC,CAAC;gBAC/C,OAAO,SAAS,CAAC;YAClB,CAAC;YACD,KAAK,CAAC,IAAI,CAAC,SAAwB;gBAClC,UAAU,EAAE,CAAC;gBACb,KAAK,CAAC,IAAI,CAAC,EAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,SAAS,CAAC,EAAC,CAAC,CAAC;YAC/C,CAAC;YACD,KAAK,CAAC,OAAO;gBACZ,UAAU,EAAE,CAAC;gBACb,KAAK,CAAC,IAAI,CAAC,EAAC,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,EAAC,CAAC,CAAC;gBACxC,OAAO,EAAE,CAAC;YACX,CAAC;YACD,KAAK,CAAC,UAAU,CAAC,OAAO;gBACvB,UAAU,EAAE,CAAC;gBACb,KAAK,CAAC,IAAI,CAAC,EAAC,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,OAAO,CAAC,EAAC,CAAC,CAAC;YACnD,CAAC;SACD,CAAC;QAEF,OAAO;YACN,IAAI;YACJ,KAAK,CAAC,KAAK;gBACV,MAAM,GAAG,IAAI,CAAC;YACf,CAAC;SACD,CAAC;IACH,CAAC;CACD"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The controlled static fixture pages served by {@link startFixtureServer}.
3
+ *
4
+ * Kept as in-module strings (rather than separate `.html` assets) so they
5
+ * survive `tsc` compilation into `dist` without a copy step, and so the
6
+ * deterministic verb tests have a single source of truth for the markup they
7
+ * assert against. Later verb-behaviour tasks extend these pages with whatever
8
+ * controlled elements they need; the seam scaffold ships a minimal index page.
9
+ */
10
+ /** Map of request path (relative to root, no leading slash) to page markup. */
11
+ export declare const FIXTURE_PAGES: Readonly<Record<string, string>>;
12
+ //# sourceMappingURL=fixture-pages.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fixture-pages.d.ts","sourceRoot":"","sources":["../../src/test-fixtures/fixture-pages.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAiMH,+EAA+E;AAC/E,eAAO,MAAM,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAO1D,CAAC"}