clearcote 0.39.0 → 0.40.1
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/README.md +2 -0
- package/dist/cli-commands.d.ts +66 -1
- package/dist/cli-commands.d.ts.map +1 -1
- package/dist/cli-commands.js +298 -39
- package/dist/cli-commands.js.map +1 -1
- package/dist/devicelogin.d.ts +77 -0
- package/dist/devicelogin.d.ts.map +1 -0
- package/dist/devicelogin.js +215 -0
- package/dist/devicelogin.js.map +1 -0
- package/dist/docker.d.ts +153 -0
- package/dist/docker.d.ts.map +1 -0
- package/dist/docker.js +821 -0
- package/dist/docker.js.map +1 -0
- package/dist/download.d.ts +3 -2
- package/dist/download.d.ts.map +1 -1
- package/dist/download.js +3 -3
- package/dist/download.js.map +1 -1
- package/dist/index.d.ts +21 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +47 -55
- package/dist/index.js.map +1 -1
- package/dist/license.d.ts +24 -1
- package/dist/license.d.ts.map +1 -1
- package/dist/license.js +71 -1
- package/dist/license.js.map +1 -1
- package/dist/net.d.ts +5 -0
- package/dist/net.d.ts.map +1 -1
- package/dist/net.js +9 -4
- package/dist/net.js.map +1 -1
- package/dist/winlaunch.d.ts +54 -0
- package/dist/winlaunch.d.ts.map +1 -0
- package/dist/winlaunch.js +333 -0
- package/dist/winlaunch.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -140,6 +140,8 @@ npx clearcote cloud run|sessions|stop|events|recording|profile sync|webhooks ...
|
|
|
140
140
|
|
|
141
141
|
`info` never downloads: it reports the SDK, the cached builds, which engine features the binary supports, a launch test (skipped with `--quick`), licence seats, fonts and missing system libraries.
|
|
142
142
|
|
|
143
|
+
Windows: when a cached build cannot start from the cache (`spawn UNKNOWN`, "the side-by-side configuration is incorrect"; this happens when the SDK runs inside an MSIX-packaged app, whose writes to `%LOCALAPPDATA%` Windows redirects), launches use one copy of that build in `~/.clearcote/recovered/` instead, made once and reused by every later launch from Python or Node. `clear-cache` removes those copies too.
|
|
144
|
+
|
|
143
145
|
### Engine options (PRO 152 r22+)
|
|
144
146
|
|
|
145
147
|
| Option | Effect |
|
package/dist/cli-commands.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type SessionSeats } from "./index.js";
|
|
2
|
+
import { type LicenseExpiry } from "./license.js";
|
|
2
3
|
import { type Json } from "./cloud.js";
|
|
3
4
|
export declare const USAGE: string;
|
|
4
5
|
/** `clearcote cloud --help`. Kept byte-identical to CLOUD_USAGE in the Python SDK's _commands.py. */
|
|
@@ -13,6 +14,7 @@ export interface InfoReport {
|
|
|
13
14
|
license: {
|
|
14
15
|
source: string;
|
|
15
16
|
key?: string;
|
|
17
|
+
expiry?: LicenseExpiry;
|
|
16
18
|
seats?: SessionSeats;
|
|
17
19
|
};
|
|
18
20
|
binary: {
|
|
@@ -25,12 +27,15 @@ export interface InfoReport {
|
|
|
25
27
|
}>;
|
|
26
28
|
pinnedFree: string;
|
|
27
29
|
releaseChannel: string;
|
|
30
|
+
/** Why `path` is the build launch() would run for the current licence state (or why there is none). */
|
|
31
|
+
selectedBy?: string;
|
|
28
32
|
};
|
|
29
33
|
engineFeatures?: Record<string, boolean>;
|
|
30
34
|
launch?: {
|
|
31
35
|
tested: boolean;
|
|
32
36
|
ok?: boolean;
|
|
33
37
|
version?: string;
|
|
38
|
+
build?: string;
|
|
34
39
|
error?: string;
|
|
35
40
|
missingLibs?: string[];
|
|
36
41
|
reason?: string;
|
|
@@ -51,10 +56,70 @@ export interface InfoReport {
|
|
|
51
56
|
};
|
|
52
57
|
};
|
|
53
58
|
}
|
|
59
|
+
/** Whether a cached licensed build's tag (pro-<version>-r<N>) is what `version` selects: a revision ("r30",
|
|
60
|
+
* "154.0.8037.57-r30"), an exact version (its newest revision), a major ("154"), or "latest". */
|
|
61
|
+
export declare function proTagMatches(tag: string, version: string): boolean;
|
|
62
|
+
/**
|
|
63
|
+
* The build launch() would run for the current licence state, from what is already cached (`info` never
|
|
64
|
+
* downloads). CLEARCOTE_BINARY wins. A version selector (CLEARCOTE_BROWSER_VERSION) is resolved the way
|
|
65
|
+
* launch() resolves it -- through the version catalog (the bundled copy when `offline`), a licensed revision
|
|
66
|
+
* straight to the licensed builds -- and picks the cached build it names. Otherwise, with a licence key,
|
|
67
|
+
* launch() runs a licensed build: the newest one cached (launch() itself asks the licence server, which may
|
|
68
|
+
* name a newer one). Without a key it runs the open build this SDK pins (the newest cached open build with
|
|
69
|
+
* CLEARCOTE_AUTO_UPDATE). A cached licensed build is never picked for a keyless setup: it cannot run without
|
|
70
|
+
* a key, which is what made the old "newest cached build" launch test fail for open-build users.
|
|
71
|
+
*/
|
|
72
|
+
export declare function launchBuild(cached: Array<{
|
|
73
|
+
tag: string;
|
|
74
|
+
path: string;
|
|
75
|
+
}>, envBinary?: string, licenseKey?: string, autoUpdate?: boolean, version?: string, offline?: boolean): Promise<{
|
|
76
|
+
pick?: {
|
|
77
|
+
path: string;
|
|
78
|
+
tag?: string;
|
|
79
|
+
};
|
|
80
|
+
selectedBy: string;
|
|
81
|
+
}>;
|
|
54
82
|
export declare function buildInfo(flags: {
|
|
55
83
|
quick?: boolean;
|
|
56
84
|
proxy?: string;
|
|
57
|
-
}
|
|
85
|
+
}, launchFn?: (o: Record<string, unknown>) => Promise<{
|
|
86
|
+
version(): string;
|
|
87
|
+
close(): Promise<void>;
|
|
88
|
+
}>): Promise<InfoReport>;
|
|
89
|
+
/**
|
|
90
|
+
* `clearcote login --device`: show a link and a short code, wait for the approval in the browser, then
|
|
91
|
+
* save the key it issues exactly as `clearcote login <key>` does. The key is never printed.
|
|
92
|
+
* Instructions go to stderr and the result to stdout, like the paste login's prompt. Output and exit
|
|
93
|
+
* codes match the Python CLI (test/device-login.test.ts).
|
|
94
|
+
*
|
|
95
|
+
* Ctrl-C is deterministic: it never tears down a request already on its way (its answer may be the key,
|
|
96
|
+
* which the server hands out once). Ctrl-C before the key arrives saves nothing (exit 130); one that lands
|
|
97
|
+
* while the request that brings the key is in flight still saves it, and says so.
|
|
98
|
+
*/
|
|
99
|
+
export declare function deviceLogin(opts?: {
|
|
100
|
+
sleep?: (s: number, signal?: AbortSignal) => Promise<void>;
|
|
101
|
+
clock?: () => number;
|
|
102
|
+
}): Promise<void>;
|
|
103
|
+
/** The parts of a TTY input stream readHidden uses. */
|
|
104
|
+
export interface HiddenInput {
|
|
105
|
+
isTTY?: boolean;
|
|
106
|
+
setRawMode?(mode: boolean): unknown;
|
|
107
|
+
setEncoding(enc: BufferEncoding): unknown;
|
|
108
|
+
on(ev: "data", fn: (chunk: string) => void): unknown;
|
|
109
|
+
removeListener(ev: "data", fn: (chunk: string) => void): unknown;
|
|
110
|
+
resume(): unknown;
|
|
111
|
+
pause(): unknown;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Read one line from a terminal without echoing it: raw mode, so the terminal shows nothing of what is typed
|
|
115
|
+
* or pasted (a screen share, a recording or the scrollback must not show a key). Enter ends it, Backspace
|
|
116
|
+
* edits, Ctrl-C rejects with "cancelled". Terminal escape sequences (bracketed paste) are dropped.
|
|
117
|
+
*/
|
|
118
|
+
export declare function readHidden(input: HiddenInput, output: {
|
|
119
|
+
write(s: string): unknown;
|
|
120
|
+
}, prompt: string): Promise<string>;
|
|
121
|
+
/** `clearcote <cmd> --help`: that command's lines from USAGE (with their flag sections) and a note. */
|
|
122
|
+
export declare function commandUsage(cmd: string): string;
|
|
58
123
|
export declare function main(argv?: string[]): Promise<void>;
|
|
59
124
|
/** The human summary `clearcote cloud run` prints. */
|
|
60
125
|
export declare function formatRun(run: Json): string[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli-commands.d.ts","sourceRoot":"","sources":["../src/cli-commands.ts"],"names":[],"mappings":"AAkBA,OAAO,EAcL,KAAK,YAAY,EAClB,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"cli-commands.d.ts","sourceRoot":"","sources":["../src/cli-commands.ts"],"names":[],"mappings":"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"}
|
package/dist/cli-commands.js
CHANGED
|
@@ -4,22 +4,26 @@
|
|
|
4
4
|
// clearcote info [--quick] [--json] [--proxy URL] diagnostics (alias: doctor)
|
|
5
5
|
// clearcote update [--channel preview] fetch a newer build if one exists
|
|
6
6
|
// clearcote clear-cache delete every cached binary
|
|
7
|
-
// clearcote login [key]
|
|
7
|
+
// clearcote login [key] | --device save a licence key (validated first), or sign in
|
|
8
|
+
// from a browser and save the key it issues
|
|
8
9
|
// clearcote logout remove the saved key
|
|
9
10
|
// clearcote serve [--port 9222] [--idle-timeout 300] … multi-identity CDP endpoint
|
|
10
11
|
// clearcote cloud run|sessions|stop|events|recording|profile sync|webhooks … the hosted API
|
|
11
12
|
//
|
|
12
13
|
// `info` never downloads: it reports what is already cached and what a launch would resolve to.
|
|
13
14
|
import { parseArgs } from "node:util";
|
|
14
|
-
import * as readline from "node:readline";
|
|
15
15
|
import { existsSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
16
16
|
import { spawnSync } from "node:child_process";
|
|
17
17
|
import { dirname, join } from "node:path";
|
|
18
18
|
import { download, launch, getSessionSeats, saveLicenseKey, removeLicenseKey, licenseKeySource, licenseKeyPath, resolveLicenseKey, resolveGeoDetailed, resolveReleaseChannel, engineSupportsSwitch, serveMultiplex, RELEASE, } from "./index.js";
|
|
19
|
-
import { defaultCacheRoot, listCachedBuilds } from "./download.js";
|
|
19
|
+
import { autoUpdateRequested, defaultCacheRoot, isProRevisionSelector, listCachedBuilds, resolveVersion } from "./download.js";
|
|
20
|
+
import { CATALOG_FALLBACK } from "./release.js";
|
|
21
|
+
import { DeviceLoginError, pollForKey, requestDeviceCode } from "./devicelogin.js";
|
|
22
|
+
import { licenseExpiry, removeLicenseMeta, saveLicenseMeta } from "./license.js";
|
|
20
23
|
import { geoCacheRoot } from "./geoip.js";
|
|
21
24
|
import { GATED_ENGINE_SWITCHES } from "./launchopts.js";
|
|
22
25
|
import { toProxySpec } from "./net.js";
|
|
26
|
+
import { clearRecovered, recoverRoot } from "./winlaunch.js";
|
|
23
27
|
import { Cloud, CloudError, announceHandoff } from "./cloud.js";
|
|
24
28
|
const SDK_VERSION = (() => {
|
|
25
29
|
try {
|
|
@@ -37,6 +41,7 @@ USAGE
|
|
|
37
41
|
clearcote update [--channel stable|preview]
|
|
38
42
|
clearcote clear-cache
|
|
39
43
|
clearcote login [key]
|
|
44
|
+
clearcote login --device (sign in from a browser; no key to paste)
|
|
40
45
|
clearcote logout
|
|
41
46
|
clearcote serve [--port 9222] [--host 127.0.0.1] [--idle-timeout <s>] [--data-dir <dir>]
|
|
42
47
|
[--max-browsers 16] [--allow-origin <origin>]... [--allow-host <name>]... [--headed]
|
|
@@ -97,7 +102,72 @@ function missingSharedLibs(exe) {
|
|
|
97
102
|
const r = spawnSync("ldd", ["--", exe], { encoding: "utf8" });
|
|
98
103
|
return (r.stdout || "").split("\n").filter((l) => l.includes("not found")).map((l) => l.trim().split(" ")[0]);
|
|
99
104
|
}
|
|
100
|
-
|
|
105
|
+
/** Whether a cached licensed build's tag (pro-<version>-r<N>) is what `version` selects: a revision ("r30",
|
|
106
|
+
* "154.0.8037.57-r30"), an exact version (its newest revision), a major ("154"), or "latest". */
|
|
107
|
+
export function proTagMatches(tag, version) {
|
|
108
|
+
if (!tag.startsWith("pro-"))
|
|
109
|
+
return false;
|
|
110
|
+
const t = tag.slice(4).toLowerCase();
|
|
111
|
+
const v = String(version).trim().toLowerCase();
|
|
112
|
+
if (v === "latest" || v === "newest")
|
|
113
|
+
return true;
|
|
114
|
+
if (/^r\d+$/.test(v))
|
|
115
|
+
return t.endsWith(`-${v}`);
|
|
116
|
+
if (/^\d+$/.test(v))
|
|
117
|
+
return t.startsWith(`${v}.`);
|
|
118
|
+
return t === v || t.startsWith(`${v}-`);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The build launch() would run for the current licence state, from what is already cached (`info` never
|
|
122
|
+
* downloads). CLEARCOTE_BINARY wins. A version selector (CLEARCOTE_BROWSER_VERSION) is resolved the way
|
|
123
|
+
* launch() resolves it -- through the version catalog (the bundled copy when `offline`), a licensed revision
|
|
124
|
+
* straight to the licensed builds -- and picks the cached build it names. Otherwise, with a licence key,
|
|
125
|
+
* launch() runs a licensed build: the newest one cached (launch() itself asks the licence server, which may
|
|
126
|
+
* name a newer one). Without a key it runs the open build this SDK pins (the newest cached open build with
|
|
127
|
+
* CLEARCOTE_AUTO_UPDATE). A cached licensed build is never picked for a keyless setup: it cannot run without
|
|
128
|
+
* a key, which is what made the old "newest cached build" launch test fail for open-build users.
|
|
129
|
+
*/
|
|
130
|
+
export async function launchBuild(cached, envBinary, licenseKey, autoUpdate = false, version, offline = false) {
|
|
131
|
+
if (envBinary)
|
|
132
|
+
return { pick: { path: envBinary }, selectedBy: "CLEARCOTE_BINARY" };
|
|
133
|
+
const sel = String(version ?? "").trim();
|
|
134
|
+
if (sel) {
|
|
135
|
+
const named = `CLEARCOTE_BROWSER_VERSION=${sel}`;
|
|
136
|
+
let plan;
|
|
137
|
+
try {
|
|
138
|
+
plan = licenseKey && isProRevisionSelector(sel)
|
|
139
|
+
? { kind: "pro", version: sel }
|
|
140
|
+
: await resolveVersion(sel, !!licenseKey, true, offline ? CATALOG_FALLBACK : undefined);
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
return { selectedBy: `${named}: ${e.message.split("\n")[0]}` };
|
|
144
|
+
}
|
|
145
|
+
if (plan.kind === "pro") {
|
|
146
|
+
const m = cached.find((c) => proTagMatches(c.tag, plan.version));
|
|
147
|
+
return m ? { pick: m, selectedBy: `licensed: the newest cached build ${named} selects` }
|
|
148
|
+
: { selectedBy: `no licensed build for ${named} is cached — run: clearcote install --version ${sel}` };
|
|
149
|
+
}
|
|
150
|
+
const hit = cached.find((c) => c.tag === plan.rel.tag);
|
|
151
|
+
return hit ? { pick: hit, selectedBy: `open build ${plan.rel.tag} (${named})` }
|
|
152
|
+
: { selectedBy: `the open build ${plan.rel.tag} (${named}) is not installed — run: clearcote install --version ${sel}` };
|
|
153
|
+
}
|
|
154
|
+
if (licenseKey) {
|
|
155
|
+
const licensed = cached.filter((c) => c.tag.startsWith("pro-"));
|
|
156
|
+
return licensed.length
|
|
157
|
+
? { pick: licensed[0], selectedBy: "licensed: the newest cached licensed build (launch() asks the licence server, which may name a newer one)" }
|
|
158
|
+
: { selectedBy: "no licensed build is cached — run: clearcote install" };
|
|
159
|
+
}
|
|
160
|
+
if (autoUpdate) {
|
|
161
|
+
const open = cached.filter((c) => !c.tag.startsWith("pro-"));
|
|
162
|
+
if (open.length)
|
|
163
|
+
return { pick: open[0], selectedBy: "open build: the newest cached (CLEARCOTE_AUTO_UPDATE)" };
|
|
164
|
+
}
|
|
165
|
+
const pinned = cached.find((c) => c.tag === RELEASE.tag);
|
|
166
|
+
return pinned
|
|
167
|
+
? { pick: pinned, selectedBy: `open build pinned by this SDK (${RELEASE.tag})` }
|
|
168
|
+
: { selectedBy: `the open build this SDK pins (${RELEASE.tag}) is not installed — run: clearcote install` };
|
|
169
|
+
}
|
|
170
|
+
export async function buildInfo(flags, launchFn = launch) {
|
|
101
171
|
const src = licenseKeySource();
|
|
102
172
|
let channel;
|
|
103
173
|
try {
|
|
@@ -108,20 +178,24 @@ export async function buildInfo(flags) {
|
|
|
108
178
|
}
|
|
109
179
|
const cached = listCachedBuilds();
|
|
110
180
|
const envBinary = process.env.CLEARCOTE_BINARY;
|
|
111
|
-
const pick
|
|
181
|
+
const { pick, selectedBy } = await launchBuild(cached, envBinary, resolveLicenseKey(), autoUpdateRequested(undefined), process.env.CLEARCOTE_BROWSER_VERSION, !!flags.quick);
|
|
112
182
|
const report = {
|
|
113
183
|
sdk: { version: SDK_VERSION, node: process.version, platform: `${process.platform}-${process.arch}` },
|
|
114
184
|
license: { source: src.source, ...(src.masked ? { key: src.masked } : {}) },
|
|
115
185
|
binary: {
|
|
116
186
|
source: envBinary ? "CLEARCOTE_BINARY" : pick ? "cache" : "none",
|
|
117
187
|
...(pick ? { path: pick.path } : {}),
|
|
118
|
-
...(!envBinary && pick ? { tag: pick.tag } : {}),
|
|
188
|
+
...(!envBinary && pick?.tag ? { tag: pick.tag } : {}),
|
|
119
189
|
cached,
|
|
120
190
|
pinnedFree: `${RELEASE.version} (${RELEASE.tag})`,
|
|
121
191
|
releaseChannel: channel,
|
|
192
|
+
selectedBy,
|
|
122
193
|
},
|
|
123
194
|
geoip: { databaseCached: existsSync(geoDbPath()), path: geoDbPath() },
|
|
124
195
|
};
|
|
196
|
+
const expiry = licenseExpiry(); // a local record only: no network, so --quick reports it too
|
|
197
|
+
if (expiry)
|
|
198
|
+
report.license.expiry = expiry;
|
|
125
199
|
if (pick && existsSync(pick.path)) {
|
|
126
200
|
report.engineFeatures = Object.fromEntries(["proxy-auth", "socks5-credentials", "socks5-udp", ...Object.keys(GATED_ENGINE_SWITCHES).map((s) => s.slice(2))]
|
|
127
201
|
.map((name) => [name, engineSupportsSwitch(pick.path, name)]));
|
|
@@ -139,19 +213,19 @@ export async function buildInfo(flags) {
|
|
|
139
213
|
report.launch = { tested: false, reason: "skipped (--quick)" };
|
|
140
214
|
}
|
|
141
215
|
else if (!pick) {
|
|
142
|
-
report.launch = { tested: false, reason: "no binary installed — run: clearcote install" };
|
|
216
|
+
report.launch = { tested: false, reason: cached.length ? selectedBy : "no binary installed — run: clearcote install" };
|
|
143
217
|
}
|
|
144
218
|
else {
|
|
145
219
|
try {
|
|
146
220
|
// cloud: false — this tests the LOCAL install, whatever CLEARCOTE_CLOUD says.
|
|
147
|
-
const b = await
|
|
221
|
+
const b = await launchFn({ executablePath: pick.path, headless: true, quiet: true, ephemeralProfile: false, cloud: false });
|
|
148
222
|
const version = b.version();
|
|
149
223
|
await b.close();
|
|
150
|
-
report.launch = { tested: true, ok: true, version };
|
|
224
|
+
report.launch = { tested: true, ok: true, version, build: pick.tag ?? pick.path };
|
|
151
225
|
}
|
|
152
226
|
catch (e) {
|
|
153
227
|
const libs = missingSharedLibs(pick.path);
|
|
154
|
-
report.launch = { tested: true, ok: false, error: e.message.split("\n")[0], ...(libs.length ? { missingLibs: libs } : {}) };
|
|
228
|
+
report.launch = { tested: true, ok: false, error: e.message.split("\n")[0], build: pick.tag ?? pick.path, ...(libs.length ? { missingLibs: libs } : {}) };
|
|
155
229
|
}
|
|
156
230
|
}
|
|
157
231
|
if (flags.proxy) {
|
|
@@ -166,6 +240,9 @@ function printInfo(r) {
|
|
|
166
240
|
const ok = (b) => (b ? "yes" : "no");
|
|
167
241
|
out(`clearcote SDK ${r.sdk.version} (node ${r.sdk.node}, ${r.sdk.platform})`);
|
|
168
242
|
out(`Licence ${r.license.source === "none" ? "none (free build)" : `${r.license.key} from ${r.license.source}`}`);
|
|
243
|
+
const exp = r.license.expiry;
|
|
244
|
+
if (exp && exp.source === "device-login")
|
|
245
|
+
out(`Expires ${exp.expiresAt || "never"} (as reported at device login)`);
|
|
169
246
|
const seats = r.license.seats;
|
|
170
247
|
if (seats) {
|
|
171
248
|
if (seats.state === "ok")
|
|
@@ -173,7 +250,16 @@ function printInfo(r) {
|
|
|
173
250
|
else
|
|
174
251
|
out(`Seats unavailable: ${seats.reason ?? seats.state}`);
|
|
175
252
|
}
|
|
176
|
-
|
|
253
|
+
if (r.binary.path) {
|
|
254
|
+
out(`Binary ${r.binary.path}${r.binary.tag ? ` [${r.binary.tag}]` : ""} (${r.binary.source})`);
|
|
255
|
+
out(` launch() runs this one: ${r.binary.selectedBy}`);
|
|
256
|
+
}
|
|
257
|
+
else if (r.binary.cached.length) {
|
|
258
|
+
out(`Binary none for this setup: ${r.binary.selectedBy}`);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
out("Binary not installed — run: clearcote install");
|
|
262
|
+
}
|
|
177
263
|
out(`Release channel ${r.binary.releaseChannel}`);
|
|
178
264
|
out(`Free pin ${r.binary.pinnedFree}`);
|
|
179
265
|
if (r.binary.cached.length > 1)
|
|
@@ -185,9 +271,9 @@ function printInfo(r) {
|
|
|
185
271
|
if (!r.launch.tested)
|
|
186
272
|
out(`Launch test ${r.launch.reason}`);
|
|
187
273
|
else if (r.launch.ok)
|
|
188
|
-
out(`Launch test ok (${r.launch.version})`);
|
|
274
|
+
out(`Launch test ok (${r.launch.version}) with ${r.launch.build}`);
|
|
189
275
|
else {
|
|
190
|
-
out(`Launch test FAILED: ${r.launch.error}`);
|
|
276
|
+
out(`Launch test FAILED with ${r.launch.build}: ${r.launch.error}`);
|
|
191
277
|
for (const lib of r.launch.missingLibs ?? [])
|
|
192
278
|
out(` missing library: ${lib}`);
|
|
193
279
|
}
|
|
@@ -200,14 +286,166 @@ function printInfo(r) {
|
|
|
200
286
|
out(p.error ? `Proxy geo FAILED: ${p.error}` : `Proxy geo exit ${p.exitIp} (${p.country}) timezone ${p.timezone} language ${p.acceptLanguage}`);
|
|
201
287
|
}
|
|
202
288
|
}
|
|
289
|
+
function err(line = "") {
|
|
290
|
+
process.stderr.write(line + "\n");
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* `clearcote login --device`: show a link and a short code, wait for the approval in the browser, then
|
|
294
|
+
* save the key it issues exactly as `clearcote login <key>` does. The key is never printed.
|
|
295
|
+
* Instructions go to stderr and the result to stdout, like the paste login's prompt. Output and exit
|
|
296
|
+
* codes match the Python CLI (test/device-login.test.ts).
|
|
297
|
+
*
|
|
298
|
+
* Ctrl-C is deterministic: it never tears down a request already on its way (its answer may be the key,
|
|
299
|
+
* which the server hands out once). Ctrl-C before the key arrives saves nothing (exit 130); one that lands
|
|
300
|
+
* while the request that brings the key is in flight still saves it, and says so.
|
|
301
|
+
*/
|
|
302
|
+
export async function deviceLogin(opts = {}) {
|
|
303
|
+
const ctrl = new AbortController(); // Ctrl-C: stop between requests; one already sent is answered first
|
|
304
|
+
const hard = new AbortController(); // a second Ctrl-C: give up on the request in flight too
|
|
305
|
+
const onSigint = () => {
|
|
306
|
+
if (ctrl.signal.aborted) {
|
|
307
|
+
hard.abort();
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
ctrl.abort();
|
|
311
|
+
err("");
|
|
312
|
+
err("stopping (a request already sent is answered first; press Ctrl-C again to stop at once)");
|
|
313
|
+
};
|
|
314
|
+
process.on("SIGINT", onSigint);
|
|
315
|
+
let grant;
|
|
316
|
+
try {
|
|
317
|
+
const code = await requestDeviceCode();
|
|
318
|
+
if (ctrl.signal.aborted)
|
|
319
|
+
throw new DeviceLoginError("cancelled", "cancelled");
|
|
320
|
+
err("To sign in, open this link in a browser and approve the code shown there:");
|
|
321
|
+
err("");
|
|
322
|
+
err(` ${code.verification_uri_complete}`);
|
|
323
|
+
err("");
|
|
324
|
+
err(`Code: ${code.user_code} (or enter it at ${code.verification_uri})`);
|
|
325
|
+
err("Waiting for the approval (Ctrl-C to cancel)...");
|
|
326
|
+
let warned = false;
|
|
327
|
+
grant = await pollForKey(code, {
|
|
328
|
+
...opts,
|
|
329
|
+
signal: ctrl.signal,
|
|
330
|
+
abort: hard.signal,
|
|
331
|
+
onRetry: (reason) => {
|
|
332
|
+
if (!warned) { // once: a flaky network must not flood the terminal
|
|
333
|
+
warned = true;
|
|
334
|
+
err(`note: ${reason}; still waiting`);
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
catch (e) {
|
|
340
|
+
if (e instanceof DeviceLoginError) {
|
|
341
|
+
if (e.code === "cancelled")
|
|
342
|
+
fail("cancelled. Nothing was saved.", 130);
|
|
343
|
+
if (e.code === "aborted")
|
|
344
|
+
fail("stopped at once (second Ctrl-C). Nothing was saved.", 130);
|
|
345
|
+
const again = e.code === "expired_token" || e.code === "outcome_unknown" ? " Run `clearcote login --device` again." : "";
|
|
346
|
+
fail(`${e.message}. Nothing was saved.${again}`);
|
|
347
|
+
}
|
|
348
|
+
throw e;
|
|
349
|
+
}
|
|
350
|
+
finally {
|
|
351
|
+
process.off("SIGINT", onSigint);
|
|
352
|
+
}
|
|
353
|
+
const where = saveLicenseKey(grant.licenseKey);
|
|
354
|
+
saveLicenseMeta(grant.licenseKey, grant.plan, grant.expiresAt);
|
|
355
|
+
if (grant.accountEmail)
|
|
356
|
+
out(`Signed in as ${grant.accountEmail}`);
|
|
357
|
+
out(`saved to ${where}`);
|
|
358
|
+
out(`plan ${grant.plan || "unknown"}, ${grant.expiresAt ? `expires ${grant.expiresAt}` : "no expiry"}`);
|
|
359
|
+
if (ctrl.signal.aborted)
|
|
360
|
+
out("note: Ctrl-C came after the licence server had sent the key, so it was saved anyway");
|
|
361
|
+
if (process.env.CLEARCOTE_LICENSE_KEY)
|
|
362
|
+
out("note: CLEARCOTE_LICENSE_KEY is set in this environment and takes precedence over the saved key");
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Read one line from a terminal without echoing it: raw mode, so the terminal shows nothing of what is typed
|
|
366
|
+
* or pasted (a screen share, a recording or the scrollback must not show a key). Enter ends it, Backspace
|
|
367
|
+
* edits, Ctrl-C rejects with "cancelled". Terminal escape sequences (bracketed paste) are dropped.
|
|
368
|
+
*/
|
|
369
|
+
export function readHidden(input, output, prompt) {
|
|
370
|
+
return new Promise((resolve, reject) => {
|
|
371
|
+
output.write(prompt);
|
|
372
|
+
let value = "";
|
|
373
|
+
const finish = (err) => {
|
|
374
|
+
input.removeListener("data", onData);
|
|
375
|
+
input.setRawMode?.(false);
|
|
376
|
+
input.pause();
|
|
377
|
+
output.write("\n");
|
|
378
|
+
if (err)
|
|
379
|
+
reject(err);
|
|
380
|
+
else
|
|
381
|
+
resolve(value.replace(/\x1b\[[0-9;]*[~A-Za-z]/g, "").trim());
|
|
382
|
+
};
|
|
383
|
+
const onData = (chunk) => {
|
|
384
|
+
for (const ch of chunk) {
|
|
385
|
+
if (ch === "\r" || ch === "\n" || ch === "\u0004")
|
|
386
|
+
return finish();
|
|
387
|
+
if (ch === "\u0003")
|
|
388
|
+
return finish(new Error("cancelled"));
|
|
389
|
+
if (ch === "\u007f" || ch === "\b")
|
|
390
|
+
value = value.slice(0, -1);
|
|
391
|
+
else
|
|
392
|
+
value += ch;
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
input.setRawMode?.(true);
|
|
396
|
+
input.setEncoding("utf8");
|
|
397
|
+
input.on("data", onData);
|
|
398
|
+
input.resume();
|
|
399
|
+
});
|
|
400
|
+
}
|
|
203
401
|
async function promptKey() {
|
|
204
402
|
if (!process.stdin.isTTY) {
|
|
205
403
|
fail("no key given. Run `clearcote login <key>`, or copy a key from https://www.clearcotelabs.com/dashboard/licenses");
|
|
206
404
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
405
|
+
try {
|
|
406
|
+
return await readHidden(process.stdin, process.stderr, "Paste your licence key (https://www.clearcotelabs.com/dashboard/licenses; it is not shown): ");
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
return fail("cancelled. Nothing was saved.", 130);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/** What `clearcote <command> --help` adds to that command's USAGE lines. */
|
|
413
|
+
const COMMAND_NOTES = {
|
|
414
|
+
login: "Saves a licence key to ~/.clearcote/license.key, where every launch finds it.\n"
|
|
415
|
+
+ " <key> the key, checked with the licence server first\n"
|
|
416
|
+
+ " (no key) asks for it on the terminal; what you paste is not shown\n"
|
|
417
|
+
+ " --device sign in from a browser instead: shows a link and a code, and saves the key the\n"
|
|
418
|
+
+ " site issues once you approve it there. Nothing to copy or paste.",
|
|
419
|
+
logout: "Removes the saved key (and what device login recorded about it).",
|
|
420
|
+
info: "Reports the SDK, the licence, the cached builds and what a launch would use. Never downloads.",
|
|
421
|
+
install: "Downloads and verifies the build a launch would use.",
|
|
422
|
+
update: "Fetches a newer build if one exists.",
|
|
423
|
+
"clear-cache": "Deletes every cached browser build (nothing else in the cache directory), and on Windows the " +
|
|
424
|
+
"recovered copies in ~/.clearcote/recovered.",
|
|
425
|
+
serve: "A CDP endpoint that gives every connection its own browser and identity.",
|
|
426
|
+
};
|
|
427
|
+
/** `clearcote <cmd> --help`: that command's lines from USAGE (with their flag sections) and a note. */
|
|
428
|
+
export function commandUsage(cmd) {
|
|
429
|
+
const name = cmd === "doctor" ? "info" : cmd;
|
|
430
|
+
const lines = USAGE.split("\n");
|
|
431
|
+
const picked = [];
|
|
432
|
+
let keep = false;
|
|
433
|
+
for (const line of lines.slice(lines.indexOf("USAGE") + 1)) {
|
|
434
|
+
if (!line.trim())
|
|
435
|
+
break;
|
|
436
|
+
if (line.startsWith(" clearcote "))
|
|
437
|
+
keep = line.trim().split(/\s+/)[1] === name;
|
|
438
|
+
if (keep)
|
|
439
|
+
picked.push(line);
|
|
440
|
+
}
|
|
441
|
+
let text = `USAGE\n${picked.join("\n")}`;
|
|
442
|
+
if (name === "info") {
|
|
443
|
+
const flags = lines.slice(lines.indexOf("INFO FLAGS"));
|
|
444
|
+
const end = flags.indexOf("");
|
|
445
|
+
text += `\n\n${flags.slice(0, end < 0 ? flags.length : end).join("\n")}`;
|
|
446
|
+
}
|
|
447
|
+
const note = COMMAND_NOTES[name];
|
|
448
|
+
return note ? `clearcote ${name} -- ${note}\n\n${text}` : text;
|
|
211
449
|
}
|
|
212
450
|
function dirSize(p) {
|
|
213
451
|
let total = 0;
|
|
@@ -236,6 +474,11 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
236
474
|
process.exitCode = code;
|
|
237
475
|
return;
|
|
238
476
|
}
|
|
477
|
+
const known = ["info", "doctor", "install", "update", "clear-cache", "login", "logout", "serve"];
|
|
478
|
+
if (known.includes(cmd) && rest.some((a) => a === "-h" || a === "--help")) {
|
|
479
|
+
out(commandUsage(cmd));
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
239
482
|
if (cmd === "info" || cmd === "doctor") {
|
|
240
483
|
const { values } = parseArgs({ args: rest, options: { quick: { type: "boolean" }, "no-launch": { type: "boolean" }, json: { type: "boolean" }, proxy: { type: "string" } } });
|
|
241
484
|
const report = await buildInfo({ quick: !!(values.quick || values["no-launch"]), proxy: values.proxy });
|
|
@@ -263,34 +506,49 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
263
506
|
const root = defaultCacheRoot();
|
|
264
507
|
if (!existsSync(root)) {
|
|
265
508
|
out(`nothing to clear (${root} does not exist)`);
|
|
266
|
-
return;
|
|
267
509
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
510
|
+
else {
|
|
511
|
+
// Only build directories: a CLEARCOTE_CACHE pointing at $HOME or a shared directory must not
|
|
512
|
+
// become `rm -rf` of that directory. A build dir carries a .verified marker once complete; a
|
|
513
|
+
// half-finished download has the build-tag name but no marker yet.
|
|
514
|
+
let bytes = 0;
|
|
515
|
+
const removed = [];
|
|
516
|
+
for (const name of readdirSync(root)) {
|
|
517
|
+
const dir = join(root, name);
|
|
518
|
+
let isDir = false;
|
|
519
|
+
try {
|
|
520
|
+
isDir = statSync(dir).isDirectory();
|
|
521
|
+
}
|
|
522
|
+
catch { /* vanished */ }
|
|
523
|
+
if (!isDir)
|
|
524
|
+
continue;
|
|
525
|
+
if (!existsSync(join(dir, ".verified")) && !/^(pro-\d|v\d)/.test(name))
|
|
526
|
+
continue;
|
|
527
|
+
bytes += dirSize(dir);
|
|
528
|
+
rmSync(dir, { recursive: true, force: true });
|
|
529
|
+
removed.push(name);
|
|
278
530
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
removed.
|
|
531
|
+
out(removed.length
|
|
532
|
+
? `removed ${removed.length} cached build${removed.length === 1 ? "" : "s"} from ${root} (${(bytes / 1e6).toFixed(0)} MB)`
|
|
533
|
+
: `no cached builds in ${root}`);
|
|
534
|
+
}
|
|
535
|
+
// Windows: the copies launches fall back to when a cached build cannot start in place.
|
|
536
|
+
const rec = clearRecovered();
|
|
537
|
+
if (rec.removed) {
|
|
538
|
+
out(`removed ${rec.removed} recovered build cop${rec.removed === 1 ? "y" : "ies"} from ${recoverRoot()} (${(rec.bytes / 1e6).toFixed(0)} MB)`);
|
|
539
|
+
}
|
|
540
|
+
if (rec.inUse) {
|
|
541
|
+
out(`kept ${rec.inUse} recovered build cop${rec.inUse === 1 ? "y" : "ies"} a running browser is using (in ${recoverRoot()})`);
|
|
287
542
|
}
|
|
288
|
-
out(removed.length
|
|
289
|
-
? `removed ${removed.length} cached build${removed.length === 1 ? "" : "s"} from ${root} (${(bytes / 1e6).toFixed(0)} MB)`
|
|
290
|
-
: `no cached builds in ${root}`);
|
|
291
543
|
return;
|
|
292
544
|
}
|
|
293
545
|
if (cmd === "login") {
|
|
546
|
+
if (rest.includes("--device")) {
|
|
547
|
+
if (rest.some((a) => a !== "--device"))
|
|
548
|
+
fail("pass a key or --device, not both", 2);
|
|
549
|
+
await deviceLogin();
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
294
552
|
const key = rest[0] ?? (await promptKey());
|
|
295
553
|
if (!key)
|
|
296
554
|
fail("empty key");
|
|
@@ -298,6 +556,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
298
556
|
if (seats.state === "invalid")
|
|
299
557
|
fail(`the licence server rejected this key (${seats.reason}). Nothing was saved.`);
|
|
300
558
|
const where = saveLicenseKey(key);
|
|
559
|
+
removeLicenseMeta(key.trim()); // a device login's record of another key no longer applies
|
|
301
560
|
out(`saved to ${where}`);
|
|
302
561
|
if (seats.state === "ok")
|
|
303
562
|
out(`valid: ${seats.used} of ${seats.limit ?? "unlimited"} seats in use${seats.plan ? `, plan ${seats.plan}` : ""}`);
|