ai-task-board-bridge 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +253 -0
- package/dist/activity-sanitizer.d.ts +5 -0
- package/dist/activity-sanitizer.d.ts.map +1 -0
- package/dist/activity-sanitizer.js +55 -0
- package/dist/activity-sanitizer.js.map +1 -0
- package/dist/app-server-client.d.ts +328 -0
- package/dist/app-server-client.d.ts.map +1 -0
- package/dist/app-server-client.js +524 -0
- package/dist/app-server-client.js.map +1 -0
- package/dist/bridge.d.ts +139 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +2769 -0
- package/dist/bridge.js.map +1 -0
- package/dist/claim-retry.d.ts +10 -0
- package/dist/claim-retry.d.ts.map +1 -0
- package/dist/claim-retry.js +19 -0
- package/dist/claim-retry.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +92 -0
- package/dist/cli.js.map +1 -0
- package/dist/history-sync.d.ts +120 -0
- package/dist/history-sync.d.ts.map +1 -0
- package/dist/history-sync.js +718 -0
- package/dist/history-sync.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/setup.d.ts +32 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +822 -0
- package/dist/setup.js.map +1 -0
- package/dist/wake-client.d.ts +35 -0
- package/dist/wake-client.d.ts.map +1 -0
- package/dist/wake-client.js +219 -0
- package/dist/wake-client.js.map +1 -0
- package/dist/working-directories.d.ts +24 -0
- package/dist/working-directories.d.ts.map +1 -0
- package/dist/working-directories.js +158 -0
- package/dist/working-directories.js.map +1 -0
- package/package.json +51 -0
package/dist/setup.js
ADDED
|
@@ -0,0 +1,822 @@
|
|
|
1
|
+
import { constants as fsConstants } from "node:fs";
|
|
2
|
+
import { access, chmod, copyFile, cp, mkdir, readFile, rename, rm, stat, writeFile, } from "node:fs/promises";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { randomBytes } from "node:crypto";
|
|
5
|
+
import { userInfo } from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { createInterface } from "node:readline/promises";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
export const BRIDGE_SYSTEMD_SERVICE = "ai-task-board-bridge.service";
|
|
10
|
+
export const LEGACY_BRIDGE_SYSTEMD_SERVICE = "ai-task-board-codex-bridge.service";
|
|
11
|
+
function xdgDirectory(value, fallback) {
|
|
12
|
+
return value && path.isAbsolute(value) ? path.normalize(value) : fallback;
|
|
13
|
+
}
|
|
14
|
+
function safeVersionSegment(version) {
|
|
15
|
+
const segment = version.replace(/[^A-Za-z0-9._-]/g, "-");
|
|
16
|
+
if (!segment || segment === "." || segment === "..") {
|
|
17
|
+
throw new Error(`无法使用 npm 包版本 ${JSON.stringify(version)} 作为安装目录`);
|
|
18
|
+
}
|
|
19
|
+
return segment;
|
|
20
|
+
}
|
|
21
|
+
export function resolveSetupPaths(homeDirectory, packageVersion, environment = process.env) {
|
|
22
|
+
if (!path.isAbsolute(homeDirectory)) {
|
|
23
|
+
throw new Error("用户主目录必须是绝对路径");
|
|
24
|
+
}
|
|
25
|
+
const configHome = xdgDirectory(environment.XDG_CONFIG_HOME, path.join(homeDirectory, ".config"));
|
|
26
|
+
const dataHome = xdgDirectory(environment.XDG_DATA_HOME, path.join(homeDirectory, ".local", "share"));
|
|
27
|
+
const configDirectory = path.join(configHome, "ai-task-board");
|
|
28
|
+
const runtimeDirectory = path.join(dataHome, "ai-task-board", "codex-bridge", "versions", safeVersionSegment(packageVersion));
|
|
29
|
+
return {
|
|
30
|
+
configDirectory,
|
|
31
|
+
environmentFile: path.join(configDirectory, "codex-bridge.env"),
|
|
32
|
+
unitFile: path.join(configHome, "systemd", "user", BRIDGE_SYSTEMD_SERVICE),
|
|
33
|
+
runtimeDirectory,
|
|
34
|
+
runtimeCli: path.join(runtimeDirectory, "dist", "cli.js"),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function assertSingleLine(value, description) {
|
|
38
|
+
if (/[\0\r\n]/.test(value)) {
|
|
39
|
+
throw new Error(`${description} 不能包含换行符或 NUL`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function quoteEnvironmentValue(value) {
|
|
43
|
+
assertSingleLine(value, "环境变量值");
|
|
44
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
45
|
+
}
|
|
46
|
+
export function serializeEnvironmentFile(values) {
|
|
47
|
+
const lines = [];
|
|
48
|
+
for (const [name, value] of Object.entries(values).sort(([left], [right]) => left.localeCompare(right))) {
|
|
49
|
+
if (value === undefined)
|
|
50
|
+
continue;
|
|
51
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
52
|
+
throw new Error(`无效的环境变量名:${name}`);
|
|
53
|
+
}
|
|
54
|
+
lines.push(`${name}=${quoteEnvironmentValue(value)}`);
|
|
55
|
+
}
|
|
56
|
+
return `${lines.join("\n")}\n`;
|
|
57
|
+
}
|
|
58
|
+
function decodeEnvironmentValue(rawValue) {
|
|
59
|
+
const value = rawValue.trim();
|
|
60
|
+
if (value.length >= 2 && value.startsWith("'") && value.endsWith("'")) {
|
|
61
|
+
return value.slice(1, -1);
|
|
62
|
+
}
|
|
63
|
+
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
|
64
|
+
let decoded = "";
|
|
65
|
+
for (let index = 1; index < value.length - 1; index += 1) {
|
|
66
|
+
const character = value[index];
|
|
67
|
+
if (character === "\\" && index + 1 < value.length - 1) {
|
|
68
|
+
const next = value[index + 1];
|
|
69
|
+
if (next === "\\" || next === '"') {
|
|
70
|
+
decoded += next;
|
|
71
|
+
index += 1;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
decoded += character;
|
|
76
|
+
}
|
|
77
|
+
return decoded;
|
|
78
|
+
}
|
|
79
|
+
let decoded = "";
|
|
80
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
81
|
+
if (value[index] === "\\" && index + 1 < value.length) {
|
|
82
|
+
index += 1;
|
|
83
|
+
}
|
|
84
|
+
decoded += value[index];
|
|
85
|
+
}
|
|
86
|
+
return decoded;
|
|
87
|
+
}
|
|
88
|
+
export function parseEnvironmentFile(contents) {
|
|
89
|
+
const values = {};
|
|
90
|
+
for (const line of contents.split(/\r?\n/)) {
|
|
91
|
+
if (!line.trim() || line.trimStart().startsWith("#"))
|
|
92
|
+
continue;
|
|
93
|
+
const match = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/.exec(line);
|
|
94
|
+
if (!match)
|
|
95
|
+
continue;
|
|
96
|
+
values[match[1]] = decodeEnvironmentValue(match[2]);
|
|
97
|
+
}
|
|
98
|
+
return values;
|
|
99
|
+
}
|
|
100
|
+
function stripTomlComment(line) {
|
|
101
|
+
let quote = null;
|
|
102
|
+
let escaped = false;
|
|
103
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
104
|
+
const character = line[index];
|
|
105
|
+
if (quote === '"' && character === "\\" && !escaped) {
|
|
106
|
+
escaped = true;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (character === quote && !escaped) {
|
|
110
|
+
quote = null;
|
|
111
|
+
}
|
|
112
|
+
else if (!quote && (character === '"' || character === "'")) {
|
|
113
|
+
quote = character;
|
|
114
|
+
}
|
|
115
|
+
else if (!quote && character === "#") {
|
|
116
|
+
return line.slice(0, index);
|
|
117
|
+
}
|
|
118
|
+
escaped = false;
|
|
119
|
+
}
|
|
120
|
+
return line;
|
|
121
|
+
}
|
|
122
|
+
function addEnvironmentName(names, value) {
|
|
123
|
+
if (value && /^[A-Za-z_][A-Za-z0-9_]*$/.test(value))
|
|
124
|
+
names.add(value);
|
|
125
|
+
}
|
|
126
|
+
export function discoverCodexProviderEnvironmentVariables(configToml) {
|
|
127
|
+
const names = new Set();
|
|
128
|
+
const uncommentedLines = configToml.split(/\r?\n/).map(stripTomlComment);
|
|
129
|
+
let inEnvironmentHeadersTable = false;
|
|
130
|
+
for (const line of uncommentedLines) {
|
|
131
|
+
const section = /^\s*\[\s*([^\]]+)\s*\]\s*$/.exec(line);
|
|
132
|
+
if (section) {
|
|
133
|
+
const sectionName = section[1].trim();
|
|
134
|
+
inEnvironmentHeadersTable =
|
|
135
|
+
sectionName.startsWith("model_providers.") &&
|
|
136
|
+
sectionName.endsWith(".env_http_headers");
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const envKey = /^\s*env_key\s*=\s*["']([A-Za-z_][A-Za-z0-9_]*)["']/.exec(line);
|
|
140
|
+
addEnvironmentName(names, envKey?.[1]);
|
|
141
|
+
if (inEnvironmentHeadersTable) {
|
|
142
|
+
const headerValue = /^\s*(?:[A-Za-z0-9_-]+|["'][^"']+["'])\s*=\s*["']([A-Za-z_][A-Za-z0-9_]*)["']/.exec(line);
|
|
143
|
+
addEnvironmentName(names, headerValue?.[1]);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const uncommented = uncommentedLines.join("\n");
|
|
147
|
+
for (const block of uncommented.matchAll(/\benv_http_headers\s*=\s*\{([^}]*)\}/gs)) {
|
|
148
|
+
for (const entry of block[1].matchAll(/=\s*["']([A-Za-z_][A-Za-z0-9_]*)["']/g)) {
|
|
149
|
+
addEnvironmentName(names, entry[1]);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return [...names].sort();
|
|
153
|
+
}
|
|
154
|
+
function quoteSystemdArgument(value) {
|
|
155
|
+
assertSingleLine(value, "systemd unit 值");
|
|
156
|
+
return `"${value
|
|
157
|
+
.replace(/%/g, "%%")
|
|
158
|
+
.replace(/\\/g, "\\\\")
|
|
159
|
+
.replace(/"/g, '\\"')}"`;
|
|
160
|
+
}
|
|
161
|
+
function escapeSystemdPath(value) {
|
|
162
|
+
assertSingleLine(value, "systemd 路径");
|
|
163
|
+
if (!path.isAbsolute(value))
|
|
164
|
+
throw new Error(`systemd 路径必须是绝对路径:${value}`);
|
|
165
|
+
return value
|
|
166
|
+
.replace(/%/g, "%%")
|
|
167
|
+
.replace(/\\/g, "\\x5c")
|
|
168
|
+
.replace(/ /g, "\\x20")
|
|
169
|
+
.replace(/\t/g, "\\x09")
|
|
170
|
+
.replace(/"/g, "\\x22")
|
|
171
|
+
.replace(/'/g, "\\x27");
|
|
172
|
+
}
|
|
173
|
+
export function renderSystemdUserUnit(options) {
|
|
174
|
+
const command = [options.nodeBinary, options.runtimeCli, "run"]
|
|
175
|
+
.map(quoteSystemdArgument)
|
|
176
|
+
.join(" ");
|
|
177
|
+
return `[Unit]
|
|
178
|
+
Description=AI Task Board Bridge
|
|
179
|
+
Wants=network-online.target
|
|
180
|
+
After=network-online.target
|
|
181
|
+
|
|
182
|
+
[Service]
|
|
183
|
+
Type=simple
|
|
184
|
+
WorkingDirectory=${escapeSystemdPath(options.workingDirectory)}
|
|
185
|
+
Environment=${quoteSystemdArgument(`HOME=${options.homeDirectory}`)}
|
|
186
|
+
Environment=${quoteSystemdArgument(`CODEX_HOME=${options.codexHome}`)}
|
|
187
|
+
EnvironmentFile=${escapeSystemdPath(options.environmentFile)}
|
|
188
|
+
ExecStart=${command}
|
|
189
|
+
Restart=on-failure
|
|
190
|
+
RestartSec=5
|
|
191
|
+
KillSignal=SIGTERM
|
|
192
|
+
TimeoutStopSec=30
|
|
193
|
+
|
|
194
|
+
[Install]
|
|
195
|
+
WantedBy=default.target
|
|
196
|
+
`;
|
|
197
|
+
}
|
|
198
|
+
async function pathIsDirectory(candidate) {
|
|
199
|
+
try {
|
|
200
|
+
return (await stat(candidate)).isDirectory();
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function expandPath(value, homeDirectory, workingDirectory) {
|
|
207
|
+
const expanded = value === "~"
|
|
208
|
+
? homeDirectory
|
|
209
|
+
: value.startsWith("~/")
|
|
210
|
+
? path.join(homeDirectory, value.slice(2))
|
|
211
|
+
: value;
|
|
212
|
+
return path.resolve(workingDirectory, expanded);
|
|
213
|
+
}
|
|
214
|
+
export async function resolveExecutable(value, options) {
|
|
215
|
+
const executable = value.trim();
|
|
216
|
+
if (!executable)
|
|
217
|
+
return null;
|
|
218
|
+
const candidates = executable.includes("/")
|
|
219
|
+
? [expandPath(executable, options.homeDirectory, options.cwd)]
|
|
220
|
+
: (options.pathValue ?? "")
|
|
221
|
+
.split(path.delimiter)
|
|
222
|
+
.filter(Boolean)
|
|
223
|
+
.map((directory) => path.join(directory, executable));
|
|
224
|
+
for (const candidate of candidates) {
|
|
225
|
+
try {
|
|
226
|
+
await access(candidate, fsConstants.X_OK);
|
|
227
|
+
if (!(await stat(candidate)).isDirectory())
|
|
228
|
+
return path.resolve(candidate);
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
// Try the next PATH entry.
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
function normalizeBoardUrl(value) {
|
|
237
|
+
const normalized = value.trim().replace(/\/+$/, "");
|
|
238
|
+
let parsed;
|
|
239
|
+
try {
|
|
240
|
+
parsed = new URL(normalized);
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
throw new Error("请输入完整的 http:// 或 https:// 地址");
|
|
244
|
+
}
|
|
245
|
+
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
246
|
+
throw new Error("Board 地址只支持 http:// 或 https://");
|
|
247
|
+
}
|
|
248
|
+
if (parsed.username || parsed.password) {
|
|
249
|
+
throw new Error("Board 地址不能包含用户名或密码");
|
|
250
|
+
}
|
|
251
|
+
return normalized;
|
|
252
|
+
}
|
|
253
|
+
async function readExistingEnvironment(environmentFile) {
|
|
254
|
+
try {
|
|
255
|
+
return parseEnvironmentFile(await readFile(environmentFile, "utf8"));
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
if (error.code === "ENOENT")
|
|
259
|
+
return {};
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
async function atomicWrite(destination, contents, mode) {
|
|
264
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
265
|
+
const temporary = `${destination}.tmp-${process.pid}-${randomBytes(6).toString("hex")}`;
|
|
266
|
+
try {
|
|
267
|
+
await writeFile(temporary, contents, { encoding: "utf8", flag: "wx", mode });
|
|
268
|
+
await rename(temporary, destination);
|
|
269
|
+
await chmod(destination, mode);
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
await rm(temporary, { force: true }).catch(() => undefined);
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
async function installRuntime(sourcePackageDirectory, paths) {
|
|
277
|
+
const sourceCli = path.join(sourcePackageDirectory, "dist", "cli.js");
|
|
278
|
+
try {
|
|
279
|
+
await access(sourceCli, fsConstants.R_OK);
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
throw new Error(`找不到已构建的 Bridge CLI:${sourceCli}。从源码运行 setup 前请先构建 npm 包。`);
|
|
283
|
+
}
|
|
284
|
+
let runtimeExists = false;
|
|
285
|
+
try {
|
|
286
|
+
const runtimeStat = await stat(paths.runtimeDirectory);
|
|
287
|
+
if (!runtimeStat.isDirectory()) {
|
|
288
|
+
throw new Error(`Bridge 运行路径已存在且不是目录:${paths.runtimeDirectory}`);
|
|
289
|
+
}
|
|
290
|
+
runtimeExists = true;
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
if (error.code !== "ENOENT")
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
296
|
+
await mkdir(path.dirname(paths.runtimeDirectory), {
|
|
297
|
+
recursive: true,
|
|
298
|
+
mode: 0o700,
|
|
299
|
+
});
|
|
300
|
+
const staging = `${paths.runtimeDirectory}.tmp-${process.pid}-${randomBytes(6).toString("hex")}`;
|
|
301
|
+
const previous = `${paths.runtimeDirectory}.previous-${process.pid}-${randomBytes(6).toString("hex")}`;
|
|
302
|
+
let movedPrevious = false;
|
|
303
|
+
try {
|
|
304
|
+
await mkdir(staging, { mode: 0o700 });
|
|
305
|
+
await cp(path.join(sourcePackageDirectory, "dist"), path.join(staging, "dist"), {
|
|
306
|
+
recursive: true,
|
|
307
|
+
force: true,
|
|
308
|
+
});
|
|
309
|
+
await copyFile(path.join(sourcePackageDirectory, "package.json"), path.join(staging, "package.json"));
|
|
310
|
+
try {
|
|
311
|
+
await copyFile(path.join(sourcePackageDirectory, "README.md"), path.join(staging, "README.md"));
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
if (error.code !== "ENOENT")
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
317
|
+
await chmod(path.join(staging, "dist", "cli.js"), 0o755);
|
|
318
|
+
if (runtimeExists) {
|
|
319
|
+
await rename(paths.runtimeDirectory, previous);
|
|
320
|
+
movedPrevious = true;
|
|
321
|
+
}
|
|
322
|
+
await rename(staging, paths.runtimeDirectory);
|
|
323
|
+
if (movedPrevious) {
|
|
324
|
+
movedPrevious = false;
|
|
325
|
+
await rm(previous, { recursive: true, force: true }).catch(() => {
|
|
326
|
+
process.stderr.write(`旧 Bridge 运行目录保留在:${previous}\n`);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
await rm(staging, { recursive: true, force: true }).catch(() => undefined);
|
|
332
|
+
if (movedPrevious) {
|
|
333
|
+
try {
|
|
334
|
+
await rename(previous, paths.runtimeDirectory);
|
|
335
|
+
movedPrevious = false;
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
// Keep the previous directory in place for manual recovery.
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
throw error;
|
|
342
|
+
}
|
|
343
|
+
finally {
|
|
344
|
+
if (movedPrevious) {
|
|
345
|
+
// A failed restore is intentionally retained rather than deleted.
|
|
346
|
+
process.stderr.write(`旧 Bridge 运行目录保留在:${previous}\n`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function runCommand(command, args) {
|
|
351
|
+
return new Promise((resolve, reject) => {
|
|
352
|
+
const child = spawn(command, [...args], { stdio: "inherit" });
|
|
353
|
+
child.once("error", reject);
|
|
354
|
+
child.once("exit", (code, signal) => {
|
|
355
|
+
if (code === 0) {
|
|
356
|
+
resolve();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
reject(new Error(`${command} ${args.join(" ")} 执行失败${signal ? `(signal ${signal})` : `(退出码 ${code ?? "unknown"})`}`));
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
function captureCommand(command, args) {
|
|
364
|
+
return new Promise((resolve) => {
|
|
365
|
+
const child = spawn(command, [...args], {
|
|
366
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
367
|
+
});
|
|
368
|
+
let output = "";
|
|
369
|
+
child.stdout?.on("data", (chunk) => {
|
|
370
|
+
output += chunk.toString();
|
|
371
|
+
});
|
|
372
|
+
child.once("error", () => resolve(null));
|
|
373
|
+
child.once("exit", (code) => resolve(code === 0 ? output.trim() : null));
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
class TerminalPrompter {
|
|
377
|
+
input;
|
|
378
|
+
output;
|
|
379
|
+
readline;
|
|
380
|
+
originalWriteToOutput;
|
|
381
|
+
muted = false;
|
|
382
|
+
constructor(input, output) {
|
|
383
|
+
this.input = input;
|
|
384
|
+
this.output = output;
|
|
385
|
+
this.readline = createInterface({
|
|
386
|
+
input,
|
|
387
|
+
output,
|
|
388
|
+
terminal: Boolean(input.isTTY && output.isTTY),
|
|
389
|
+
});
|
|
390
|
+
this.originalWriteToOutput = this.readline._writeToOutput?.bind(this.readline);
|
|
391
|
+
if (this.originalWriteToOutput) {
|
|
392
|
+
this.readline._writeToOutput = (value) => {
|
|
393
|
+
if (!this.muted)
|
|
394
|
+
this.originalWriteToOutput?.(value);
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
write(value) {
|
|
399
|
+
this.output.write(value);
|
|
400
|
+
}
|
|
401
|
+
async text(label, options = {}) {
|
|
402
|
+
while (true) {
|
|
403
|
+
const defaultHint = options.defaultValue === undefined ? "" : ` [${options.defaultValue}]`;
|
|
404
|
+
const answer = (await this.readline.question(`${label}${defaultHint}: `)).trim();
|
|
405
|
+
const value = answer || options.defaultValue || "";
|
|
406
|
+
if (options.required && !value) {
|
|
407
|
+
this.write(" 该项不能为空。\n");
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
try {
|
|
411
|
+
return options.validate ? options.validate(value) : value;
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
this.write(` ${error instanceof Error ? error.message : String(error)}\n`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
async secret(label, existingValue) {
|
|
419
|
+
while (true) {
|
|
420
|
+
const suffix = existingValue ? "(回车保留现有值)" : "";
|
|
421
|
+
this.write(`${label}${suffix}: `);
|
|
422
|
+
this.muted = Boolean(this.input.isTTY && this.output.isTTY);
|
|
423
|
+
let answer;
|
|
424
|
+
try {
|
|
425
|
+
answer = (await this.readline.question("")).trim();
|
|
426
|
+
}
|
|
427
|
+
finally {
|
|
428
|
+
if (this.muted)
|
|
429
|
+
this.write("\n");
|
|
430
|
+
this.muted = false;
|
|
431
|
+
}
|
|
432
|
+
if (answer)
|
|
433
|
+
return answer;
|
|
434
|
+
if (existingValue)
|
|
435
|
+
return existingValue;
|
|
436
|
+
this.write(" 该项不能为空。\n");
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
async confirm(label, defaultValue) {
|
|
440
|
+
const hint = defaultValue ? "Y/n" : "y/N";
|
|
441
|
+
while (true) {
|
|
442
|
+
const answer = (await this.readline.question(`${label} [${hint}]: `))
|
|
443
|
+
.trim()
|
|
444
|
+
.toLowerCase();
|
|
445
|
+
if (!answer)
|
|
446
|
+
return defaultValue;
|
|
447
|
+
if (["y", "yes", "是"].includes(answer))
|
|
448
|
+
return true;
|
|
449
|
+
if (["n", "no", "否"].includes(answer))
|
|
450
|
+
return false;
|
|
451
|
+
this.write(" 请输入 y 或 n。\n");
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
async choice(label, choices, defaultValue) {
|
|
455
|
+
this.write(`${label}\n`);
|
|
456
|
+
choices.forEach((choice, index) => {
|
|
457
|
+
this.write(` ${index + 1}) ${choice.label}\n`);
|
|
458
|
+
});
|
|
459
|
+
const defaultIndex = Math.max(0, choices.findIndex((choice) => choice.value === defaultValue));
|
|
460
|
+
while (true) {
|
|
461
|
+
const answer = (await this.readline.question(`请选择 [${defaultIndex + 1}]: `)).trim();
|
|
462
|
+
if (!answer)
|
|
463
|
+
return choices[defaultIndex].value;
|
|
464
|
+
const index = Number(answer) - 1;
|
|
465
|
+
if (Number.isInteger(index) && choices[index])
|
|
466
|
+
return choices[index].value;
|
|
467
|
+
const named = choices.find((choice) => choice.value === answer);
|
|
468
|
+
if (named)
|
|
469
|
+
return named.value;
|
|
470
|
+
this.write(` 请输入 1 到 ${choices.length}。\n`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
close() {
|
|
474
|
+
this.readline.close();
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
function configuredValue(existing, environment, name) {
|
|
478
|
+
return environment[name]?.trim() || existing[name]?.trim() || undefined;
|
|
479
|
+
}
|
|
480
|
+
function validChoice(value, choices, fallback) {
|
|
481
|
+
return value && choices.includes(value)
|
|
482
|
+
? value
|
|
483
|
+
: fallback;
|
|
484
|
+
}
|
|
485
|
+
function firstConfiguredWorkingDirectory(raw) {
|
|
486
|
+
try {
|
|
487
|
+
const parsed = JSON.parse(raw);
|
|
488
|
+
if (!Array.isArray(parsed))
|
|
489
|
+
return null;
|
|
490
|
+
const first = parsed[0];
|
|
491
|
+
return typeof first?.path === "string" && path.isAbsolute(first.path)
|
|
492
|
+
? path.normalize(first.path)
|
|
493
|
+
: null;
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
function parseEnvironmentNames(value) {
|
|
500
|
+
if (!value || value === "-")
|
|
501
|
+
return [];
|
|
502
|
+
const names = [...new Set(value.split(/[\s,]+/).filter(Boolean))];
|
|
503
|
+
const invalid = names.find((name) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name));
|
|
504
|
+
if (invalid)
|
|
505
|
+
throw new Error(`无效的环境变量名:${invalid}`);
|
|
506
|
+
return names;
|
|
507
|
+
}
|
|
508
|
+
const SETUP_MANAGED_ENVIRONMENT_NAMES = new Set([
|
|
509
|
+
"AI_TASK_BOARD_URL",
|
|
510
|
+
"AI_TASK_BOARD_CONNECTION_TOKEN",
|
|
511
|
+
"CODEX_HOME",
|
|
512
|
+
"CODEX_BINARY",
|
|
513
|
+
"HOME",
|
|
514
|
+
"PATH",
|
|
515
|
+
]);
|
|
516
|
+
function parseProviderEnvironmentNames(value) {
|
|
517
|
+
const names = parseEnvironmentNames(value);
|
|
518
|
+
const managed = names.find((name) => SETUP_MANAGED_ENVIRONMENT_NAMES.has(name));
|
|
519
|
+
if (managed) {
|
|
520
|
+
throw new Error(`${managed} 由安装器管理,不能作为 provider 环境变量`);
|
|
521
|
+
}
|
|
522
|
+
return names;
|
|
523
|
+
}
|
|
524
|
+
async function ownerUid(candidate) {
|
|
525
|
+
try {
|
|
526
|
+
return (await stat(candidate)).uid;
|
|
527
|
+
}
|
|
528
|
+
catch {
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
export async function runInteractiveSetup(options) {
|
|
533
|
+
if (process.platform !== "linux") {
|
|
534
|
+
throw new Error("交互式 systemd 安装目前只支持 Linux");
|
|
535
|
+
}
|
|
536
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
537
|
+
throw new Error("setup 需要交互式终端;自动化运行请继续通过环境变量启动 Bridge");
|
|
538
|
+
}
|
|
539
|
+
if ((await captureCommand("systemctl", ["--user", "show-environment"])) === null) {
|
|
540
|
+
throw new Error("无法连接当前用户的 systemd user manager;请在目标用户的登录会话中运行 setup");
|
|
541
|
+
}
|
|
542
|
+
const identity = userInfo();
|
|
543
|
+
const effectiveUid = process.geteuid?.() ?? identity.uid;
|
|
544
|
+
const homeDirectory = path.resolve(identity.homedir);
|
|
545
|
+
const environment = process.env;
|
|
546
|
+
const paths = resolveSetupPaths(homeDirectory, options.packageVersion, environment);
|
|
547
|
+
const existing = await readExistingEnvironment(paths.environmentFile);
|
|
548
|
+
const prompt = new TerminalPrompter(process.stdin, process.stdout);
|
|
549
|
+
try {
|
|
550
|
+
prompt.write("\nAI Task Board Bridge 交互式安装\n\n");
|
|
551
|
+
prompt.write(`运行身份:${identity.username} (UID ${effectiveUid})\n用户目录:${homeDirectory}\n`);
|
|
552
|
+
prompt.write("将安装为该 UID 的 systemd 用户服务;unit 不会写入 User=,也不会使用 sudo。\n\n");
|
|
553
|
+
if (environment.HOME &&
|
|
554
|
+
path.resolve(environment.HOME) !== homeDirectory) {
|
|
555
|
+
prompt.write(`提示:环境中的 HOME=${environment.HOME} 与当前 UID 的主目录不同;安装器将使用 ${homeDirectory}。\n\n`);
|
|
556
|
+
}
|
|
557
|
+
if (effectiveUid === 0) {
|
|
558
|
+
prompt.write("警告:当前有效用户是 root,继续会安装 root 的用户服务并使用 root 的 Codex 配置。\n");
|
|
559
|
+
if (!(await prompt.confirm("确认以 root 身份继续", false))) {
|
|
560
|
+
prompt.write("已取消,未修改任何文件。\n");
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
const boardUrl = await prompt.text("Board 地址", {
|
|
565
|
+
defaultValue: configuredValue(existing, environment, "AI_TASK_BOARD_URL"),
|
|
566
|
+
required: true,
|
|
567
|
+
validate: normalizeBoardUrl,
|
|
568
|
+
});
|
|
569
|
+
const connectionToken = await prompt.secret("Connection Token(输入内容不会回显)", configuredValue(existing, environment, "AI_TASK_BOARD_CONNECTION_TOKEN"));
|
|
570
|
+
const rawMultipleDirectories = configuredValue(existing, environment, "CODEX_WORKING_DIRECTORIES");
|
|
571
|
+
let preserveMultipleDirectories = false;
|
|
572
|
+
let workingDirectory;
|
|
573
|
+
if (rawMultipleDirectories) {
|
|
574
|
+
const firstDirectory = firstConfiguredWorkingDirectory(rawMultipleDirectories);
|
|
575
|
+
if (firstDirectory) {
|
|
576
|
+
prompt.write(`\n检测到现有多目录配置,首目录为 ${firstDirectory}。\n`);
|
|
577
|
+
preserveMultipleDirectories = await prompt.confirm("保留现有 CODEX_WORKING_DIRECTORIES", true);
|
|
578
|
+
workingDirectory = firstDirectory;
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
prompt.write("\n现有 CODEX_WORKING_DIRECTORIES 无法解析,本次将改为单目录配置。\n");
|
|
582
|
+
workingDirectory = process.cwd();
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
else {
|
|
586
|
+
workingDirectory = process.cwd();
|
|
587
|
+
}
|
|
588
|
+
if (!preserveMultipleDirectories) {
|
|
589
|
+
workingDirectory = await prompt.text("Bridge 工作目录", {
|
|
590
|
+
defaultValue: configuredValue(existing, environment, "CODEX_WORKING_DIRECTORY") ??
|
|
591
|
+
workingDirectory,
|
|
592
|
+
required: true,
|
|
593
|
+
validate: (value) => expandPath(value, homeDirectory, process.cwd()),
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
if (!(await pathIsDirectory(workingDirectory))) {
|
|
597
|
+
throw new Error(`工作目录不存在或不是目录:${workingDirectory}`);
|
|
598
|
+
}
|
|
599
|
+
const codexHome = await prompt.text("Codex 配置目录", {
|
|
600
|
+
defaultValue: configuredValue(existing, environment, "CODEX_HOME") ??
|
|
601
|
+
path.join(homeDirectory, ".codex"),
|
|
602
|
+
required: true,
|
|
603
|
+
validate: (value) => expandPath(value, homeDirectory, process.cwd()),
|
|
604
|
+
});
|
|
605
|
+
const codexHomeUid = await ownerUid(codexHome);
|
|
606
|
+
if (codexHomeUid === null) {
|
|
607
|
+
prompt.write(` 提示:${codexHome} 尚不存在;启动服务前请以 ${identity.username} 运行 codex login。\n`);
|
|
608
|
+
}
|
|
609
|
+
else if (codexHomeUid !== effectiveUid) {
|
|
610
|
+
prompt.write(` 警告:Codex 配置目录属于 UID ${codexHomeUid},不是当前 UID ${effectiveUid}。\n`);
|
|
611
|
+
if (!(await prompt.confirm("仍然使用这个 Codex 配置目录", false))) {
|
|
612
|
+
throw new Error("Codex 配置目录所有者不匹配");
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
const pathValue = environment.PATH || "/usr/local/bin:/usr/bin:/bin";
|
|
616
|
+
const existingCodexBinary = configuredValue(existing, environment, "CODEX_BINARY");
|
|
617
|
+
const detectedCodexBinary = await resolveExecutable(existingCodexBinary ?? "codex", { cwd: process.cwd(), homeDirectory, pathValue });
|
|
618
|
+
const codexBinaryInput = await prompt.text("Codex 可执行文件", {
|
|
619
|
+
defaultValue: detectedCodexBinary ?? existingCodexBinary ?? "codex",
|
|
620
|
+
required: true,
|
|
621
|
+
});
|
|
622
|
+
const codexBinary = await resolveExecutable(codexBinaryInput, {
|
|
623
|
+
cwd: process.cwd(),
|
|
624
|
+
homeDirectory,
|
|
625
|
+
pathValue,
|
|
626
|
+
});
|
|
627
|
+
if (!codexBinary) {
|
|
628
|
+
throw new Error(`找不到可执行的 Codex CLI:${codexBinaryInput}。请先以 ${identity.username} 安装 Codex。`);
|
|
629
|
+
}
|
|
630
|
+
let detectedProviderEnvironmentNames = [];
|
|
631
|
+
try {
|
|
632
|
+
detectedProviderEnvironmentNames = discoverCodexProviderEnvironmentVariables(await readFile(path.join(codexHome, "config.toml"), "utf8"));
|
|
633
|
+
}
|
|
634
|
+
catch (error) {
|
|
635
|
+
if (error.code !== "ENOENT") {
|
|
636
|
+
prompt.write(` 提示:无法检查 ${path.join(codexHome, "config.toml")} 中的 provider 环境变量。\n`);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
if (detectedProviderEnvironmentNames.length > 0) {
|
|
640
|
+
prompt.write(`检测到 Codex provider 引用环境变量:${detectedProviderEnvironmentNames.join(", ")}\n`);
|
|
641
|
+
}
|
|
642
|
+
const providerEnvironmentNames = await prompt.text("传给 Codex provider 的环境变量名(逗号分隔;输入 - 表示无)", {
|
|
643
|
+
defaultValue: detectedProviderEnvironmentNames.length > 0
|
|
644
|
+
? detectedProviderEnvironmentNames.join(",")
|
|
645
|
+
: undefined,
|
|
646
|
+
validate: (value) => parseProviderEnvironmentNames(value).join(","),
|
|
647
|
+
});
|
|
648
|
+
const providerEnvironment = {};
|
|
649
|
+
for (const name of parseProviderEnvironmentNames(providerEnvironmentNames)) {
|
|
650
|
+
providerEnvironment[name] = await prompt.secret(`${name}(输入内容不会回显)`, environment[name] ?? existing[name]);
|
|
651
|
+
}
|
|
652
|
+
const threadScope = await prompt.choice("Thread 范围", [
|
|
653
|
+
{ value: "cwd", label: "cwd — 仅管理已配置工作目录(推荐)" },
|
|
654
|
+
{ value: "all", label: "all — 管理当前用户的跨项目 Thread(高风险)" },
|
|
655
|
+
], validChoice(configuredValue(existing, environment, "CODEX_THREAD_SCOPE"), ["cwd", "all"], "cwd"));
|
|
656
|
+
const maxThreads = await prompt.text("最多管理的 Thread 数", {
|
|
657
|
+
defaultValue: configuredValue(existing, environment, "CODEX_MAX_THREADS") ?? "50",
|
|
658
|
+
required: true,
|
|
659
|
+
validate: (value) => {
|
|
660
|
+
const parsed = Number(value);
|
|
661
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 500) {
|
|
662
|
+
throw new Error("请输入 1 到 500 的整数");
|
|
663
|
+
}
|
|
664
|
+
return String(parsed);
|
|
665
|
+
},
|
|
666
|
+
});
|
|
667
|
+
const permissionMode = await prompt.choice("Codex 权限模式", [
|
|
668
|
+
{
|
|
669
|
+
value: "safe",
|
|
670
|
+
label: "safe — 仅工作区可写且禁用网络(推荐)",
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
value: "danger-full-access",
|
|
674
|
+
label: "danger-full-access — 当前用户权限内完全访问(高风险)",
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
value: "inherit",
|
|
678
|
+
label: "inherit — 完全沿用本地 Codex/Thread 配置",
|
|
679
|
+
},
|
|
680
|
+
], validChoice(configuredValue(existing, environment, "CODEX_BRIDGE_PERMISSION_MODE"), ["safe", "danger-full-access", "inherit"], "safe"));
|
|
681
|
+
const approvalMode = await prompt.choice("设备端审批模式", [
|
|
682
|
+
{ value: "decline", label: "decline — 自动拒绝审批请求(推荐)" },
|
|
683
|
+
{ value: "accept", label: "accept — 自动批准当前 Turn(高风险)" },
|
|
684
|
+
{
|
|
685
|
+
value: "accept-session",
|
|
686
|
+
label: "accept-session — 可批准整个 Session(更高风险)",
|
|
687
|
+
},
|
|
688
|
+
], validChoice(configuredValue(existing, environment, "CODEX_BRIDGE_APPROVAL_MODE"), ["decline", "accept", "accept-session"], "decline"));
|
|
689
|
+
const webConfiguration = await prompt.confirm("允许 Board 调整受本机边界限制的运行配置", configuredValue(existing, environment, "CODEX_BRIDGE_WEB_CONFIG") ===
|
|
690
|
+
"true");
|
|
691
|
+
prompt.write("\n即将写入:\n");
|
|
692
|
+
prompt.write(` 环境文件:${paths.environmentFile} (0600)\n`);
|
|
693
|
+
prompt.write(` 用户服务:${paths.unitFile}\n`);
|
|
694
|
+
prompt.write(` Bridge 运行副本:${paths.runtimeDirectory}\n`);
|
|
695
|
+
prompt.write(` Codex 配置:${codexHome}\n`);
|
|
696
|
+
if (Object.keys(providerEnvironment).length > 0) {
|
|
697
|
+
prompt.write(` Provider 环境变量:${Object.keys(providerEnvironment).join(", ")}(值已隐藏)\n`);
|
|
698
|
+
}
|
|
699
|
+
prompt.write(" Connection Token:[已隐藏]\n\n");
|
|
700
|
+
if (!(await prompt.confirm("安装并立即启动 systemd 用户服务", true))) {
|
|
701
|
+
prompt.write("已取消,未修改任何文件。\n");
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
const sourcePackageDirectory = fileURLToPath(new URL("../", import.meta.url));
|
|
705
|
+
await installRuntime(sourcePackageDirectory, paths);
|
|
706
|
+
await mkdir(paths.configDirectory, { recursive: true, mode: 0o700 });
|
|
707
|
+
await chmod(paths.configDirectory, 0o700);
|
|
708
|
+
const installedEnvironment = {
|
|
709
|
+
...existing,
|
|
710
|
+
...providerEnvironment,
|
|
711
|
+
AI_TASK_BOARD_URL: boardUrl,
|
|
712
|
+
AI_TASK_BOARD_CONNECTION_TOKEN: connectionToken,
|
|
713
|
+
CODEX_WORKING_DIRECTORY: workingDirectory,
|
|
714
|
+
CODEX_THREAD_SCOPE: threadScope,
|
|
715
|
+
CODEX_MAX_THREADS: maxThreads,
|
|
716
|
+
CODEX_BRIDGE_PERMISSION_MODE: permissionMode,
|
|
717
|
+
CODEX_BRIDGE_APPROVAL_MODE: approvalMode,
|
|
718
|
+
CODEX_BRIDGE_WEB_CONFIG: webConfiguration ? "true" : "false",
|
|
719
|
+
CODEX_BINARY: codexBinary,
|
|
720
|
+
CODEX_HOME: codexHome,
|
|
721
|
+
HOME: homeDirectory,
|
|
722
|
+
PATH: pathValue,
|
|
723
|
+
};
|
|
724
|
+
if (preserveMultipleDirectories && rawMultipleDirectories) {
|
|
725
|
+
installedEnvironment.CODEX_WORKING_DIRECTORIES = rawMultipleDirectories;
|
|
726
|
+
}
|
|
727
|
+
else {
|
|
728
|
+
delete installedEnvironment.CODEX_WORKING_DIRECTORIES;
|
|
729
|
+
}
|
|
730
|
+
await atomicWrite(paths.environmentFile, serializeEnvironmentFile(installedEnvironment), 0o600);
|
|
731
|
+
await atomicWrite(paths.unitFile, renderSystemdUserUnit({
|
|
732
|
+
nodeBinary: process.execPath,
|
|
733
|
+
runtimeCli: paths.runtimeCli,
|
|
734
|
+
workingDirectory,
|
|
735
|
+
homeDirectory,
|
|
736
|
+
codexHome,
|
|
737
|
+
environmentFile: paths.environmentFile,
|
|
738
|
+
}), 0o644);
|
|
739
|
+
await runCommand("systemctl", ["--user", "daemon-reload"]);
|
|
740
|
+
await runCommand("systemctl", ["--user", "enable", paths.unitFile]);
|
|
741
|
+
const legacyUnitPath = path.join(path.dirname(paths.unitFile), LEGACY_BRIDGE_SYSTEMD_SERVICE);
|
|
742
|
+
let legacyWasActive = false;
|
|
743
|
+
let legacyWasEnabled = false;
|
|
744
|
+
try {
|
|
745
|
+
await access(legacyUnitPath, fsConstants.F_OK);
|
|
746
|
+
legacyWasActive =
|
|
747
|
+
(await captureCommand("systemctl", [
|
|
748
|
+
"--user",
|
|
749
|
+
"is-active",
|
|
750
|
+
LEGACY_BRIDGE_SYSTEMD_SERVICE,
|
|
751
|
+
])) === "active";
|
|
752
|
+
legacyWasEnabled =
|
|
753
|
+
(await captureCommand("systemctl", [
|
|
754
|
+
"--user",
|
|
755
|
+
"is-enabled",
|
|
756
|
+
LEGACY_BRIDGE_SYSTEMD_SERVICE,
|
|
757
|
+
])) === "enabled";
|
|
758
|
+
prompt.write(`检测到旧服务 ${LEGACY_BRIDGE_SYSTEMD_SERVICE},正在停用以避免重复运行。\n`);
|
|
759
|
+
await runCommand("systemctl", [
|
|
760
|
+
"--user",
|
|
761
|
+
"disable",
|
|
762
|
+
"--now",
|
|
763
|
+
LEGACY_BRIDGE_SYSTEMD_SERVICE,
|
|
764
|
+
]);
|
|
765
|
+
}
|
|
766
|
+
catch (error) {
|
|
767
|
+
if (error.code !== "ENOENT")
|
|
768
|
+
throw error;
|
|
769
|
+
}
|
|
770
|
+
try {
|
|
771
|
+
await runCommand("systemctl", [
|
|
772
|
+
"--user",
|
|
773
|
+
"restart",
|
|
774
|
+
BRIDGE_SYSTEMD_SERVICE,
|
|
775
|
+
]);
|
|
776
|
+
}
|
|
777
|
+
catch (error) {
|
|
778
|
+
await runCommand("systemctl", [
|
|
779
|
+
"--user",
|
|
780
|
+
"disable",
|
|
781
|
+
"--now",
|
|
782
|
+
BRIDGE_SYSTEMD_SERVICE,
|
|
783
|
+
]).catch(() => undefined);
|
|
784
|
+
if (legacyWasActive) {
|
|
785
|
+
prompt.write("新服务启动失败,正在恢复旧服务。\n");
|
|
786
|
+
await runCommand("systemctl", [
|
|
787
|
+
"--user",
|
|
788
|
+
"enable",
|
|
789
|
+
"--now",
|
|
790
|
+
LEGACY_BRIDGE_SYSTEMD_SERVICE,
|
|
791
|
+
]).catch(() => undefined);
|
|
792
|
+
}
|
|
793
|
+
else if (legacyWasEnabled) {
|
|
794
|
+
await runCommand("systemctl", [
|
|
795
|
+
"--user",
|
|
796
|
+
"enable",
|
|
797
|
+
LEGACY_BRIDGE_SYSTEMD_SERVICE,
|
|
798
|
+
]).catch(() => undefined);
|
|
799
|
+
}
|
|
800
|
+
throw error;
|
|
801
|
+
}
|
|
802
|
+
prompt.write("\n安装完成。\n");
|
|
803
|
+
prompt.write(`服务 ${BRIDGE_SYSTEMD_SERVICE} 正以 ${identity.username} (UID ${effectiveUid}) 运行。\n`);
|
|
804
|
+
prompt.write(`查看状态:systemctl --user status ${BRIDGE_SYSTEMD_SERVICE}\n`);
|
|
805
|
+
prompt.write(`查看日志:journalctl --user -u ${BRIDGE_SYSTEMD_SERVICE} -f\n`);
|
|
806
|
+
prompt.write("安装器未设置模型覆盖;默认模型由这个用户的 Codex 配置和目标 Thread 决定。\n");
|
|
807
|
+
const linger = await captureCommand("loginctl", [
|
|
808
|
+
"show-user",
|
|
809
|
+
String(effectiveUid),
|
|
810
|
+
"-p",
|
|
811
|
+
"Linger",
|
|
812
|
+
"--value",
|
|
813
|
+
]);
|
|
814
|
+
if (linger !== "yes") {
|
|
815
|
+
prompt.write(`提示:若需退出登录后仍运行,请由管理员执行 sudo loginctl enable-linger ${identity.username}\n`);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
finally {
|
|
819
|
+
prompt.close();
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
//# sourceMappingURL=setup.js.map
|