texlite 0.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/LICENSE +661 -0
- package/README.md +403 -0
- package/README.zh-CN.md +391 -0
- package/dist/client/assets/GitDialog-BHHxSBn3.js +3 -0
- package/dist/client/assets/HistoryDialog-Dlv57tYf.js +5 -0
- package/dist/client/assets/LatexEditor-B-0VRUPv.js +55 -0
- package/dist/client/assets/PdfPreview-617TWFJN.js +1 -0
- package/dist/client/assets/ProjectNavigationDialogs-Cpt2LVEt.js +1 -0
- package/dist/client/assets/SystemMetricsDialog-fkFHVVzJ.js +1 -0
- package/dist/client/assets/editor-JxT6uSYX.js +14 -0
- package/dist/client/assets/index-Bc5CmFMl.js +15 -0
- package/dist/client/assets/index-CMqYAbDx.css +1 -0
- package/dist/client/assets/pdf-DPnwNQHb.js +55 -0
- package/dist/client/assets/pdf.worker.min-CHFwMXne.mjs +29 -0
- package/dist/client/assets/plus-DzbLkp6l.js +1 -0
- package/dist/client/assets/react-CsO_xvbl.js +49 -0
- package/dist/client/assets/spellCheck-R6fUM9oA.js +49783 -0
- package/dist/client/index.html +20 -0
- package/dist/client/logo.svg +4 -0
- package/dist/server/anchors.js +95 -0
- package/dist/server/app.js +1624 -0
- package/dist/server/archive.js +26 -0
- package/dist/server/auth.js +42 -0
- package/dist/server/cli.js +318 -0
- package/dist/server/collaboration.js +635 -0
- package/dist/server/compileArtifacts.js +86 -0
- package/dist/server/compileDiagnostics.js +227 -0
- package/dist/server/compiler.js +514 -0
- package/dist/server/config.js +336 -0
- package/dist/server/db.js +280 -0
- package/dist/server/environment.js +57 -0
- package/dist/server/files.js +121 -0
- package/dist/server/git.js +414 -0
- package/dist/server/history.js +379 -0
- package/dist/server/http.js +7 -0
- package/dist/server/index.js +75 -0
- package/dist/server/latexCompletion.js +296 -0
- package/dist/server/metrics.js +34 -0
- package/dist/server/pm2.js +234 -0
- package/dist/server/projectOutline.js +101 -0
- package/dist/server/projectSearch.js +112 -0
- package/dist/server/projects.js +29 -0
- package/dist/server/routes/compile.js +343 -0
- package/dist/server/runtimePaths.js +42 -0
- package/dist/server/security.js +29 -0
- package/dist/server/spellCheck.js +195 -0
- package/dist/server/synctex.js +56 -0
- package/dist/server/zip.js +185 -0
- package/package.json +110 -0
- package/preview.png +0 -0
- package/texlite.config.example.json +33 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ZipFile } from "yazl";
|
|
4
|
+
import { sourceRoot } from "./files.js";
|
|
5
|
+
export function createProjectArchive(config, projectId) {
|
|
6
|
+
const root = sourceRoot(config, projectId);
|
|
7
|
+
const archive = new ZipFile();
|
|
8
|
+
const addDirectory = (directory, prefix) => {
|
|
9
|
+
const entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
10
|
+
if (entries.length === 0 && prefix)
|
|
11
|
+
archive.addEmptyDirectory(`${prefix}/`);
|
|
12
|
+
for (const entry of entries) {
|
|
13
|
+
if (entry.name === ".git" || entry.isSymbolicLink())
|
|
14
|
+
continue;
|
|
15
|
+
const absolute = path.join(directory, entry.name);
|
|
16
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
17
|
+
if (entry.isDirectory())
|
|
18
|
+
addDirectory(absolute, relative);
|
|
19
|
+
else if (entry.isFile())
|
|
20
|
+
archive.addFile(absolute, relative);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
addDirectory(root, "");
|
|
24
|
+
archive.end();
|
|
25
|
+
return archive;
|
|
26
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { digestToken } from "./security.js";
|
|
2
|
+
export function publicUser(user) {
|
|
3
|
+
return {
|
|
4
|
+
id: user.id,
|
|
5
|
+
username: user.username,
|
|
6
|
+
displayName: user.display_name,
|
|
7
|
+
role: user.role,
|
|
8
|
+
disabled: Boolean(user.disabled),
|
|
9
|
+
mustChangePassword: Boolean(user.must_change_password),
|
|
10
|
+
canCreateProjects: Boolean(user.can_create_projects) || user.role === "admin",
|
|
11
|
+
createdAt: user.created_at
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function currentUser(request, db) {
|
|
15
|
+
const token = request.cookies.texlite_session;
|
|
16
|
+
if (!token)
|
|
17
|
+
return null;
|
|
18
|
+
const row = db.prepare(`
|
|
19
|
+
SELECT u.* FROM sessions s
|
|
20
|
+
JOIN users u ON u.id = s.user_id
|
|
21
|
+
WHERE s.id = ? AND s.expires_at > ? AND u.disabled = 0
|
|
22
|
+
`).get(digestToken(token), new Date().toISOString());
|
|
23
|
+
return row ?? null;
|
|
24
|
+
}
|
|
25
|
+
export function requireUser(request, reply, db) {
|
|
26
|
+
const user = currentUser(request, db);
|
|
27
|
+
if (!user) {
|
|
28
|
+
void reply.code(401).send({ code: "AUTH_REQUIRED", error: "请先登录" });
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return user;
|
|
32
|
+
}
|
|
33
|
+
export function requireAdmin(request, reply, db) {
|
|
34
|
+
const user = requireUser(request, reply, db);
|
|
35
|
+
if (!user)
|
|
36
|
+
return null;
|
|
37
|
+
if (user.role !== "admin") {
|
|
38
|
+
void reply.code(403).send({ code: "ADMIN_REQUIRED", error: "需要管理员权限" });
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
return user;
|
|
42
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { createInterface } from "node:readline/promises";
|
|
6
|
+
import { stdin, stdout } from "node:process";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { CONFIG_DEFAULTS, loadConfig } from "./config.js";
|
|
9
|
+
import { activeAdminCount, openDatabase } from "./db.js";
|
|
10
|
+
import { hashPassword } from "./security.js";
|
|
11
|
+
import { assertEnvironment, assertGitAvailable } from "./environment.js";
|
|
12
|
+
import { serve } from "./index.js";
|
|
13
|
+
import { processStatus, restartManaged, startManaged, stopManaged, streamLogs, waitForOnline } from "./pm2.js";
|
|
14
|
+
import { defaultDataDirectory, resolveConfigPath } from "./runtimePaths.js";
|
|
15
|
+
const HELP = `TexLite ${packageVersion()}
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
texlite <command> [options]
|
|
19
|
+
|
|
20
|
+
Commands:
|
|
21
|
+
init Create the configuration, initialize storage, and create the first administrator.
|
|
22
|
+
serve Run the server in the foreground for debugging, Docker, or systemd.
|
|
23
|
+
start Start the server in the background under PM2.
|
|
24
|
+
status Show the status of the PM2-managed server.
|
|
25
|
+
stop Stop the PM2-managed server.
|
|
26
|
+
restart Restart (or start) the PM2-managed server.
|
|
27
|
+
logs Stream logs from the PM2-managed server.
|
|
28
|
+
doctor Validate configuration, paths, LaTeX, and the administrator.
|
|
29
|
+
config Print the effective configuration as JSON without changing it.
|
|
30
|
+
help Show this help message.
|
|
31
|
+
version Print the installed TexLite version.
|
|
32
|
+
|
|
33
|
+
Options:
|
|
34
|
+
--config PATH Use this configuration file instead of TEXLITE_CONFIG or the XDG default.
|
|
35
|
+
--git Make doctor check the optional Git integration as well.
|
|
36
|
+
-h, --help Show this help message.
|
|
37
|
+
-v, --version Print the installed TexLite version.
|
|
38
|
+
`;
|
|
39
|
+
export function parseArgs(args) {
|
|
40
|
+
let command = "help";
|
|
41
|
+
let configPath;
|
|
42
|
+
let checkGit = false;
|
|
43
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
44
|
+
const argument = args[index];
|
|
45
|
+
if (!argument)
|
|
46
|
+
continue;
|
|
47
|
+
if (argument === "-h" || argument === "--help")
|
|
48
|
+
return { command: "help", configPath, checkGit };
|
|
49
|
+
if (argument === "-v" || argument === "--version")
|
|
50
|
+
return { command: "version", configPath, checkGit };
|
|
51
|
+
if (argument === "--git") {
|
|
52
|
+
checkGit = true;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (argument === "--config") {
|
|
56
|
+
const value = args[++index];
|
|
57
|
+
if (!value)
|
|
58
|
+
throw new Error("--config requires a configuration file path");
|
|
59
|
+
configPath = value;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (argument.startsWith("--config=")) {
|
|
63
|
+
const value = argument.slice("--config=".length);
|
|
64
|
+
if (!value)
|
|
65
|
+
throw new Error("--config requires a configuration file path");
|
|
66
|
+
configPath = value;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (argument.startsWith("-"))
|
|
70
|
+
throw new Error(`Unknown option: ${argument}`);
|
|
71
|
+
if (command !== "help")
|
|
72
|
+
throw new Error(`Only one command may be specified (already received ${command})`);
|
|
73
|
+
command = argument;
|
|
74
|
+
}
|
|
75
|
+
return { command, configPath, checkGit };
|
|
76
|
+
}
|
|
77
|
+
function output(message) {
|
|
78
|
+
stdout.write(`${message}\n`);
|
|
79
|
+
}
|
|
80
|
+
function configPathFor(options) {
|
|
81
|
+
return resolveConfigPath(options.configPath);
|
|
82
|
+
}
|
|
83
|
+
function writeInitialConfig(configPath, siteName, adminEmail) {
|
|
84
|
+
const dataDirectory = defaultDataDirectory();
|
|
85
|
+
const configuredDataDirectory = process.env.TEXLITE_DATA_DIR?.trim();
|
|
86
|
+
const effectiveDataDirectory = configuredDataDirectory
|
|
87
|
+
? path.resolve(path.dirname(configPath), configuredDataDirectory)
|
|
88
|
+
: dataDirectory;
|
|
89
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true, mode: 0o700 });
|
|
90
|
+
// Data parents are not guaranteed to exist on a fresh account. Create only
|
|
91
|
+
// the parent here; the configured data directory itself is created by
|
|
92
|
+
// openDatabase after configuration validation.
|
|
93
|
+
for (const parent of new Set([path.dirname(dataDirectory), path.dirname(effectiveDataDirectory)])) {
|
|
94
|
+
fs.mkdirSync(parent, { recursive: true, mode: 0o700 });
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
fs.chmodSync(path.dirname(configPath), 0o700);
|
|
98
|
+
}
|
|
99
|
+
catch { /* Windows and restricted filesystems may not support chmod. */ }
|
|
100
|
+
const configFile = {
|
|
101
|
+
siteName,
|
|
102
|
+
adminEmail,
|
|
103
|
+
sessionDays: CONFIG_DEFAULTS.sessionDays,
|
|
104
|
+
server: { host: CONFIG_DEFAULTS.host, port: CONFIG_DEFAULTS.port },
|
|
105
|
+
storage: { dataDir: dataDirectory },
|
|
106
|
+
uploads: { maxFileSizeMB: CONFIG_DEFAULTS.maxFileSizeMB },
|
|
107
|
+
history: {
|
|
108
|
+
maxVersions: CONFIG_DEFAULTS.historyMaxVersions,
|
|
109
|
+
maxStorageMB: CONFIG_DEFAULTS.historyMaxStorageMB
|
|
110
|
+
},
|
|
111
|
+
git: {
|
|
112
|
+
binary: CONFIG_DEFAULTS.git,
|
|
113
|
+
operationTimeoutSeconds: CONFIG_DEFAULTS.gitOperationTimeoutSeconds,
|
|
114
|
+
githubApiBaseUrl: CONFIG_DEFAULTS.githubApiBaseUrl
|
|
115
|
+
},
|
|
116
|
+
latex: {
|
|
117
|
+
latexmk: CONFIG_DEFAULTS.latexmk,
|
|
118
|
+
defaultEngine: CONFIG_DEFAULTS.defaultEngine,
|
|
119
|
+
allowedEngines: CONFIG_DEFAULTS.allowedEngines,
|
|
120
|
+
extraArgs: CONFIG_DEFAULTS.extraArgs,
|
|
121
|
+
allowProjectLatexmkrc: CONFIG_DEFAULTS.allowProjectLatexmkrc,
|
|
122
|
+
compileTimeoutSeconds: CONFIG_DEFAULTS.compileTimeoutSeconds,
|
|
123
|
+
maxCompileJobs: CONFIG_DEFAULTS.maxCompileJobs
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
fs.writeFileSync(configPath, `${JSON.stringify(configFile, null, 2)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
127
|
+
try {
|
|
128
|
+
fs.chmodSync(configPath, 0o600);
|
|
129
|
+
}
|
|
130
|
+
catch { /* Windows and restricted filesystems may not support chmod. */ }
|
|
131
|
+
}
|
|
132
|
+
async function initialize(options) {
|
|
133
|
+
const configPath = configPathFor(options);
|
|
134
|
+
const interactive = Boolean(stdin.isTTY);
|
|
135
|
+
const rl = interactive ? createInterface({ input: stdin, output: stdout }) : null;
|
|
136
|
+
let db = null;
|
|
137
|
+
try {
|
|
138
|
+
if (!fs.existsSync(configPath)) {
|
|
139
|
+
const siteName = rl
|
|
140
|
+
? (await rl.question(`Site name [${CONFIG_DEFAULTS.siteName}]: `)).trim() || CONFIG_DEFAULTS.siteName
|
|
141
|
+
: CONFIG_DEFAULTS.siteName;
|
|
142
|
+
const adminEmail = rl ? (await rl.question("Administrator contact email (optional): ")).trim() : "";
|
|
143
|
+
writeInitialConfig(configPath, siteName, adminEmail);
|
|
144
|
+
output(`Created configuration file: ${configPath}`);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
output(`Using existing configuration file: ${configPath}`);
|
|
148
|
+
}
|
|
149
|
+
const config = loadConfig(configPath);
|
|
150
|
+
const environment = await assertEnvironment(config);
|
|
151
|
+
output(`Environment checks passed: ${environment.map((item) => `${item.name} ${item.version}`).join("; ")}`);
|
|
152
|
+
db = openDatabase(config);
|
|
153
|
+
if (activeAdminCount(db) > 0) {
|
|
154
|
+
throw new Error("An active administrator already exists. Add additional administrators from the administration page.");
|
|
155
|
+
}
|
|
156
|
+
const username = process.env.TEXLITE_INIT_USERNAME
|
|
157
|
+
?? (rl ? (await rl.question("Administrator username [admin]: ")).trim() || "admin" : "admin");
|
|
158
|
+
const displayName = process.env.TEXLITE_INIT_DISPLAY_NAME
|
|
159
|
+
?? (rl ? (await rl.question("Administrator display name [Administrator]: ")).trim() || "Administrator" : "Administrator");
|
|
160
|
+
const password = process.env.TEXLITE_INIT_PASSWORD
|
|
161
|
+
?? (rl ? await rl.question("Administrator password (at least 8 characters; input is visible): ") : "");
|
|
162
|
+
if (!password)
|
|
163
|
+
throw new Error("Non-interactive initialization requires TEXLITE_INIT_PASSWORD to be set.");
|
|
164
|
+
const timestamp = new Date().toISOString();
|
|
165
|
+
db.prepare(`INSERT INTO users
|
|
166
|
+
(id, username, display_name, password_hash, role, disabled, must_change_password, can_create_projects, created_at)
|
|
167
|
+
VALUES (?, ?, ?, ?, 'admin', 0, 0, 1, ?)`)
|
|
168
|
+
.run(randomUUID(), username, displayName, await hashPassword(password), timestamp);
|
|
169
|
+
output(`Administrator ${username} created. Configuration file: ${config.configPath}`);
|
|
170
|
+
output(`Data directory: ${config.dataDir}`);
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
db?.close();
|
|
174
|
+
rl?.close();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async function loadValidatedConfig(options) {
|
|
178
|
+
const configPath = configPathFor(options);
|
|
179
|
+
const config = loadConfig(configPath);
|
|
180
|
+
return { config, configPath };
|
|
181
|
+
}
|
|
182
|
+
async function assertAdmin(config) {
|
|
183
|
+
const db = openDatabase(config);
|
|
184
|
+
try {
|
|
185
|
+
if (activeAdminCount(db) === 0)
|
|
186
|
+
throw new Error("No active administrator found. Run `texlite init` first; the server will not start.");
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
db.close();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async function doctor(options) {
|
|
193
|
+
const { config, configPath } = await loadValidatedConfig(options);
|
|
194
|
+
const environment = await assertEnvironment(config);
|
|
195
|
+
await assertAdmin(config);
|
|
196
|
+
output(`Configuration is valid: ${configPath}`);
|
|
197
|
+
output(`Data directory: ${config.dataDir}`);
|
|
198
|
+
output(`Client assets: ${config.clientDir}`);
|
|
199
|
+
output(`Environment: ${environment.map((item) => `${item.name} ${item.version}`).join("; ")}`);
|
|
200
|
+
if (options.checkGit)
|
|
201
|
+
output(`Git: ${(await assertGitAvailable(config)).version}`);
|
|
202
|
+
else
|
|
203
|
+
output("Git: not checked (Git integration is optional; use --git to check it)");
|
|
204
|
+
}
|
|
205
|
+
async function printConfig(options) {
|
|
206
|
+
const { config, configPath } = await loadValidatedConfig(options);
|
|
207
|
+
output(JSON.stringify({
|
|
208
|
+
configPath,
|
|
209
|
+
siteName: config.siteName,
|
|
210
|
+
host: config.host,
|
|
211
|
+
port: config.port,
|
|
212
|
+
dataDir: config.dataDir,
|
|
213
|
+
databasePath: config.databasePath,
|
|
214
|
+
projectsDir: config.projectsDir,
|
|
215
|
+
clientDir: config.clientDir,
|
|
216
|
+
sessionDays: config.sessionDays,
|
|
217
|
+
compileTimeoutSeconds: config.compileTimeoutMs / 1000,
|
|
218
|
+
maxCompileJobs: config.maxCompileJobs,
|
|
219
|
+
latexmk: config.latexmk,
|
|
220
|
+
defaultEngine: config.defaultEngine,
|
|
221
|
+
allowedEngines: config.allowedEngines,
|
|
222
|
+
maxUploadBytes: config.maxUploadBytes,
|
|
223
|
+
historyMaxVersions: config.historyMaxVersions,
|
|
224
|
+
historyMaxStorageBytes: config.historyMaxStorageBytes,
|
|
225
|
+
git: config.git
|
|
226
|
+
}, null, 2));
|
|
227
|
+
}
|
|
228
|
+
async function start(options, restart = false) {
|
|
229
|
+
const { config, configPath } = await loadValidatedConfig(options);
|
|
230
|
+
await assertEnvironment(config);
|
|
231
|
+
await assertAdmin(config);
|
|
232
|
+
restart ? await restartManaged(config) : await startManaged(config);
|
|
233
|
+
const managed = await waitForOnline(config);
|
|
234
|
+
const status = managed.description?.pm2_env?.status ?? "unknown";
|
|
235
|
+
output(`TexLite ${status}: ${config.siteName}`);
|
|
236
|
+
output(`Address: http://${config.host}:${config.port}`);
|
|
237
|
+
output(`Configuration file: ${configPath}`);
|
|
238
|
+
output(`PM2 process: ${managed.name}`);
|
|
239
|
+
}
|
|
240
|
+
async function status(options) {
|
|
241
|
+
const { config } = await loadValidatedConfig(options);
|
|
242
|
+
const result = await processStatus(config);
|
|
243
|
+
output(JSON.stringify(result, null, 2));
|
|
244
|
+
if (result.status !== "online")
|
|
245
|
+
process.exitCode = 2;
|
|
246
|
+
}
|
|
247
|
+
async function stop(options) {
|
|
248
|
+
const { config } = await loadValidatedConfig(options);
|
|
249
|
+
const managed = await stopManaged(config.configPath);
|
|
250
|
+
output(managed.description ? `TexLite stopped: ${managed.name}` : `TexLite is not running: ${managed.name}`);
|
|
251
|
+
}
|
|
252
|
+
async function run(options) {
|
|
253
|
+
switch (options.command) {
|
|
254
|
+
case "help":
|
|
255
|
+
output(HELP);
|
|
256
|
+
return;
|
|
257
|
+
case "version":
|
|
258
|
+
output(packageVersion());
|
|
259
|
+
return;
|
|
260
|
+
case "init":
|
|
261
|
+
await initialize(options);
|
|
262
|
+
return;
|
|
263
|
+
case "serve":
|
|
264
|
+
await serve(configPathFor(options));
|
|
265
|
+
return;
|
|
266
|
+
case "start":
|
|
267
|
+
await start(options);
|
|
268
|
+
return;
|
|
269
|
+
case "restart":
|
|
270
|
+
await start(options, true);
|
|
271
|
+
return;
|
|
272
|
+
case "status":
|
|
273
|
+
await status(options);
|
|
274
|
+
return;
|
|
275
|
+
case "stop":
|
|
276
|
+
await stop(options);
|
|
277
|
+
return;
|
|
278
|
+
case "doctor":
|
|
279
|
+
await doctor(options);
|
|
280
|
+
return;
|
|
281
|
+
case "config":
|
|
282
|
+
await printConfig(options);
|
|
283
|
+
return;
|
|
284
|
+
case "logs": {
|
|
285
|
+
const { config } = await loadValidatedConfig(options);
|
|
286
|
+
await streamLogs(config);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
default: throw new Error(`Unknown command: ${options.command}\n\n${HELP}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function packageVersion() {
|
|
293
|
+
try {
|
|
294
|
+
const packagePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
|
|
295
|
+
const parsed = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
|
296
|
+
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
return "unknown";
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
303
|
+
await run(parseArgs(argv));
|
|
304
|
+
}
|
|
305
|
+
if (isMainModule()) {
|
|
306
|
+
main().catch((error) => {
|
|
307
|
+
console.error(error instanceof Error ? error.message : error);
|
|
308
|
+
process.exitCode = 1;
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
function isMainModule() {
|
|
312
|
+
try {
|
|
313
|
+
return fs.realpathSync(process.argv[1] ?? "") === fs.realpathSync(fileURLToPath(import.meta.url));
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
}
|