@jiayunxie/aerial 0.2.1 → 0.2.2
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 +1 -1
- package/docs/usage.md +2 -2
- package/package.json +5 -5
- package/src/cli/args.js +28 -0
- package/src/cli/config-command.js +39 -0
- package/src/cli/disable-command.js +28 -0
- package/src/{doctor.js → cli/doctor.js} +3 -3
- package/src/cli/help.js +23 -0
- package/src/cli/index.js +92 -0
- package/src/cli/key-command.js +20 -0
- package/src/cli/login-command.js +28 -0
- package/src/{model-selection.js → cli/model-selection.js} +5 -5
- package/src/cli/output.js +75 -0
- package/src/{probe.js → cli/probe.js} +3 -3
- package/src/cli/proxy-command.js +120 -0
- package/src/cli/runtime-auth.js +21 -0
- package/src/cli/service-command.js +120 -0
- package/src/cli/setup-command.js +122 -0
- package/src/{setup-selection.js → cli/setup-selection.js} +3 -20
- package/src/cli/start-command.js +11 -0
- package/src/cli/status-command.js +26 -0
- package/src/{version.js → cli/version.js} +1 -1
- package/src/proxy/cache-policy.js +89 -0
- package/src/proxy/cache-telemetry.js +172 -0
- package/src/proxy/effort.js +120 -0
- package/src/proxy/headers.js +33 -0
- package/src/proxy/index.js +85 -0
- package/src/proxy/models.js +27 -0
- package/src/{responses-websocket.js → proxy/responses-websocket.js} +2 -2
- package/src/{server.js → proxy/server.js} +5 -5
- package/src/proxy/transport.js +55 -0
- package/src/service/health.js +54 -0
- package/src/service/index.js +38 -0
- package/src/service/lifecycle.js +301 -0
- package/src/service/platform.js +227 -0
- package/src/service/runner.js +45 -0
- package/src/service/status.js +296 -0
- package/src/service/wrapper-render.js +228 -0
- package/src/setup/backup.js +38 -0
- package/src/{setup.js → setup/clients.js} +11 -198
- package/src/setup/index.js +4 -0
- package/src/setup/restore.js +90 -0
- package/src/setup/status.js +24 -0
- package/src/setup/toml.js +41 -0
- package/src/{auth.js → shared/auth.js} +1 -1
- package/src/{config.js → shared/config.js} +2 -2
- package/src/shared/effort.js +19 -0
- package/src/shared/file-utils.js +31 -0
- package/src/{paths.js → shared/paths.js} +2 -3
- package/src/{upstream-fetch.js → upstream/fetch.js} +1 -1
- package/src/cli.js +0 -684
- package/src/copilot.js +0 -572
- package/src/service.js +0 -1182
- /package/src/{app-status.js → cli/app-status.js} +0 -0
- /package/src/{model-catalog.js → proxy/model-catalog.js} +0 -0
- /package/src/{model-utils.js → proxy/model-utils.js} +0 -0
- /package/src/{constants.js → shared/constants.js} +0 -0
- /package/src/{crypto.js → shared/crypto.js} +0 -0
- /package/src/{http-utils.js → shared/http-utils.js} +0 -0
- /package/src/{log.js → shared/log.js} +0 -0
- /package/src/{prompt-utils.js → shared/prompt-utils.js} +0 -0
- /package/src/{proxy-config.js → upstream/proxy-config.js} +0 -0
- /package/src/{socks5-bridge.js → upstream/socks5-bridge.js} +0 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { apiKeyPath, githubTokenPath } from "../shared/paths.js";
|
|
3
|
+
import { loadConfig } from "../shared/config.js";
|
|
4
|
+
import { defaultHealthFetch, classifyHealth } from "./health.js";
|
|
5
|
+
import { defaultRunCommand } from "./runner.js";
|
|
6
|
+
import { isUnsupportedPlatform, serviceState } from "./platform.js";
|
|
7
|
+
import {
|
|
8
|
+
aerialLogPath,
|
|
9
|
+
buildSchtasksArgs,
|
|
10
|
+
darwinWrapperPath,
|
|
11
|
+
logsDir,
|
|
12
|
+
stdioLogPath,
|
|
13
|
+
winWrapperPath,
|
|
14
|
+
wrapperLogConfig
|
|
15
|
+
} from "./wrapper-render.js";
|
|
16
|
+
|
|
17
|
+
function unescapeShSingleQuoted(line, prefix) {
|
|
18
|
+
if (!line.startsWith(prefix)) return undefined;
|
|
19
|
+
const rest = line.slice(prefix.length);
|
|
20
|
+
if (!rest.startsWith("'")) return undefined;
|
|
21
|
+
let i = 1;
|
|
22
|
+
let out = "";
|
|
23
|
+
while (i < rest.length) {
|
|
24
|
+
const ch = rest[i];
|
|
25
|
+
if (ch === "'") {
|
|
26
|
+
if (rest.slice(i, i + 4) === "'\\''") {
|
|
27
|
+
out += "'";
|
|
28
|
+
i += 4;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
out += ch;
|
|
34
|
+
i += 1;
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function unescapePsSingleQuoted(line, prefix) {
|
|
40
|
+
if (!line.startsWith(prefix)) return undefined;
|
|
41
|
+
const rest = line.slice(prefix.length);
|
|
42
|
+
if (!rest.startsWith("'")) return undefined;
|
|
43
|
+
let i = 1;
|
|
44
|
+
let out = "";
|
|
45
|
+
while (i < rest.length) {
|
|
46
|
+
const ch = rest[i];
|
|
47
|
+
if (ch === "'") {
|
|
48
|
+
if (rest[i + 1] === "'") {
|
|
49
|
+
out += "'";
|
|
50
|
+
i += 2;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
out += ch;
|
|
56
|
+
i += 1;
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function parseWrapperPaths(wrapperFile) {
|
|
62
|
+
if (!wrapperFile) return { node: undefined, cli: undefined };
|
|
63
|
+
try {
|
|
64
|
+
if (!fs.existsSync(wrapperFile)) return { node: undefined, cli: undefined };
|
|
65
|
+
const data = fs.readFileSync(wrapperFile, "utf8");
|
|
66
|
+
const lines = data.split(/\r?\n/);
|
|
67
|
+
if (wrapperFile.endsWith(".sh")) {
|
|
68
|
+
let node;
|
|
69
|
+
let cli;
|
|
70
|
+
for (const line of lines) {
|
|
71
|
+
if (node === undefined) {
|
|
72
|
+
const candidate = unescapeShSingleQuoted(line, "NODE_BIN=");
|
|
73
|
+
if (candidate !== undefined) node = candidate;
|
|
74
|
+
}
|
|
75
|
+
if (cli === undefined) {
|
|
76
|
+
const candidate = unescapeShSingleQuoted(line, "CLI_ENTRY=");
|
|
77
|
+
if (candidate !== undefined) cli = candidate;
|
|
78
|
+
}
|
|
79
|
+
if (node !== undefined && cli !== undefined) break;
|
|
80
|
+
}
|
|
81
|
+
return { node, cli };
|
|
82
|
+
}
|
|
83
|
+
if (wrapperFile.endsWith(".ps1")) {
|
|
84
|
+
let node;
|
|
85
|
+
let cli;
|
|
86
|
+
for (const line of lines) {
|
|
87
|
+
if (node === undefined) {
|
|
88
|
+
const m = line.match(/^\$node\s*=\s*(.*)$/);
|
|
89
|
+
if (m) {
|
|
90
|
+
const candidate = unescapePsSingleQuoted(m[1].trim(), "");
|
|
91
|
+
if (candidate !== undefined) node = candidate;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (cli === undefined) {
|
|
95
|
+
const m = line.match(/^\$cli\s*=\s*(.*)$/);
|
|
96
|
+
if (m) {
|
|
97
|
+
const candidate = unescapePsSingleQuoted(m[1].trim(), "");
|
|
98
|
+
if (candidate !== undefined) cli = candidate;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (node !== undefined && cli !== undefined) break;
|
|
102
|
+
}
|
|
103
|
+
return { node, cli };
|
|
104
|
+
}
|
|
105
|
+
} catch {}
|
|
106
|
+
return { node: undefined, cli: undefined };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function readWrapperNodePath(wrapperFile) {
|
|
110
|
+
return parseWrapperPaths(wrapperFile).node;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const STALE_REASONS = Object.freeze({
|
|
114
|
+
WRAPPER_MISSING: "wrapper_missing",
|
|
115
|
+
WRAPPER_NODE_MISSING: "wrapper_node_missing",
|
|
116
|
+
WRAPPER_CLI_MISSING: "wrapper_cli_missing",
|
|
117
|
+
WRAPPER_LOG_CONFIG_UNPARSEABLE: "wrapper_log_config_unparseable"
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
export function wrapperBlock(state) {
|
|
121
|
+
if (!state || state.installed !== true) {
|
|
122
|
+
return { stale: false, staleReasons: [] };
|
|
123
|
+
}
|
|
124
|
+
let wrapperPath;
|
|
125
|
+
if (process.platform === "darwin") wrapperPath = darwinWrapperPath();
|
|
126
|
+
else if (process.platform === "win32") wrapperPath = winWrapperPath();
|
|
127
|
+
const wrapperFileExists = wrapperPath ? fs.existsSync(wrapperPath) : false;
|
|
128
|
+
const staleReasons = [];
|
|
129
|
+
if (!wrapperFileExists) {
|
|
130
|
+
return {
|
|
131
|
+
path: wrapperPath,
|
|
132
|
+
nodePath: undefined,
|
|
133
|
+
nodeExists: undefined,
|
|
134
|
+
cliPath: undefined,
|
|
135
|
+
cliExists: undefined,
|
|
136
|
+
logConfigParseable: undefined,
|
|
137
|
+
stale: true,
|
|
138
|
+
staleReasons: [STALE_REASONS.WRAPPER_MISSING]
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
const { node: nodePath, cli: cliPath } = parseWrapperPaths(wrapperPath);
|
|
142
|
+
const nodeExists = nodePath ? fs.existsSync(nodePath) : false;
|
|
143
|
+
const cliExists = cliPath ? fs.existsSync(cliPath) : false;
|
|
144
|
+
const logConfigParseable = parseWrapperLogValues(wrapperPath) !== null;
|
|
145
|
+
if (!nodeExists) staleReasons.push(STALE_REASONS.WRAPPER_NODE_MISSING);
|
|
146
|
+
if (!cliExists) staleReasons.push(STALE_REASONS.WRAPPER_CLI_MISSING);
|
|
147
|
+
if (!logConfigParseable) staleReasons.push(STALE_REASONS.WRAPPER_LOG_CONFIG_UNPARSEABLE);
|
|
148
|
+
return {
|
|
149
|
+
path: wrapperPath,
|
|
150
|
+
nodePath,
|
|
151
|
+
nodeExists,
|
|
152
|
+
cliPath,
|
|
153
|
+
cliExists,
|
|
154
|
+
logConfigParseable,
|
|
155
|
+
stale: staleReasons.length > 0,
|
|
156
|
+
staleReasons
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function authFileStatus(file) {
|
|
161
|
+
if (!fs.existsSync(file)) return { file, state: "missing" };
|
|
162
|
+
try {
|
|
163
|
+
const data = fs.readFileSync(file, "utf8");
|
|
164
|
+
if (!data || !data.trim()) return { file, state: "invalid", reason: "empty" };
|
|
165
|
+
return { file, state: "present" };
|
|
166
|
+
} catch (err) {
|
|
167
|
+
return { file, state: "invalid", reason: err.message };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function authBlock() {
|
|
172
|
+
return {
|
|
173
|
+
api_key: authFileStatus(apiKeyPath()),
|
|
174
|
+
github_token: authFileStatus(githubTokenPath())
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function statFile(file) {
|
|
179
|
+
if (!file) return { exists: false };
|
|
180
|
+
try {
|
|
181
|
+
if (!fs.existsSync(file)) return { exists: false };
|
|
182
|
+
return { exists: true, size: fs.statSync(file).size };
|
|
183
|
+
} catch {
|
|
184
|
+
return { exists: false };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function logsBlock() {
|
|
189
|
+
const dir = logsDir();
|
|
190
|
+
const primary = aerialLogPath();
|
|
191
|
+
const stdio = stdioLogPath();
|
|
192
|
+
let wrapperFile;
|
|
193
|
+
if (process.platform === "darwin") wrapperFile = darwinWrapperPath();
|
|
194
|
+
else if (process.platform === "win32") wrapperFile = winWrapperPath();
|
|
195
|
+
const parsed = wrapperFile ? parseWrapperLogValues(wrapperFile) : null;
|
|
196
|
+
let maxBytes;
|
|
197
|
+
let rotateKeep;
|
|
198
|
+
let source;
|
|
199
|
+
if (parsed) {
|
|
200
|
+
maxBytes = parsed.maxBytes;
|
|
201
|
+
rotateKeep = parsed.backups;
|
|
202
|
+
source = "installed-wrapper";
|
|
203
|
+
} else {
|
|
204
|
+
const cfg = wrapperLogConfig();
|
|
205
|
+
maxBytes = cfg.maxBytes;
|
|
206
|
+
rotateKeep = cfg.backups;
|
|
207
|
+
source = "next-install-default";
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
dir,
|
|
211
|
+
primary: { file: primary, ...statFile(primary) },
|
|
212
|
+
stdio: { file: stdio, ...statFile(stdio) },
|
|
213
|
+
maxFileBytes: maxBytes,
|
|
214
|
+
rotateKeep,
|
|
215
|
+
source
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function parseWrapperLogValues(file) {
|
|
220
|
+
if (!file) return null;
|
|
221
|
+
try {
|
|
222
|
+
if (!fs.existsSync(file)) return null;
|
|
223
|
+
const data = fs.readFileSync(file, "utf8");
|
|
224
|
+
let maxBytes;
|
|
225
|
+
let backups;
|
|
226
|
+
if (file.endsWith(".sh")) {
|
|
227
|
+
const m = data.match(/^MAX_BYTES=(\d+)\s*$/m);
|
|
228
|
+
const b = data.match(/^BACKUPS=(\d+)\s*$/m);
|
|
229
|
+
if (m) maxBytes = Number(m[1]);
|
|
230
|
+
if (b) backups = Number(b[1]);
|
|
231
|
+
} else if (file.endsWith(".ps1")) {
|
|
232
|
+
const m = data.match(/^\$maxBytes\s*=\s*(\d+)\s*$/m);
|
|
233
|
+
const b = data.match(/^\$backups\s*=\s*(\d+)\s*$/m);
|
|
234
|
+
if (m) maxBytes = Number(m[1]);
|
|
235
|
+
if (b) backups = Number(b[1]);
|
|
236
|
+
}
|
|
237
|
+
if (!Number.isInteger(maxBytes) || maxBytes <= 0) return null;
|
|
238
|
+
if (!Number.isInteger(backups) || backups < 1) return null;
|
|
239
|
+
return { maxBytes, backups };
|
|
240
|
+
} catch {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export async function serviceStatus({ run = defaultRunCommand, healthFetch } = {}) {
|
|
246
|
+
const config = loadConfig();
|
|
247
|
+
const ctx = { run };
|
|
248
|
+
if (isUnsupportedPlatform()) {
|
|
249
|
+
return {
|
|
250
|
+
schema: "aerial.service-status.v1",
|
|
251
|
+
platform: process.platform,
|
|
252
|
+
supported: false,
|
|
253
|
+
config: { host: config.host, port: config.port },
|
|
254
|
+
service: { installed: false, loaded: false, reason: "unsupported_platform", platform: process.platform, wrapper: wrapperBlock({ installed: false }) },
|
|
255
|
+
health: { ok: false, error: "unsupported_platform" },
|
|
256
|
+
logs: logsBlock(),
|
|
257
|
+
auth: authBlock(),
|
|
258
|
+
summary: "unsupported"
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const state = serviceState(ctx);
|
|
262
|
+
const probe = await (healthFetch || defaultHealthFetch)(config.host, config.port);
|
|
263
|
+
const cls = classifyHealth(probe);
|
|
264
|
+
const wrapper = wrapperBlock(state);
|
|
265
|
+
let supervisor;
|
|
266
|
+
if (cls.mode === "aerial_running") {
|
|
267
|
+
supervisor = state.installed && state.loaded ? "service-managed" : "foreground";
|
|
268
|
+
}
|
|
269
|
+
const health = { ...probe };
|
|
270
|
+
if (cls.mode === "port_conflict") {
|
|
271
|
+
health.portConflict = true;
|
|
272
|
+
health.conflictReason = cls.reason;
|
|
273
|
+
}
|
|
274
|
+
if (cls.mode === "aerial_running") {
|
|
275
|
+
health.aerial = true;
|
|
276
|
+
health.supervisor = supervisor;
|
|
277
|
+
}
|
|
278
|
+
let summary;
|
|
279
|
+
if (cls.mode === "aerial_running" && supervisor === "service-managed") summary = "running (service-managed)";
|
|
280
|
+
else if (cls.mode === "aerial_running" && supervisor === "foreground") summary = "running (foreground)";
|
|
281
|
+
else if (cls.mode === "port_conflict") summary = "port conflict (non-Aerial process on port)";
|
|
282
|
+
else if (state.installed && state.loaded) summary = "manager reports up but health failed";
|
|
283
|
+
else if (state.installed) summary = "installed (not running)";
|
|
284
|
+
else summary = "not installed";
|
|
285
|
+
return {
|
|
286
|
+
schema: "aerial.service-status.v1",
|
|
287
|
+
platform: process.platform,
|
|
288
|
+
supported: true,
|
|
289
|
+
config: { host: config.host, port: config.port },
|
|
290
|
+
service: { platform: process.platform, ...state, wrapper },
|
|
291
|
+
health,
|
|
292
|
+
logs: logsBlock(),
|
|
293
|
+
auth: authBlock(),
|
|
294
|
+
summary
|
|
295
|
+
};
|
|
296
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { configDir } from "../shared/paths.js";
|
|
5
|
+
|
|
6
|
+
export const SERVICE_LABEL = "com.jiayunxie.aerial";
|
|
7
|
+
export const WIN_TASK_NAME = "AerialLocalProxy";
|
|
8
|
+
|
|
9
|
+
const PLIST_HEADER = "<!-- Generated by aerial; do not edit. Run `aerial service install` to regenerate. -->";
|
|
10
|
+
const POSIX_HEADER = "# Generated by aerial; do not edit. Run `aerial service install` to regenerate.";
|
|
11
|
+
const DEFAULT_WRAPPER_LOG_MAX_BYTES = 5 * 1024 * 1024;
|
|
12
|
+
const DEFAULT_WRAPPER_LOG_BACKUPS = 3;
|
|
13
|
+
|
|
14
|
+
export function wrapperLogConfig() {
|
|
15
|
+
const out = { maxBytes: DEFAULT_WRAPPER_LOG_MAX_BYTES, backups: DEFAULT_WRAPPER_LOG_BACKUPS };
|
|
16
|
+
const rawMax = process.env.AERIAL_LOG_MAX_BYTES;
|
|
17
|
+
if (rawMax && /^\d+$/.test(String(rawMax).trim())) {
|
|
18
|
+
const n = Number(String(rawMax).trim());
|
|
19
|
+
if (n > 0) out.maxBytes = n;
|
|
20
|
+
}
|
|
21
|
+
const rawBackups = process.env.AERIAL_LOG_BACKUPS;
|
|
22
|
+
if (rawBackups && /^\d+$/.test(String(rawBackups).trim())) {
|
|
23
|
+
const n = Number(String(rawBackups).trim());
|
|
24
|
+
if (n >= 1) out.backups = n;
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function nodeBinary() {
|
|
30
|
+
return process.env.AERIAL_SERVICE_NODE || process.execPath;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function cliEntry() {
|
|
34
|
+
if (process.env.AERIAL_SERVICE_CLI) return process.env.AERIAL_SERVICE_CLI;
|
|
35
|
+
return path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "cli", "index.js");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function logsDir() {
|
|
39
|
+
return process.env.AERIAL_LOG_DIR || path.join(configDir(), "logs");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function aerialLogPath() {
|
|
43
|
+
return path.join(logsDir(), "aerial.log");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function stdioLogPath() {
|
|
47
|
+
return path.join(logsDir(), "aerial-stdio.log");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function plistPath() {
|
|
51
|
+
return path.join(os.homedir(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function darwinWrapperPath() {
|
|
55
|
+
return path.join(configDir(), "bin", "aerial-service.sh");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function winWrapperPath() {
|
|
59
|
+
return path.join(configDir(), "bin", "aerial-service.ps1");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function xmlEscape(value) {
|
|
63
|
+
return String(value)
|
|
64
|
+
.replace(/&/g, "&")
|
|
65
|
+
.replace(/</g, "<")
|
|
66
|
+
.replace(/>/g, ">")
|
|
67
|
+
.replace(/"/g, """)
|
|
68
|
+
.replace(/'/g, "'");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function shEscape(value) {
|
|
72
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function psEscape(value) {
|
|
76
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function uidString() {
|
|
80
|
+
if (process.platform !== "darwin") return "";
|
|
81
|
+
const uid = process.getuid?.();
|
|
82
|
+
return uid === undefined || uid === null ? "" : String(uid);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function explicitConfigDir() {
|
|
86
|
+
const v = process.env.AERIAL_CONFIG_DIR;
|
|
87
|
+
return v && v.trim() ? v : undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function renderPlist({ label = SERVICE_LABEL, wrapperPath: wrapper }) {
|
|
91
|
+
return [
|
|
92
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
93
|
+
PLIST_HEADER,
|
|
94
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
95
|
+
'<plist version="1.0">',
|
|
96
|
+
"<dict>",
|
|
97
|
+
" <key>Label</key>",
|
|
98
|
+
` <string>${xmlEscape(label)}</string>`,
|
|
99
|
+
" <key>ProgramArguments</key>",
|
|
100
|
+
" <array>",
|
|
101
|
+
" <string>/bin/sh</string>",
|
|
102
|
+
` <string>${xmlEscape(wrapper)}</string>`,
|
|
103
|
+
" </array>",
|
|
104
|
+
" <key>RunAtLoad</key>",
|
|
105
|
+
" <true/>",
|
|
106
|
+
" <key>KeepAlive</key>",
|
|
107
|
+
" <dict>",
|
|
108
|
+
" <key>SuccessfulExit</key>",
|
|
109
|
+
" <false/>",
|
|
110
|
+
" <key>Crashed</key>",
|
|
111
|
+
" <true/>",
|
|
112
|
+
" </dict>",
|
|
113
|
+
" <key>ThrottleInterval</key>",
|
|
114
|
+
" <integer>10</integer>",
|
|
115
|
+
"</dict>",
|
|
116
|
+
"</plist>",
|
|
117
|
+
""
|
|
118
|
+
].join("\n");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function renderDarwinWrapper({ nodePath, cliPath, host, port, stdioLog, aerialLog, configDir: cfg, maxBytes, backups }) {
|
|
122
|
+
const max = Number.isInteger(maxBytes) && maxBytes > 0 ? maxBytes : DEFAULT_WRAPPER_LOG_MAX_BYTES;
|
|
123
|
+
const keep = Number.isInteger(backups) && backups >= 1 ? backups : DEFAULT_WRAPPER_LOG_BACKUPS;
|
|
124
|
+
const lines = [
|
|
125
|
+
"#!/bin/sh",
|
|
126
|
+
POSIX_HEADER,
|
|
127
|
+
"set -e",
|
|
128
|
+
`STDIO_LOG=${shEscape(stdioLog)}`,
|
|
129
|
+
`AERIAL_LOG_FILE=${shEscape(aerialLog)}`,
|
|
130
|
+
`MAX_BYTES=${max}`,
|
|
131
|
+
`BACKUPS=${keep}`,
|
|
132
|
+
`NODE_BIN=${shEscape(nodePath)}`,
|
|
133
|
+
`CLI_ENTRY=${shEscape(cliPath)}`,
|
|
134
|
+
`HOST=${shEscape(host)}`,
|
|
135
|
+
`PORT=${Number(port)}`,
|
|
136
|
+
"STDIO_DIR=$(dirname \"$STDIO_LOG\")",
|
|
137
|
+
"AERIAL_DIR=$(dirname \"$AERIAL_LOG_FILE\")",
|
|
138
|
+
"mkdir -p \"$STDIO_DIR\" \"$AERIAL_DIR\"",
|
|
139
|
+
"if [ -f \"$STDIO_LOG\" ]; then",
|
|
140
|
+
" SIZE=$(wc -c < \"$STDIO_LOG\" | tr -d ' ')",
|
|
141
|
+
" if [ \"$SIZE\" -gt \"$MAX_BYTES\" ]; then",
|
|
142
|
+
" i=$((BACKUPS - 1))",
|
|
143
|
+
" while [ \"$i\" -ge 1 ]; do",
|
|
144
|
+
" SRC=\"$STDIO_LOG.$i\"",
|
|
145
|
+
" DST=\"$STDIO_LOG.$((i + 1))\"",
|
|
146
|
+
" if [ -f \"$SRC\" ]; then mv \"$SRC\" \"$DST\"; fi",
|
|
147
|
+
" i=$((i - 1))",
|
|
148
|
+
" done",
|
|
149
|
+
" mv \"$STDIO_LOG\" \"$STDIO_LOG.1\"",
|
|
150
|
+
" fi",
|
|
151
|
+
"fi",
|
|
152
|
+
"export AERIAL_LOG_FILE",
|
|
153
|
+
`export AERIAL_LOG_MAX_BYTES=${max}`,
|
|
154
|
+
`export AERIAL_LOG_BACKUPS=${keep}`
|
|
155
|
+
];
|
|
156
|
+
if (cfg) lines.push(`export AERIAL_CONFIG_DIR=${shEscape(cfg)}`);
|
|
157
|
+
lines.push("exec \"$NODE_BIN\" \"$CLI_ENTRY\" start --host \"$HOST\" --port \"$PORT\" >> \"$STDIO_LOG\" 2>&1");
|
|
158
|
+
lines.push("");
|
|
159
|
+
return lines.join("\n");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function renderWindowsWrapper({ nodePath, cliPath, host, port, stdioLog, aerialLog, configDir: cfg, maxBytes, backups }) {
|
|
163
|
+
const max = Number.isInteger(maxBytes) && maxBytes > 0 ? maxBytes : DEFAULT_WRAPPER_LOG_MAX_BYTES;
|
|
164
|
+
const keep = Number.isInteger(backups) && backups >= 1 ? backups : DEFAULT_WRAPPER_LOG_BACKUPS;
|
|
165
|
+
const lines = [
|
|
166
|
+
POSIX_HEADER,
|
|
167
|
+
"$ErrorActionPreference = 'Stop'",
|
|
168
|
+
`$node = ${psEscape(nodePath)}`,
|
|
169
|
+
`$cli = ${psEscape(cliPath)}`,
|
|
170
|
+
`$serviceHost = ${psEscape(host)}`,
|
|
171
|
+
`$servicePort = ${Number(port)}`,
|
|
172
|
+
`$stdioLog = ${psEscape(stdioLog)}`,
|
|
173
|
+
`$aerialLog = ${psEscape(aerialLog)}`,
|
|
174
|
+
`$maxBytes = ${max}`,
|
|
175
|
+
`$backups = ${keep}`,
|
|
176
|
+
"$stdioDir = Split-Path -Parent $stdioLog",
|
|
177
|
+
"$aerialDir = Split-Path -Parent $aerialLog",
|
|
178
|
+
"if (-not (Test-Path -LiteralPath $stdioDir)) { New-Item -ItemType Directory -Path $stdioDir | Out-Null }",
|
|
179
|
+
"if (-not (Test-Path -LiteralPath $aerialDir)) { New-Item -ItemType Directory -Path $aerialDir | Out-Null }",
|
|
180
|
+
"if (Test-Path -LiteralPath $stdioLog) {",
|
|
181
|
+
" $size = (Get-Item -LiteralPath $stdioLog).Length",
|
|
182
|
+
" if ($size -gt $maxBytes) {",
|
|
183
|
+
" for ($i = $backups - 1; $i -ge 1; $i--) {",
|
|
184
|
+
" $src = \"$stdioLog.$i\"",
|
|
185
|
+
" $dst = \"$stdioLog.$($i + 1)\"",
|
|
186
|
+
" if (Test-Path -LiteralPath $src) {",
|
|
187
|
+
" if (Test-Path -LiteralPath $dst) { Remove-Item -LiteralPath $dst -Force }",
|
|
188
|
+
" Move-Item -LiteralPath $src -Destination $dst",
|
|
189
|
+
" }",
|
|
190
|
+
" }",
|
|
191
|
+
" if (Test-Path -LiteralPath \"$stdioLog.1\") { Remove-Item -LiteralPath \"$stdioLog.1\" -Force }",
|
|
192
|
+
" Move-Item -LiteralPath $stdioLog -Destination \"$stdioLog.1\"",
|
|
193
|
+
" }",
|
|
194
|
+
"}",
|
|
195
|
+
"$env:AERIAL_LOG_FILE = $aerialLog",
|
|
196
|
+
"$env:AERIAL_LOG_MAX_BYTES = \"$maxBytes\"",
|
|
197
|
+
"$env:AERIAL_LOG_BACKUPS = \"$backups\""
|
|
198
|
+
];
|
|
199
|
+
if (cfg) lines.push(`$env:AERIAL_CONFIG_DIR = ${psEscape(cfg)}`);
|
|
200
|
+
lines.push("& $node $cli start --host $serviceHost --port $servicePort *>> $stdioLog");
|
|
201
|
+
lines.push("");
|
|
202
|
+
return lines.join("\r\n");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function quoteSchtasksTR(command) {
|
|
206
|
+
const wrapped = command.replace(/"/g, '\\"');
|
|
207
|
+
return `\\"${wrapped}\\"`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function buildSchtasksCreateArgs({ taskName = WIN_TASK_NAME, wrapperPath: wrapper }) {
|
|
211
|
+
const cmd = `powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "${wrapper}"`;
|
|
212
|
+
return [
|
|
213
|
+
"/Create",
|
|
214
|
+
"/TN", taskName,
|
|
215
|
+
"/SC", "ONLOGON",
|
|
216
|
+
"/RL", "LIMITED",
|
|
217
|
+
"/F",
|
|
218
|
+
"/TR", quoteSchtasksTR(cmd)
|
|
219
|
+
];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function buildSchtasksArgs(action, taskName = WIN_TASK_NAME) {
|
|
223
|
+
if (action === "delete") return ["/Delete", "/TN", taskName, "/F"];
|
|
224
|
+
if (action === "run") return ["/Run", "/TN", taskName];
|
|
225
|
+
if (action === "end") return ["/End", "/TN", taskName];
|
|
226
|
+
if (action === "query") return ["/Query", "/TN", taskName, "/FO", "LIST"];
|
|
227
|
+
throw new Error(`Unsupported schtasks action: ${action}`);
|
|
228
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const BACKUP_PREFIX = ".aerial-backup-";
|
|
5
|
+
const ISO_STAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z$/;
|
|
6
|
+
|
|
7
|
+
export function backupIfExists(file) {
|
|
8
|
+
if (!fs.existsSync(file)) return undefined;
|
|
9
|
+
const backup = `${file}.aerial-backup-${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
|
10
|
+
fs.copyFileSync(file, backup);
|
|
11
|
+
return backup;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function listBackups(file) {
|
|
15
|
+
const dir = path.dirname(file);
|
|
16
|
+
const base = path.basename(file);
|
|
17
|
+
if (!fs.existsSync(dir)) return [];
|
|
18
|
+
const prefix = `${base}${BACKUP_PREFIX}`;
|
|
19
|
+
const entries = fs.readdirSync(dir);
|
|
20
|
+
const matches = [];
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
if (!entry.startsWith(prefix)) continue;
|
|
23
|
+
const stamp = entry.slice(prefix.length);
|
|
24
|
+
if (!ISO_STAMP_RE.test(stamp)) continue;
|
|
25
|
+
matches.push({ name: entry, path: path.join(dir, entry), stamp });
|
|
26
|
+
}
|
|
27
|
+
matches.sort((a, b) => (a.stamp < b.stamp ? -1 : a.stamp > b.stamp ? 1 : 0));
|
|
28
|
+
return matches;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function findLatestBackup(file) {
|
|
32
|
+
const all = listBackups(file);
|
|
33
|
+
return all.length ? all[all.length - 1] : undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function backupPathsFor(file) {
|
|
37
|
+
return listBackups(file).map((entry) => entry.path);
|
|
38
|
+
}
|