clearcote 0.38.1 → 0.40.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/README.md +3 -1
- package/dist/cli-commands.d.ts +66 -1
- package/dist/cli-commands.d.ts.map +1 -1
- package/dist/cli-commands.js +265 -17
- 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 +814 -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 +17 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +36 -9
- package/dist/index.js.map +1 -1
- package/dist/launchopts.d.ts +90 -0
- package/dist/launchopts.d.ts.map +1 -1
- package/dist/launchopts.js +209 -1
- package/dist/launchopts.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/profileauto.d.ts +15 -0
- package/dist/profileauto.d.ts.map +1 -1
- package/dist/profileauto.js +36 -0
- package/dist/profileauto.js.map +1 -1
- package/dist/shaderdialect.d.ts +19 -6
- package/dist/shaderdialect.d.ts.map +1 -1
- package/dist/shaderdialect.js +61 -11
- package/dist/shaderdialect.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -203,7 +203,9 @@ if (!verdict.coherent) console.warn(verdict.warnings);
|
|
|
203
203
|
|
|
204
204
|
It reads the (unmasked) WebGL vendor/renderer the page sees, flags a software rasterizer (a fatal headless tell — enable the canvas bridge or run headed on a real GPU) and an incoherent vendor/renderer pair. Pass a second arg (`claimedGpu`) to also assert the rendered family.
|
|
205
205
|
|
|
206
|
-
The renderer *string* alone is not enough: a persona renames the backend, so a SwiftShader fallback stops looking like one. The check therefore also measures limits the string cannot move — `MAX_TEXTURE_SIZE`, the vertex/fragment uniform-vector pair, and an actual 16384-wide texture allocation — and reports them as `maxTextureSize`, `maxVertexUniformVectors`, `maxFragmentUniformVectors` and `canAllocate16kTexture`. A renderer naming a desktop GPU while `MAX_TEXTURE_SIZE` is below 16384 is a spoof over a software rasterizer and comes back `coherent: false`. **Headless on Linux with no GPU hits exactly this**
|
|
206
|
+
The renderer *string* alone is not enough: a persona renames the backend, so a SwiftShader fallback stops looking like one. The check therefore also measures limits the string cannot move — `MAX_TEXTURE_SIZE`, the vertex/fragment uniform-vector pair, and an actual 16384-wide texture allocation — and reports them as `maxTextureSize`, `maxVertexUniformVectors`, `maxFragmentUniformVectors` and `canAllocate16kTexture`. A renderer naming a desktop GPU while `MAX_TEXTURE_SIZE` is below 16384 is a spoof over a software rasterizer and comes back `coherent: false`. **Headless on Linux with no GPU hits exactly this** when Chromium renders through SwiftShader (8192): a Windows persona always does, and a Linux persona does on a host without Mesa's EGL (`libegl1`). Run headed under Xvfb, or use the canvas bridge.
|
|
207
|
+
|
|
208
|
+
On a Linux host the SDK picks the software backend that matches the GPU the persona names: a Linux persona (Mesa/OpenGL) renders through Mesa's own GL — over the X display when one is reachable (headed, or headless with `DISPLAY` pointing at an Xvfb), otherwise over EGL when the host has Mesa's EGL (`libegl1` on Debian/Ubuntu) — and a Windows persona (Direct3D11) renders through SwiftShader, whose limits match Direct3D11. Your own `--use-angle=` / `--use-gl=` in `args` wins.
|
|
207
209
|
|
|
208
210
|
### Hardened launch defaults
|
|
209
211
|
|
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,19 +4,22 @@
|
|
|
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";
|
|
@@ -37,6 +40,7 @@ USAGE
|
|
|
37
40
|
clearcote update [--channel stable|preview]
|
|
38
41
|
clearcote clear-cache
|
|
39
42
|
clearcote login [key]
|
|
43
|
+
clearcote login --device (sign in from a browser; no key to paste)
|
|
40
44
|
clearcote logout
|
|
41
45
|
clearcote serve [--port 9222] [--host 127.0.0.1] [--idle-timeout <s>] [--data-dir <dir>]
|
|
42
46
|
[--max-browsers 16] [--allow-origin <origin>]... [--allow-host <name>]... [--headed]
|
|
@@ -97,7 +101,72 @@ function missingSharedLibs(exe) {
|
|
|
97
101
|
const r = spawnSync("ldd", ["--", exe], { encoding: "utf8" });
|
|
98
102
|
return (r.stdout || "").split("\n").filter((l) => l.includes("not found")).map((l) => l.trim().split(" ")[0]);
|
|
99
103
|
}
|
|
100
|
-
|
|
104
|
+
/** Whether a cached licensed build's tag (pro-<version>-r<N>) is what `version` selects: a revision ("r30",
|
|
105
|
+
* "154.0.8037.57-r30"), an exact version (its newest revision), a major ("154"), or "latest". */
|
|
106
|
+
export function proTagMatches(tag, version) {
|
|
107
|
+
if (!tag.startsWith("pro-"))
|
|
108
|
+
return false;
|
|
109
|
+
const t = tag.slice(4).toLowerCase();
|
|
110
|
+
const v = String(version).trim().toLowerCase();
|
|
111
|
+
if (v === "latest" || v === "newest")
|
|
112
|
+
return true;
|
|
113
|
+
if (/^r\d+$/.test(v))
|
|
114
|
+
return t.endsWith(`-${v}`);
|
|
115
|
+
if (/^\d+$/.test(v))
|
|
116
|
+
return t.startsWith(`${v}.`);
|
|
117
|
+
return t === v || t.startsWith(`${v}-`);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The build launch() would run for the current licence state, from what is already cached (`info` never
|
|
121
|
+
* downloads). CLEARCOTE_BINARY wins. A version selector (CLEARCOTE_BROWSER_VERSION) is resolved the way
|
|
122
|
+
* launch() resolves it -- through the version catalog (the bundled copy when `offline`), a licensed revision
|
|
123
|
+
* straight to the licensed builds -- and picks the cached build it names. Otherwise, with a licence key,
|
|
124
|
+
* launch() runs a licensed build: the newest one cached (launch() itself asks the licence server, which may
|
|
125
|
+
* name a newer one). Without a key it runs the open build this SDK pins (the newest cached open build with
|
|
126
|
+
* CLEARCOTE_AUTO_UPDATE). A cached licensed build is never picked for a keyless setup: it cannot run without
|
|
127
|
+
* a key, which is what made the old "newest cached build" launch test fail for open-build users.
|
|
128
|
+
*/
|
|
129
|
+
export async function launchBuild(cached, envBinary, licenseKey, autoUpdate = false, version, offline = false) {
|
|
130
|
+
if (envBinary)
|
|
131
|
+
return { pick: { path: envBinary }, selectedBy: "CLEARCOTE_BINARY" };
|
|
132
|
+
const sel = String(version ?? "").trim();
|
|
133
|
+
if (sel) {
|
|
134
|
+
const named = `CLEARCOTE_BROWSER_VERSION=${sel}`;
|
|
135
|
+
let plan;
|
|
136
|
+
try {
|
|
137
|
+
plan = licenseKey && isProRevisionSelector(sel)
|
|
138
|
+
? { kind: "pro", version: sel }
|
|
139
|
+
: await resolveVersion(sel, !!licenseKey, true, offline ? CATALOG_FALLBACK : undefined);
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
return { selectedBy: `${named}: ${e.message.split("\n")[0]}` };
|
|
143
|
+
}
|
|
144
|
+
if (plan.kind === "pro") {
|
|
145
|
+
const m = cached.find((c) => proTagMatches(c.tag, plan.version));
|
|
146
|
+
return m ? { pick: m, selectedBy: `licensed: the newest cached build ${named} selects` }
|
|
147
|
+
: { selectedBy: `no licensed build for ${named} is cached — run: clearcote install --version ${sel}` };
|
|
148
|
+
}
|
|
149
|
+
const hit = cached.find((c) => c.tag === plan.rel.tag);
|
|
150
|
+
return hit ? { pick: hit, selectedBy: `open build ${plan.rel.tag} (${named})` }
|
|
151
|
+
: { selectedBy: `the open build ${plan.rel.tag} (${named}) is not installed — run: clearcote install --version ${sel}` };
|
|
152
|
+
}
|
|
153
|
+
if (licenseKey) {
|
|
154
|
+
const licensed = cached.filter((c) => c.tag.startsWith("pro-"));
|
|
155
|
+
return licensed.length
|
|
156
|
+
? { pick: licensed[0], selectedBy: "licensed: the newest cached licensed build (launch() asks the licence server, which may name a newer one)" }
|
|
157
|
+
: { selectedBy: "no licensed build is cached — run: clearcote install" };
|
|
158
|
+
}
|
|
159
|
+
if (autoUpdate) {
|
|
160
|
+
const open = cached.filter((c) => !c.tag.startsWith("pro-"));
|
|
161
|
+
if (open.length)
|
|
162
|
+
return { pick: open[0], selectedBy: "open build: the newest cached (CLEARCOTE_AUTO_UPDATE)" };
|
|
163
|
+
}
|
|
164
|
+
const pinned = cached.find((c) => c.tag === RELEASE.tag);
|
|
165
|
+
return pinned
|
|
166
|
+
? { pick: pinned, selectedBy: `open build pinned by this SDK (${RELEASE.tag})` }
|
|
167
|
+
: { selectedBy: `the open build this SDK pins (${RELEASE.tag}) is not installed — run: clearcote install` };
|
|
168
|
+
}
|
|
169
|
+
export async function buildInfo(flags, launchFn = launch) {
|
|
101
170
|
const src = licenseKeySource();
|
|
102
171
|
let channel;
|
|
103
172
|
try {
|
|
@@ -108,20 +177,24 @@ export async function buildInfo(flags) {
|
|
|
108
177
|
}
|
|
109
178
|
const cached = listCachedBuilds();
|
|
110
179
|
const envBinary = process.env.CLEARCOTE_BINARY;
|
|
111
|
-
const pick
|
|
180
|
+
const { pick, selectedBy } = await launchBuild(cached, envBinary, resolveLicenseKey(), autoUpdateRequested(undefined), process.env.CLEARCOTE_BROWSER_VERSION, !!flags.quick);
|
|
112
181
|
const report = {
|
|
113
182
|
sdk: { version: SDK_VERSION, node: process.version, platform: `${process.platform}-${process.arch}` },
|
|
114
183
|
license: { source: src.source, ...(src.masked ? { key: src.masked } : {}) },
|
|
115
184
|
binary: {
|
|
116
185
|
source: envBinary ? "CLEARCOTE_BINARY" : pick ? "cache" : "none",
|
|
117
186
|
...(pick ? { path: pick.path } : {}),
|
|
118
|
-
...(!envBinary && pick ? { tag: pick.tag } : {}),
|
|
187
|
+
...(!envBinary && pick?.tag ? { tag: pick.tag } : {}),
|
|
119
188
|
cached,
|
|
120
189
|
pinnedFree: `${RELEASE.version} (${RELEASE.tag})`,
|
|
121
190
|
releaseChannel: channel,
|
|
191
|
+
selectedBy,
|
|
122
192
|
},
|
|
123
193
|
geoip: { databaseCached: existsSync(geoDbPath()), path: geoDbPath() },
|
|
124
194
|
};
|
|
195
|
+
const expiry = licenseExpiry(); // a local record only: no network, so --quick reports it too
|
|
196
|
+
if (expiry)
|
|
197
|
+
report.license.expiry = expiry;
|
|
125
198
|
if (pick && existsSync(pick.path)) {
|
|
126
199
|
report.engineFeatures = Object.fromEntries(["proxy-auth", "socks5-credentials", "socks5-udp", ...Object.keys(GATED_ENGINE_SWITCHES).map((s) => s.slice(2))]
|
|
127
200
|
.map((name) => [name, engineSupportsSwitch(pick.path, name)]));
|
|
@@ -139,19 +212,19 @@ export async function buildInfo(flags) {
|
|
|
139
212
|
report.launch = { tested: false, reason: "skipped (--quick)" };
|
|
140
213
|
}
|
|
141
214
|
else if (!pick) {
|
|
142
|
-
report.launch = { tested: false, reason: "no binary installed — run: clearcote install" };
|
|
215
|
+
report.launch = { tested: false, reason: cached.length ? selectedBy : "no binary installed — run: clearcote install" };
|
|
143
216
|
}
|
|
144
217
|
else {
|
|
145
218
|
try {
|
|
146
219
|
// cloud: false — this tests the LOCAL install, whatever CLEARCOTE_CLOUD says.
|
|
147
|
-
const b = await
|
|
220
|
+
const b = await launchFn({ executablePath: pick.path, headless: true, quiet: true, ephemeralProfile: false, cloud: false });
|
|
148
221
|
const version = b.version();
|
|
149
222
|
await b.close();
|
|
150
|
-
report.launch = { tested: true, ok: true, version };
|
|
223
|
+
report.launch = { tested: true, ok: true, version, build: pick.tag ?? pick.path };
|
|
151
224
|
}
|
|
152
225
|
catch (e) {
|
|
153
226
|
const libs = missingSharedLibs(pick.path);
|
|
154
|
-
report.launch = { tested: true, ok: false, error: e.message.split("\n")[0], ...(libs.length ? { missingLibs: libs } : {}) };
|
|
227
|
+
report.launch = { tested: true, ok: false, error: e.message.split("\n")[0], build: pick.tag ?? pick.path, ...(libs.length ? { missingLibs: libs } : {}) };
|
|
155
228
|
}
|
|
156
229
|
}
|
|
157
230
|
if (flags.proxy) {
|
|
@@ -166,6 +239,9 @@ function printInfo(r) {
|
|
|
166
239
|
const ok = (b) => (b ? "yes" : "no");
|
|
167
240
|
out(`clearcote SDK ${r.sdk.version} (node ${r.sdk.node}, ${r.sdk.platform})`);
|
|
168
241
|
out(`Licence ${r.license.source === "none" ? "none (free build)" : `${r.license.key} from ${r.license.source}`}`);
|
|
242
|
+
const exp = r.license.expiry;
|
|
243
|
+
if (exp && exp.source === "device-login")
|
|
244
|
+
out(`Expires ${exp.expiresAt || "never"} (as reported at device login)`);
|
|
169
245
|
const seats = r.license.seats;
|
|
170
246
|
if (seats) {
|
|
171
247
|
if (seats.state === "ok")
|
|
@@ -173,7 +249,16 @@ function printInfo(r) {
|
|
|
173
249
|
else
|
|
174
250
|
out(`Seats unavailable: ${seats.reason ?? seats.state}`);
|
|
175
251
|
}
|
|
176
|
-
|
|
252
|
+
if (r.binary.path) {
|
|
253
|
+
out(`Binary ${r.binary.path}${r.binary.tag ? ` [${r.binary.tag}]` : ""} (${r.binary.source})`);
|
|
254
|
+
out(` launch() runs this one: ${r.binary.selectedBy}`);
|
|
255
|
+
}
|
|
256
|
+
else if (r.binary.cached.length) {
|
|
257
|
+
out(`Binary none for this setup: ${r.binary.selectedBy}`);
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
out("Binary not installed — run: clearcote install");
|
|
261
|
+
}
|
|
177
262
|
out(`Release channel ${r.binary.releaseChannel}`);
|
|
178
263
|
out(`Free pin ${r.binary.pinnedFree}`);
|
|
179
264
|
if (r.binary.cached.length > 1)
|
|
@@ -185,9 +270,9 @@ function printInfo(r) {
|
|
|
185
270
|
if (!r.launch.tested)
|
|
186
271
|
out(`Launch test ${r.launch.reason}`);
|
|
187
272
|
else if (r.launch.ok)
|
|
188
|
-
out(`Launch test ok (${r.launch.version})`);
|
|
273
|
+
out(`Launch test ok (${r.launch.version}) with ${r.launch.build}`);
|
|
189
274
|
else {
|
|
190
|
-
out(`Launch test FAILED: ${r.launch.error}`);
|
|
275
|
+
out(`Launch test FAILED with ${r.launch.build}: ${r.launch.error}`);
|
|
191
276
|
for (const lib of r.launch.missingLibs ?? [])
|
|
192
277
|
out(` missing library: ${lib}`);
|
|
193
278
|
}
|
|
@@ -200,14 +285,165 @@ function printInfo(r) {
|
|
|
200
285
|
out(p.error ? `Proxy geo FAILED: ${p.error}` : `Proxy geo exit ${p.exitIp} (${p.country}) timezone ${p.timezone} language ${p.acceptLanguage}`);
|
|
201
286
|
}
|
|
202
287
|
}
|
|
288
|
+
function err(line = "") {
|
|
289
|
+
process.stderr.write(line + "\n");
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* `clearcote login --device`: show a link and a short code, wait for the approval in the browser, then
|
|
293
|
+
* save the key it issues exactly as `clearcote login <key>` does. The key is never printed.
|
|
294
|
+
* Instructions go to stderr and the result to stdout, like the paste login's prompt. Output and exit
|
|
295
|
+
* codes match the Python CLI (test/device-login.test.ts).
|
|
296
|
+
*
|
|
297
|
+
* Ctrl-C is deterministic: it never tears down a request already on its way (its answer may be the key,
|
|
298
|
+
* which the server hands out once). Ctrl-C before the key arrives saves nothing (exit 130); one that lands
|
|
299
|
+
* while the request that brings the key is in flight still saves it, and says so.
|
|
300
|
+
*/
|
|
301
|
+
export async function deviceLogin(opts = {}) {
|
|
302
|
+
const ctrl = new AbortController(); // Ctrl-C: stop between requests; one already sent is answered first
|
|
303
|
+
const hard = new AbortController(); // a second Ctrl-C: give up on the request in flight too
|
|
304
|
+
const onSigint = () => {
|
|
305
|
+
if (ctrl.signal.aborted) {
|
|
306
|
+
hard.abort();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
ctrl.abort();
|
|
310
|
+
err("");
|
|
311
|
+
err("stopping (a request already sent is answered first; press Ctrl-C again to stop at once)");
|
|
312
|
+
};
|
|
313
|
+
process.on("SIGINT", onSigint);
|
|
314
|
+
let grant;
|
|
315
|
+
try {
|
|
316
|
+
const code = await requestDeviceCode();
|
|
317
|
+
if (ctrl.signal.aborted)
|
|
318
|
+
throw new DeviceLoginError("cancelled", "cancelled");
|
|
319
|
+
err("To sign in, open this link in a browser and approve the code shown there:");
|
|
320
|
+
err("");
|
|
321
|
+
err(` ${code.verification_uri_complete}`);
|
|
322
|
+
err("");
|
|
323
|
+
err(`Code: ${code.user_code} (or enter it at ${code.verification_uri})`);
|
|
324
|
+
err("Waiting for the approval (Ctrl-C to cancel)...");
|
|
325
|
+
let warned = false;
|
|
326
|
+
grant = await pollForKey(code, {
|
|
327
|
+
...opts,
|
|
328
|
+
signal: ctrl.signal,
|
|
329
|
+
abort: hard.signal,
|
|
330
|
+
onRetry: (reason) => {
|
|
331
|
+
if (!warned) { // once: a flaky network must not flood the terminal
|
|
332
|
+
warned = true;
|
|
333
|
+
err(`note: ${reason}; still waiting`);
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
catch (e) {
|
|
339
|
+
if (e instanceof DeviceLoginError) {
|
|
340
|
+
if (e.code === "cancelled")
|
|
341
|
+
fail("cancelled. Nothing was saved.", 130);
|
|
342
|
+
if (e.code === "aborted")
|
|
343
|
+
fail("stopped at once (second Ctrl-C). Nothing was saved.", 130);
|
|
344
|
+
const again = e.code === "expired_token" || e.code === "outcome_unknown" ? " Run `clearcote login --device` again." : "";
|
|
345
|
+
fail(`${e.message}. Nothing was saved.${again}`);
|
|
346
|
+
}
|
|
347
|
+
throw e;
|
|
348
|
+
}
|
|
349
|
+
finally {
|
|
350
|
+
process.off("SIGINT", onSigint);
|
|
351
|
+
}
|
|
352
|
+
const where = saveLicenseKey(grant.licenseKey);
|
|
353
|
+
saveLicenseMeta(grant.licenseKey, grant.plan, grant.expiresAt);
|
|
354
|
+
if (grant.accountEmail)
|
|
355
|
+
out(`Signed in as ${grant.accountEmail}`);
|
|
356
|
+
out(`saved to ${where}`);
|
|
357
|
+
out(`plan ${grant.plan || "unknown"}, ${grant.expiresAt ? `expires ${grant.expiresAt}` : "no expiry"}`);
|
|
358
|
+
if (ctrl.signal.aborted)
|
|
359
|
+
out("note: Ctrl-C came after the licence server had sent the key, so it was saved anyway");
|
|
360
|
+
if (process.env.CLEARCOTE_LICENSE_KEY)
|
|
361
|
+
out("note: CLEARCOTE_LICENSE_KEY is set in this environment and takes precedence over the saved key");
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Read one line from a terminal without echoing it: raw mode, so the terminal shows nothing of what is typed
|
|
365
|
+
* or pasted (a screen share, a recording or the scrollback must not show a key). Enter ends it, Backspace
|
|
366
|
+
* edits, Ctrl-C rejects with "cancelled". Terminal escape sequences (bracketed paste) are dropped.
|
|
367
|
+
*/
|
|
368
|
+
export function readHidden(input, output, prompt) {
|
|
369
|
+
return new Promise((resolve, reject) => {
|
|
370
|
+
output.write(prompt);
|
|
371
|
+
let value = "";
|
|
372
|
+
const finish = (err) => {
|
|
373
|
+
input.removeListener("data", onData);
|
|
374
|
+
input.setRawMode?.(false);
|
|
375
|
+
input.pause();
|
|
376
|
+
output.write("\n");
|
|
377
|
+
if (err)
|
|
378
|
+
reject(err);
|
|
379
|
+
else
|
|
380
|
+
resolve(value.replace(/\x1b\[[0-9;]*[~A-Za-z]/g, "").trim());
|
|
381
|
+
};
|
|
382
|
+
const onData = (chunk) => {
|
|
383
|
+
for (const ch of chunk) {
|
|
384
|
+
if (ch === "\r" || ch === "\n" || ch === "\u0004")
|
|
385
|
+
return finish();
|
|
386
|
+
if (ch === "\u0003")
|
|
387
|
+
return finish(new Error("cancelled"));
|
|
388
|
+
if (ch === "\u007f" || ch === "\b")
|
|
389
|
+
value = value.slice(0, -1);
|
|
390
|
+
else
|
|
391
|
+
value += ch;
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
input.setRawMode?.(true);
|
|
395
|
+
input.setEncoding("utf8");
|
|
396
|
+
input.on("data", onData);
|
|
397
|
+
input.resume();
|
|
398
|
+
});
|
|
399
|
+
}
|
|
203
400
|
async function promptKey() {
|
|
204
401
|
if (!process.stdin.isTTY) {
|
|
205
402
|
fail("no key given. Run `clearcote login <key>`, or copy a key from https://www.clearcotelabs.com/dashboard/licenses");
|
|
206
403
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
404
|
+
try {
|
|
405
|
+
return await readHidden(process.stdin, process.stderr, "Paste your licence key (https://www.clearcotelabs.com/dashboard/licenses; it is not shown): ");
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
return fail("cancelled. Nothing was saved.", 130);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
/** What `clearcote <command> --help` adds to that command's USAGE lines. */
|
|
412
|
+
const COMMAND_NOTES = {
|
|
413
|
+
login: "Saves a licence key to ~/.clearcote/license.key, where every launch finds it.\n"
|
|
414
|
+
+ " <key> the key, checked with the licence server first\n"
|
|
415
|
+
+ " (no key) asks for it on the terminal; what you paste is not shown\n"
|
|
416
|
+
+ " --device sign in from a browser instead: shows a link and a code, and saves the key the\n"
|
|
417
|
+
+ " site issues once you approve it there. Nothing to copy or paste.",
|
|
418
|
+
logout: "Removes the saved key (and what device login recorded about it).",
|
|
419
|
+
info: "Reports the SDK, the licence, the cached builds and what a launch would use. Never downloads.",
|
|
420
|
+
install: "Downloads and verifies the build a launch would use.",
|
|
421
|
+
update: "Fetches a newer build if one exists.",
|
|
422
|
+
"clear-cache": "Deletes every cached browser build (nothing else in the cache directory).",
|
|
423
|
+
serve: "A CDP endpoint that gives every connection its own browser and identity.",
|
|
424
|
+
};
|
|
425
|
+
/** `clearcote <cmd> --help`: that command's lines from USAGE (with their flag sections) and a note. */
|
|
426
|
+
export function commandUsage(cmd) {
|
|
427
|
+
const name = cmd === "doctor" ? "info" : cmd;
|
|
428
|
+
const lines = USAGE.split("\n");
|
|
429
|
+
const picked = [];
|
|
430
|
+
let keep = false;
|
|
431
|
+
for (const line of lines.slice(lines.indexOf("USAGE") + 1)) {
|
|
432
|
+
if (!line.trim())
|
|
433
|
+
break;
|
|
434
|
+
if (line.startsWith(" clearcote "))
|
|
435
|
+
keep = line.trim().split(/\s+/)[1] === name;
|
|
436
|
+
if (keep)
|
|
437
|
+
picked.push(line);
|
|
438
|
+
}
|
|
439
|
+
let text = `USAGE\n${picked.join("\n")}`;
|
|
440
|
+
if (name === "info") {
|
|
441
|
+
const flags = lines.slice(lines.indexOf("INFO FLAGS"));
|
|
442
|
+
const end = flags.indexOf("");
|
|
443
|
+
text += `\n\n${flags.slice(0, end < 0 ? flags.length : end).join("\n")}`;
|
|
444
|
+
}
|
|
445
|
+
const note = COMMAND_NOTES[name];
|
|
446
|
+
return note ? `clearcote ${name} -- ${note}\n\n${text}` : text;
|
|
211
447
|
}
|
|
212
448
|
function dirSize(p) {
|
|
213
449
|
let total = 0;
|
|
@@ -236,6 +472,11 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
236
472
|
process.exitCode = code;
|
|
237
473
|
return;
|
|
238
474
|
}
|
|
475
|
+
const known = ["info", "doctor", "install", "update", "clear-cache", "login", "logout", "serve"];
|
|
476
|
+
if (known.includes(cmd) && rest.some((a) => a === "-h" || a === "--help")) {
|
|
477
|
+
out(commandUsage(cmd));
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
239
480
|
if (cmd === "info" || cmd === "doctor") {
|
|
240
481
|
const { values } = parseArgs({ args: rest, options: { quick: { type: "boolean" }, "no-launch": { type: "boolean" }, json: { type: "boolean" }, proxy: { type: "string" } } });
|
|
241
482
|
const report = await buildInfo({ quick: !!(values.quick || values["no-launch"]), proxy: values.proxy });
|
|
@@ -291,6 +532,12 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
291
532
|
return;
|
|
292
533
|
}
|
|
293
534
|
if (cmd === "login") {
|
|
535
|
+
if (rest.includes("--device")) {
|
|
536
|
+
if (rest.some((a) => a !== "--device"))
|
|
537
|
+
fail("pass a key or --device, not both", 2);
|
|
538
|
+
await deviceLogin();
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
294
541
|
const key = rest[0] ?? (await promptKey());
|
|
295
542
|
if (!key)
|
|
296
543
|
fail("empty key");
|
|
@@ -298,6 +545,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
298
545
|
if (seats.state === "invalid")
|
|
299
546
|
fail(`the licence server rejected this key (${seats.reason}). Nothing was saved.`);
|
|
300
547
|
const where = saveLicenseKey(key);
|
|
548
|
+
removeLicenseMeta(key.trim()); // a device login's record of another key no longer applies
|
|
301
549
|
out(`saved to ${where}`);
|
|
302
550
|
if (seats.state === "ok")
|
|
303
551
|
out(`valid: ${seats.used} of ${seats.limit ?? "unlimited"} seats in use${seats.plan ? `, plan ${seats.plan}` : ""}`);
|