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/cookies.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
|
|
3
|
+
export interface Cookie {
|
|
4
|
+
name: string;
|
|
5
|
+
value: string;
|
|
6
|
+
domain: string;
|
|
7
|
+
path: string;
|
|
8
|
+
expires?: number;
|
|
9
|
+
httpOnly?: boolean;
|
|
10
|
+
secure?: boolean;
|
|
11
|
+
sameSite?: "Strict" | "Lax" | "None";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface BrowserExportCookie {
|
|
15
|
+
name: string;
|
|
16
|
+
value: string;
|
|
17
|
+
domain: string;
|
|
18
|
+
path: string;
|
|
19
|
+
expirationDate?: number;
|
|
20
|
+
hostOnly?: boolean;
|
|
21
|
+
httpOnly?: boolean;
|
|
22
|
+
secure?: boolean;
|
|
23
|
+
session?: boolean;
|
|
24
|
+
sameSite?: string;
|
|
25
|
+
storeId?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type CookieFileEntry = Cookie | BrowserExportCookie;
|
|
29
|
+
|
|
30
|
+
function normalizeSameSite(
|
|
31
|
+
sameSite: string | undefined
|
|
32
|
+
): Cookie["sameSite"] | undefined {
|
|
33
|
+
if (!sameSite) return undefined;
|
|
34
|
+
|
|
35
|
+
switch (sameSite.toLowerCase()) {
|
|
36
|
+
case "strict":
|
|
37
|
+
return "Strict";
|
|
38
|
+
case "lax":
|
|
39
|
+
return "Lax";
|
|
40
|
+
case "none":
|
|
41
|
+
case "no_restriction":
|
|
42
|
+
return "None";
|
|
43
|
+
case "unspecified":
|
|
44
|
+
return undefined;
|
|
45
|
+
default:
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function normalizeCookie(raw: Cookie | BrowserExportCookie): Cookie {
|
|
51
|
+
const cookie: Cookie = {
|
|
52
|
+
name: raw.name,
|
|
53
|
+
value: raw.value,
|
|
54
|
+
domain: raw.domain,
|
|
55
|
+
path: raw.path,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
if (typeof raw.httpOnly === "boolean") {
|
|
59
|
+
cookie.httpOnly = raw.httpOnly;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (typeof raw.secure === "boolean") {
|
|
63
|
+
cookie.secure = raw.secure;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if ("expires" in raw && typeof raw.expires === "number") {
|
|
67
|
+
cookie.expires = Math.trunc(raw.expires);
|
|
68
|
+
} else if (
|
|
69
|
+
"expirationDate" in raw &&
|
|
70
|
+
typeof raw.expirationDate === "number"
|
|
71
|
+
) {
|
|
72
|
+
cookie.expires = Math.trunc(raw.expirationDate);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const sameSite = normalizeSameSite(raw.sameSite);
|
|
76
|
+
if (sameSite) {
|
|
77
|
+
cookie.sameSite = sameSite;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return cookie;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isCookieEntry(value: unknown): value is CookieFileEntry {
|
|
84
|
+
return (
|
|
85
|
+
Boolean(value) &&
|
|
86
|
+
typeof value === "object" &&
|
|
87
|
+
typeof (value as CookieFileEntry).name === "string" &&
|
|
88
|
+
typeof (value as CookieFileEntry).value === "string" &&
|
|
89
|
+
typeof (value as CookieFileEntry).domain === "string" &&
|
|
90
|
+
typeof (value as CookieFileEntry).path === "string"
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function parseCookieFileContents(
|
|
95
|
+
value: unknown,
|
|
96
|
+
sourceLabel: string
|
|
97
|
+
): CookieFileEntry[] {
|
|
98
|
+
let entries: unknown[] | undefined;
|
|
99
|
+
|
|
100
|
+
if (Array.isArray(value)) {
|
|
101
|
+
entries = value;
|
|
102
|
+
} else if (
|
|
103
|
+
value &&
|
|
104
|
+
typeof value === "object" &&
|
|
105
|
+
"cookies" in value &&
|
|
106
|
+
Array.isArray(value.cookies)
|
|
107
|
+
) {
|
|
108
|
+
entries = value.cookies;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!entries) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`${sourceLabel} must contain a JSON array of cookies or a JSON object with a cookies array`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return entries.map((entry: unknown, index: number) => {
|
|
118
|
+
if (!isCookieEntry(entry)) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`${sourceLabel} contains an invalid cookie at index ${index}`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return entry;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function parseCookieFile(
|
|
129
|
+
contents: string,
|
|
130
|
+
sourceLabel = "cookie file"
|
|
131
|
+
): Cookie[] {
|
|
132
|
+
let parsed: unknown;
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
parsed = JSON.parse(contents);
|
|
136
|
+
} catch {
|
|
137
|
+
throw new Error(`${sourceLabel} must contain valid JSON`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return parseCookieFileContents(parsed, sourceLabel).map(normalizeCookie);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function readCookiesFromFile(
|
|
144
|
+
filePath: string,
|
|
145
|
+
readFile: (path: string, encoding: "utf8") => string = fs.readFileSync
|
|
146
|
+
): Cookie[] {
|
|
147
|
+
const contents = readFile(filePath, "utf8");
|
|
148
|
+
return parseCookieFile(contents, filePath);
|
|
149
|
+
}
|
package/src/daemon.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import childProcess from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import type { StoredRunCommand } from "./state-db.js";
|
|
4
|
+
|
|
5
|
+
type SpawnedProcess = {
|
|
6
|
+
pid?: number;
|
|
7
|
+
unref(): void;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type SpawnOptions = {
|
|
11
|
+
execPath: string;
|
|
12
|
+
execArgv: string[];
|
|
13
|
+
scriptPath: string;
|
|
14
|
+
command: StoredRunCommand;
|
|
15
|
+
logPath: string;
|
|
16
|
+
spawnProcess?: typeof childProcess.spawn;
|
|
17
|
+
openFile?: (path: string, flags: string) => number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type StopProcessDependencies = {
|
|
21
|
+
killProcess?: (pid: number, signal?: NodeJS.Signals | number) => void;
|
|
22
|
+
isProcessRunning?: (pid: number) => boolean;
|
|
23
|
+
sleep?: (milliseconds: number) => Promise<void>;
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
intervalMs?: number;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function buildRunArguments(command: StoredRunCommand): string[] {
|
|
29
|
+
const args = ["run"];
|
|
30
|
+
|
|
31
|
+
if (!command.headless) {
|
|
32
|
+
args.push("--window");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (command.profile) {
|
|
36
|
+
args.push("--profile", command.profile);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (command.cookieFile) {
|
|
40
|
+
args.push("--cookie-file", command.cookieFile);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for (const url of command.cookieUrls) {
|
|
44
|
+
args.push("--cookie-url", url);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return args;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function spawnDaemonProcess(options: SpawnOptions): number {
|
|
51
|
+
const spawnProcess = options.spawnProcess ?? childProcess.spawn;
|
|
52
|
+
const openFile = options.openFile ?? fs.openSync;
|
|
53
|
+
const logFd = openFile(options.logPath, "a");
|
|
54
|
+
const child = spawnProcess(
|
|
55
|
+
options.execPath,
|
|
56
|
+
[
|
|
57
|
+
...options.execArgv,
|
|
58
|
+
options.scriptPath,
|
|
59
|
+
...buildRunArguments(options.command),
|
|
60
|
+
],
|
|
61
|
+
{
|
|
62
|
+
detached: true,
|
|
63
|
+
stdio: ["ignore", logFd, logFd],
|
|
64
|
+
}
|
|
65
|
+
) as SpawnedProcess;
|
|
66
|
+
|
|
67
|
+
child.unref();
|
|
68
|
+
|
|
69
|
+
if (typeof child.pid !== "number") {
|
|
70
|
+
throw new Error("Failed to start cloak daemon.");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return child.pid;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function isProcessRunning(
|
|
77
|
+
pid: number,
|
|
78
|
+
killProcess: (pid: number, signal?: NodeJS.Signals | number) => void = process.kill
|
|
79
|
+
): boolean {
|
|
80
|
+
try {
|
|
81
|
+
killProcess(pid, 0);
|
|
82
|
+
return true;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (
|
|
85
|
+
error &&
|
|
86
|
+
typeof error === "object" &&
|
|
87
|
+
"code" in error &&
|
|
88
|
+
(error as { code?: string }).code === "ESRCH"
|
|
89
|
+
) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function stopProcess(
|
|
98
|
+
pid: number,
|
|
99
|
+
dependencies: StopProcessDependencies = {}
|
|
100
|
+
): Promise<boolean> {
|
|
101
|
+
const killProcess = dependencies.killProcess ?? process.kill;
|
|
102
|
+
const processIsRunning =
|
|
103
|
+
dependencies.isProcessRunning ?? ((candidatePid: number) => isProcessRunning(candidatePid, killProcess));
|
|
104
|
+
const sleep =
|
|
105
|
+
dependencies.sleep ?? ((milliseconds: number) => new Promise((resolve) => setTimeout(resolve, milliseconds)));
|
|
106
|
+
const timeoutMs = dependencies.timeoutMs ?? 5000;
|
|
107
|
+
const intervalMs = dependencies.intervalMs ?? 100;
|
|
108
|
+
|
|
109
|
+
if (!processIsRunning(pid)) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
killProcess(pid, "SIGTERM");
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (
|
|
117
|
+
error &&
|
|
118
|
+
typeof error === "object" &&
|
|
119
|
+
"code" in error &&
|
|
120
|
+
(error as { code?: string }).code === "ESRCH"
|
|
121
|
+
) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const deadline = Date.now() + timeoutMs;
|
|
129
|
+
|
|
130
|
+
while (Date.now() < deadline) {
|
|
131
|
+
if (!processIsRunning(pid)) {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
await sleep(intervalMs);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (processIsRunning(pid)) {
|
|
139
|
+
killProcess(pid, "SIGKILL");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return !processIsRunning(pid);
|
|
143
|
+
}
|
package/src/extension.ts
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import AdmZip from "adm-zip";
|
|
6
|
+
import { defaultAppRootDir } from "./app-paths.js";
|
|
7
|
+
import { formatInfo } from "./output.js";
|
|
8
|
+
|
|
9
|
+
export const REQUIRED_EXTENSION_URL =
|
|
10
|
+
"https://github.com/jackwener/opencli/releases/download/v1.6.8/opencli-extension.zip";
|
|
11
|
+
export const REQUIRED_EXTENSION_ARCHIVE_NAME = "opencli-extension.zip";
|
|
12
|
+
export const REQUIRED_EXTENSION_SHA256 =
|
|
13
|
+
"a5f51d111e49e7215191a80c2dc822d4ea94950c9e239dc077862a8044bc8d2e";
|
|
14
|
+
|
|
15
|
+
type DownloadRequiredExtensionDependencies = {
|
|
16
|
+
fetchImpl?: typeof fetch;
|
|
17
|
+
validateExtensionArchive?: (archivePath: string) => void;
|
|
18
|
+
log?: (message: string) => void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type EnsureRequiredExtensionDependencies = {
|
|
22
|
+
validateExtensionArchive?: (archivePath: string) => void;
|
|
23
|
+
downloadRequiredExtension?: (
|
|
24
|
+
extensionsDir: string,
|
|
25
|
+
dependencies?: DownloadRequiredExtensionDependencies
|
|
26
|
+
) => Promise<string>;
|
|
27
|
+
log?: (message: string) => void;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type PrepareRequiredExtensionDependencies = {
|
|
31
|
+
ensureRequiredExtensionArchive?: (
|
|
32
|
+
extensionsDir: string,
|
|
33
|
+
dependencies?: EnsureRequiredExtensionDependencies
|
|
34
|
+
) => Promise<string>;
|
|
35
|
+
makeTempDir?: (prefix: string) => string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function validateExtensionArchive(archivePath: string): void {
|
|
39
|
+
let zip: AdmZip;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
zip = new AdmZip(archivePath);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
throw new Error(`Invalid required extension archive at ${archivePath}`, {
|
|
45
|
+
cause: error,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const entryNames = zip
|
|
50
|
+
.getEntries()
|
|
51
|
+
.filter((entry) => !entry.isDirectory)
|
|
52
|
+
.map((entry) => normalizeZipEntryName(entry.entryName));
|
|
53
|
+
|
|
54
|
+
if (entryNames.length === 0) {
|
|
55
|
+
throw new Error(`Required extension archive is empty: ${archivePath}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!findManifestEntry(entryNames)) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Required extension archive must contain manifest.json at the root or one directory deep: ${archivePath}`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const archiveDigest = createHash("sha256")
|
|
65
|
+
.update(fs.readFileSync(archivePath))
|
|
66
|
+
.digest("hex");
|
|
67
|
+
|
|
68
|
+
if (archiveDigest !== REQUIRED_EXTENSION_SHA256) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Required extension archive digest mismatch at ${archivePath}: expected ${REQUIRED_EXTENSION_SHA256}, got ${archiveDigest}`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function downloadRequiredExtensionArchive(
|
|
76
|
+
extensionsDir: string,
|
|
77
|
+
dependencies: DownloadRequiredExtensionDependencies = {}
|
|
78
|
+
): Promise<string> {
|
|
79
|
+
const fetchImpl = dependencies.fetchImpl ?? fetch;
|
|
80
|
+
const validateArchive = dependencies.validateExtensionArchive ?? validateExtensionArchive;
|
|
81
|
+
const log = dependencies.log ?? ((message: string) => console.log(formatInfo(message)));
|
|
82
|
+
const archivePath = path.join(extensionsDir, REQUIRED_EXTENSION_ARCHIVE_NAME);
|
|
83
|
+
const tempArchivePath = path.join(
|
|
84
|
+
extensionsDir,
|
|
85
|
+
`${REQUIRED_EXTENSION_ARCHIVE_NAME}.download-${process.pid}-${Date.now()}`
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
fs.mkdirSync(extensionsDir, { recursive: true });
|
|
89
|
+
|
|
90
|
+
const response = await fetchImpl(REQUIRED_EXTENSION_URL);
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`Failed to download required extension: ${response.status} ${response.statusText}`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const archiveBody = Buffer.from(await response.arrayBuffer());
|
|
98
|
+
|
|
99
|
+
fs.writeFileSync(tempArchivePath, archiveBody);
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
validateArchive(tempArchivePath);
|
|
103
|
+
fs.rmSync(archivePath, { force: true });
|
|
104
|
+
fs.renameSync(tempArchivePath, archivePath);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
fs.rmSync(tempArchivePath, { force: true });
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
log(`Prepared required extension archive at ${archivePath}`);
|
|
111
|
+
|
|
112
|
+
return archivePath;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function ensureRequiredExtensionArchive(
|
|
116
|
+
extensionsDir: string,
|
|
117
|
+
dependencies: EnsureRequiredExtensionDependencies = {}
|
|
118
|
+
): Promise<string> {
|
|
119
|
+
const validateArchive = dependencies.validateExtensionArchive ?? validateExtensionArchive;
|
|
120
|
+
const downloadRequiredExtension =
|
|
121
|
+
dependencies.downloadRequiredExtension ?? downloadRequiredExtensionArchive;
|
|
122
|
+
const log = dependencies.log ?? ((message: string) => console.log(formatInfo(message)));
|
|
123
|
+
const archivePath = path.join(extensionsDir, REQUIRED_EXTENSION_ARCHIVE_NAME);
|
|
124
|
+
|
|
125
|
+
if (fs.existsSync(archivePath)) {
|
|
126
|
+
try {
|
|
127
|
+
validateArchive(archivePath);
|
|
128
|
+
return archivePath;
|
|
129
|
+
} catch {
|
|
130
|
+
log(`Repairing required extension archive at ${archivePath}`);
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
log(`Downloading required extension archive to ${archivePath}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const refreshedArchivePath = await downloadRequiredExtension(extensionsDir, {
|
|
137
|
+
validateExtensionArchive: validateArchive,
|
|
138
|
+
log,
|
|
139
|
+
});
|
|
140
|
+
validateArchive(refreshedArchivePath);
|
|
141
|
+
|
|
142
|
+
return refreshedArchivePath;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function prepareRequiredExtension(
|
|
146
|
+
extensionsDir: string,
|
|
147
|
+
dependencies: PrepareRequiredExtensionDependencies = {}
|
|
148
|
+
): Promise<string> {
|
|
149
|
+
const ensureArchive =
|
|
150
|
+
dependencies.ensureRequiredExtensionArchive ?? ensureRequiredExtensionArchive;
|
|
151
|
+
const makeTempDir = dependencies.makeTempDir ?? fs.mkdtempSync;
|
|
152
|
+
const archivePath = await ensureArchive(extensionsDir);
|
|
153
|
+
const tempDir = makeTempDir(path.join(os.tmpdir(), "cloak-ext-"));
|
|
154
|
+
|
|
155
|
+
new AdmZip(archivePath).extractAllTo(tempDir, true);
|
|
156
|
+
|
|
157
|
+
return resolveExtractedExtensionRoot(tempDir);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function installRequiredExtension(
|
|
161
|
+
extensionsDir: string = defaultAppRootDir(),
|
|
162
|
+
dependencies: EnsureRequiredExtensionDependencies = {}
|
|
163
|
+
): Promise<string> {
|
|
164
|
+
return ensureRequiredExtensionArchive(extensionsDir, dependencies);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function normalizeZipEntryName(entryName: string): string {
|
|
168
|
+
return entryName.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function findManifestEntry(entryNames: string[]): string | undefined {
|
|
172
|
+
return entryNames.find((entryName) => {
|
|
173
|
+
if (entryName === "manifest.json") {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const segments = entryName.split("/");
|
|
178
|
+
return segments.length === 2 && segments[1] === "manifest.json";
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function resolveExtractedExtensionRoot(extractedDir: string): string {
|
|
183
|
+
if (fs.existsSync(path.join(extractedDir, "manifest.json"))) {
|
|
184
|
+
return extractedDir;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const nestedExtension = fs
|
|
188
|
+
.readdirSync(extractedDir, { withFileTypes: true })
|
|
189
|
+
.filter((entry) => entry.isDirectory())
|
|
190
|
+
.find((entry) =>
|
|
191
|
+
fs.existsSync(path.join(extractedDir, entry.name, "manifest.json"))
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
if (!nestedExtension) {
|
|
195
|
+
throw new Error(
|
|
196
|
+
`Required extension archive did not extract a manifest.json: ${extractedDir}`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return path.join(extractedDir, nestedExtension.name);
|
|
201
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { installRequiredExtension } from "./extension.js";
|
|
3
|
+
import { formatError, formatSuccess } from "./output.js";
|
|
4
|
+
|
|
5
|
+
async function run() {
|
|
6
|
+
const archivePath = await installRequiredExtension();
|
|
7
|
+
console.log(formatSuccess(`Required extension ready at ${archivePath}`));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
run().catch((error) => {
|
|
11
|
+
console.error(formatError(String(error)));
|
|
12
|
+
process.exit(1);
|
|
13
|
+
});
|