@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,175 @@
1
+ import { chromium, } from 'playwright';
2
+ import { AttachNoContextError, AttachNotChromiumError } from './errors.js';
3
+ import { clickLocator, resolveLocator, waitFor, } from './playwright-launch-transport.js';
4
+ /**
5
+ * The `attach` concrete transport: connect (`chromium.connectOverCDP`) to a
6
+ * browser the USER already started with remote debugging enabled, and reuse the
7
+ * user's EXISTING authenticated context — `browser.contexts()[0]`, never
8
+ * `newContext()` — so the controller drives the live, logged-in tabs on the
9
+ * user's real fingerprint and IP (PRD "Solution, attach"; ADR-0002).
10
+ *
11
+ * CDP-attach is Chromium-only (ADR-0003: Firefox attaches via a different
12
+ * mechanism). That constraint is SURFACED as a typed `core` error
13
+ * ({@link AttachNotChromiumError}) rather than leaking any CDP/Chromium-only
14
+ * type into the seam: the Playwright/CDP types are confined to this module and
15
+ * the seam stays transport-neutral (ADR-0003).
16
+ *
17
+ * It handles ONLY `mode: 'attach'`. `mode: 'launch'` is a SEPARATE transport
18
+ * ({@link PlaywrightLaunchTransport}); calling `open` with `mode: 'launch'`
19
+ * here throws, because mixing the two open mechanisms in one transport is what
20
+ * ADR-0003's seam exists to avoid.
21
+ *
22
+ * There is NO browser-relaunch helper: a settled PRD decision is that the user
23
+ * starts their own browser with `--remote-debugging-port` and supplies the
24
+ * resulting endpoint (PRD "needsAnswers" #5). This transport only connects to a
25
+ * running one.
26
+ */
27
+ export class PlaywrightAttachTransport {
28
+ async open(target) {
29
+ if (target.mode !== 'attach') {
30
+ throw new Error(`PlaywrightAttachTransport only handles 'attach'; ` +
31
+ `'${target.mode}' is owned by the launch transport.`);
32
+ }
33
+ // `endpoint` is the opaque, transport-resolved remote-debugging endpoint
34
+ // (e.g. `http://127.0.0.1:9222`). The seam keeps it a plain string so no
35
+ // CDP type leaks (ADR-0003); this transport interprets it as a CDP URL.
36
+ const browser = await chromium.connectOverCDP(target.endpoint);
37
+ try {
38
+ // CDP-attach is Chromium-only. If the reached engine is not Chromium,
39
+ // refuse with a typed condition instead of driving an unsupported
40
+ // browser (Firefox attaches differently — ADR-0003).
41
+ const engine = browser.browserType().name();
42
+ if (engine !== 'chromium') {
43
+ throw new AttachNotChromiumError(engine);
44
+ }
45
+ // Reuse the EXISTING authenticated context, never `newContext()`
46
+ // (ADR-0002): a fresh context would discard the user's live login.
47
+ const context = browser.contexts()[0];
48
+ if (context === undefined) {
49
+ throw new AttachNoContextError(target.endpoint);
50
+ }
51
+ // Drive the context's existing active page; open one only if the
52
+ // browser exposes a context with no page yet (single active session in
53
+ // v1, PRD Out of Scope).
54
+ const pwPage = context.pages()[0] ?? (await context.newPage());
55
+ return makeAttachedSession(browser, pwPage);
56
+ }
57
+ catch (cause) {
58
+ // On any open-time refusal, disconnect from the user's browser without
59
+ // closing it (a CDP connection close detaches; it does not kill the
60
+ // browser the user started).
61
+ await browser.close().catch(() => { });
62
+ throw cause;
63
+ }
64
+ }
65
+ }
66
+ /**
67
+ * Wrap a CDP-attached browser into the seam's {@link Session}.
68
+ *
69
+ * `close()` DISCONNECTS the controller from the user's browser; it must not
70
+ * kill the browser the user started (`Browser.close()` on a `connectOverCDP`
71
+ * connection detaches rather than terminating the remote process). We resolve
72
+ * cookies through the reused context so they reflect the live, authenticated
73
+ * session.
74
+ */
75
+ function makeAttachedSession(browser, pwPage) {
76
+ const context = pwPage.context();
77
+ let closed = false;
78
+ const ensureOpen = () => {
79
+ if (closed) {
80
+ throw new Error('session is closed');
81
+ }
82
+ };
83
+ const page = {
84
+ async navigate(url) {
85
+ ensureOpen();
86
+ // "Settled" = the `load` event; XHR/JS-rendered content that appears
87
+ // after load is the `wait` verb's job. Same rationale (and the
88
+ // no-`networkidle` reasoning) as the launch transport's `navigate`.
89
+ await pwPage.goto(url, { waitUntil: 'load' });
90
+ },
91
+ async snapshot(options) {
92
+ ensureOpen();
93
+ const url = pwPage.url();
94
+ if (options?.full === true) {
95
+ const content = await pwPage.evaluate(() => document.documentElement.outerHTML);
96
+ return { url, view: 'full', content };
97
+ }
98
+ // Default: the token-cheap accessibility tree + visible text with stable
99
+ // `[ref=...]` refs (see the launch transport and `Snapshot` for the
100
+ // rationale; the string crosses the seam as opaque, transport-neutral
101
+ // text, ADR-0003).
102
+ const content = await pwPage.ariaSnapshot({ mode: 'ai' });
103
+ return { url, view: 'accessibility', content };
104
+ },
105
+ // `resolveLocator`/`clickLocator`/`waitFor` are imported from the launch
106
+ // transport so both transports resolve locators and run the verbs through
107
+ // ONE path (no parallel addressing scheme; the forward-note).
108
+ async click(t) {
109
+ ensureOpen();
110
+ // Shared `clickLocator`: normal actionability-checked click with the
111
+ // hidden-element dispatch fallback (PRD story 8), identical to launch.
112
+ await clickLocator(pwPage, t);
113
+ },
114
+ async type(t, text) {
115
+ ensureOpen();
116
+ await resolveLocator(pwPage, t).fill(text);
117
+ },
118
+ async eval(expression) {
119
+ ensureOpen();
120
+ return pwPage.evaluate(expression);
121
+ },
122
+ async wait(condition) {
123
+ ensureOpen();
124
+ // Identical to the launch transport (shared `waitFor`): selector /
125
+ // navigation / timeout, so the verb behaves the same on both.
126
+ await waitFor(pwPage, condition);
127
+ },
128
+ async cookies() {
129
+ ensureOpen();
130
+ const raw = await context.cookies();
131
+ return raw.map(toSeamCookie);
132
+ },
133
+ async setCookies(cookies) {
134
+ ensureOpen();
135
+ await context.addCookies(cookies.map(fromSeamCookie));
136
+ },
137
+ };
138
+ return {
139
+ page,
140
+ async close() {
141
+ if (closed)
142
+ return;
143
+ closed = true;
144
+ // Detach from the user's browser; do NOT terminate it.
145
+ await browser.close();
146
+ },
147
+ };
148
+ }
149
+ /** Map a Playwright cookie to the transport-neutral seam {@link Cookie}. */
150
+ function toSeamCookie(c) {
151
+ return {
152
+ name: c.name,
153
+ value: c.value,
154
+ domain: c.domain,
155
+ path: c.path,
156
+ expires: c.expires,
157
+ httpOnly: c.httpOnly,
158
+ secure: c.secure,
159
+ sameSite: c.sameSite,
160
+ };
161
+ }
162
+ /** Map a seam {@link Cookie} to a Playwright cookie shape. */
163
+ function fromSeamCookie(c) {
164
+ return {
165
+ name: c.name,
166
+ value: c.value,
167
+ domain: c.domain,
168
+ path: c.path,
169
+ expires: c.expires,
170
+ httpOnly: c.httpOnly,
171
+ secure: c.secure,
172
+ sameSite: c.sameSite,
173
+ };
174
+ }
175
+ //# sourceMappingURL=playwright-attach-transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwright-attach-transport.js","sourceRoot":"","sources":["../src/playwright-attach-transport.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,90 @@
1
+ import { type Page as PwPage } from 'playwright';
2
+ import { type ProfileLocationOptions } from './profile-location.js';
3
+ import type { OpenTarget, Session, Transport, WaitCondition } from './seam.js';
4
+ /**
5
+ * The v1 concrete transport: a Playwright browser the controller LAUNCHES
6
+ * against a dedicated, persistent profile directory it owns (PRD "Solution,
7
+ * launch"; ADR-0002). It implements the `core` {@link Transport}/`Driver` seam
8
+ * with NO Playwright/CDP types in its public surface (ADR-0003): the
9
+ * Playwright types are confined to this module.
10
+ *
11
+ * It handles ONLY `mode: 'launch'`. The `attach` mode (`connectOverCDP`) is a
12
+ * SEPARATE transport (task `attach-transport-cdp-chromium`); calling `open`
13
+ * with `mode: 'attach'` here throws, because mixing the two launch mechanisms
14
+ * in one transport is what ADR-0003's seam exists to avoid.
15
+ *
16
+ * Profile location is resolved from the constructor options (or the
17
+ * `WEBHANDS_HOME` env var, or `~/.webhands`). See
18
+ * {@link resolveProfileLocation}. Because that is a SHARED location, tests pass
19
+ * a temp `root` (or set the env var) and assert the real home is untouched.
20
+ */
21
+ export declare class PlaywrightLaunchTransport implements Transport {
22
+ #private;
23
+ /**
24
+ * @param location overrides for where profiles live (a `root` dir and/or an
25
+ * `env`). Omit in production to use `~/.webhands`; pass a temp
26
+ * `root` in tests to isolate the shared profile location.
27
+ */
28
+ constructor(location?: ProfileLocationOptions);
29
+ open(target: OpenTarget): Promise<Session>;
30
+ }
31
+ /**
32
+ * Run the `wait` verb's three forms (PRD story 10) against a Playwright page.
33
+ *
34
+ * - `timeout` — pace by a fixed delay (`waitForTimeout`), so an agent can act
35
+ * like a human and let XHR-rendered content land.
36
+ * - `locator` — block until the addressed element appears (`Locator.waitFor()`),
37
+ * the form for content rendered AFTER `goto` settled on `load`.
38
+ * - `navigation` — block until the NEXT navigation settles to `load`. We use
39
+ * `waitForNavigation()` even though Playwright marks it `@deprecated` ("racy,
40
+ * use waitForURL"): that deprecation targets in-process TEST code that can arm
41
+ * the wait BEFORE the action and pass a target URL. Neither holds here. Across
42
+ * this seam verbs are DISCRETE sequential calls (`click` then `wait`), so we
43
+ * CANNOT arm before the trigger; and the realistic trigger is an async,
44
+ * JS-driven transition (a redirect / SPA route change that fires AFTER the
45
+ * agent's action, the "let XHR-rendered content load" case of story 10), so
46
+ * "wait for the NEXT navigation" is exactly right — whereas `waitForLoadState`
47
+ * would see the already-loaded current page and return before the pending
48
+ * transition. `waitForURL` is unusable because the verb has no target URL by
49
+ * design (the agent waits for "a navigation", not a known address). (See the
50
+ * task's ## Decisions note.)
51
+ *
52
+ * Shared by both Playwright transports so the verb behaviour stays identical
53
+ * (the forward-note's "do NOT write a parallel second implementation").
54
+ */
55
+ export declare function waitFor(page: PwPage, condition: WaitCondition): Promise<void>;
56
+ /**
57
+ * Resolve a raw Playwright locator EXPRESSION (ADR-0004) against the page. The
58
+ * verb surface passes locator expressions like `getByRole('button', …)`; we
59
+ * evaluate them in a small sandbox where `page`/`p` is the page, so the full
60
+ * Playwright locator grammar is available without leaking the type across the
61
+ * seam.
62
+ *
63
+ * Exported (with {@link clickLocator}/{@link waitFor}) so the attach transport
64
+ * resolves locators IDENTICALLY — one resolution path, no parallel addressing
65
+ * scheme (the forward-note's "do NOT write a parallel second implementation").
66
+ */
67
+ export declare function resolveLocator(page: PwPage, expression: string): import("playwright").Locator;
68
+ /**
69
+ * Run the `click` verb against a Playwright page (PRD story 8), shared by both
70
+ * Playwright transports so the verb behaves identically (mirrors {@link waitFor};
71
+ * the forward-note's "do NOT write a parallel second implementation").
72
+ *
73
+ * First try a normal `Locator.click()`, which AUTO-WAITS for the element to be
74
+ * visible and actionable — the right behaviour for a real button. A hidden
75
+ * custom input (the case the prd calls out) NEVER becomes actionable, so that
76
+ * click times out; on a Playwright `TimeoutError` we fall back to
77
+ * `dispatchEvent('click')`, which fires a click WITHOUT the actionability
78
+ * checks. The fallback is deliberately the documented Playwright escape (a
79
+ * sibling to the `eval` hatch, ADR-0004), not a reimplemented click: we keep
80
+ * the locator a raw resolved expression and only change HOW the resolved
81
+ * locator is clicked.
82
+ *
83
+ * Only a timeout triggers the fallback. The fallback `dispatchEvent` is itself
84
+ * bounded by the same short timeout, so a locator that resolves NO element (a
85
+ * bad locator) surfaces its timeout quickly instead of hanging the dispatch on
86
+ * Playwright's 30s default — the dispatch escape is for elements that EXIST but
87
+ * are not actionable (hidden custom inputs), not for absent ones.
88
+ */
89
+ export declare function clickLocator(page: PwPage, expression: string): Promise<void>;
90
+ //# sourceMappingURL=playwright-launch-transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwright-launch-transport.d.ts","sourceRoot":"","sources":["../src/playwright-launch-transport.ts"],"names":[],"mappings":"AACA,OAAO,EAIN,KAAK,IAAI,IAAI,MAAM,EACnB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAEN,KAAK,sBAAsB,EAC3B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAEX,UAAU,EAEV,OAAO,EAGP,SAAS,EACT,aAAa,EACb,MAAM,WAAW,CAAC;AAEnB;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,yBAA0B,YAAW,SAAS;;IAG1D;;;;OAIG;gBACS,QAAQ,GAAE,sBAA2B;IAI3C,IAAI,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC;CAwChD;AA2HD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,OAAO,CAC5B,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,aAAa,GACtB,OAAO,CAAC,IAAI,CAAC,CAaf;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,gCAO9D;AAcD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,YAAY,CACjC,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAYf"}
@@ -0,0 +1,305 @@
1
+ import { stat } from 'node:fs/promises';
2
+ import { chromium, errors as pwErrors, } from 'playwright';
3
+ import { MissingBrowserBinaryError, MissingProfileError } from './errors.js';
4
+ import { resolveProfileLocation, } from './profile-location.js';
5
+ /**
6
+ * The v1 concrete transport: a Playwright browser the controller LAUNCHES
7
+ * against a dedicated, persistent profile directory it owns (PRD "Solution,
8
+ * launch"; ADR-0002). It implements the `core` {@link Transport}/`Driver` seam
9
+ * with NO Playwright/CDP types in its public surface (ADR-0003): the
10
+ * Playwright types are confined to this module.
11
+ *
12
+ * It handles ONLY `mode: 'launch'`. The `attach` mode (`connectOverCDP`) is a
13
+ * SEPARATE transport (task `attach-transport-cdp-chromium`); calling `open`
14
+ * with `mode: 'attach'` here throws, because mixing the two launch mechanisms
15
+ * in one transport is what ADR-0003's seam exists to avoid.
16
+ *
17
+ * Profile location is resolved from the constructor options (or the
18
+ * `WEBHANDS_HOME` env var, or `~/.webhands`). See
19
+ * {@link resolveProfileLocation}. Because that is a SHARED location, tests pass
20
+ * a temp `root` (or set the env var) and assert the real home is untouched.
21
+ */
22
+ export class PlaywrightLaunchTransport {
23
+ #location;
24
+ /**
25
+ * @param location overrides for where profiles live (a `root` dir and/or an
26
+ * `env`). Omit in production to use `~/.webhands`; pass a temp
27
+ * `root` in tests to isolate the shared profile location.
28
+ */
29
+ constructor(location = {}) {
30
+ this.#location = location;
31
+ }
32
+ async open(target) {
33
+ if (target.mode !== 'launch') {
34
+ throw new Error(`PlaywrightLaunchTransport only handles 'launch'; ` +
35
+ `'${target.mode}' is owned by the attach transport.`);
36
+ }
37
+ const loc = resolveProfileLocation(target.profile, this.#location);
38
+ // A profile is "set up" iff its dedicated dir exists on disk. Creating it
39
+ // is the headed `setup-profile` flow's job (a later task); `launch`
40
+ // against a missing profile is the typed MissingProfileError so the CLI
41
+ // can tell the user to run `setup-profile` first (PRD story 17). We never
42
+ // create the dir here, so a `launch` typo cannot silently spawn a blank
43
+ // profile.
44
+ if (!(await isExistingDirectory(loc.profileDir))) {
45
+ throw new MissingProfileError(loc.profile, loc.profileDir);
46
+ }
47
+ const headless = target.headed !== true;
48
+ let context;
49
+ try {
50
+ context = await chromium.launchPersistentContext(loc.profileDir, {
51
+ headless,
52
+ });
53
+ }
54
+ catch (cause) {
55
+ if (isMissingBrowserBinary(cause)) {
56
+ throw new MissingBrowserBinaryError('chromium', undefined, { cause });
57
+ }
58
+ throw cause;
59
+ }
60
+ // launchPersistentContext always opens with exactly one page; reuse it as
61
+ // the single active page (PRD: single active session in v1). Create one if
62
+ // the build ever changes that invariant.
63
+ const pwPage = context.pages()[0] ?? (await context.newPage());
64
+ return makeSession(context, pwPage);
65
+ }
66
+ }
67
+ /** True iff `path` exists and is a directory. */
68
+ async function isExistingDirectory(path) {
69
+ try {
70
+ const s = await stat(path);
71
+ return s.isDirectory();
72
+ }
73
+ catch {
74
+ return false;
75
+ }
76
+ }
77
+ /**
78
+ * Recognise Playwright's "browser executable doesn't exist" failure. Playwright
79
+ * does not export a typed error for this, so we detect on the message (it
80
+ * instructs the user to run `playwright install`). We confine that brittle
81
+ * string match to this one spot and re-raise as a stable typed error.
82
+ */
83
+ function isMissingBrowserBinary(cause) {
84
+ const message = cause instanceof Error ? cause.message : String(cause ?? '');
85
+ return (/Executable doesn't exist/i.test(message) ||
86
+ /please run the following command to download new browsers/i.test(message) ||
87
+ /playwright install/i.test(message));
88
+ }
89
+ /** Wrap a live Playwright persistent context into the seam's {@link Session}. */
90
+ function makeSession(context, pwPage) {
91
+ let closed = false;
92
+ const ensureOpen = () => {
93
+ if (closed) {
94
+ throw new Error('session is closed');
95
+ }
96
+ };
97
+ const page = {
98
+ async navigate(url) {
99
+ ensureOpen();
100
+ // "Settled" for `goto` = the `load` event: the document and its
101
+ // subresources have loaded (PRD story 6, "navigate ... and wait for it
102
+ // to settle"). We deliberately do NOT wait for `networkidle`:
103
+ // Playwright discourages it, and it hangs forever on pages with
104
+ // long-poll / streaming / analytics beacons (exactly the logged-in apps
105
+ // this tool targets). Content rendered AFTER load (XHR-injected prices,
106
+ // hydrated lists) is the job of the explicit `wait` verb (story 10), not
107
+ // of `goto`.
108
+ await pwPage.goto(url, { waitUntil: 'load' });
109
+ },
110
+ async snapshot(options) {
111
+ ensureOpen();
112
+ const url = pwPage.url();
113
+ if (options?.full === true) {
114
+ // `--full`: the raw DOM. `documentElement.outerHTML` is the serialized
115
+ // live DOM (post-script render), which is what an agent that wants the
116
+ // real HTML expects — not the original network response.
117
+ const content = await pwPage.evaluate(() => document.documentElement.outerHTML);
118
+ return { url, view: 'full', content };
119
+ }
120
+ // Default: the token-cheap accessibility tree + visible text with stable
121
+ // `[ref=...]` element refs. Playwright's `ariaSnapshot({mode: 'ai'})`
122
+ // emits exactly that — a YAML aria tree (roles + accessible names +
123
+ // text) where each node carries a stable `[ref=eN]` reference, assigned
124
+ // deterministically by traversal order so re-snapshotting an unchanged
125
+ // page yields the same refs. The string crosses the seam as opaque,
126
+ // transport-neutral text (no Playwright type leaks, ADR-0003).
127
+ const content = await pwPage.ariaSnapshot({ mode: 'ai' });
128
+ return { url, view: 'accessibility', content };
129
+ },
130
+ async click(t) {
131
+ ensureOpen();
132
+ await clickLocator(pwPage, t);
133
+ },
134
+ async type(t, text) {
135
+ ensureOpen();
136
+ await resolveLocator(pwPage, t).fill(text);
137
+ },
138
+ async eval(expression) {
139
+ ensureOpen();
140
+ // The `eval` escape hatch (PRD story 9): run the raw JS EXPRESSION in the
141
+ // page and return its serializable result. Playwright's `evaluate`
142
+ // already IS the seam's serialization contract (see {@link Page.eval}):
143
+ // it passes a string as an expression, awaits a returned Promise, and
144
+ // structurally clones the result out of the page by VALUE. That clone is
145
+ // richer than JSON: it preserves NaN/Infinity/BigInt and circular
146
+ // structures (back-refs become a `[Circular]` marker), yields `undefined`
147
+ // for functions/symbols, and returns an opaque preview string for a live
148
+ // host object (a DOM node never crosses the process boundary). A page-side
149
+ // throw rejects. We pass it straight through rather than re-encode it:
150
+ // wrapping the value in a transport-specific envelope would invent a
151
+ // dialect the seam deliberately avoids. The thrown error is a plain
152
+ // `Error`, so no Playwright/CDP type leaks across the seam (ADR-0003).
153
+ return pwPage.evaluate(expression);
154
+ },
155
+ async wait(condition) {
156
+ ensureOpen();
157
+ await waitFor(pwPage, condition);
158
+ },
159
+ async cookies() {
160
+ ensureOpen();
161
+ const raw = await context.cookies();
162
+ return raw.map(toSeamCookie);
163
+ },
164
+ async setCookies(cookies) {
165
+ ensureOpen();
166
+ await context.addCookies(cookies.map(fromSeamCookie));
167
+ },
168
+ };
169
+ return {
170
+ page,
171
+ async close() {
172
+ if (closed)
173
+ return;
174
+ closed = true;
175
+ await context.close();
176
+ },
177
+ };
178
+ }
179
+ /**
180
+ * Run the `wait` verb's three forms (PRD story 10) against a Playwright page.
181
+ *
182
+ * - `timeout` — pace by a fixed delay (`waitForTimeout`), so an agent can act
183
+ * like a human and let XHR-rendered content land.
184
+ * - `locator` — block until the addressed element appears (`Locator.waitFor()`),
185
+ * the form for content rendered AFTER `goto` settled on `load`.
186
+ * - `navigation` — block until the NEXT navigation settles to `load`. We use
187
+ * `waitForNavigation()` even though Playwright marks it `@deprecated` ("racy,
188
+ * use waitForURL"): that deprecation targets in-process TEST code that can arm
189
+ * the wait BEFORE the action and pass a target URL. Neither holds here. Across
190
+ * this seam verbs are DISCRETE sequential calls (`click` then `wait`), so we
191
+ * CANNOT arm before the trigger; and the realistic trigger is an async,
192
+ * JS-driven transition (a redirect / SPA route change that fires AFTER the
193
+ * agent's action, the "let XHR-rendered content load" case of story 10), so
194
+ * "wait for the NEXT navigation" is exactly right — whereas `waitForLoadState`
195
+ * would see the already-loaded current page and return before the pending
196
+ * transition. `waitForURL` is unusable because the verb has no target URL by
197
+ * design (the agent waits for "a navigation", not a known address). (See the
198
+ * task's ## Decisions note.)
199
+ *
200
+ * Shared by both Playwright transports so the verb behaviour stays identical
201
+ * (the forward-note's "do NOT write a parallel second implementation").
202
+ */
203
+ export async function waitFor(page, condition) {
204
+ switch (condition.kind) {
205
+ case 'timeout':
206
+ await page.waitForTimeout(condition.ms);
207
+ return;
208
+ case 'locator':
209
+ await resolveLocator(page, condition.target).waitFor();
210
+ return;
211
+ case 'navigation':
212
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
213
+ await page.waitForNavigation();
214
+ return;
215
+ }
216
+ }
217
+ /**
218
+ * Resolve a raw Playwright locator EXPRESSION (ADR-0004) against the page. The
219
+ * verb surface passes locator expressions like `getByRole('button', …)`; we
220
+ * evaluate them in a small sandbox where `page`/`p` is the page, so the full
221
+ * Playwright locator grammar is available without leaking the type across the
222
+ * seam.
223
+ *
224
+ * Exported (with {@link clickLocator}/{@link waitFor}) so the attach transport
225
+ * resolves locators IDENTICALLY — one resolution path, no parallel addressing
226
+ * scheme (the forward-note's "do NOT write a parallel second implementation").
227
+ */
228
+ export function resolveLocator(page, expression) {
229
+ // eslint-disable-next-line no-new-func
230
+ const factory = new Function('page', 'p', `return (${expression});`);
231
+ return factory(page, page);
232
+ }
233
+ /**
234
+ * How long a normal, actionability-checked `click` may wait before we treat the
235
+ * element as un-clickable and fall back to a dispatched click. Short on purpose:
236
+ * a hidden custom input never becomes actionable, so the regular click would
237
+ * otherwise burn Playwright's full default timeout (30s) before the escape path
238
+ * runs. The visible-element happy path clicks immediately and never hits this;
239
+ * this bound is the latency cost paid ONLY on the hidden/non-actionable path,
240
+ * and is long enough to tolerate a slow-but-eventually-actionable element
241
+ * (animations, late layout) before deciding to dispatch.
242
+ */
243
+ const NORMAL_CLICK_TIMEOUT_MS = 1_000;
244
+ /**
245
+ * Run the `click` verb against a Playwright page (PRD story 8), shared by both
246
+ * Playwright transports so the verb behaves identically (mirrors {@link waitFor};
247
+ * the forward-note's "do NOT write a parallel second implementation").
248
+ *
249
+ * First try a normal `Locator.click()`, which AUTO-WAITS for the element to be
250
+ * visible and actionable — the right behaviour for a real button. A hidden
251
+ * custom input (the case the prd calls out) NEVER becomes actionable, so that
252
+ * click times out; on a Playwright `TimeoutError` we fall back to
253
+ * `dispatchEvent('click')`, which fires a click WITHOUT the actionability
254
+ * checks. The fallback is deliberately the documented Playwright escape (a
255
+ * sibling to the `eval` hatch, ADR-0004), not a reimplemented click: we keep
256
+ * the locator a raw resolved expression and only change HOW the resolved
257
+ * locator is clicked.
258
+ *
259
+ * Only a timeout triggers the fallback. The fallback `dispatchEvent` is itself
260
+ * bounded by the same short timeout, so a locator that resolves NO element (a
261
+ * bad locator) surfaces its timeout quickly instead of hanging the dispatch on
262
+ * Playwright's 30s default — the dispatch escape is for elements that EXIST but
263
+ * are not actionable (hidden custom inputs), not for absent ones.
264
+ */
265
+ export async function clickLocator(page, expression) {
266
+ const target = resolveLocator(page, expression);
267
+ try {
268
+ await target.click({ timeout: NORMAL_CLICK_TIMEOUT_MS });
269
+ }
270
+ catch (cause) {
271
+ if (!(cause instanceof pwErrors.TimeoutError)) {
272
+ throw cause;
273
+ }
274
+ // The element never became actionable (e.g. a hidden custom input). Fire
275
+ // the click without actionability checks, the prd's explicit escape path.
276
+ await target.dispatchEvent('click', { timeout: NORMAL_CLICK_TIMEOUT_MS });
277
+ }
278
+ }
279
+ /** Map a Playwright cookie to the transport-neutral seam {@link Cookie}. */
280
+ function toSeamCookie(c) {
281
+ return {
282
+ name: c.name,
283
+ value: c.value,
284
+ domain: c.domain,
285
+ path: c.path,
286
+ expires: c.expires,
287
+ httpOnly: c.httpOnly,
288
+ secure: c.secure,
289
+ sameSite: c.sameSite,
290
+ };
291
+ }
292
+ /** Map a seam {@link Cookie} to a Playwright cookie shape. */
293
+ function fromSeamCookie(c) {
294
+ return {
295
+ name: c.name,
296
+ value: c.value,
297
+ domain: c.domain,
298
+ path: c.path,
299
+ expires: c.expires,
300
+ httpOnly: c.httpOnly,
301
+ secure: c.secure,
302
+ sameSite: c.sameSite,
303
+ };
304
+ }
305
+ //# sourceMappingURL=playwright-launch-transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwright-launch-transport.js","sourceRoot":"","sources":["../src/playwright-launch-transport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,IAAI,EAAC,MAAM,kBAAkB,CAAC;AACtC,OAAO,EACN,QAAQ,EACR,MAAM,IAAI,QAAQ,GAGlB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAC,yBAAyB,EAAE,mBAAmB,EAAC,MAAM,aAAa,CAAC;AAC3E,OAAO,EACN,sBAAsB,GAEtB,MAAM,uBAAuB,CAAC;AAY/B;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,OAAO,yBAAyB;IAC5B,SAAS,CAAyB;IAE3C;;;;OAIG;IACH,YAAY,WAAmC,EAAE;QAChD,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC;IAC3B,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,MAAkB;QAC5B,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC9B,MAAM,IAAI,KAAK,CACd,mDAAmD;gBAClD,IAAI,MAAM,CAAC,IAAI,qCAAqC,CACrD,CAAC;QACH,CAAC;QAED,MAAM,GAAG,GAAG,sBAAsB,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QAEnE,0EAA0E;QAC1E,oEAAoE;QACpE,wEAAwE;QACxE,0EAA0E;QAC1E,wEAAwE;QACxE,WAAW;QACX,IAAI,CAAC,CAAC,MAAM,mBAAmB,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC;YAClD,MAAM,IAAI,mBAAmB,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,UAAU,CAAC,CAAC;QAC5D,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,KAAK,IAAI,CAAC;QAExC,IAAI,OAAuB,CAAC;QAC5B,IAAI,CAAC;YACJ,OAAO,GAAG,MAAM,QAAQ,CAAC,uBAAuB,CAAC,GAAG,CAAC,UAAU,EAAE;gBAChE,QAAQ;aACR,CAAC,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,IAAI,sBAAsB,CAAC,KAAK,CAAC,EAAE,CAAC;gBACnC,MAAM,IAAI,yBAAyB,CAAC,UAAU,EAAE,SAAS,EAAE,EAAC,KAAK,EAAC,CAAC,CAAC;YACrE,CAAC;YACD,MAAM,KAAK,CAAC;QACb,CAAC;QAED,0EAA0E;QAC1E,2EAA2E;QAC3E,yCAAyC;QACzC,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;QAC/D,OAAO,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IACrC,CAAC;CACD;AAED,iDAAiD;AACjD,KAAK,UAAU,mBAAmB,CAAC,IAAY;IAC9C,IAAI,CAAC;QACJ,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC;QAC3B,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC;IACxB,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,KAAK,CAAC;IACd,CAAC;AACF,CAAC;AAED;;;;;GAKG;AACH,SAAS,sBAAsB,CAAC,KAAc;IAC7C,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;IAC7E,OAAO,CACN,2BAA2B,CAAC,IAAI,CAAC,OAAO,CAAC;QACzC,4DAA4D,CAAC,IAAI,CAChE,OAAO,CACP;QACD,qBAAqB,CAAC,IAAI,CAAC,OAAO,CAAC,CACnC,CAAC;AACH,CAAC;AAED,iFAAiF;AACjF,SAAS,WAAW,CAAC,OAAuB,EAAE,MAAc;IAC3D,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,MAAM,UAAU,GAAG,GAAG,EAAE;QACvB,IAAI,MAAM,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;QACtC,CAAC;IACF,CAAC,CAAC;IAEF,MAAM,IAAI,GAAS;QAClB,KAAK,CAAC,QAAQ,CAAC,GAAW;YACzB,UAAU,EAAE,CAAC;YACb,gEAAgE;YAChE,uEAAuE;YACvE,8DAA8D;YAC9D,gEAAgE;YAChE,wEAAwE;YACxE,wEAAwE;YACxE,yEAAyE;YACzE,aAAa;YACb,MAAM,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,EAAC,SAAS,EAAE,MAAM,EAAC,CAAC,CAAC;QAC7C,CAAC;QACD,KAAK,CAAC,QAAQ,CAAC,OAAyB;YACvC,UAAU,EAAE,CAAC;YACb,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,EAAE,CAAC;YACzB,IAAI,OAAO,EAAE,IAAI,KAAK,IAAI,EAAE,CAAC;gBAC5B,uEAAuE;gBACvE,uEAAuE;gBACvE,yDAAyD;gBACzD,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,QAAQ,CACpC,GAAG,EAAE,CAAC,QAAQ,CAAC,eAAe,CAAC,SAAS,CACxC,CAAC;gBACF,OAAO,EAAC,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAC,CAAC;YACrC,CAAC;YACD,yEAAyE;YACzE,sEAAsE;YACtE,oEAAoE;YACpE,wEAAwE;YACxE,uEAAuE;YACvE,oEAAoE;YACpE,+DAA+D;YAC/D,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,EAAC,IAAI,EAAE,IAAI,EAAC,CAAC,CAAC;YACxD,OAAO,EAAC,GAAG,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,EAAC,CAAC;QAC9C,CAAC;QACD,KAAK,CAAC,KAAK,CAAC,CAAC;YACZ,UAAU,EAAE,CAAC;YACb,MAAM,YAAY,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QAC/B,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI;YACjB,UAAU,EAAE,CAAC;YACb,MAAM,cAAc,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5C,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,UAAkB;YAC5B,UAAU,EAAE,CAAC;YACb,0EAA0E;YAC1E,mEAAmE;YACnE,wEAAwE;YACxE,sEAAsE;YACtE,yEAAyE;YACzE,kEAAkE;YAClE,0EAA0E;YAC1E,yEAAyE;YACzE,2EAA2E;YAC3E,uEAAuE;YACvE,qEAAqE;YACrE,oEAAoE;YACpE,uEAAuE;YACvE,OAAO,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;QACpC,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,SAAwB;YAClC,UAAU,EAAE,CAAC;YACb,MAAM,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;QAClC,CAAC;QACD,KAAK,CAAC,OAAO;YACZ,UAAU,EAAE,CAAC;YACb,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;YACpC,OAAO,GAAG,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;QAC9B,CAAC;QACD,KAAK,CAAC,UAAU,CAAC,OAAO;YACvB,UAAU,EAAE,CAAC;YACb,MAAM,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC;QACvD,CAAC;KACD,CAAC;IAEF,OAAO;QACN,IAAI;QACJ,KAAK,CAAC,KAAK;YACV,IAAI,MAAM;gBAAE,OAAO;YACnB,MAAM,GAAG,IAAI,CAAC;YACd,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;QACvB,CAAC;KACD,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAC5B,IAAY,EACZ,SAAwB;IAExB,QAAQ,SAAS,CAAC,IAAI,EAAE,CAAC;QACxB,KAAK,SAAS;YACb,MAAM,IAAI,CAAC,cAAc,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;YACxC,OAAO;QACR,KAAK,SAAS;YACb,MAAM,cAAc,CAAC,IAAI,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,CAAC;YACvD,OAAO;QACR,KAAK,YAAY;YAChB,4DAA4D;YAC5D,MAAM,IAAI,CAAC,iBAAiB,EAAE,CAAC;YAC/B,OAAO;IACT,CAAC;AACF,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,UAAkB;IAC9D,uCAAuC;IACvC,MAAM,OAAO,GAAG,IAAI,QAAQ,CAAC,MAAM,EAAE,GAAG,EAAE,WAAW,UAAU,IAAI,CAGjC,CAAC;IACnC,OAAO,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;AAC5B,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,uBAAuB,GAAG,KAAK,CAAC;AAEtC;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CACjC,IAAY,EACZ,UAAkB;IAElB,MAAM,MAAM,GAAG,cAAc,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;IAChD,IAAI,CAAC;QACJ,MAAM,MAAM,CAAC,KAAK,CAAC,EAAC,OAAO,EAAE,uBAAuB,EAAC,CAAC,CAAC;IACxD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,IAAI,CAAC,CAAC,KAAK,YAAY,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;YAC/C,MAAM,KAAK,CAAC;QACb,CAAC;QACD,yEAAyE;QACzE,0EAA0E;QAC1E,MAAM,MAAM,CAAC,aAAa,CAAC,OAAO,EAAE,EAAC,OAAO,EAAE,uBAAuB,EAAC,CAAC,CAAC;IACzE,CAAC;AACF,CAAC;AAED,4EAA4E;AAC5E,SAAS,YAAY,CAAC,CASrB;IACA,OAAO;QACN,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,QAAQ,EAAE,CAAC,CAAC,QAAQ;KACpB,CAAC;AACH,CAAC;AAED,8DAA8D;AAC9D,SAAS,cAAc,CAAC,CAAS;IAChC,OAAO;QACN,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,QAAQ,EAAE,CAAC,CAAC,QAAQ;KACpB,CAAC;AACH,CAAC"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Where the controller's dedicated profiles (and other config state) live.
3
+ *
4
+ * This is a SHARED/GLOBAL, per-user location: by default
5
+ * `~/.webhands`. Profiles are dedicated browser user-data dirs
6
+ * under `<root>/profiles/<name>` (PRD "Profile management"; ADR-0002: never the
7
+ * OS default Chrome profile). The endpoint file from ADR-0005 also lives under
8
+ * this root, owned by a later task.
9
+ *
10
+ * Because writing here touches a real, shared location, TESTS MUST override the
11
+ * root to a scratch dir and assert the real one is untouched. The override is
12
+ * the {@link CONTROLLER_HOME_ENV} environment variable (or an explicit
13
+ * `root` passed to {@link resolveProfileLocation}); nothing else points a
14
+ * launch at the real home.
15
+ */
16
+ /** The directory name appended to the user's home for the default root. */
17
+ export declare const DEFAULT_HOME_DIRNAME = ".webhands";
18
+ /**
19
+ * Environment variable that overrides the controller home root. Set this (to a
20
+ * temp dir) in tests, or to relocate state in production. When set to a
21
+ * non-empty value it fully replaces the `~/.webhands` default.
22
+ */
23
+ export declare const CONTROLLER_HOME_ENV = "WEBHANDS_HOME";
24
+ /** The subdirectory under the home root that holds dedicated profiles. */
25
+ export declare const PROFILES_DIRNAME = "profiles";
26
+ /** Inputs that influence where a profile resolves (all optional, for tests). */
27
+ export interface ProfileLocationOptions {
28
+ /**
29
+ * Explicit home root, highest precedence. When omitted, falls back to the
30
+ * {@link CONTROLLER_HOME_ENV} env var, then `~/.webhands`.
31
+ */
32
+ readonly root?: string;
33
+ /** Environment to read the override from. Defaults to `process.env`. */
34
+ readonly env?: NodeJS.ProcessEnv;
35
+ }
36
+ /** A resolved set of controller paths for a given profile name. */
37
+ export interface ProfileLocation {
38
+ /** The controller home root (e.g. `~/.webhands`). */
39
+ readonly homeRoot: string;
40
+ /** The directory holding all dedicated profiles (`<homeRoot>/profiles`). */
41
+ readonly profilesRoot: string;
42
+ /** The dedicated user-data dir for this profile (`<profilesRoot>/<name>`). */
43
+ readonly profileDir: string;
44
+ /** The profile name that was resolved. */
45
+ readonly profile: string;
46
+ }
47
+ /**
48
+ * Resolve the controller home root. Precedence:
49
+ * 1. an explicit `options.root`,
50
+ * 2. the {@link CONTROLLER_HOME_ENV} env var (if non-empty),
51
+ * 3. `~/.webhands`.
52
+ */
53
+ export declare function resolveHomeRoot(options?: ProfileLocationOptions): string;
54
+ /**
55
+ * Resolve every path for a named profile. Does NOT touch the filesystem (no
56
+ * dir is created or checked here) so it is pure and safe to call freely; the
57
+ * transport decides what to do when the dir is absent (raise
58
+ * `MissingProfileError`) or present.
59
+ */
60
+ export declare function resolveProfileLocation(profile: string, options?: ProfileLocationOptions): ProfileLocation;
61
+ //# sourceMappingURL=profile-location.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile-location.d.ts","sourceRoot":"","sources":["../src/profile-location.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;GAcG;AAEH,2EAA2E;AAC3E,eAAO,MAAM,oBAAoB,cAAc,CAAC;AAEhD;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,kBAAkB,CAAC;AAEnD,0EAA0E;AAC1E,eAAO,MAAM,gBAAgB,aAAa,CAAC;AAE3C,gFAAgF;AAChF,MAAM,WAAW,sBAAsB;IACtC;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,wEAAwE;IACxE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACjC;AAED,mEAAmE;AACnE,MAAM,WAAW,eAAe;IAC/B,qDAAqD;IACrD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,4EAA4E;IAC5E,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,8EAA8E;IAC9E,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,0CAA0C;IAC1C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CACzB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,GAAE,sBAA2B,GAAG,MAAM,CAU5E;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACrC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,sBAA2B,GAClC,eAAe,CASjB"}