patchrome 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.
- package/LICENSE +21 -0
- package/README.md +514 -0
- package/bin/patchrome.js +10 -0
- package/dist/build-id.d.ts +2 -0
- package/dist/build-id.js +21 -0
- package/dist/challenges.d.ts +22 -0
- package/dist/challenges.js +97 -0
- package/dist/chrome-profiles.d.ts +17 -0
- package/dist/chrome-profiles.js +141 -0
- package/dist/cli-options.d.ts +131 -0
- package/dist/cli-options.js +43 -0
- package/dist/cli.d.ts +48 -0
- package/dist/cli.js +572 -0
- package/dist/client.d.ts +16 -0
- package/dist/client.js +210 -0
- package/dist/commands.d.ts +58 -0
- package/dist/commands.js +1076 -0
- package/dist/completions.d.ts +1 -0
- package/dist/completions.js +114 -0
- package/dist/copy-guard.d.ts +75 -0
- package/dist/copy-guard.js +167 -0
- package/dist/daemon.d.ts +7 -0
- package/dist/daemon.js +313 -0
- package/dist/diagnostics.d.ts +44 -0
- package/dist/diagnostics.js +117 -0
- package/dist/engine.d.ts +51 -0
- package/dist/engine.js +257 -0
- package/dist/events.d.ts +41 -0
- package/dist/events.js +106 -0
- package/dist/extract.d.ts +27 -0
- package/dist/extract.js +62 -0
- package/dist/focus.d.ts +1 -0
- package/dist/focus.js +44 -0
- package/dist/glob.d.ts +4 -0
- package/dist/glob.js +63 -0
- package/dist/har.d.ts +105 -0
- package/dist/har.js +88 -0
- package/dist/history.d.ts +35 -0
- package/dist/history.js +277 -0
- package/dist/host-platform.d.ts +5 -0
- package/dist/host-platform.js +19 -0
- package/dist/host-prompts-macos.d.ts +2 -0
- package/dist/host-prompts-macos.js +102 -0
- package/dist/host-prompts-wsl.d.ts +6 -0
- package/dist/host-prompts-wsl.js +64 -0
- package/dist/host-prompts.d.ts +3 -0
- package/dist/host-prompts.js +25 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +47 -0
- package/dist/network.d.ts +54 -0
- package/dist/network.js +204 -0
- package/dist/origin-storage.d.ts +31 -0
- package/dist/origin-storage.js +82 -0
- package/dist/paths.d.ts +17 -0
- package/dist/paths.js +52 -0
- package/dist/pipe.d.ts +9 -0
- package/dist/pipe.js +73 -0
- package/dist/profile-mode.d.ts +10 -0
- package/dist/profile-mode.js +42 -0
- package/dist/protocol-help.d.ts +34 -0
- package/dist/protocol-help.js +66 -0
- package/dist/protocol.d.ts +49 -0
- package/dist/protocol.js +89 -0
- package/dist/refs.d.ts +9 -0
- package/dist/refs.js +46 -0
- package/dist/routes.d.ts +20 -0
- package/dist/routes.js +106 -0
- package/dist/runner.d.ts +20 -0
- package/dist/runner.js +81 -0
- package/dist/session-name.d.ts +9 -0
- package/dist/session-name.js +50 -0
- package/dist/session-store.d.ts +5 -0
- package/dist/session-store.js +58 -0
- package/dist/sessions.d.ts +47 -0
- package/dist/sessions.js +171 -0
- package/dist/tab-groups.d.ts +9 -0
- package/dist/tab-groups.js +13 -0
- package/dist/targets.d.ts +43 -0
- package/dist/targets.js +229 -0
- package/dist/validate.d.ts +3 -0
- package/dist/validate.js +31 -0
- package/dist/wait.d.ts +24 -0
- package/dist/wait.js +88 -0
- package/examples/go/go.mod +3 -0
- package/examples/go/main.go +104 -0
- package/examples/hn-front-page.sh +18 -0
- package/examples/hn-front-page.ts +24 -0
- package/examples/hn_front_page.py +56 -0
- package/extension/tab-groups/manifest.json +8 -0
- package/extension/tab-groups/service-worker.js +41 -0
- package/package.json +60 -0
- package/skills/patchrome/SKILL.md +74 -0
- package/skills/patchrome/references/commands.md +130 -0
- package/skills/patchrome/references/debugging.md +20 -0
- package/skills/patchrome/references/hard-pages.md +49 -0
- package/skills/patchrome/references/logins.md +46 -0
- package/skills/patchrome/references/scraping.md +51 -0
- package/skills/patchrome/references/scripting.md +79 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { CommandError } from "./protocol.js";
|
|
2
|
+
export const challengeVendors = [
|
|
3
|
+
"turnstile",
|
|
4
|
+
"recaptcha",
|
|
5
|
+
"hcaptcha",
|
|
6
|
+
"datadome",
|
|
7
|
+
"mtcaptcha",
|
|
8
|
+
"cloudflare-interstitial",
|
|
9
|
+
"unknown",
|
|
10
|
+
];
|
|
11
|
+
// Widgets write their pass token into a form field of the host page; a filled field means the check passed.
|
|
12
|
+
const tokenFieldSelector = [
|
|
13
|
+
'[name="cf-turnstile-response"]',
|
|
14
|
+
'[name="g-recaptcha-response"]',
|
|
15
|
+
'[name="h-captcha-response"]',
|
|
16
|
+
'[name="mtcaptcha-verifiedtoken"]',
|
|
17
|
+
].join(", ");
|
|
18
|
+
const handoffPollMs = 500;
|
|
19
|
+
export function vendorOfFrameUrl(url) {
|
|
20
|
+
let parsed;
|
|
21
|
+
try {
|
|
22
|
+
parsed = new URL(url);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
const host = parsed.hostname;
|
|
28
|
+
if (host === "challenges.cloudflare.com")
|
|
29
|
+
return "turnstile";
|
|
30
|
+
if ((host === "www.google.com" || host === "www.recaptcha.net") && parsed.pathname.startsWith("/recaptcha/"))
|
|
31
|
+
return "recaptcha";
|
|
32
|
+
if (host === "hcaptcha.com" || host.endsWith(".hcaptcha.com"))
|
|
33
|
+
return "hcaptcha";
|
|
34
|
+
if (host.endsWith(".captcha-delivery.com"))
|
|
35
|
+
return "datadome";
|
|
36
|
+
if (host === "service.mtcaptcha.com")
|
|
37
|
+
return "mtcaptcha";
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
// Cloudflare's full-page check shows this title until it lets the visitor through.
|
|
41
|
+
export function isInterstitialTitle(title) {
|
|
42
|
+
return /^just a moment\.*$/i.test(title.trim());
|
|
43
|
+
}
|
|
44
|
+
export async function inspectChallenges(page) {
|
|
45
|
+
const widgets = [];
|
|
46
|
+
// The main frame is the site itself, even on a vendor's own demo page.
|
|
47
|
+
for (const frame of page.frames().filter((candidate) => candidate !== page.mainFrame())) {
|
|
48
|
+
const vendor = vendorOfFrameUrl(frame.url());
|
|
49
|
+
if (vendor === undefined)
|
|
50
|
+
continue;
|
|
51
|
+
widgets.push({ vendor, url: frame.url(), box: await frameBox(frame) });
|
|
52
|
+
}
|
|
53
|
+
const title = await page.title().catch(() => "");
|
|
54
|
+
if (isInterstitialTitle(title))
|
|
55
|
+
widgets.push({ vendor: "cloudflare-interstitial", url: page.url(), box: undefined });
|
|
56
|
+
const tokens = await tokenValues(page);
|
|
57
|
+
if (widgets.length === 0 && tokens.length > 0)
|
|
58
|
+
widgets.push({ vendor: "unknown", url: page.url(), box: undefined });
|
|
59
|
+
const isSolved = tokens.some((token) => token !== "");
|
|
60
|
+
const state = widgets.length === 0 ? "none" : isSolved ? "solved" : "pending";
|
|
61
|
+
return { state, widgets };
|
|
62
|
+
}
|
|
63
|
+
// Raises the tab for a person to solve, then returns once the widget passes, the page moves past it, or the
|
|
64
|
+
// person closes the tab.
|
|
65
|
+
export async function waitForPersonToSolve(page, timeoutMs) {
|
|
66
|
+
await page.bringToFront();
|
|
67
|
+
const deadlineMs = Date.now() + timeoutMs;
|
|
68
|
+
for (;;) {
|
|
69
|
+
if (page.isClosed())
|
|
70
|
+
throw new CommandError("tab_gone", "the tab closed before the challenge was solved", "run `patchrome open <url>`");
|
|
71
|
+
const report = await inspectChallenges(page).catch(() => undefined);
|
|
72
|
+
if (report !== undefined && report.state !== "pending")
|
|
73
|
+
return report;
|
|
74
|
+
if (Date.now() >= deadlineMs)
|
|
75
|
+
throw new CommandError("timeout", `the challenge was not solved within ${timeoutMs} ms`, "raise --timeout-ms, or check the tab with screenshot");
|
|
76
|
+
await new Promise((resolve) => setTimeout(resolve, handoffPollMs));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function frameBox(frame) {
|
|
80
|
+
const element = await frame.frameElement().catch(() => undefined);
|
|
81
|
+
const box = await element?.boundingBox().catch(() => null);
|
|
82
|
+
return box ?? undefined;
|
|
83
|
+
}
|
|
84
|
+
async function tokenValues(page) {
|
|
85
|
+
const values = [];
|
|
86
|
+
for (const frame of page.frames()) {
|
|
87
|
+
const fields = frame.locator(tokenFieldSelector);
|
|
88
|
+
const count = await fields.count().catch(() => 0);
|
|
89
|
+
for (let index = 0; index < count; index++) {
|
|
90
|
+
values.push(await fields
|
|
91
|
+
.nth(index)
|
|
92
|
+
.inputValue({ timeout: 1000 })
|
|
93
|
+
.catch(() => ""));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return values;
|
|
97
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ChromeProfile {
|
|
2
|
+
folder: string;
|
|
3
|
+
name: string;
|
|
4
|
+
email: string | undefined;
|
|
5
|
+
}
|
|
6
|
+
export declare function chromeUserDataDirFrom(env: NodeJS.ProcessEnv, platform?: NodeJS.Platform, home?: string): string;
|
|
7
|
+
export declare function listChromeProfiles(userDataDir: string): Promise<{
|
|
8
|
+
profiles: ChromeProfile[];
|
|
9
|
+
lastUsedFolder: string | undefined;
|
|
10
|
+
}>;
|
|
11
|
+
export declare function resolveChromeProfile(profiles: ChromeProfile[], from: string | undefined, lastUsedFolder: string | undefined): ChromeProfile;
|
|
12
|
+
export declare function describeChromeProfile(profile: ChromeProfile): string;
|
|
13
|
+
export declare function siteFromInput(input: string): string;
|
|
14
|
+
export declare function hostBelongsToSite(host: string, site: string): boolean;
|
|
15
|
+
export declare function copySiteLoginStorage(profileDir: string, site: string, copyUserDataDir: string): Promise<string[]>;
|
|
16
|
+
export declare function localStorageOrigins(leveldbDir: string): Promise<string[]>;
|
|
17
|
+
export declare function originOfIndexedDbFolder(folder: string): string | undefined;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { cp, mkdir, readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { basename, join } from "node:path";
|
|
4
|
+
import { CommandError } from "./protocol.js";
|
|
5
|
+
// Where the everyday Google Chrome keeps its profiles. PATCHROME_CHROME_USER_DATA_DIR points elsewhere,
|
|
6
|
+
// for Chrome Beta or a test profile.
|
|
7
|
+
export function chromeUserDataDirFrom(env, platform = process.platform, home = homedir()) {
|
|
8
|
+
if (env.PATCHROME_CHROME_USER_DATA_DIR !== undefined)
|
|
9
|
+
return env.PATCHROME_CHROME_USER_DATA_DIR;
|
|
10
|
+
if (platform === "darwin")
|
|
11
|
+
return join(home, "Library", "Application Support", "Google", "Chrome");
|
|
12
|
+
if (platform === "win32")
|
|
13
|
+
return join(env.LOCALAPPDATA ?? join(home, "AppData", "Local"), "Google", "Chrome", "User Data");
|
|
14
|
+
return join(home, ".config", "google-chrome");
|
|
15
|
+
}
|
|
16
|
+
export async function listChromeProfiles(userDataDir) {
|
|
17
|
+
const raw = await readFile(join(userDataDir, "Local State"), "utf8").catch(() => {
|
|
18
|
+
throw new CommandError("bad_args", `no Chrome profiles found in ${userDataDir}`, "set PATCHROME_CHROME_USER_DATA_DIR to the Chrome user data dir");
|
|
19
|
+
});
|
|
20
|
+
const profileState = (JSON.parse(raw).profile ?? {});
|
|
21
|
+
const profiles = Object.entries(profileState.info_cache ?? {}).map(([folder, info]) => ({
|
|
22
|
+
folder,
|
|
23
|
+
name: info.name ?? folder,
|
|
24
|
+
email: info.user_name === undefined || info.user_name === "" ? undefined : info.user_name,
|
|
25
|
+
}));
|
|
26
|
+
return { profiles, lastUsedFolder: profileState.last_used };
|
|
27
|
+
}
|
|
28
|
+
// Accepts what a person sees or knows: the menu name, the folder, or the signed-in email.
|
|
29
|
+
export function resolveChromeProfile(profiles, from, lastUsedFolder) {
|
|
30
|
+
const wanted = (from ?? lastUsedFolder ?? "Default").toLowerCase();
|
|
31
|
+
const matches = profiles.filter((profile) => [profile.folder, profile.name, profile.email].some((candidate) => candidate?.toLowerCase() === wanted));
|
|
32
|
+
const choices = profiles.map(describeChromeProfile).join(", ");
|
|
33
|
+
if (matches.length === 1 && matches[0] !== undefined)
|
|
34
|
+
return matches[0];
|
|
35
|
+
if (matches.length > 1)
|
|
36
|
+
throw new CommandError("bad_args", `${from} names more than one Chrome profile: ${choices}`, "pass the folder name with --from");
|
|
37
|
+
throw new CommandError("bad_args", `no Chrome profile named ${from ?? wanted}`, `pass --from with one of: ${choices}`);
|
|
38
|
+
}
|
|
39
|
+
export function describeChromeProfile(profile) {
|
|
40
|
+
return `"${profile.name}" (${profile.folder}${profile.email === undefined ? "" : `, ${profile.email}`})`;
|
|
41
|
+
}
|
|
42
|
+
// A site is a host name; a pasted URL keeps only its host.
|
|
43
|
+
export function siteFromInput(input) {
|
|
44
|
+
const trimmed = input.trim().toLowerCase();
|
|
45
|
+
const host = /^[a-z][a-z0-9+.-]*:\/\//.test(trimmed)
|
|
46
|
+
? new URL(trimmed).hostname
|
|
47
|
+
: (trimmed.replace(/^\./, "").split("/")[0] ?? "");
|
|
48
|
+
if (!/^[a-z0-9][a-z0-9.-]*$/.test(host))
|
|
49
|
+
throw new CommandError("bad_args", `${input} is not a site`, "pass a host such as github.com");
|
|
50
|
+
return host;
|
|
51
|
+
}
|
|
52
|
+
export function hostBelongsToSite(host, site) {
|
|
53
|
+
const bare = host.replace(/^\./, "").toLowerCase();
|
|
54
|
+
return bare === site || bare.endsWith(`.${site}`);
|
|
55
|
+
}
|
|
56
|
+
// Copies only what a login lives in: the cookie jar, localStorage, and the site's own IndexedDB. The copy is
|
|
57
|
+
// what Chrome opens, so the everyday profile is never locked or written. Returns the site's origins that hold
|
|
58
|
+
// localStorage or IndexedDB.
|
|
59
|
+
export async function copySiteLoginStorage(profileDir, site, copyUserDataDir) {
|
|
60
|
+
const source = (name) => join(profileDir, name);
|
|
61
|
+
const target = join(copyUserDataDir, "Default");
|
|
62
|
+
await mkdir(target, { recursive: true });
|
|
63
|
+
// Chrome holds LOCK while it runs; the copy gets its own.
|
|
64
|
+
const skipLock = (path) => basename(path) !== "LOCK";
|
|
65
|
+
const origins = new Set();
|
|
66
|
+
for (const name of ["Cookies", "Cookies-journal"])
|
|
67
|
+
await cp(source(name), join(target, name)).catch(ignoreMissing);
|
|
68
|
+
await cp(source("Local Storage"), join(target, "Local Storage"), { recursive: true, filter: skipLock }).catch(ignoreMissing);
|
|
69
|
+
for (const origin of await localStorageOrigins(join(target, "Local Storage", "leveldb"))) {
|
|
70
|
+
if (hostBelongsToSite(new URL(origin).hostname, site))
|
|
71
|
+
origins.add(origin);
|
|
72
|
+
}
|
|
73
|
+
for (const folder of await readdir(source("IndexedDB")).catch(() => [])) {
|
|
74
|
+
const origin = originOfIndexedDbFolder(folder);
|
|
75
|
+
if (origin === undefined || !hostBelongsToSite(new URL(origin).hostname, site))
|
|
76
|
+
continue;
|
|
77
|
+
origins.add(origin);
|
|
78
|
+
await cp(join(source("IndexedDB"), folder), join(target, "IndexedDB", folder), {
|
|
79
|
+
recursive: true,
|
|
80
|
+
filter: skipLock,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
// Newer Chrome files IndexedDB under numbered storage buckets, listed in the QuotaManager database.
|
|
84
|
+
await mkdir(join(target, "WebStorage"), { recursive: true });
|
|
85
|
+
const hasQuotaManager = await cp(source("WebStorage/QuotaManager"), join(target, "WebStorage", "QuotaManager")).then(() => true, () => false);
|
|
86
|
+
if (hasQuotaManager) {
|
|
87
|
+
await cp(source("WebStorage/QuotaManager-journal"), join(target, "WebStorage", "QuotaManager-journal")).catch(ignoreMissing);
|
|
88
|
+
for (const { bucketId, origin } of await bucketOrigins(join(target, "WebStorage", "QuotaManager"))) {
|
|
89
|
+
if (!hostBelongsToSite(new URL(origin).hostname, site))
|
|
90
|
+
continue;
|
|
91
|
+
origins.add(origin);
|
|
92
|
+
await cp(source(`WebStorage/${bucketId}`), join(target, "WebStorage", String(bucketId)), {
|
|
93
|
+
recursive: true,
|
|
94
|
+
filter: skipLock,
|
|
95
|
+
}).catch(ignoreMissing);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return [...origins].toSorted();
|
|
99
|
+
}
|
|
100
|
+
function ignoreMissing(err) {
|
|
101
|
+
if (err.code !== "ENOENT")
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
// LevelDB compresses table files, but Chrome writes a META:<origin> key per origin whose text stays readable.
|
|
105
|
+
export async function localStorageOrigins(leveldbDir) {
|
|
106
|
+
const origins = new Set();
|
|
107
|
+
for (const file of await readdir(leveldbDir).catch(() => [])) {
|
|
108
|
+
if (!/\.(log|ldb)$/.test(file))
|
|
109
|
+
continue;
|
|
110
|
+
const text = (await readFile(join(leveldbDir, file))).toString("latin1");
|
|
111
|
+
for (const match of text.matchAll(/META:(https?:\/\/[a-z0-9.-]+(?::\d+)?)/g)) {
|
|
112
|
+
if (match[1] !== undefined)
|
|
113
|
+
origins.add(match[1]);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return [...origins];
|
|
117
|
+
}
|
|
118
|
+
// Folders read like https_app.example.com_0.indexeddb.leveldb, where port 0 is the scheme's default.
|
|
119
|
+
export function originOfIndexedDbFolder(folder) {
|
|
120
|
+
const match = folder.match(/^(https?)_([a-z0-9.-]+)_(\d+)\.indexeddb\.leveldb$/);
|
|
121
|
+
if (!match)
|
|
122
|
+
return undefined;
|
|
123
|
+
const [, scheme, host, port] = match;
|
|
124
|
+
return port === "0" ? `${scheme}://${host}` : `${scheme}://${host}:${port}`;
|
|
125
|
+
}
|
|
126
|
+
// Partitioned keys (a site embedded in another) carry ^ markers and are left out: they are not the site's login.
|
|
127
|
+
// node:sqlite loads here, not at the top, because it prints an experimental warning into every CLI run.
|
|
128
|
+
async function bucketOrigins(quotaManagerPath) {
|
|
129
|
+
const { DatabaseSync } = await import("node:sqlite");
|
|
130
|
+
const db = new DatabaseSync(quotaManagerPath, { readOnly: true });
|
|
131
|
+
try {
|
|
132
|
+
const rows = db.prepare("select id, storage_key from buckets").all();
|
|
133
|
+
return rows.flatMap((row) => {
|
|
134
|
+
const match = row.storage_key.match(/^(https?:\/\/[a-z0-9.-]+(?::\d+)?)\/$/);
|
|
135
|
+
return match?.[1] === undefined ? [] : [{ bucketId: row.id, origin: match[1] }];
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
db.close();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
export declare const cliOptions: {
|
|
2
|
+
readonly profile: {
|
|
3
|
+
readonly type: "string";
|
|
4
|
+
};
|
|
5
|
+
readonly session: {
|
|
6
|
+
readonly type: "string";
|
|
7
|
+
};
|
|
8
|
+
readonly json: {
|
|
9
|
+
readonly type: "boolean";
|
|
10
|
+
readonly default: false;
|
|
11
|
+
};
|
|
12
|
+
readonly "timeout-ms": {
|
|
13
|
+
readonly type: "string";
|
|
14
|
+
};
|
|
15
|
+
readonly wait: {
|
|
16
|
+
readonly type: "string";
|
|
17
|
+
};
|
|
18
|
+
readonly all: {
|
|
19
|
+
readonly type: "boolean";
|
|
20
|
+
readonly default: false;
|
|
21
|
+
};
|
|
22
|
+
readonly inline: {
|
|
23
|
+
readonly type: "boolean";
|
|
24
|
+
readonly default: false;
|
|
25
|
+
};
|
|
26
|
+
readonly full: {
|
|
27
|
+
readonly type: "boolean";
|
|
28
|
+
readonly default: false;
|
|
29
|
+
};
|
|
30
|
+
readonly ref: {
|
|
31
|
+
readonly type: "string";
|
|
32
|
+
};
|
|
33
|
+
readonly "main-world": {
|
|
34
|
+
readonly type: "boolean";
|
|
35
|
+
readonly default: false;
|
|
36
|
+
};
|
|
37
|
+
readonly url: {
|
|
38
|
+
readonly type: "string";
|
|
39
|
+
};
|
|
40
|
+
readonly type: {
|
|
41
|
+
readonly type: "string";
|
|
42
|
+
};
|
|
43
|
+
readonly status: {
|
|
44
|
+
readonly type: "string";
|
|
45
|
+
};
|
|
46
|
+
readonly body: {
|
|
47
|
+
readonly type: "boolean";
|
|
48
|
+
readonly default: false;
|
|
49
|
+
};
|
|
50
|
+
readonly until: {
|
|
51
|
+
readonly type: "string";
|
|
52
|
+
};
|
|
53
|
+
readonly domain: {
|
|
54
|
+
readonly type: "string";
|
|
55
|
+
};
|
|
56
|
+
readonly level: {
|
|
57
|
+
readonly type: "string";
|
|
58
|
+
};
|
|
59
|
+
readonly follow: {
|
|
60
|
+
readonly type: "boolean";
|
|
61
|
+
readonly default: false;
|
|
62
|
+
};
|
|
63
|
+
readonly mode: {
|
|
64
|
+
readonly type: "string";
|
|
65
|
+
};
|
|
66
|
+
readonly isolated: {
|
|
67
|
+
readonly type: "boolean";
|
|
68
|
+
readonly default: false;
|
|
69
|
+
};
|
|
70
|
+
readonly from: {
|
|
71
|
+
readonly type: "string";
|
|
72
|
+
};
|
|
73
|
+
readonly selector: {
|
|
74
|
+
readonly type: "string";
|
|
75
|
+
};
|
|
76
|
+
readonly text: {
|
|
77
|
+
readonly type: "string";
|
|
78
|
+
};
|
|
79
|
+
readonly title: {
|
|
80
|
+
readonly type: "string";
|
|
81
|
+
};
|
|
82
|
+
readonly load: {
|
|
83
|
+
readonly type: "string";
|
|
84
|
+
};
|
|
85
|
+
readonly gone: {
|
|
86
|
+
readonly type: "boolean";
|
|
87
|
+
readonly default: false;
|
|
88
|
+
};
|
|
89
|
+
readonly events: {
|
|
90
|
+
readonly type: "string";
|
|
91
|
+
};
|
|
92
|
+
readonly count: {
|
|
93
|
+
readonly type: "string";
|
|
94
|
+
};
|
|
95
|
+
readonly frame: {
|
|
96
|
+
readonly type: "string";
|
|
97
|
+
};
|
|
98
|
+
readonly at: {
|
|
99
|
+
readonly type: "string";
|
|
100
|
+
};
|
|
101
|
+
readonly handoff: {
|
|
102
|
+
readonly type: "boolean";
|
|
103
|
+
readonly default: false;
|
|
104
|
+
};
|
|
105
|
+
readonly role: {
|
|
106
|
+
readonly type: "string";
|
|
107
|
+
};
|
|
108
|
+
readonly name: {
|
|
109
|
+
readonly type: "string";
|
|
110
|
+
};
|
|
111
|
+
readonly exact: {
|
|
112
|
+
readonly type: "boolean";
|
|
113
|
+
readonly default: false;
|
|
114
|
+
};
|
|
115
|
+
readonly nth: {
|
|
116
|
+
readonly type: "string";
|
|
117
|
+
};
|
|
118
|
+
readonly label: {
|
|
119
|
+
readonly type: "string";
|
|
120
|
+
};
|
|
121
|
+
readonly out: {
|
|
122
|
+
readonly type: "string";
|
|
123
|
+
};
|
|
124
|
+
readonly format: {
|
|
125
|
+
readonly type: "string";
|
|
126
|
+
};
|
|
127
|
+
readonly bail: {
|
|
128
|
+
readonly type: "boolean";
|
|
129
|
+
readonly default: false;
|
|
130
|
+
};
|
|
131
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Every flag the CLI knows. History reads recorded words with the same table, so `--ref e5` parses the
|
|
2
|
+
// way the CLI parsed it.
|
|
3
|
+
export const cliOptions = {
|
|
4
|
+
profile: { type: "string" },
|
|
5
|
+
session: { type: "string" },
|
|
6
|
+
json: { type: "boolean", default: false },
|
|
7
|
+
"timeout-ms": { type: "string" },
|
|
8
|
+
wait: { type: "string" },
|
|
9
|
+
all: { type: "boolean", default: false },
|
|
10
|
+
inline: { type: "boolean", default: false },
|
|
11
|
+
full: { type: "boolean", default: false },
|
|
12
|
+
ref: { type: "string" },
|
|
13
|
+
"main-world": { type: "boolean", default: false },
|
|
14
|
+
url: { type: "string" },
|
|
15
|
+
type: { type: "string" },
|
|
16
|
+
status: { type: "string" },
|
|
17
|
+
body: { type: "boolean", default: false },
|
|
18
|
+
until: { type: "string" },
|
|
19
|
+
domain: { type: "string" },
|
|
20
|
+
level: { type: "string" },
|
|
21
|
+
follow: { type: "boolean", default: false },
|
|
22
|
+
mode: { type: "string" },
|
|
23
|
+
isolated: { type: "boolean", default: false },
|
|
24
|
+
from: { type: "string" },
|
|
25
|
+
selector: { type: "string" },
|
|
26
|
+
text: { type: "string" },
|
|
27
|
+
title: { type: "string" },
|
|
28
|
+
load: { type: "string" },
|
|
29
|
+
gone: { type: "boolean", default: false },
|
|
30
|
+
events: { type: "string" },
|
|
31
|
+
count: { type: "string" },
|
|
32
|
+
frame: { type: "string" },
|
|
33
|
+
at: { type: "string" },
|
|
34
|
+
handoff: { type: "boolean", default: false },
|
|
35
|
+
role: { type: "string" },
|
|
36
|
+
name: { type: "string" },
|
|
37
|
+
exact: { type: "boolean", default: false },
|
|
38
|
+
nth: { type: "string" },
|
|
39
|
+
label: { type: "string" },
|
|
40
|
+
out: { type: "string" },
|
|
41
|
+
format: { type: "string" },
|
|
42
|
+
bail: { type: "boolean", default: false },
|
|
43
|
+
};
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type CommandArgs, type CommandData, type CommandName } from "./protocol.ts";
|
|
2
|
+
import { type HistoryFormat } from "./history.ts";
|
|
3
|
+
interface ParsedCli {
|
|
4
|
+
profile: string;
|
|
5
|
+
session: string;
|
|
6
|
+
isJson: boolean;
|
|
7
|
+
timeoutMs: number;
|
|
8
|
+
command: CommandName;
|
|
9
|
+
args: CommandArgs;
|
|
10
|
+
shouldStartDaemon: boolean;
|
|
11
|
+
}
|
|
12
|
+
export type LocalAction = {
|
|
13
|
+
kind: "logs";
|
|
14
|
+
profile: string;
|
|
15
|
+
isJson: boolean;
|
|
16
|
+
} | {
|
|
17
|
+
kind: "audit";
|
|
18
|
+
count: number;
|
|
19
|
+
isJson: boolean;
|
|
20
|
+
} | {
|
|
21
|
+
kind: "create-profile";
|
|
22
|
+
profile: string;
|
|
23
|
+
mode: string;
|
|
24
|
+
isJson: boolean;
|
|
25
|
+
} | {
|
|
26
|
+
kind: "completions";
|
|
27
|
+
shell: "zsh";
|
|
28
|
+
} | {
|
|
29
|
+
kind: "history";
|
|
30
|
+
profile: string;
|
|
31
|
+
session: string;
|
|
32
|
+
format: HistoryFormat;
|
|
33
|
+
out: string | undefined;
|
|
34
|
+
isClear: boolean;
|
|
35
|
+
isJson: boolean;
|
|
36
|
+
} | {
|
|
37
|
+
kind: "pipe";
|
|
38
|
+
profile: string;
|
|
39
|
+
session: string;
|
|
40
|
+
isBail: boolean;
|
|
41
|
+
};
|
|
42
|
+
export declare function parseCli(argv: string[], env: NodeJS.ProcessEnv, sessionFallback: () => string): ParsedCli | LocalAction;
|
|
43
|
+
export declare function localActionData(action: Exclude<LocalAction, {
|
|
44
|
+
kind: "completions" | "pipe";
|
|
45
|
+
}>): Promise<CommandData>;
|
|
46
|
+
declare function main(argv: string[]): Promise<number>;
|
|
47
|
+
export declare const runCli: typeof main;
|
|
48
|
+
export {};
|