@habemus-papadum/aiui-util 0.1.0 → 0.3.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/dist/browser.d.ts +91 -0
- package/dist/environment.d.ts +52 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +185 -16
- package/dist/index.js.map +1 -1
- package/dist/provenance.d.ts +22 -0
- package/package.json +8 -3
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/** The installed-Chrome release channels a launch can target. */
|
|
2
|
+
export declare const CHROME_CHANNELS: readonly ["stable", "beta", "dev", "canary"];
|
|
3
|
+
export type ChromeChannel = (typeof CHROME_CHANNELS)[number];
|
|
4
|
+
export interface SessionBrowser {
|
|
5
|
+
/** The DevTools endpoint chrome-devtools-mcp attaches to. */
|
|
6
|
+
browserUrl: string;
|
|
7
|
+
port: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A live session browser for this profile, if one is running.
|
|
11
|
+
* Never launches anything; safe to call from non-interactive paths.
|
|
12
|
+
*/
|
|
13
|
+
export declare function discoverSessionBrowser(userDataDir: string): Promise<SessionBrowser | undefined>;
|
|
14
|
+
/**
|
|
15
|
+
* Which binary a session-browser launch should run: an explicit
|
|
16
|
+
* executablePath (usually the managed Chrome for Testing) wins; otherwise the
|
|
17
|
+
* system install of the requested channel (default stable). Throws when no
|
|
18
|
+
* such browser is installed.
|
|
19
|
+
*/
|
|
20
|
+
export declare function sessionBrowserBinary(settings: {
|
|
21
|
+
executablePath?: string;
|
|
22
|
+
channel?: ChromeChannel;
|
|
23
|
+
}): string;
|
|
24
|
+
/**
|
|
25
|
+
* Launch the session browser detached (it deliberately outlives the launching
|
|
26
|
+
* process — it's the user's window too) and wait for its debug endpoint.
|
|
27
|
+
*
|
|
28
|
+
* `debugPort` 0 lets the OS pick a free port; Chrome reports the choice via
|
|
29
|
+
* `DevToolsActivePort`, which is removed first so a stale file from a previous
|
|
30
|
+
* run can't win the poll. Fails fast if the process exits early — the classic
|
|
31
|
+
* cause being an already-running Chrome on the same profile *without* a debug
|
|
32
|
+
* port, which swallows the new invocation as a URL-handoff and exits.
|
|
33
|
+
*/
|
|
34
|
+
export declare function launchSessionBrowser(opts: {
|
|
35
|
+
binary: string;
|
|
36
|
+
userDataDir: string;
|
|
37
|
+
debugPort?: number;
|
|
38
|
+
extensionDir?: string;
|
|
39
|
+
headless?: boolean;
|
|
40
|
+
startUrl?: string;
|
|
41
|
+
}): Promise<SessionBrowser>;
|
|
42
|
+
/**
|
|
43
|
+
* Open a URL as a new tab in a session browser, via the DevTools HTTP API
|
|
44
|
+
* (`PUT /json/new` — PUT is required by current Chrome).
|
|
45
|
+
*/
|
|
46
|
+
export declare function openInSessionBrowser(browserUrl: string, url: string): Promise<void>;
|
|
47
|
+
/** The force/suppress escape hatches a sidecar's caller can express. */
|
|
48
|
+
export interface BrowserAutoOpenFlags {
|
|
49
|
+
/** Force opening even under CI/SSH/no-display (`--aiui-browser`, `WORKBENCH_BROWSER=1`). */
|
|
50
|
+
browser?: boolean;
|
|
51
|
+
/** Never open a browser for this run (`--aiui-no-browser`, `WORKBENCH_BROWSER=0`). */
|
|
52
|
+
noBrowser?: boolean;
|
|
53
|
+
}
|
|
54
|
+
/** The config a sidecar's project may have voted with (aiui's `chrome` section). */
|
|
55
|
+
export interface BrowserAutoOpenConfig {
|
|
56
|
+
/** `false` opts the project out of browser integration wholesale. */
|
|
57
|
+
enabled?: boolean;
|
|
58
|
+
/** A browser managed elsewhere (usually reverse-tunneled) — attach there. */
|
|
59
|
+
browserUrl?: string;
|
|
60
|
+
}
|
|
61
|
+
/** What a browser sidecar should do once its dev-server URL is known. */
|
|
62
|
+
export type BrowserAction = {
|
|
63
|
+
kind: "open";
|
|
64
|
+
} | {
|
|
65
|
+
kind: "skip";
|
|
66
|
+
} | {
|
|
67
|
+
kind: "hint";
|
|
68
|
+
reason: string;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Decide the browser sidecar's move — pure (env and platform are parameters)
|
|
72
|
+
* so every rung is unit-testable. The ladder, most explicit first, mirrors
|
|
73
|
+
* the aiui CLI's flag-beats-config ordering:
|
|
74
|
+
*
|
|
75
|
+
* 1. Suppress flag → skip, silently. The user said no for this run.
|
|
76
|
+
* 2. Force flag → open, even under CI, over SSH, or with
|
|
77
|
+
* `chrome.enabled: false` — the force flag exists precisely to overrule
|
|
78
|
+
* the defaults (e.g. the dev box is "headless" but its display is a
|
|
79
|
+
* forwarded port away).
|
|
80
|
+
* 3. `chrome.enabled: false` → skip: the config opted this project out of
|
|
81
|
+
* browser integration wholesale, same as it disables the DevTools MCP.
|
|
82
|
+
* 4. A configured `chrome.browserUrl` → open. The browser deliberately lives
|
|
83
|
+
* elsewhere (typically the user's local machine, reverse-tunneled — see
|
|
84
|
+
* docs/guide/remote), so *this* machine being headless is irrelevant:
|
|
85
|
+
* opening a tab there is exactly the point of the setup.
|
|
86
|
+
* 5. CI or headless (see ./environment) → don't launch a browser nobody
|
|
87
|
+
* can see; hand back the reason so the caller can print the
|
|
88
|
+
* port-forwarding hint instead.
|
|
89
|
+
* 6. Otherwise → open.
|
|
90
|
+
*/
|
|
91
|
+
export declare function decideBrowserAction(args: BrowserAutoOpenFlags, config?: BrowserAutoOpenConfig, env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): BrowserAction;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where is this process running — CI? over SSH? a machine with no display?
|
|
3
|
+
*
|
|
4
|
+
* Several commands adapt to their surroundings: `aiui claude` leaves the
|
|
5
|
+
* Chrome DevTools MCP off under CI, and `aiui vite` / `pnpm workbench` only
|
|
6
|
+
* auto-open their dev server in the session browser when there is a display
|
|
7
|
+
* to show it on. Those decisions share the detection logic here — kept pure
|
|
8
|
+
* (`env` and `platform` are parameters defaulting to the real ones) so every
|
|
9
|
+
* branch is unit-testable without mutating the test process's own
|
|
10
|
+
* environment.
|
|
11
|
+
*
|
|
12
|
+
* The signals, and why each is trusted:
|
|
13
|
+
*
|
|
14
|
+
* - **CI** — effectively every provider (GitHub Actions, GitLab, CircleCI,
|
|
15
|
+
* Travis, Buildkite, …) sets `CI=true`. A CI runner may technically have a
|
|
16
|
+
* display, but nobody is watching it, which is what "headless" means for
|
|
17
|
+
* the decisions built on this module.
|
|
18
|
+
* - **SSH** — sshd sets `SSH_CONNECTION` and `SSH_CLIENT` in every session
|
|
19
|
+
* and `SSH_TTY` in interactive ones. Any of them means this shell lives at
|
|
20
|
+
* the far end of a network connection: a browser launched here would render
|
|
21
|
+
* on the remote box's display (if it even has one), never in front of the
|
|
22
|
+
* user. X11 forwarding (`ssh -X`, which sets `DISPLAY`) is deliberately
|
|
23
|
+
* *not* honored as an exception — it's rare, slow, and almost never where
|
|
24
|
+
* the user wants a Chrome window; `--aiui-browser`-style force flags are
|
|
25
|
+
* the escape hatch.
|
|
26
|
+
* - **Linux display** — X11 clients find their server via `DISPLAY`; Wayland
|
|
27
|
+
* clients via `WAYLAND_DISPLAY`. Neither being set means there is no
|
|
28
|
+
* compositor to render a window into.
|
|
29
|
+
* - **macOS** — has no display env-var convention; WindowServer is always up
|
|
30
|
+
* on a booted Mac, so assume a GUI unless SSH says the shell is remote.
|
|
31
|
+
* - **Other platforms** (Windows) likewise have no display env var: assume a
|
|
32
|
+
* GUI and rely on the CI/SSH signals alone.
|
|
33
|
+
*/
|
|
34
|
+
/** Truthy `CI` env var, with the conventional "false"/"0" escape hatches. */
|
|
35
|
+
export declare function isCi(env?: NodeJS.ProcessEnv): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Whether this shell is an SSH session. Checks all three of sshd's markers —
|
|
38
|
+
* `SSH_TTY` is only set for interactive sessions, and `SSH_CONNECTION` /
|
|
39
|
+
* `SSH_CLIENT` can be scrubbed by a paranoid shell profile, so any one of
|
|
40
|
+
* them is taken at its word. Empty values count as unset.
|
|
41
|
+
*/
|
|
42
|
+
export declare function isSsh(env?: NodeJS.ProcessEnv): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Why this environment is considered headless — a short human-readable
|
|
45
|
+
* fragment for messages like "detected a headless environment (`<reason>`)" —
|
|
46
|
+
* or undefined when a GUI is presumed available. The checks run most-explicit
|
|
47
|
+
* first (CI, then SSH, then the platform's display convention); see the
|
|
48
|
+
* module doc for the reasoning behind each signal.
|
|
49
|
+
*/
|
|
50
|
+
export declare function headlessReason(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): string | undefined;
|
|
51
|
+
/** {@link headlessReason} as a boolean, for callers that don't print it. */
|
|
52
|
+
export declare function isHeadless(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared utilities for the aiui packages
|
|
2
|
+
* Shared utilities for the aiui packages: cache directories, environment
|
|
3
|
+
* detection (CI / SSH / headless), and the session-browser plumbing that
|
|
4
|
+
* dev-server sidecars build their browser auto-open on.
|
|
3
5
|
*
|
|
4
6
|
* @packageDocumentation
|
|
5
7
|
*/
|
|
8
|
+
export * from "./browser";
|
|
9
|
+
export * from "./environment";
|
|
10
|
+
export * from "./provenance";
|
|
6
11
|
export interface CacheDirOptions {
|
|
7
12
|
/**
|
|
8
13
|
* Create the directory (recursively) if it doesn't exist. Defaults to `true` —
|
package/dist/index.js
CHANGED
|
@@ -1,20 +1,189 @@
|
|
|
1
|
-
import { mkdirSync as
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
import { mkdirSync as d, rmSync as m, writeFileSync as w, readFileSync as b, existsSync as l } from "node:fs";
|
|
2
|
+
import { join as o, dirname as S, isAbsolute as A } from "node:path";
|
|
3
|
+
import { computeSystemExecutablePath as v, ChromeReleaseChannel as a, Browser as D } from "@puppeteer/browsers";
|
|
4
|
+
import { execa as E } from "execa";
|
|
5
|
+
import { createRequire as C } from "node:module";
|
|
6
|
+
import { homedir as y } from "node:os";
|
|
7
|
+
function g(e = process.env) {
|
|
8
|
+
const r = e.CI;
|
|
9
|
+
return r !== void 0 && r !== "" && r !== "0" && r.toLowerCase() !== "false";
|
|
10
|
+
}
|
|
11
|
+
function x(e = process.env) {
|
|
12
|
+
return s(e.SSH_CONNECTION) || s(e.SSH_TTY) || s(e.SSH_CLIENT);
|
|
13
|
+
}
|
|
14
|
+
function h(e = process.env, r = process.platform) {
|
|
15
|
+
if (g(e))
|
|
16
|
+
return "the CI environment variable is set";
|
|
17
|
+
if (x(e))
|
|
18
|
+
return "this is an SSH session";
|
|
19
|
+
if (r === "linux" && !s(e.DISPLAY) && !s(e.WAYLAND_DISPLAY))
|
|
20
|
+
return "Linux with neither DISPLAY nor WAYLAND_DISPLAY set";
|
|
21
|
+
}
|
|
22
|
+
function F(e = process.env, r = process.platform) {
|
|
23
|
+
return h(e, r) !== void 0;
|
|
24
|
+
}
|
|
25
|
+
function s(e) {
|
|
26
|
+
return e !== void 0 && e !== "";
|
|
27
|
+
}
|
|
28
|
+
const p = "DevToolsActivePort", u = 2e4, M = ["stable", "beta", "dev", "canary"];
|
|
29
|
+
async function I(e) {
|
|
30
|
+
const r = k(e);
|
|
31
|
+
if (r !== void 0 && await T(r))
|
|
32
|
+
return { browserUrl: `http://127.0.0.1:${r}`, port: r };
|
|
33
|
+
}
|
|
34
|
+
function k(e) {
|
|
35
|
+
try {
|
|
36
|
+
const [r] = b(o(e, p), "utf8").split(`
|
|
37
|
+
`), t = Number(r);
|
|
38
|
+
return Number.isInteger(t) && t > 0 ? t : void 0;
|
|
39
|
+
} catch {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function T(e) {
|
|
44
|
+
try {
|
|
45
|
+
return (await fetch(`http://127.0.0.1:${e}/json/version`, {
|
|
46
|
+
signal: AbortSignal.timeout(1e3)
|
|
47
|
+
})).ok;
|
|
48
|
+
} catch {
|
|
49
|
+
return !1;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function q(e) {
|
|
53
|
+
return e.executablePath ? e.executablePath : v({
|
|
54
|
+
browser: D.CHROME,
|
|
55
|
+
channel: L[e.channel ?? "stable"]
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const L = {
|
|
59
|
+
stable: a.STABLE,
|
|
60
|
+
beta: a.BETA,
|
|
61
|
+
dev: a.DEV,
|
|
62
|
+
canary: a.CANARY
|
|
63
|
+
};
|
|
64
|
+
async function V(e) {
|
|
65
|
+
d(e.userDataDir, { recursive: !0 }), m(o(e.userDataDir, p), { force: !0 });
|
|
66
|
+
const r = [
|
|
67
|
+
`--remote-debugging-port=${e.debugPort ?? 0}`,
|
|
68
|
+
`--user-data-dir=${e.userDataDir}`,
|
|
69
|
+
"--no-first-run",
|
|
70
|
+
"--no-default-browser-check",
|
|
71
|
+
// Media prompts, pre-answered — this is a *dev* browser (unauthenticated
|
|
72
|
+
// debug port, project-local profile; see docs/guide/warning.md), and the
|
|
73
|
+
// intent tool's dictation + screenshots otherwise re-prompt constantly:
|
|
74
|
+
// Chrome scopes mic permission per-origin (every dev-server PORT is its
|
|
75
|
+
// own origin), and the getDisplayMedia share picker can never be
|
|
76
|
+
// persisted at all.
|
|
77
|
+
// - fake-ui: auto-accept getUserMedia permission prompts. The *default,
|
|
78
|
+
// real* devices are used — fake devices only come from the separate
|
|
79
|
+
// --use-fake-device-for-media-stream, deliberately not passed.
|
|
80
|
+
"--use-fake-ui-for-media-stream",
|
|
81
|
+
// - auto-accept the current-tab share the shot tool asks for
|
|
82
|
+
// (getDisplayMedia({ preferCurrentTab: true })) — no picker dialog.
|
|
83
|
+
"--auto-accept-this-tab-capture"
|
|
84
|
+
];
|
|
85
|
+
e.extensionDir && r.push(`--load-extension=${e.extensionDir}`), e.headless && r.push("--headless"), r.push(e.startUrl ?? "about:blank");
|
|
86
|
+
const t = E(e.binary, r, {
|
|
87
|
+
detached: !0,
|
|
88
|
+
stdio: "ignore",
|
|
89
|
+
reject: !1,
|
|
90
|
+
cleanup: !1
|
|
91
|
+
});
|
|
92
|
+
t.unref();
|
|
93
|
+
let n = !1;
|
|
94
|
+
t.then(() => {
|
|
95
|
+
n = !0;
|
|
96
|
+
});
|
|
97
|
+
const i = Date.now() + u;
|
|
98
|
+
for (; Date.now() < i; ) {
|
|
99
|
+
const c = await I(e.userDataDir);
|
|
100
|
+
if (c) {
|
|
101
|
+
try {
|
|
102
|
+
w(
|
|
103
|
+
o(e.userDataDir, "aiui-browser.json"),
|
|
104
|
+
`${JSON.stringify({ pid: t.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() })}
|
|
105
|
+
`
|
|
106
|
+
);
|
|
107
|
+
} catch {
|
|
108
|
+
}
|
|
109
|
+
return c;
|
|
110
|
+
}
|
|
111
|
+
if (n)
|
|
112
|
+
throw new Error(
|
|
113
|
+
"the browser exited before exposing its DevTools endpoint — is another Chrome already running on this profile without a debug port? Close it and retry."
|
|
114
|
+
);
|
|
115
|
+
await P(250);
|
|
116
|
+
}
|
|
117
|
+
throw new Error(
|
|
118
|
+
`the browser did not expose its DevTools endpoint within ${u / 1e3}s`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
async function W(e, r) {
|
|
122
|
+
const t = e.replace(/\/+$/, ""), n = await fetch(`${t}/json/new?${encodeURI(r)}`, {
|
|
123
|
+
method: "PUT",
|
|
124
|
+
signal: AbortSignal.timeout(3e3)
|
|
125
|
+
});
|
|
126
|
+
if (!n.ok)
|
|
127
|
+
throw new Error(`the browser refused to open the tab (${n.status} ${n.statusText})`);
|
|
128
|
+
}
|
|
129
|
+
function G(e, r = {}, t = process.env, n = process.platform) {
|
|
130
|
+
if (e.noBrowser)
|
|
131
|
+
return { kind: "skip" };
|
|
132
|
+
if (e.browser)
|
|
133
|
+
return { kind: "open" };
|
|
134
|
+
if (r.enabled === !1)
|
|
135
|
+
return { kind: "skip" };
|
|
136
|
+
if (r.browserUrl)
|
|
137
|
+
return { kind: "open" };
|
|
138
|
+
const i = h(t, n);
|
|
139
|
+
return i ? { kind: "hint", reason: i } : { kind: "open" };
|
|
140
|
+
}
|
|
141
|
+
function P(e) {
|
|
142
|
+
return new Promise((r) => setTimeout(r, e));
|
|
143
|
+
}
|
|
144
|
+
const H = C(import.meta.url);
|
|
145
|
+
function _(e) {
|
|
146
|
+
const r = e.split("/");
|
|
147
|
+
for (const t of H.resolve.paths(e) ?? []) {
|
|
148
|
+
const n = o(t, ...r, "package.json");
|
|
149
|
+
if (l(n))
|
|
150
|
+
return S(n);
|
|
151
|
+
}
|
|
152
|
+
throw new Error(`could not locate the "${e}" package (is it installed?)`);
|
|
153
|
+
}
|
|
154
|
+
function N(e) {
|
|
155
|
+
return l(o(e, "src"));
|
|
156
|
+
}
|
|
157
|
+
function J(e) {
|
|
158
|
+
return N(_(e));
|
|
159
|
+
}
|
|
160
|
+
const $ = "aiui";
|
|
161
|
+
function X(e, r = {}) {
|
|
162
|
+
const { create: t = !0 } = r, n = e ? o(f(), e) : f();
|
|
163
|
+
return t && d(n, { recursive: !0 }), n;
|
|
164
|
+
}
|
|
165
|
+
function f() {
|
|
166
|
+
var n, i;
|
|
167
|
+
const e = (n = process.env.AIUI_CACHE) == null ? void 0 : n.trim();
|
|
168
|
+
if (e)
|
|
169
|
+
return e;
|
|
170
|
+
const r = (i = process.env.XDG_CACHE_HOME) == null ? void 0 : i.trim(), t = r && A(r) ? r : o(y(), ".cache");
|
|
171
|
+
return o(t, $);
|
|
16
172
|
}
|
|
17
173
|
export {
|
|
18
|
-
|
|
174
|
+
M as CHROME_CHANNELS,
|
|
175
|
+
X as cacheDir,
|
|
176
|
+
G as decideBrowserAction,
|
|
177
|
+
I as discoverSessionBrowser,
|
|
178
|
+
h as headlessReason,
|
|
179
|
+
g as isCi,
|
|
180
|
+
F as isHeadless,
|
|
181
|
+
x as isSsh,
|
|
182
|
+
V as launchSessionBrowser,
|
|
183
|
+
W as openInSessionBrowser,
|
|
184
|
+
J as packageFromSource,
|
|
185
|
+
_ as packageRoot,
|
|
186
|
+
N as runningFromSource,
|
|
187
|
+
q as sessionBrowserBinary
|
|
19
188
|
};
|
|
20
189
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * Shared utilities for the aiui packages.\n *\n * @packageDocumentation\n */\n\nimport { mkdirSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { isAbsolute, join } from \"node:path\";\n\n/** The application name — the leaf under the platform cache directory. */\nconst APP = \"aiui\";\n\nexport interface CacheDirOptions {\n /**\n * Create the directory (recursively) if it doesn't exist. Defaults to `true` —\n * callers generally want a ready-to-use directory back.\n */\n create?: boolean;\n}\n\n/**\n * Resolve the cache directory for aiui, creating it by default.\n *\n * Resolution order:\n * 1. `$AIUI_CACHE` — an explicit override; used verbatim as the aiui cache root.\n * 2. `$XDG_CACHE_HOME/aiui` — honoring the XDG Base Directory spec (the variable\n * is ignored unless it's an absolute path, per the spec).\n * 3. `~/.cache/aiui` — the XDG default.\n *\n * Pass `namespace` to carve out a subdirectory for a particular kind of cached\n * data, e.g. `cacheDir(\"claude\")` → `~/.cache/aiui/claude`. Different callers\n * cache different things, so each should pick its own namespace rather than\n * writing into the shared root.\n *\n * @example\n * const dir = cacheDir(\"screenshots\"); // created, ready to write into\n * const dir = cacheDir(\"claude\", { create: false }); // resolve the path only\n */\nexport function cacheDir(namespace?: string, options: CacheDirOptions = {}): string {\n const { create = true } = options;\n const dir = namespace ? join(cacheRoot(), namespace) : cacheRoot();\n if (create) {\n mkdirSync(dir, { recursive: true });\n }\n return dir;\n}\n\n/** Resolve the aiui cache root (no directory creation). */\nfunction cacheRoot(): string {\n const override = process.env.AIUI_CACHE?.trim();\n if (override) {\n return override;\n }\n\n const xdg = process.env.XDG_CACHE_HOME?.trim();\n const cacheHome = xdg && isAbsolute(xdg) ? xdg : join(homedir(), \".cache\");\n return join(cacheHome, APP);\n}\n"],"names":["APP","cacheDir","namespace","options","create","dir","join","cacheRoot","mkdirSync","override","_a","xdg","_b","cacheHome","isAbsolute","homedir"],"mappings":";;;AAWA,MAAMA,IAAM;AA4BL,SAASC,EAASC,GAAoBC,IAA2B,IAAY;AAClF,QAAM,EAAE,QAAAC,IAAS,GAAA,IAASD,GACpBE,IAAMH,IAAYI,EAAKC,KAAaL,CAAS,IAAIK,EAAA;AACvD,SAAIH,KACFI,EAAUH,GAAK,EAAE,WAAW,GAAA,CAAM,GAE7BA;AACT;AAGA,SAASE,IAAoB;;AAC3B,QAAME,KAAWC,IAAA,QAAQ,IAAI,eAAZ,gBAAAA,EAAwB;AACzC,MAAID;AACF,WAAOA;AAGT,QAAME,KAAMC,IAAA,QAAQ,IAAI,mBAAZ,gBAAAA,EAA4B,QAClCC,IAAYF,KAAOG,EAAWH,CAAG,IAAIA,IAAML,EAAKS,EAAA,GAAW,QAAQ;AACzE,SAAOT,EAAKO,GAAWb,CAAG;AAC5B;"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/environment.ts","../src/browser.ts","../src/provenance.ts","../src/index.ts"],"sourcesContent":["/**\n * Where is this process running — CI? over SSH? a machine with no display?\n *\n * Several commands adapt to their surroundings: `aiui claude` leaves the\n * Chrome DevTools MCP off under CI, and `aiui vite` / `pnpm workbench` only\n * auto-open their dev server in the session browser when there is a display\n * to show it on. Those decisions share the detection logic here — kept pure\n * (`env` and `platform` are parameters defaulting to the real ones) so every\n * branch is unit-testable without mutating the test process's own\n * environment.\n *\n * The signals, and why each is trusted:\n *\n * - **CI** — effectively every provider (GitHub Actions, GitLab, CircleCI,\n * Travis, Buildkite, …) sets `CI=true`. A CI runner may technically have a\n * display, but nobody is watching it, which is what \"headless\" means for\n * the decisions built on this module.\n * - **SSH** — sshd sets `SSH_CONNECTION` and `SSH_CLIENT` in every session\n * and `SSH_TTY` in interactive ones. Any of them means this shell lives at\n * the far end of a network connection: a browser launched here would render\n * on the remote box's display (if it even has one), never in front of the\n * user. X11 forwarding (`ssh -X`, which sets `DISPLAY`) is deliberately\n * *not* honored as an exception — it's rare, slow, and almost never where\n * the user wants a Chrome window; `--aiui-browser`-style force flags are\n * the escape hatch.\n * - **Linux display** — X11 clients find their server via `DISPLAY`; Wayland\n * clients via `WAYLAND_DISPLAY`. Neither being set means there is no\n * compositor to render a window into.\n * - **macOS** — has no display env-var convention; WindowServer is always up\n * on a booted Mac, so assume a GUI unless SSH says the shell is remote.\n * - **Other platforms** (Windows) likewise have no display env var: assume a\n * GUI and rely on the CI/SSH signals alone.\n */\n\n/** Truthy `CI` env var, with the conventional \"false\"/\"0\" escape hatches. */\nexport function isCi(env: NodeJS.ProcessEnv = process.env): boolean {\n const ci = env.CI;\n return ci !== undefined && ci !== \"\" && ci !== \"0\" && ci.toLowerCase() !== \"false\";\n}\n\n/**\n * Whether this shell is an SSH session. Checks all three of sshd's markers —\n * `SSH_TTY` is only set for interactive sessions, and `SSH_CONNECTION` /\n * `SSH_CLIENT` can be scrubbed by a paranoid shell profile, so any one of\n * them is taken at its word. Empty values count as unset.\n */\nexport function isSsh(env: NodeJS.ProcessEnv = process.env): boolean {\n return isSet(env.SSH_CONNECTION) || isSet(env.SSH_TTY) || isSet(env.SSH_CLIENT);\n}\n\n/**\n * Why this environment is considered headless — a short human-readable\n * fragment for messages like \"detected a headless environment (`<reason>`)\" —\n * or undefined when a GUI is presumed available. The checks run most-explicit\n * first (CI, then SSH, then the platform's display convention); see the\n * module doc for the reasoning behind each signal.\n */\nexport function headlessReason(\n env: NodeJS.ProcessEnv = process.env,\n platform: NodeJS.Platform = process.platform,\n): string | undefined {\n if (isCi(env)) {\n return \"the CI environment variable is set\";\n }\n if (isSsh(env)) {\n return \"this is an SSH session\";\n }\n if (platform === \"linux\" && !isSet(env.DISPLAY) && !isSet(env.WAYLAND_DISPLAY)) {\n return \"Linux with neither DISPLAY nor WAYLAND_DISPLAY set\";\n }\n return undefined;\n}\n\n/** {@link headlessReason} as a boolean, for callers that don't print it. */\nexport function isHeadless(\n env: NodeJS.ProcessEnv = process.env,\n platform: NodeJS.Platform = process.platform,\n): boolean {\n return headlessReason(env, platform) !== undefined;\n}\n\n/** Set and non-empty — `VAR=` in a profile shouldn't count as a signal. */\nfunction isSet(value: string | undefined): boolean {\n return value !== undefined && value !== \"\";\n}\n","/**\n * The session browser: one user-visible Chrome shared by the human and the\n * agent.\n *\n * In the aiui CLI's default \"attach\" mode, `aiui claude` launches the browser\n * itself — with a DevTools debug port, the project profile, and the aiui\n * devtools extension — and chrome-devtools-mcp *attaches* to it\n * (`--browser-url`) instead of launching a private one. That's what makes the\n * agent's browser the same window the human is looking at: shared tabs,\n * shared state, visible from session start.\n *\n * This module is the shared plumbing under that story — discovery, launch,\n * open-a-tab, and the auto-open decision ladder — so every dev-server sidecar\n * (`aiui vite`, the workbench's `pnpm workbench`) puts its page in the same\n * shared window instead of re-deriving the mechanics. The aiui CLI layers its\n * own affordances on top (config resolution, Chrome for Testing sync, the\n * devtools-extension autoload); nothing here reads config or prompts.\n *\n * There is deliberately no registry file for browsers. Chrome itself writes\n * `DevToolsActivePort` into the user data dir of any instance started with a\n * debug port, and the user data dir is already the profile's identity — so\n * discovery is: read that file, confirm the endpoint answers `/json/version`.\n * A dead file (crash leftovers, stale port) just fails the liveness probe.\n *\n * Security note (documented in docs/guide/warning): the debug endpoint is\n * unauthenticated — any local process can drive the browser through it. It\n * binds to loopback only.\n */\nimport { mkdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { Browser, ChromeReleaseChannel, computeSystemExecutablePath } from \"@puppeteer/browsers\";\nimport { execa } from \"execa\";\nimport { headlessReason } from \"./environment\";\n\n/** Chrome writes this into the user data dir when debugging is enabled. */\nconst ACTIVE_PORT_FILE = \"DevToolsActivePort\";\n\n/** How long a fresh Chrome gets to bring up its debug endpoint. */\nconst LAUNCH_TIMEOUT_MS = 20_000;\n\n/** The installed-Chrome release channels a launch can target. */\nexport const CHROME_CHANNELS = [\"stable\", \"beta\", \"dev\", \"canary\"] as const;\nexport type ChromeChannel = (typeof CHROME_CHANNELS)[number];\n\nexport interface SessionBrowser {\n /** The DevTools endpoint chrome-devtools-mcp attaches to. */\n browserUrl: string;\n port: number;\n}\n\n/**\n * A live session browser for this profile, if one is running.\n * Never launches anything; safe to call from non-interactive paths.\n */\nexport async function discoverSessionBrowser(\n userDataDir: string,\n): Promise<SessionBrowser | undefined> {\n const port = readActivePort(userDataDir);\n if (port === undefined) {\n return undefined;\n }\n if (!(await debugEndpointAlive(port))) {\n return undefined;\n }\n return { browserUrl: `http://127.0.0.1:${port}`, port };\n}\n\nfunction readActivePort(userDataDir: string): number | undefined {\n try {\n const [first] = readFileSync(join(userDataDir, ACTIVE_PORT_FILE), \"utf8\").split(\"\\n\");\n const port = Number(first);\n return Number.isInteger(port) && port > 0 ? port : undefined;\n } catch {\n return undefined;\n }\n}\n\nasync function debugEndpointAlive(port: number): Promise<boolean> {\n try {\n const res = await fetch(`http://127.0.0.1:${port}/json/version`, {\n signal: AbortSignal.timeout(1000),\n });\n return res.ok;\n } catch {\n return false;\n }\n}\n\n/**\n * Which binary a session-browser launch should run: an explicit\n * executablePath (usually the managed Chrome for Testing) wins; otherwise the\n * system install of the requested channel (default stable). Throws when no\n * such browser is installed.\n */\nexport function sessionBrowserBinary(settings: {\n executablePath?: string;\n channel?: ChromeChannel;\n}): string {\n if (settings.executablePath) {\n return settings.executablePath;\n }\n return computeSystemExecutablePath({\n browser: Browser.CHROME,\n channel: RELEASE_CHANNELS[settings.channel ?? \"stable\"],\n });\n}\n\nconst RELEASE_CHANNELS: Record<ChromeChannel, ChromeReleaseChannel> = {\n stable: ChromeReleaseChannel.STABLE,\n beta: ChromeReleaseChannel.BETA,\n dev: ChromeReleaseChannel.DEV,\n canary: ChromeReleaseChannel.CANARY,\n};\n\n/**\n * Launch the session browser detached (it deliberately outlives the launching\n * process — it's the user's window too) and wait for its debug endpoint.\n *\n * `debugPort` 0 lets the OS pick a free port; Chrome reports the choice via\n * `DevToolsActivePort`, which is removed first so a stale file from a previous\n * run can't win the poll. Fails fast if the process exits early — the classic\n * cause being an already-running Chrome on the same profile *without* a debug\n * port, which swallows the new invocation as a URL-handoff and exits.\n */\nexport async function launchSessionBrowser(opts: {\n binary: string;\n userDataDir: string;\n debugPort?: number;\n extensionDir?: string;\n headless?: boolean;\n startUrl?: string;\n}): Promise<SessionBrowser> {\n mkdirSync(opts.userDataDir, { recursive: true });\n rmSync(join(opts.userDataDir, ACTIVE_PORT_FILE), { force: true });\n\n const args = [\n `--remote-debugging-port=${opts.debugPort ?? 0}`,\n `--user-data-dir=${opts.userDataDir}`,\n \"--no-first-run\",\n \"--no-default-browser-check\",\n // Media prompts, pre-answered — this is a *dev* browser (unauthenticated\n // debug port, project-local profile; see docs/guide/warning.md), and the\n // intent tool's dictation + screenshots otherwise re-prompt constantly:\n // Chrome scopes mic permission per-origin (every dev-server PORT is its\n // own origin), and the getDisplayMedia share picker can never be\n // persisted at all.\n // - fake-ui: auto-accept getUserMedia permission prompts. The *default,\n // real* devices are used — fake devices only come from the separate\n // --use-fake-device-for-media-stream, deliberately not passed.\n \"--use-fake-ui-for-media-stream\",\n // - auto-accept the current-tab share the shot tool asks for\n // (getDisplayMedia({ preferCurrentTab: true })) — no picker dialog.\n \"--auto-accept-this-tab-capture\",\n ];\n if (opts.extensionDir) {\n args.push(`--load-extension=${opts.extensionDir}`);\n }\n if (opts.headless) {\n args.push(\"--headless\");\n }\n args.push(opts.startUrl ?? \"about:blank\");\n\n const child = execa(opts.binary, args, {\n detached: true,\n stdio: \"ignore\",\n reject: false,\n cleanup: false,\n });\n child.unref();\n let exited = false;\n void child.then(() => {\n exited = true;\n });\n\n const deadline = Date.now() + LAUNCH_TIMEOUT_MS;\n while (Date.now() < deadline) {\n const found = await discoverSessionBrowser(opts.userDataDir);\n if (found) {\n // Purely informational breadcrumb (pid, when) for humans poking around.\n try {\n writeFileSync(\n join(opts.userDataDir, \"aiui-browser.json\"),\n `${JSON.stringify({ pid: child.pid, startedAt: new Date().toISOString() })}\\n`,\n );\n } catch {}\n return found;\n }\n if (exited) {\n throw new Error(\n \"the browser exited before exposing its DevTools endpoint — is another Chrome \" +\n \"already running on this profile without a debug port? Close it and retry.\",\n );\n }\n await sleep(250);\n }\n throw new Error(\n `the browser did not expose its DevTools endpoint within ${LAUNCH_TIMEOUT_MS / 1000}s`,\n );\n}\n\n/**\n * Open a URL as a new tab in a session browser, via the DevTools HTTP API\n * (`PUT /json/new` — PUT is required by current Chrome).\n */\nexport async function openInSessionBrowser(browserUrl: string, url: string): Promise<void> {\n const base = browserUrl.replace(/\\/+$/, \"\");\n const res = await fetch(`${base}/json/new?${encodeURI(url)}`, {\n method: \"PUT\",\n signal: AbortSignal.timeout(3000),\n });\n if (!res.ok) {\n throw new Error(`the browser refused to open the tab (${res.status} ${res.statusText})`);\n }\n}\n\n/** The force/suppress escape hatches a sidecar's caller can express. */\nexport interface BrowserAutoOpenFlags {\n /** Force opening even under CI/SSH/no-display (`--aiui-browser`, `WORKBENCH_BROWSER=1`). */\n browser?: boolean;\n /** Never open a browser for this run (`--aiui-no-browser`, `WORKBENCH_BROWSER=0`). */\n noBrowser?: boolean;\n}\n\n/** The config a sidecar's project may have voted with (aiui's `chrome` section). */\nexport interface BrowserAutoOpenConfig {\n /** `false` opts the project out of browser integration wholesale. */\n enabled?: boolean;\n /** A browser managed elsewhere (usually reverse-tunneled) — attach there. */\n browserUrl?: string;\n}\n\n/** What a browser sidecar should do once its dev-server URL is known. */\nexport type BrowserAction = { kind: \"open\" } | { kind: \"skip\" } | { kind: \"hint\"; reason: string };\n\n/**\n * Decide the browser sidecar's move — pure (env and platform are parameters)\n * so every rung is unit-testable. The ladder, most explicit first, mirrors\n * the aiui CLI's flag-beats-config ordering:\n *\n * 1. Suppress flag → skip, silently. The user said no for this run.\n * 2. Force flag → open, even under CI, over SSH, or with\n * `chrome.enabled: false` — the force flag exists precisely to overrule\n * the defaults (e.g. the dev box is \"headless\" but its display is a\n * forwarded port away).\n * 3. `chrome.enabled: false` → skip: the config opted this project out of\n * browser integration wholesale, same as it disables the DevTools MCP.\n * 4. A configured `chrome.browserUrl` → open. The browser deliberately lives\n * elsewhere (typically the user's local machine, reverse-tunneled — see\n * docs/guide/remote), so *this* machine being headless is irrelevant:\n * opening a tab there is exactly the point of the setup.\n * 5. CI or headless (see ./environment) → don't launch a browser nobody\n * can see; hand back the reason so the caller can print the\n * port-forwarding hint instead.\n * 6. Otherwise → open.\n */\nexport function decideBrowserAction(\n args: BrowserAutoOpenFlags,\n config: BrowserAutoOpenConfig = {},\n env: NodeJS.ProcessEnv = process.env,\n platform: NodeJS.Platform = process.platform,\n): BrowserAction {\n if (args.noBrowser) {\n return { kind: \"skip\" };\n }\n if (args.browser) {\n return { kind: \"open\" };\n }\n if (config.enabled === false) {\n return { kind: \"skip\" };\n }\n if (config.browserUrl) {\n return { kind: \"open\" };\n }\n const reason = headlessReason(env, platform);\n if (reason) {\n return { kind: \"hint\", reason };\n }\n return { kind: \"open\" };\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","/**\n * Was a package obtained as an editable **source checkout** (this monorepo) or\n * **installed** from a published tarball? `resolvePackageCli` (in the aiui CLI)\n * uses this to decide how to spawn a workspace CLI: through tsx straight from\n * `src/` in a dev checkout, or plain `node` on `dist/` once installed.\n *\n * The signal is a filesystem fact, not an env var: a published tarball ships\n * only its `dist/` (the `files` allowlist excludes `src/`), so a package directory\n * that still carries a `src/` folder is a dev checkout. Locate a package's root\n * with {@link packageRoot}, then ask {@link runningFromSource} about it — or use\n * {@link packageFromSource} for both in one call.\n */\nimport { existsSync } from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport { dirname, join } from \"node:path\";\n\n// Resolve modules the way this package would at runtime.\nconst nodeRequire = createRequire(import.meta.url);\n\n/**\n * Absolute root directory of an installed (or workspace-linked) dependency.\n *\n * Rather than resolve the package *through* the module system — which would hit\n * its `exports` map (forcing a built `dist/`, and normally blocking access to\n * `package.json`) — we ask Node for the `node_modules` dirs it would search and\n * read `package.json` straight off disk. This needs nothing special from the\n * target package (no `exports` entry) and works even when it has not been built,\n * so dev iteration requires no compile step.\n */\nexport function packageRoot(packageName: string): string {\n const segments = packageName.split(\"/\");\n for (const base of nodeRequire.resolve.paths(packageName) ?? []) {\n const manifest = join(base, ...segments, \"package.json\");\n if (existsSync(manifest)) {\n return dirname(manifest);\n }\n }\n throw new Error(`could not locate the \"${packageName}\" package (is it installed?)`);\n}\n\n/**\n * Whether a package directory is an editable source checkout (it still carries\n * a `src/` folder) rather than an installed tarball (which ships only `dist/`).\n * Pass the package's own root, e.g. from {@link packageRoot}.\n */\nexport function runningFromSource(packageDir: string): boolean {\n return existsSync(join(packageDir, \"src\"));\n}\n\n/**\n * {@link runningFromSource} for a package resolved by name — the common case\n * (\"is `@habemus-papadum/aiui-dev-overlay` a source checkout here?\").\n */\nexport function packageFromSource(packageName: string): boolean {\n return runningFromSource(packageRoot(packageName));\n}\n","/**\n * Shared utilities for the aiui packages: cache directories, environment\n * detection (CI / SSH / headless), and the session-browser plumbing that\n * dev-server sidecars build their browser auto-open on.\n *\n * @packageDocumentation\n */\n\nexport * from \"./browser\";\nexport * from \"./environment\";\nexport * from \"./provenance\";\n\nimport { mkdirSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { isAbsolute, join } from \"node:path\";\n\n/** The application name — the leaf under the platform cache directory. */\nconst APP = \"aiui\";\n\nexport interface CacheDirOptions {\n /**\n * Create the directory (recursively) if it doesn't exist. Defaults to `true` —\n * callers generally want a ready-to-use directory back.\n */\n create?: boolean;\n}\n\n/**\n * Resolve the cache directory for aiui, creating it by default.\n *\n * Resolution order:\n * 1. `$AIUI_CACHE` — an explicit override; used verbatim as the aiui cache root.\n * 2. `$XDG_CACHE_HOME/aiui` — honoring the XDG Base Directory spec (the variable\n * is ignored unless it's an absolute path, per the spec).\n * 3. `~/.cache/aiui` — the XDG default.\n *\n * Pass `namespace` to carve out a subdirectory for a particular kind of cached\n * data, e.g. `cacheDir(\"claude\")` → `~/.cache/aiui/claude`. Different callers\n * cache different things, so each should pick its own namespace rather than\n * writing into the shared root.\n *\n * @example\n * const dir = cacheDir(\"screenshots\"); // created, ready to write into\n * const dir = cacheDir(\"claude\", { create: false }); // resolve the path only\n */\nexport function cacheDir(namespace?: string, options: CacheDirOptions = {}): string {\n const { create = true } = options;\n const dir = namespace ? join(cacheRoot(), namespace) : cacheRoot();\n if (create) {\n mkdirSync(dir, { recursive: true });\n }\n return dir;\n}\n\n/** Resolve the aiui cache root (no directory creation). */\nfunction cacheRoot(): string {\n const override = process.env.AIUI_CACHE?.trim();\n if (override) {\n return override;\n }\n\n const xdg = process.env.XDG_CACHE_HOME?.trim();\n const cacheHome = xdg && isAbsolute(xdg) ? xdg : join(homedir(), \".cache\");\n return join(cacheHome, APP);\n}\n"],"names":["isCi","env","ci","isSsh","isSet","headlessReason","platform","isHeadless","value","ACTIVE_PORT_FILE","LAUNCH_TIMEOUT_MS","CHROME_CHANNELS","discoverSessionBrowser","userDataDir","port","readActivePort","debugEndpointAlive","first","readFileSync","join","sessionBrowserBinary","settings","computeSystemExecutablePath","Browser","RELEASE_CHANNELS","ChromeReleaseChannel","launchSessionBrowser","opts","mkdirSync","rmSync","args","child","execa","exited","deadline","found","writeFileSync","sleep","openInSessionBrowser","browserUrl","url","base","res","decideBrowserAction","config","reason","ms","resolve","nodeRequire","createRequire","packageRoot","packageName","segments","manifest","existsSync","dirname","runningFromSource","packageDir","packageFromSource","APP","cacheDir","namespace","options","create","dir","cacheRoot","override","_a","xdg","_b","cacheHome","isAbsolute","homedir"],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Absolute root directory of an installed (or workspace-linked) dependency.
|
|
3
|
+
*
|
|
4
|
+
* Rather than resolve the package *through* the module system — which would hit
|
|
5
|
+
* its `exports` map (forcing a built `dist/`, and normally blocking access to
|
|
6
|
+
* `package.json`) — we ask Node for the `node_modules` dirs it would search and
|
|
7
|
+
* read `package.json` straight off disk. This needs nothing special from the
|
|
8
|
+
* target package (no `exports` entry) and works even when it has not been built,
|
|
9
|
+
* so dev iteration requires no compile step.
|
|
10
|
+
*/
|
|
11
|
+
export declare function packageRoot(packageName: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Whether a package directory is an editable source checkout (it still carries
|
|
14
|
+
* a `src/` folder) rather than an installed tarball (which ships only `dist/`).
|
|
15
|
+
* Pass the package's own root, e.g. from {@link packageRoot}.
|
|
16
|
+
*/
|
|
17
|
+
export declare function runningFromSource(packageDir: string): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* {@link runningFromSource} for a package resolved by name — the common case
|
|
20
|
+
* ("is `@habemus-papadum/aiui-dev-overlay` a source checkout here?").
|
|
21
|
+
*/
|
|
22
|
+
export declare function packageFromSource(packageName: string): boolean;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@habemus-papadum/aiui-util",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Shared utilities for aiui packages (cache dirs,
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Shared utilities for aiui packages (cache dirs, environment detection, session-browser plumbing).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"exports": {
|
|
14
14
|
".": {
|
|
15
15
|
"types": "./dist/index.d.ts",
|
|
16
|
-
"import": "./dist/index.js"
|
|
16
|
+
"import": "./dist/index.js",
|
|
17
|
+
"default": "./dist/index.js"
|
|
17
18
|
}
|
|
18
19
|
},
|
|
19
20
|
"main": "./dist/index.js",
|
|
@@ -25,6 +26,10 @@
|
|
|
25
26
|
"publishConfig": {
|
|
26
27
|
"access": "public"
|
|
27
28
|
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@puppeteer/browsers": "^2.10.5",
|
|
31
|
+
"execa": "^9.6.1"
|
|
32
|
+
},
|
|
28
33
|
"scripts": {
|
|
29
34
|
"build": "tsc --emitDeclarationOnly -p tsconfig.json && vite build",
|
|
30
35
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|