cloak22 2.2.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/.githooks/pre-commit +12 -0
- package/.githooks/pre-push +37 -0
- package/LICENSE +21 -0
- package/README.md +181 -0
- package/README.org +187 -0
- package/bin/cloak.js +24 -0
- package/dist/app-paths.js +26 -0
- package/dist/chrome-cookies.js +420 -0
- package/dist/chrome-profile-sites.js +155 -0
- package/dist/chrome-profiles.js +71 -0
- package/dist/cli.js +627 -0
- package/dist/cookies.js +95 -0
- package/dist/daemon.js +93 -0
- package/dist/extension.js +133 -0
- package/dist/install-extension.js +13 -0
- package/dist/main.js +688 -0
- package/dist/output.js +26 -0
- package/dist/state-db.js +232 -0
- package/docs/assets/cloak-logo-readme-centered.png +0 -0
- package/package.json +66 -0
- package/scripts/postinstall.cjs +55 -0
- package/scripts/render-readme.cjs +54 -0
- package/scripts/setup-git-hooks.cjs +21 -0
- package/src/app-paths.ts +39 -0
- package/src/chrome-cookies.ts +681 -0
- package/src/chrome-profile-sites.ts +274 -0
- package/src/chrome-profiles.ts +92 -0
- package/src/cli.ts +815 -0
- package/src/cookies.ts +149 -0
- package/src/daemon.ts +143 -0
- package/src/extension.ts +201 -0
- package/src/install-extension.ts +13 -0
- package/src/main.ts +1085 -0
- package/src/output.ts +21 -0
- package/src/state-db.ts +320 -0
package/src/main.ts
ADDED
|
@@ -0,0 +1,1085 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { chromium as patchrightChromium } from "patchright"
|
|
3
|
+
import fs from "node:fs"
|
|
4
|
+
import os from "node:os"
|
|
5
|
+
import path from "node:path"
|
|
6
|
+
import readline from "node:readline/promises"
|
|
7
|
+
import { resolveAppPaths, type AppPaths } from "./app-paths.js"
|
|
8
|
+
import { buildRunArguments, isProcessRunning, spawnDaemonProcess, stopProcess } from "./daemon.js"
|
|
9
|
+
import { prepareRequiredExtension } from "./extension.js"
|
|
10
|
+
import { readCookiesFromFile, type Cookie } from "./cookies.js"
|
|
11
|
+
import { parseCli, type RunModeConfig } from "./cli.js"
|
|
12
|
+
import {
|
|
13
|
+
CHROME_COOKIE_LIMITATION_WARNING,
|
|
14
|
+
readChromeCookies,
|
|
15
|
+
} from "./chrome-cookies.js"
|
|
16
|
+
import {
|
|
17
|
+
listChromeProfileCookieUrls,
|
|
18
|
+
} from "./chrome-profile-sites.js"
|
|
19
|
+
import {
|
|
20
|
+
hasChromeUserDataDir,
|
|
21
|
+
listChromeProfiles,
|
|
22
|
+
type ChromeProfile,
|
|
23
|
+
} from "./chrome-profiles.js"
|
|
24
|
+
import {
|
|
25
|
+
CloakStateDb,
|
|
26
|
+
type DaemonState,
|
|
27
|
+
type StoredRunCommand,
|
|
28
|
+
} from "./state-db.js"
|
|
29
|
+
import { formatError, formatInfo, formatSuccess, formatWarning } from "./output.js"
|
|
30
|
+
|
|
31
|
+
type ResolveStartupCookiesDependencies = {
|
|
32
|
+
readChromeCookies?: typeof readChromeCookies;
|
|
33
|
+
readCookiesFromFile?: typeof readCookiesFromFile;
|
|
34
|
+
warn?: (message: string) => void;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type StartupContext = {
|
|
38
|
+
addCookies(cookies: Cookie[]): Promise<void>;
|
|
39
|
+
browser(): { on(event: "disconnected", listener: () => void): void } | null;
|
|
40
|
+
on(event: "close", listener: () => void): void;
|
|
41
|
+
close(): Promise<void>;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type LaunchOptions = {
|
|
45
|
+
headless: boolean;
|
|
46
|
+
args: string[];
|
|
47
|
+
executablePath?: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type SelectCookieUrlsOptions = {
|
|
51
|
+
profile: string;
|
|
52
|
+
urls: string[];
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
type MainDependencies = ResolveStartupCookiesDependencies & {
|
|
56
|
+
appPaths?: AppPaths;
|
|
57
|
+
createStateDb?: (dbPath: string) => CloakStateDb;
|
|
58
|
+
confirmCreateConfigDir?: (configDir: string) => Promise<boolean>;
|
|
59
|
+
confirmDestroyState?: (configDir: string) => Promise<boolean>;
|
|
60
|
+
selectCookieUrls?: (
|
|
61
|
+
options: SelectCookieUrlsOptions
|
|
62
|
+
) => Promise<string[] | undefined>;
|
|
63
|
+
prepareRequiredExtension?: typeof prepareRequiredExtension;
|
|
64
|
+
listChromeProfiles?: typeof listChromeProfiles;
|
|
65
|
+
hasChromeUserDataDir?: typeof hasChromeUserDataDir;
|
|
66
|
+
listChromeProfileCookieUrls?: typeof listChromeProfileCookieUrls;
|
|
67
|
+
launchPersistentContext?: (
|
|
68
|
+
userDataDir: string,
|
|
69
|
+
options: LaunchOptions
|
|
70
|
+
) => Promise<StartupContext>;
|
|
71
|
+
patchrightLaunchPersistentContext?: (
|
|
72
|
+
userDataDir: string,
|
|
73
|
+
options: LaunchOptions
|
|
74
|
+
) => Promise<StartupContext>;
|
|
75
|
+
patchrightExecutablePath?: () => string;
|
|
76
|
+
makeTempDir?: (prefix: string) => string;
|
|
77
|
+
makeDir?: (path: string, options: { recursive: true }) => void;
|
|
78
|
+
removeDir?: (path: string, options: { recursive: true; force: true }) => void;
|
|
79
|
+
readFile?: (path: string, encoding: "utf8") => string;
|
|
80
|
+
writeFile?: (path: string, data: string) => void;
|
|
81
|
+
pathExists?: (path: string) => boolean;
|
|
82
|
+
writeStdout?: (message: string) => void;
|
|
83
|
+
log?: (message: string) => void;
|
|
84
|
+
stdinIsTTY?: boolean;
|
|
85
|
+
stdoutIsTTY?: boolean;
|
|
86
|
+
processExecPath?: string;
|
|
87
|
+
processExecArgv?: string[];
|
|
88
|
+
scriptPath?: string;
|
|
89
|
+
spawnDaemonProcess?: typeof spawnDaemonProcess;
|
|
90
|
+
isProcessRunning?: typeof isProcessRunning;
|
|
91
|
+
stopProcess?: typeof stopProcess;
|
|
92
|
+
now?: () => Date;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
type ResolvedRunConfig = {
|
|
96
|
+
headless: boolean;
|
|
97
|
+
daemon: boolean;
|
|
98
|
+
profile?: string;
|
|
99
|
+
cookieUrls: string[];
|
|
100
|
+
cookieFile?: string;
|
|
101
|
+
explicitCookieUrls: string[];
|
|
102
|
+
persistCookies: boolean;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export function dedupeCookies(cookies: Cookie[]): Cookie[] {
|
|
106
|
+
const seen = new Map<string, Cookie>()
|
|
107
|
+
|
|
108
|
+
for (const cookie of cookies) {
|
|
109
|
+
const key = [cookie.name, cookie.domain, cookie.path].join("\u0000")
|
|
110
|
+
seen.set(key, cookie)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return [...seen.values()]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function parseSelectionInput(
|
|
117
|
+
input: string,
|
|
118
|
+
total: number
|
|
119
|
+
): number[] | undefined {
|
|
120
|
+
const normalized = input.trim().toLowerCase()
|
|
121
|
+
|
|
122
|
+
if (normalized.length === 0) {
|
|
123
|
+
return undefined
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (normalized === "all" || normalized === "a" || normalized === "*") {
|
|
127
|
+
return Array.from({ length: total }, (_, index) => index)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (normalized === "none" || normalized === "n" || normalized === "clear") {
|
|
131
|
+
return []
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const indexes = new Set<number>()
|
|
135
|
+
|
|
136
|
+
for (const part of normalized.split(",")) {
|
|
137
|
+
const token = part.trim()
|
|
138
|
+
|
|
139
|
+
if (!token) {
|
|
140
|
+
continue
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const rangeMatch = token.match(/^(\d+)\s*-\s*(\d+)$/)
|
|
144
|
+
|
|
145
|
+
if (rangeMatch) {
|
|
146
|
+
const start = Number(rangeMatch[1])
|
|
147
|
+
const end = Number(rangeMatch[2])
|
|
148
|
+
|
|
149
|
+
if (start < 1 || end < 1 || start > total || end > total) {
|
|
150
|
+
throw new Error("selection is out of range")
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const lower = Math.min(start, end)
|
|
154
|
+
const upper = Math.max(start, end)
|
|
155
|
+
|
|
156
|
+
for (let value = lower; value <= upper; value += 1) {
|
|
157
|
+
indexes.add(value - 1)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!/^\d+$/.test(token)) {
|
|
164
|
+
throw new Error("selection must use indexes, ranges, all, or none")
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const value = Number(token)
|
|
168
|
+
|
|
169
|
+
if (value < 1 || value > total) {
|
|
170
|
+
throw new Error("selection is out of range")
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
indexes.add(value - 1)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return [...indexes].sort((left, right) => left - right)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function resolveStartupCookies(
|
|
180
|
+
options: {
|
|
181
|
+
cookieUrls: string[];
|
|
182
|
+
cookieFile?: string;
|
|
183
|
+
profile?: string;
|
|
184
|
+
},
|
|
185
|
+
dependencies: ResolveStartupCookiesDependencies = {}
|
|
186
|
+
): Promise<Cookie[]> {
|
|
187
|
+
if (options.cookieUrls.length === 0 && !options.cookieFile) {
|
|
188
|
+
return []
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const readChromeCookiesFn =
|
|
192
|
+
dependencies.readChromeCookies ?? readChromeCookies
|
|
193
|
+
const readCookiesFromFileFn =
|
|
194
|
+
dependencies.readCookiesFromFile ?? readCookiesFromFile
|
|
195
|
+
const warn =
|
|
196
|
+
dependencies.warn ??
|
|
197
|
+
((message: string) => console.warn(formatWarning(message)))
|
|
198
|
+
const importedCookies: Cookie[] = []
|
|
199
|
+
let usedChromeProfileImport = false
|
|
200
|
+
|
|
201
|
+
for (const url of options.cookieUrls) {
|
|
202
|
+
const cookies = await readChromeCookiesFn({
|
|
203
|
+
url,
|
|
204
|
+
profile: options.profile,
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
if (cookies.length === 0) {
|
|
208
|
+
throw new Error(`No cookies found for ${url}`)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
importedCookies.push(...cookies)
|
|
212
|
+
usedChromeProfileImport = true
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (options.cookieFile) {
|
|
216
|
+
importedCookies.push(...readCookiesFromFileFn(options.cookieFile))
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (usedChromeProfileImport) {
|
|
220
|
+
warn(CHROME_COOKIE_LIMITATION_WARNING)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return dedupeCookies(importedCookies)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function readPackageVersion(): string {
|
|
227
|
+
const packageJsonPath = path.resolve(__dirname, "..", "package.json")
|
|
228
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as {
|
|
229
|
+
version?: string
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!packageJson.version) {
|
|
233
|
+
throw new Error("Unable to determine cloak version from package.json")
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return packageJson.version
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export async function main(
|
|
240
|
+
argv: string[] = process.argv,
|
|
241
|
+
dependencies: MainDependencies = {}
|
|
242
|
+
) {
|
|
243
|
+
const cli = parseCli(argv)
|
|
244
|
+
const appPaths = dependencies.appPaths ?? resolveAppPaths()
|
|
245
|
+
const writeStdout =
|
|
246
|
+
dependencies.writeStdout ??
|
|
247
|
+
((message: string) => process.stdout.write(message))
|
|
248
|
+
const log = dependencies.log ?? ((message: string) => console.log(message))
|
|
249
|
+
|
|
250
|
+
if (cli.mode === "help") {
|
|
251
|
+
writeStdout(cli.text)
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (cli.mode === "version") {
|
|
256
|
+
log(readPackageVersion())
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (cli.mode === "profile-list") {
|
|
261
|
+
await handleListProfiles(log, dependencies)
|
|
262
|
+
return
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (cli.mode === "profile-show") {
|
|
266
|
+
const stateDb = loadExistingStateDb(appPaths, dependencies)
|
|
267
|
+
const profile = stateDb?.getDefaultProfile()
|
|
268
|
+
|
|
269
|
+
if (!profile) {
|
|
270
|
+
log("No default profile selected. Use `cloak profile use <profile>`.")
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
log(`Current active profile: ${profile}`)
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (cli.mode === "profile-use") {
|
|
279
|
+
const stateDb = await ensureStateDb(appPaths, cli.consent, dependencies)
|
|
280
|
+
|
|
281
|
+
if (!stateDb) {
|
|
282
|
+
log(formatInfo("Aborted."))
|
|
283
|
+
return
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const profile = await resolveChromeProfile(cli.profile, dependencies)
|
|
287
|
+
stateDb.setDefaultProfile(profile.directory)
|
|
288
|
+
log(formatSuccess(`Saved default profile: ${profile.directory}`))
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (cli.mode === "sites-list") {
|
|
293
|
+
await handleCookiesList(cli, appPaths, log, dependencies)
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (cli.mode === "daemon-status") {
|
|
298
|
+
handleInspect(appPaths, log, dependencies)
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (cli.mode === "daemon-logs") {
|
|
303
|
+
handleDaemonLogs(appPaths, log, dependencies)
|
|
304
|
+
return
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (cli.mode === "storage-show") {
|
|
308
|
+
handleStateDisplay(appPaths, log, dependencies)
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (cli.mode === "storage-destroy") {
|
|
313
|
+
await handleStateDestroy(appPaths, log, dependencies)
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (cli.mode === "daemon-stop") {
|
|
318
|
+
await handleStop(appPaths, log, dependencies)
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (cli.mode === "daemon-restart") {
|
|
323
|
+
await handleRestart(appPaths, log, dependencies)
|
|
324
|
+
return
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const resolvedCli = await resolveRunConfig(cli, appPaths, dependencies)
|
|
328
|
+
const runSummary = formatRunSettings(resolvedCli)
|
|
329
|
+
log(runSummary)
|
|
330
|
+
|
|
331
|
+
if (resolvedCli.daemon) {
|
|
332
|
+
await rememberCookieUrls(resolvedCli, appPaths, log, dependencies)
|
|
333
|
+
await handleDaemonRun(resolvedCli, appPaths, log, dependencies)
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const cookies = await resolveStartupCookies(resolvedCli, dependencies)
|
|
338
|
+
await rememberCookieUrls(resolvedCli, appPaths, log, dependencies)
|
|
339
|
+
await launchBrowser(resolvedCli, cookies, appPaths, dependencies)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function handleListProfiles(
|
|
343
|
+
log: (message: string) => void,
|
|
344
|
+
dependencies: Pick<MainDependencies, "hasChromeUserDataDir" | "listChromeProfiles">
|
|
345
|
+
) {
|
|
346
|
+
const hasChrome =
|
|
347
|
+
dependencies.hasChromeUserDataDir ?? hasChromeUserDataDir
|
|
348
|
+
|
|
349
|
+
if (!hasChrome()) {
|
|
350
|
+
log("No Chrome data directory detected on this machine.")
|
|
351
|
+
return
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const listProfiles = dependencies.listChromeProfiles ?? listChromeProfiles
|
|
355
|
+
const profiles = listProfiles()
|
|
356
|
+
|
|
357
|
+
if (profiles.length === 0) {
|
|
358
|
+
log("Listing profiles for Chrome\n(no profiles found)")
|
|
359
|
+
return
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const lines = ["Listing profiles for Chrome"]
|
|
363
|
+
|
|
364
|
+
for (const profile of profiles) {
|
|
365
|
+
const label = profile.accountName ?? profile.name
|
|
366
|
+
|
|
367
|
+
if (!label || label === profile.directory) {
|
|
368
|
+
lines.push(`- ${profile.directory}`)
|
|
369
|
+
continue
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
lines.push(`- ${profile.directory} <${label}>`)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
log(lines.join("\n"))
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function handleCookiesList(
|
|
379
|
+
cli: { limit: number; noPager: boolean; consent: boolean },
|
|
380
|
+
appPaths: AppPaths,
|
|
381
|
+
log: (message: string) => void,
|
|
382
|
+
dependencies: MainDependencies
|
|
383
|
+
) {
|
|
384
|
+
const stateDb = loadExistingStateDb(appPaths, dependencies)
|
|
385
|
+
const profile = stateDb?.getDefaultProfile()
|
|
386
|
+
|
|
387
|
+
if (!profile) {
|
|
388
|
+
throw new Error("No default profile selected. Use `cloak profile use <profile>`.")
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
await resolveChromeProfile(profile, dependencies)
|
|
392
|
+
const listCookieUrls =
|
|
393
|
+
dependencies.listChromeProfileCookieUrls ?? listChromeProfileCookieUrls
|
|
394
|
+
const urls = await listCookieUrls({
|
|
395
|
+
profileDirectory: profile,
|
|
396
|
+
})
|
|
397
|
+
const limitedUrls = urls.slice(0, cli.limit)
|
|
398
|
+
|
|
399
|
+
if (limitedUrls.length === 0) {
|
|
400
|
+
log(`No cookie URLs found for ${profile}.`)
|
|
401
|
+
return
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const lines = [`Cookie URLs for ${profile}`]
|
|
405
|
+
|
|
406
|
+
limitedUrls.forEach((url, index) => {
|
|
407
|
+
lines.push(`${index + 1}. ${url}`)
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
log(lines.join("\n"))
|
|
411
|
+
|
|
412
|
+
if (cli.noPager || !isInteractiveTerminal(dependencies)) {
|
|
413
|
+
return
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const remembered = await selectCookieUrls(
|
|
417
|
+
{
|
|
418
|
+
profile,
|
|
419
|
+
urls: limitedUrls,
|
|
420
|
+
},
|
|
421
|
+
dependencies
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
if (remembered === undefined) {
|
|
425
|
+
return
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const stateDbForWrite = await ensureStateDb(appPaths, cli.consent, dependencies)
|
|
429
|
+
|
|
430
|
+
if (!stateDbForWrite) {
|
|
431
|
+
log(formatInfo("Aborted."))
|
|
432
|
+
return
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
stateDbForWrite.replaceRememberedCookieUrls(profile, remembered)
|
|
436
|
+
|
|
437
|
+
if (remembered.length === 0) {
|
|
438
|
+
log(formatSuccess(`Cleared remembered cookie URLs for ${profile}`))
|
|
439
|
+
return
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
log(formatSuccess(`Saved ${remembered.length} cookie URL(s) for ${profile}`))
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function handleInspect(
|
|
446
|
+
appPaths: AppPaths,
|
|
447
|
+
log: (message: string) => void,
|
|
448
|
+
dependencies: Pick<MainDependencies, "createStateDb" | "pathExists" | "isProcessRunning">
|
|
449
|
+
) {
|
|
450
|
+
const stateDb = loadExistingStateDb(appPaths, dependencies)
|
|
451
|
+
const daemon = stateDb?.getDaemonState()
|
|
452
|
+
|
|
453
|
+
if (!daemon) {
|
|
454
|
+
log("No cloak daemon running.")
|
|
455
|
+
return
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const processIsRunning =
|
|
459
|
+
dependencies.isProcessRunning ?? isProcessRunning
|
|
460
|
+
|
|
461
|
+
if (!processIsRunning(daemon.pid)) {
|
|
462
|
+
stateDb?.clearDaemonState()
|
|
463
|
+
log(`Recorded daemon pid ${daemon.pid} is not running. Cleared stale state.`)
|
|
464
|
+
return
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
log(formatDaemonState(daemon))
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function handleDaemonLogs(
|
|
471
|
+
appPaths: AppPaths,
|
|
472
|
+
log: (message: string) => void,
|
|
473
|
+
dependencies: Pick<MainDependencies, "pathExists" | "readFile">
|
|
474
|
+
) {
|
|
475
|
+
const pathExists = dependencies.pathExists ?? fs.existsSync
|
|
476
|
+
|
|
477
|
+
if (!pathExists(appPaths.daemonLogPath)) {
|
|
478
|
+
log(`No daemon log found at ${appPaths.daemonLogPath}.`)
|
|
479
|
+
return
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const readFile =
|
|
483
|
+
dependencies.readFile ??
|
|
484
|
+
((targetPath: string, encoding: "utf8") => fs.readFileSync(targetPath, encoding))
|
|
485
|
+
const contents = readFile(appPaths.daemonLogPath, "utf8").replace(/\n$/, "")
|
|
486
|
+
|
|
487
|
+
if (contents.length === 0) {
|
|
488
|
+
log(`Daemon log is empty: ${appPaths.daemonLogPath}`)
|
|
489
|
+
return
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
log(contents)
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function handleStateDisplay(
|
|
496
|
+
appPaths: AppPaths,
|
|
497
|
+
log: (message: string) => void,
|
|
498
|
+
dependencies: Pick<
|
|
499
|
+
MainDependencies,
|
|
500
|
+
"createStateDb" | "pathExists" | "isProcessRunning"
|
|
501
|
+
>
|
|
502
|
+
) {
|
|
503
|
+
const pathExists = dependencies.pathExists ?? fs.existsSync
|
|
504
|
+
const stateDbExists = pathExists(appPaths.stateDbPath)
|
|
505
|
+
const daemonLogExists = pathExists(appPaths.daemonLogPath)
|
|
506
|
+
const stateDb = stateDbExists ? loadExistingStateDb(appPaths, dependencies) : undefined
|
|
507
|
+
const defaultProfile = stateDb?.getDefaultProfile()
|
|
508
|
+
const daemon = stateDb?.getDaemonState()
|
|
509
|
+
const processIsRunning = dependencies.isProcessRunning ?? isProcessRunning
|
|
510
|
+
const daemonStatus = !daemon
|
|
511
|
+
? "(none)"
|
|
512
|
+
: processIsRunning(daemon.pid)
|
|
513
|
+
? `running (${daemon.pid})`
|
|
514
|
+
: `stale (${daemon.pid})`
|
|
515
|
+
const lines = [
|
|
516
|
+
"cloak storage",
|
|
517
|
+
`config dir: ${appPaths.configDir}`,
|
|
518
|
+
`sqlite: ${appPaths.stateDbPath}`,
|
|
519
|
+
`sqlite present: ${stateDbExists ? "yes" : "no"}`,
|
|
520
|
+
`daemon log: ${appPaths.daemonLogPath}`,
|
|
521
|
+
`daemon log present: ${daemonLogExists ? "yes" : "no"}`,
|
|
522
|
+
`default profile: ${defaultProfile ?? "(none)"}`,
|
|
523
|
+
`daemon: ${daemonStatus}`,
|
|
524
|
+
]
|
|
525
|
+
|
|
526
|
+
log(lines.join("\n"))
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async function handleStop(
|
|
530
|
+
appPaths: AppPaths,
|
|
531
|
+
log: (message: string) => void,
|
|
532
|
+
dependencies: Pick<
|
|
533
|
+
MainDependencies,
|
|
534
|
+
"createStateDb" | "pathExists" | "isProcessRunning" | "stopProcess"
|
|
535
|
+
>
|
|
536
|
+
) {
|
|
537
|
+
const stateDb = loadExistingStateDb(appPaths, dependencies)
|
|
538
|
+
const daemon = stateDb?.getDaemonState()
|
|
539
|
+
|
|
540
|
+
if (!daemon) {
|
|
541
|
+
log("No cloak daemon running.")
|
|
542
|
+
return
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const processIsRunning =
|
|
546
|
+
dependencies.isProcessRunning ?? isProcessRunning
|
|
547
|
+
|
|
548
|
+
if (!processIsRunning(daemon.pid)) {
|
|
549
|
+
stateDb?.clearDaemonState()
|
|
550
|
+
log(`Recorded daemon pid ${daemon.pid} is not running. Cleared stale state.`)
|
|
551
|
+
return
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const stopProcessFn = dependencies.stopProcess ?? stopProcess
|
|
555
|
+
await stopProcessFn(daemon.pid)
|
|
556
|
+
stateDb?.clearDaemonState()
|
|
557
|
+
log(formatSuccess(`Stopped cloak daemon (${daemon.pid})`))
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async function handleStateDestroy(
|
|
561
|
+
appPaths: AppPaths,
|
|
562
|
+
log: (message: string) => void,
|
|
563
|
+
dependencies: Pick<
|
|
564
|
+
MainDependencies,
|
|
565
|
+
| "confirmDestroyState"
|
|
566
|
+
| "createStateDb"
|
|
567
|
+
| "isProcessRunning"
|
|
568
|
+
| "pathExists"
|
|
569
|
+
| "removeDir"
|
|
570
|
+
| "stdinIsTTY"
|
|
571
|
+
| "stdoutIsTTY"
|
|
572
|
+
| "stopProcess"
|
|
573
|
+
>
|
|
574
|
+
) {
|
|
575
|
+
const pathExists = dependencies.pathExists ?? fs.existsSync
|
|
576
|
+
|
|
577
|
+
if (!pathExists(appPaths.configDir)) {
|
|
578
|
+
log("No cloak storage to destroy.")
|
|
579
|
+
return
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (!isInteractiveTerminal(dependencies)) {
|
|
583
|
+
throw new Error("`cloak storage destroy` requires an interactive terminal.")
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const confirmDestroyState =
|
|
587
|
+
dependencies.confirmDestroyState ?? defaultConfirmDestroyState
|
|
588
|
+
|
|
589
|
+
if (!(await confirmDestroyState(appPaths.configDir))) {
|
|
590
|
+
log(formatInfo("Aborted."))
|
|
591
|
+
return
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const stateDb = loadExistingStateDb(appPaths, dependencies)
|
|
595
|
+
const daemon = stateDb?.getDaemonState()
|
|
596
|
+
const processIsRunning = dependencies.isProcessRunning ?? isProcessRunning
|
|
597
|
+
|
|
598
|
+
if (daemon && processIsRunning(daemon.pid)) {
|
|
599
|
+
const stopProcessFn = dependencies.stopProcess ?? stopProcess
|
|
600
|
+
await stopProcessFn(daemon.pid)
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const removeDir = dependencies.removeDir ?? fs.rmSync
|
|
604
|
+
removeDir(appPaths.configDir, { recursive: true, force: true })
|
|
605
|
+
log(formatSuccess(`Destroyed cloak storage under ${appPaths.configDir}`))
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
async function handleRestart(
|
|
609
|
+
appPaths: AppPaths,
|
|
610
|
+
log: (message: string) => void,
|
|
611
|
+
dependencies: MainDependencies
|
|
612
|
+
) {
|
|
613
|
+
const stateDb = loadExistingStateDb(appPaths, dependencies)
|
|
614
|
+
|
|
615
|
+
if (!stateDb) {
|
|
616
|
+
throw new Error("No saved daemon command found. Run `cloak daemon start` first.")
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const currentDaemon = stateDb.getDaemonState()
|
|
620
|
+
const processIsRunning =
|
|
621
|
+
dependencies.isProcessRunning ?? isProcessRunning
|
|
622
|
+
let command = stateDb.getLastDaemonCommand()
|
|
623
|
+
|
|
624
|
+
if (currentDaemon) {
|
|
625
|
+
command = {
|
|
626
|
+
headless: currentDaemon.headless,
|
|
627
|
+
profile: currentDaemon.profile,
|
|
628
|
+
cookieFile: currentDaemon.cookieFile,
|
|
629
|
+
cookieUrls: currentDaemon.cookieUrls,
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (processIsRunning(currentDaemon.pid)) {
|
|
633
|
+
const stopProcessFn = dependencies.stopProcess ?? stopProcess
|
|
634
|
+
await stopProcessFn(currentDaemon.pid)
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
stateDb.clearDaemonState()
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (!command) {
|
|
641
|
+
throw new Error("No saved daemon command found. Run `cloak daemon start` first.")
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
await startDaemon(command, appPaths, log, dependencies)
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
async function resolveRunConfig(
|
|
648
|
+
cli: RunModeConfig,
|
|
649
|
+
appPaths: AppPaths,
|
|
650
|
+
dependencies: MainDependencies
|
|
651
|
+
): Promise<ResolvedRunConfig> {
|
|
652
|
+
const stateDb =
|
|
653
|
+
cli.daemon || cli.persistCookies
|
|
654
|
+
? await ensureStateDb(appPaths, cli.consent, dependencies)
|
|
655
|
+
: loadExistingStateDb(appPaths, dependencies)
|
|
656
|
+
|
|
657
|
+
if ((cli.daemon || cli.persistCookies) && !stateDb) {
|
|
658
|
+
throw new Error("Config directory creation was declined.")
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const defaultProfile = cli.profile ?? stateDb?.getDefaultProfile()
|
|
662
|
+
const profile = defaultProfile
|
|
663
|
+
? (await resolveChromeProfile(defaultProfile, dependencies)).directory
|
|
664
|
+
: undefined
|
|
665
|
+
const cookieFile = cli.cookieFile
|
|
666
|
+
? path.resolve(cli.cookieFile)
|
|
667
|
+
: undefined
|
|
668
|
+
const explicitCookieUrls = cli.cookieUrls
|
|
669
|
+
const rememberedCookieUrls =
|
|
670
|
+
explicitCookieUrls.length === 0 && profile && stateDb
|
|
671
|
+
? stateDb.getRememberedCookieUrls(profile)
|
|
672
|
+
: []
|
|
673
|
+
const cookieUrls =
|
|
674
|
+
explicitCookieUrls.length > 0 ? explicitCookieUrls : rememberedCookieUrls
|
|
675
|
+
|
|
676
|
+
if ((cookieUrls.length > 0 || cli.persistCookies) && !profile) {
|
|
677
|
+
throw new Error(
|
|
678
|
+
"A Chrome profile is required. Use `cloak profile use <profile>` or pass --profile."
|
|
679
|
+
)
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return {
|
|
683
|
+
headless: cli.headless,
|
|
684
|
+
daemon: cli.daemon,
|
|
685
|
+
profile,
|
|
686
|
+
cookieUrls,
|
|
687
|
+
cookieFile,
|
|
688
|
+
explicitCookieUrls,
|
|
689
|
+
persistCookies: cli.persistCookies,
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
async function handleDaemonRun(
|
|
694
|
+
config: ResolvedRunConfig,
|
|
695
|
+
appPaths: AppPaths,
|
|
696
|
+
log: (message: string) => void,
|
|
697
|
+
dependencies: MainDependencies
|
|
698
|
+
) {
|
|
699
|
+
const stateDb = await ensureStateDb(appPaths, true, dependencies)
|
|
700
|
+
|
|
701
|
+
if (!stateDb) {
|
|
702
|
+
throw new Error("Unable to create cloak storage.")
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const existing = stateDb.getDaemonState()
|
|
706
|
+
const processIsRunning =
|
|
707
|
+
dependencies.isProcessRunning ?? isProcessRunning
|
|
708
|
+
|
|
709
|
+
if (existing) {
|
|
710
|
+
if (processIsRunning(existing.pid)) {
|
|
711
|
+
throw new Error("A cloak daemon is already running. Use `cloak daemon restart` or `cloak daemon stop`.")
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
stateDb.clearDaemonState()
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
await startDaemon(
|
|
718
|
+
{
|
|
719
|
+
headless: config.headless,
|
|
720
|
+
profile: config.profile,
|
|
721
|
+
cookieFile: config.cookieFile,
|
|
722
|
+
cookieUrls: config.cookieUrls,
|
|
723
|
+
},
|
|
724
|
+
appPaths,
|
|
725
|
+
log,
|
|
726
|
+
dependencies
|
|
727
|
+
)
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
async function startDaemon(
|
|
731
|
+
command: StoredRunCommand,
|
|
732
|
+
appPaths: AppPaths,
|
|
733
|
+
log: (message: string) => void,
|
|
734
|
+
dependencies: MainDependencies
|
|
735
|
+
) {
|
|
736
|
+
const stateDb = await ensureStateDb(appPaths, true, dependencies)
|
|
737
|
+
|
|
738
|
+
if (!stateDb) {
|
|
739
|
+
throw new Error("Unable to create cloak storage.")
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const spawnDaemonProcessFn =
|
|
743
|
+
dependencies.spawnDaemonProcess ?? spawnDaemonProcess
|
|
744
|
+
const makeDir = dependencies.makeDir ?? fs.mkdirSync
|
|
745
|
+
const now = dependencies.now ?? (() => new Date())
|
|
746
|
+
makeDir(appPaths.configDir, { recursive: true })
|
|
747
|
+
makeDir(path.dirname(appPaths.daemonLogPath), { recursive: true })
|
|
748
|
+
const pid = spawnDaemonProcessFn({
|
|
749
|
+
execPath: dependencies.processExecPath ?? process.execPath,
|
|
750
|
+
execArgv: dependencies.processExecArgv ?? process.execArgv,
|
|
751
|
+
scriptPath: dependencies.scriptPath ?? process.argv[1],
|
|
752
|
+
command,
|
|
753
|
+
logPath: appPaths.daemonLogPath,
|
|
754
|
+
})
|
|
755
|
+
|
|
756
|
+
stateDb.setLastDaemonCommand(command)
|
|
757
|
+
stateDb.setDaemonState({
|
|
758
|
+
...command,
|
|
759
|
+
pid,
|
|
760
|
+
startedAt: now().toISOString(),
|
|
761
|
+
logPath: appPaths.daemonLogPath,
|
|
762
|
+
})
|
|
763
|
+
log(formatSuccess(`Started cloak daemon (${pid})`))
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
async function rememberCookieUrls(
|
|
767
|
+
config: ResolvedRunConfig,
|
|
768
|
+
appPaths: AppPaths,
|
|
769
|
+
log: (message: string) => void,
|
|
770
|
+
dependencies: MainDependencies
|
|
771
|
+
) {
|
|
772
|
+
if (!config.persistCookies || !config.profile || config.explicitCookieUrls.length === 0) {
|
|
773
|
+
return
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const stateDb = await ensureStateDb(appPaths, true, dependencies)
|
|
777
|
+
|
|
778
|
+
if (!stateDb) {
|
|
779
|
+
throw new Error("Unable to create cloak storage.")
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const remembered = stateDb.rememberCookieUrls(
|
|
783
|
+
config.profile,
|
|
784
|
+
config.explicitCookieUrls
|
|
785
|
+
)
|
|
786
|
+
log(formatSuccess(`Remembered ${remembered.length} cookie URL(s) for ${config.profile}`))
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
async function launchBrowser(
|
|
790
|
+
config: ResolvedRunConfig,
|
|
791
|
+
cookies: Cookie[],
|
|
792
|
+
appPaths: AppPaths,
|
|
793
|
+
dependencies: MainDependencies
|
|
794
|
+
) {
|
|
795
|
+
const prepareExtension =
|
|
796
|
+
dependencies.prepareRequiredExtension ?? prepareRequiredExtension
|
|
797
|
+
const launchPersistentContext =
|
|
798
|
+
dependencies.patchrightLaunchPersistentContext ??
|
|
799
|
+
dependencies.launchPersistentContext ??
|
|
800
|
+
patchrightChromium.launchPersistentContext.bind(patchrightChromium)
|
|
801
|
+
const makeTempDir = dependencies.makeTempDir ?? fs.mkdtempSync
|
|
802
|
+
const makeDir = dependencies.makeDir ?? fs.mkdirSync
|
|
803
|
+
const writeFile = dependencies.writeFile ?? fs.writeFileSync
|
|
804
|
+
const extensionPath = await prepareExtension(appPaths.extensionsDir)
|
|
805
|
+
const args = [
|
|
806
|
+
`--disable-extensions-except=${extensionPath}`,
|
|
807
|
+
`--load-extension=${extensionPath}`,
|
|
808
|
+
]
|
|
809
|
+
|
|
810
|
+
const userDataDir = makeTempDir(path.join(os.tmpdir(), "cloak-profile-"))
|
|
811
|
+
const defaultDir = path.join(userDataDir, "Default")
|
|
812
|
+
makeDir(defaultDir, { recursive: true })
|
|
813
|
+
writeFile(
|
|
814
|
+
path.join(defaultDir, "Preferences"),
|
|
815
|
+
JSON.stringify({
|
|
816
|
+
extensions: { ui: { developer_mode: true } },
|
|
817
|
+
})
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
const executablePath =
|
|
821
|
+
dependencies.patchrightExecutablePath ??
|
|
822
|
+
patchrightChromium.executablePath.bind(patchrightChromium)
|
|
823
|
+
const context = await launchPersistentContext(userDataDir, {
|
|
824
|
+
headless: config.headless,
|
|
825
|
+
executablePath: executablePath(),
|
|
826
|
+
args,
|
|
827
|
+
})
|
|
828
|
+
|
|
829
|
+
if (cookies.length > 0) {
|
|
830
|
+
await context.addCookies(cookies)
|
|
831
|
+
console.log(formatSuccess(`Injected ${cookies.length} cookies`))
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
console.log(formatInfo("Browser running. Ctrl+C to exit."))
|
|
835
|
+
|
|
836
|
+
const browser = context.browser()
|
|
837
|
+
let onSigint: (() => void) | undefined
|
|
838
|
+
let onSigterm: (() => void) | undefined
|
|
839
|
+
const handleSignal = () => {
|
|
840
|
+
console.log(formatInfo("\nShutting down..."))
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
try {
|
|
844
|
+
await new Promise<void>((resolve) => {
|
|
845
|
+
let settled = false
|
|
846
|
+
const finish = () => {
|
|
847
|
+
if (settled) {
|
|
848
|
+
return
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
settled = true
|
|
852
|
+
resolve()
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
onSigint = () => {
|
|
856
|
+
handleSignal()
|
|
857
|
+
finish()
|
|
858
|
+
}
|
|
859
|
+
onSigterm = () => {
|
|
860
|
+
handleSignal()
|
|
861
|
+
finish()
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
context.on("close", finish)
|
|
865
|
+
browser?.on("disconnected", finish)
|
|
866
|
+
process.on("SIGINT", onSigint)
|
|
867
|
+
process.on("SIGTERM", onSigterm)
|
|
868
|
+
})
|
|
869
|
+
} finally {
|
|
870
|
+
if (onSigint) {
|
|
871
|
+
process.removeListener("SIGINT", onSigint)
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
if (onSigterm) {
|
|
875
|
+
process.removeListener("SIGTERM", onSigterm)
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
loadExistingStateDb(appPaths, dependencies)?.clearDaemonState(process.pid)
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
await context.close().catch(() => {})
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
async function resolveChromeProfile(
|
|
885
|
+
profileName: string,
|
|
886
|
+
dependencies: Pick<MainDependencies, "listChromeProfiles">
|
|
887
|
+
): Promise<ChromeProfile> {
|
|
888
|
+
const listProfiles = dependencies.listChromeProfiles ?? listChromeProfiles
|
|
889
|
+
const profiles = listProfiles()
|
|
890
|
+
const profile = profiles.find((candidate) => candidate.directory === profileName)
|
|
891
|
+
|
|
892
|
+
if (!profile) {
|
|
893
|
+
throw new Error(`Chrome profile not found: ${profileName}`)
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
return profile
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function formatRunSettings(config: ResolvedRunConfig): string {
|
|
900
|
+
const lines = [
|
|
901
|
+
"Running with settings",
|
|
902
|
+
`profile: ${config.profile ?? "(none)"}`,
|
|
903
|
+
`cookie file: ${config.cookieFile ?? "(none)"}`,
|
|
904
|
+
"cookie urls:",
|
|
905
|
+
]
|
|
906
|
+
|
|
907
|
+
if (config.cookieUrls.length === 0) {
|
|
908
|
+
lines.push(" (none)")
|
|
909
|
+
} else {
|
|
910
|
+
for (const url of config.cookieUrls) {
|
|
911
|
+
lines.push(` - ${url}`)
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
return lines.join("\n")
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function formatDaemonState(state: DaemonState): string {
|
|
919
|
+
const lines = [
|
|
920
|
+
"cloak daemon",
|
|
921
|
+
`pid: ${state.pid}`,
|
|
922
|
+
"status: running",
|
|
923
|
+
`profile: ${state.profile ?? "(none)"}`,
|
|
924
|
+
`cookie file: ${state.cookieFile ?? "(none)"}`,
|
|
925
|
+
`headless: ${state.headless ? "yes" : "no"}`,
|
|
926
|
+
`started at: ${state.startedAt}`,
|
|
927
|
+
`log: ${state.logPath}`,
|
|
928
|
+
"cookie urls:",
|
|
929
|
+
]
|
|
930
|
+
|
|
931
|
+
if (state.cookieUrls.length === 0) {
|
|
932
|
+
lines.push(" (none)")
|
|
933
|
+
} else {
|
|
934
|
+
for (const url of state.cookieUrls) {
|
|
935
|
+
lines.push(` - ${url}`)
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
return lines.join("\n")
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function loadExistingStateDb(
|
|
943
|
+
appPaths: AppPaths,
|
|
944
|
+
dependencies: Pick<MainDependencies, "createStateDb" | "pathExists">
|
|
945
|
+
): CloakStateDb | undefined {
|
|
946
|
+
const pathExists = dependencies.pathExists ?? fs.existsSync
|
|
947
|
+
|
|
948
|
+
if (!pathExists(appPaths.stateDbPath)) {
|
|
949
|
+
return undefined
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const createStateDb = dependencies.createStateDb ?? ((dbPath: string) => new CloakStateDb(dbPath))
|
|
953
|
+
return createStateDb(appPaths.stateDbPath)
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
async function ensureStateDb(
|
|
957
|
+
appPaths: AppPaths,
|
|
958
|
+
consent: boolean,
|
|
959
|
+
dependencies: Pick<
|
|
960
|
+
MainDependencies,
|
|
961
|
+
| "createStateDb"
|
|
962
|
+
| "confirmCreateConfigDir"
|
|
963
|
+
| "makeDir"
|
|
964
|
+
| "pathExists"
|
|
965
|
+
| "stdinIsTTY"
|
|
966
|
+
| "stdoutIsTTY"
|
|
967
|
+
>
|
|
968
|
+
): Promise<CloakStateDb | undefined> {
|
|
969
|
+
const pathExists = dependencies.pathExists ?? fs.existsSync
|
|
970
|
+
const existing = loadExistingStateDb(appPaths, dependencies)
|
|
971
|
+
|
|
972
|
+
if (existing) {
|
|
973
|
+
return existing
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
const makeDir = dependencies.makeDir ?? fs.mkdirSync
|
|
977
|
+
const configDirExists = pathExists(appPaths.configDir)
|
|
978
|
+
|
|
979
|
+
if (!configDirExists && !consent) {
|
|
980
|
+
if (!isInteractiveTerminal(dependencies)) {
|
|
981
|
+
throw new Error(
|
|
982
|
+
`Config directory ${appPaths.configDir} does not exist. Re-run with --consent to create it.`
|
|
983
|
+
)
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
const confirmCreateConfigDir =
|
|
987
|
+
dependencies.confirmCreateConfigDir ?? defaultConfirmCreateConfigDir
|
|
988
|
+
|
|
989
|
+
if (!(await confirmCreateConfigDir(appPaths.configDir))) {
|
|
990
|
+
return undefined
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
makeDir(appPaths.configDir, { recursive: true })
|
|
995
|
+
const createStateDb = dependencies.createStateDb ?? ((dbPath: string) => new CloakStateDb(dbPath))
|
|
996
|
+
return createStateDb(appPaths.stateDbPath)
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function isInteractiveTerminal(
|
|
1000
|
+
dependencies: Pick<MainDependencies, "stdinIsTTY" | "stdoutIsTTY">
|
|
1001
|
+
): boolean {
|
|
1002
|
+
const stdinIsTTY = dependencies.stdinIsTTY ?? process.stdin.isTTY ?? false
|
|
1003
|
+
const stdoutIsTTY = dependencies.stdoutIsTTY ?? process.stdout.isTTY ?? false
|
|
1004
|
+
return stdinIsTTY && stdoutIsTTY
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
async function defaultConfirmCreateConfigDir(configDir: string): Promise<boolean> {
|
|
1008
|
+
const rl = readline.createInterface({
|
|
1009
|
+
input: process.stdin,
|
|
1010
|
+
output: process.stdout,
|
|
1011
|
+
})
|
|
1012
|
+
|
|
1013
|
+
try {
|
|
1014
|
+
const answer = await rl.question(`Create ${configDir}? [y/N] `)
|
|
1015
|
+
return /^(y|yes)$/i.test(answer.trim())
|
|
1016
|
+
} finally {
|
|
1017
|
+
rl.close()
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
async function defaultConfirmDestroyState(configDir: string): Promise<boolean> {
|
|
1022
|
+
const rl = readline.createInterface({
|
|
1023
|
+
input: process.stdin,
|
|
1024
|
+
output: process.stdout,
|
|
1025
|
+
})
|
|
1026
|
+
|
|
1027
|
+
try {
|
|
1028
|
+
const answer = await rl.question(
|
|
1029
|
+
`Destroy cloak storage under ${configDir}? This cannot be undone. [y/N] `
|
|
1030
|
+
)
|
|
1031
|
+
return /^(y|yes)$/i.test(answer.trim())
|
|
1032
|
+
} finally {
|
|
1033
|
+
rl.close()
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
async function selectCookieUrls(
|
|
1038
|
+
options: SelectCookieUrlsOptions,
|
|
1039
|
+
dependencies: Pick<MainDependencies, "selectCookieUrls">
|
|
1040
|
+
): Promise<string[] | undefined> {
|
|
1041
|
+
const promptFn = dependencies.selectCookieUrls ?? defaultSelectCookieUrls
|
|
1042
|
+
return promptFn(options)
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
async function defaultSelectCookieUrls(
|
|
1046
|
+
options: SelectCookieUrlsOptions
|
|
1047
|
+
): Promise<string[] | undefined> {
|
|
1048
|
+
const rl = readline.createInterface({
|
|
1049
|
+
input: process.stdin,
|
|
1050
|
+
output: process.stdout,
|
|
1051
|
+
})
|
|
1052
|
+
|
|
1053
|
+
try {
|
|
1054
|
+
while (true) {
|
|
1055
|
+
const answer = await rl.question(
|
|
1056
|
+
`Remember URLs for ${options.profile}? [all, none, 1,3-5, Enter to skip] `
|
|
1057
|
+
)
|
|
1058
|
+
|
|
1059
|
+
try {
|
|
1060
|
+
const indexes = parseSelectionInput(answer, options.urls.length)
|
|
1061
|
+
|
|
1062
|
+
if (indexes === undefined) {
|
|
1063
|
+
return undefined
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
return indexes.map((index) => options.urls[index])
|
|
1067
|
+
} catch (error) {
|
|
1068
|
+
console.log(formatWarning(String(error)))
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
} finally {
|
|
1072
|
+
rl.close()
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
if (require.main === module) {
|
|
1077
|
+
main().catch((error) => {
|
|
1078
|
+
console.error(formatError(String(error)))
|
|
1079
|
+
process.exit(1)
|
|
1080
|
+
})
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
export function daemonCommandToArgs(command: StoredRunCommand): string[] {
|
|
1084
|
+
return buildRunArguments(command)
|
|
1085
|
+
}
|