@skylandnpm/octopus-proxy 0.1.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 +52 -0
- package/dist/chunk-TIHWE4WN.js +16201 -0
- package/dist/cli.js +738 -0
- package/dist/main.js +11 -0
- package/package.json +44 -0
- package/proxy.example.json +8 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,738 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ConfigError,
|
|
4
|
+
SERVICE_LABEL,
|
|
5
|
+
SYSTEMD_UNIT,
|
|
6
|
+
TOOL_KINDS,
|
|
7
|
+
atomicJson,
|
|
8
|
+
daemonEntryPath,
|
|
9
|
+
defaultStateDirectory,
|
|
10
|
+
describeTool,
|
|
11
|
+
expandHome,
|
|
12
|
+
isMacOS,
|
|
13
|
+
launchdLogPath,
|
|
14
|
+
launchdPlistPath,
|
|
15
|
+
loadConfig,
|
|
16
|
+
resolveConfigPath,
|
|
17
|
+
runDaemon,
|
|
18
|
+
supportedPlatform,
|
|
19
|
+
systemdUnitPath
|
|
20
|
+
} from "./chunk-TIHWE4WN.js";
|
|
21
|
+
|
|
22
|
+
// src/cli.ts
|
|
23
|
+
import { spawn } from "node:child_process";
|
|
24
|
+
import { access as access3 } from "node:fs/promises";
|
|
25
|
+
|
|
26
|
+
// src/doctor.ts
|
|
27
|
+
import { access as access2, chmod, constants, mkdir as mkdir3, stat as stat2 } from "node:fs/promises";
|
|
28
|
+
import { isAbsolute as isAbsolute2, join as join2 } from "node:path";
|
|
29
|
+
import { homedir as homedir2 } from "node:os";
|
|
30
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
31
|
+
import { promisify as promisify4 } from "node:util";
|
|
32
|
+
|
|
33
|
+
// src/service.ts
|
|
34
|
+
import { readFile, access } from "node:fs/promises";
|
|
35
|
+
import { dirname as dirname3, isAbsolute, resolve } from "node:path";
|
|
36
|
+
|
|
37
|
+
// src/node-resolution.ts
|
|
38
|
+
import { realpath, stat } from "node:fs/promises";
|
|
39
|
+
import { execFile } from "node:child_process";
|
|
40
|
+
import { promisify } from "node:util";
|
|
41
|
+
import { join } from "node:path";
|
|
42
|
+
import { homedir, platform } from "node:os";
|
|
43
|
+
var run = promisify(execFile);
|
|
44
|
+
var VERSION_MANAGED_SEGMENTS = /* @__PURE__ */ new Set([".nvm", "nvm", ".volta", "volta", ".fnm", "fnm", ".asdf", "asdf", ".mise", "mise"]);
|
|
45
|
+
var execProbe = async (path) => {
|
|
46
|
+
try {
|
|
47
|
+
await run(path, ["--version"], { timeout: 5e3 });
|
|
48
|
+
return true;
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
function isVersionManagedPath(path) {
|
|
54
|
+
return path.split("/").some((segment) => VERSION_MANAGED_SEGMENTS.has(segment));
|
|
55
|
+
}
|
|
56
|
+
function nodeCandidates() {
|
|
57
|
+
return platform() === "darwin" ? ["/opt/homebrew/bin/node", "/opt/homebrew/opt/node/bin/node", "/usr/local/bin/node", "/usr/bin/node"] : ["/usr/local/bin/node", "/usr/bin/node", "/snap/bin/node"];
|
|
58
|
+
}
|
|
59
|
+
async function resolveNodePath(options = {}) {
|
|
60
|
+
const probe = options.probe ?? execProbe;
|
|
61
|
+
if (options.pinned) {
|
|
62
|
+
if (!await probe(options.pinned)) throw new Error(`--node-path ${options.pinned} is not a working node executable`);
|
|
63
|
+
return { nodePath: options.pinned, source: "pinned" };
|
|
64
|
+
}
|
|
65
|
+
let exec = options.execPath ?? process.execPath;
|
|
66
|
+
try {
|
|
67
|
+
exec = await realpath(exec);
|
|
68
|
+
} catch {
|
|
69
|
+
}
|
|
70
|
+
if (!isVersionManagedPath(exec) && await probe(exec)) return { nodePath: exec, source: "exec" };
|
|
71
|
+
for (const candidate of nodeCandidates()) if (!isVersionManagedPath(candidate) && await probe(candidate)) return { nodePath: candidate, source: "candidate" };
|
|
72
|
+
if (await probe(exec)) return { nodePath: exec, source: "exec", warning: `only a version-manager node was found (${exec}); install node with the system package manager or pass --node-path, otherwise the service breaks when that node is upgraded` };
|
|
73
|
+
return { nodePath: null, source: "exec" };
|
|
74
|
+
}
|
|
75
|
+
function toolSearchDirs(kind) {
|
|
76
|
+
const dirs = [...describeTool(kind).searchDirs, join(homedir(), ".local", "bin"), join(homedir(), ".opencode", "bin"), join(homedir(), ".codex", "bin")];
|
|
77
|
+
return [...new Set(dirs)];
|
|
78
|
+
}
|
|
79
|
+
async function resolveCommandOnPath(name, extraDirs = []) {
|
|
80
|
+
const dirs = [...(process.env.PATH ?? "").split(":"), ...extraDirs].filter(Boolean);
|
|
81
|
+
for (const dir of new Set(dirs)) {
|
|
82
|
+
const full = join(dir, name);
|
|
83
|
+
try {
|
|
84
|
+
const info = await stat(full);
|
|
85
|
+
if (info.isFile() && info.mode & 73) return full;
|
|
86
|
+
} catch {
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/launchd.ts
|
|
93
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
94
|
+
import { dirname } from "node:path";
|
|
95
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
96
|
+
import { promisify as promisify2 } from "node:util";
|
|
97
|
+
import { userInfo } from "node:os";
|
|
98
|
+
var run2 = promisify2(execFile2);
|
|
99
|
+
var guiDomain = () => `gui/${userInfo().uid}`;
|
|
100
|
+
function xml(value) {
|
|
101
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
102
|
+
}
|
|
103
|
+
function generatePlist(def) {
|
|
104
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
105
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
106
|
+
<plist version="1.0"><dict>
|
|
107
|
+
<key>Label</key><string>${SERVICE_LABEL}</string>
|
|
108
|
+
<key>ProgramArguments</key><array><string>${xml(def.nodePath)}</string><string>${xml(def.entryPath)}</string></array>
|
|
109
|
+
<key>EnvironmentVariables</key><dict><key>OCTOPUS_PROXY_CONFIG</key><string>${xml(def.configPath)}</string><key>PATH</key><string>${xml(def.pathEnv)}</string></dict>
|
|
110
|
+
<key>AbandonProcessGroup</key><true/>
|
|
111
|
+
<key>RunAtLoad</key><true/>
|
|
112
|
+
<key>KeepAlive</key><true/>
|
|
113
|
+
<key>StandardOutPath</key><string>${xml(launchdLogPath())}</string>
|
|
114
|
+
<key>StandardErrorPath</key><string>${xml(launchdLogPath())}</string>
|
|
115
|
+
</dict></plist>
|
|
116
|
+
`;
|
|
117
|
+
}
|
|
118
|
+
async function writePlistFile(xmlText) {
|
|
119
|
+
const path = launchdPlistPath();
|
|
120
|
+
await mkdir(dirname(path), { recursive: true });
|
|
121
|
+
await writeFile(path, xmlText, { mode: 420 });
|
|
122
|
+
return path;
|
|
123
|
+
}
|
|
124
|
+
async function ignoreMissing(action) {
|
|
125
|
+
try {
|
|
126
|
+
await action();
|
|
127
|
+
} catch {
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async function launchdLoad() {
|
|
131
|
+
await ignoreMissing(() => run2("launchctl", ["bootout", `${guiDomain()}/${SERVICE_LABEL}`]));
|
|
132
|
+
await run2("launchctl", ["bootstrap", guiDomain(), launchdPlistPath()]);
|
|
133
|
+
}
|
|
134
|
+
async function launchdStart() {
|
|
135
|
+
await ignoreMissing(() => run2("launchctl", ["bootstrap", guiDomain(), launchdPlistPath()]));
|
|
136
|
+
await run2("launchctl", ["kickstart", `${guiDomain()}/${SERVICE_LABEL}`]);
|
|
137
|
+
}
|
|
138
|
+
async function launchdStop() {
|
|
139
|
+
await ignoreMissing(() => run2("launchctl", ["bootout", `${guiDomain()}/${SERVICE_LABEL}`]));
|
|
140
|
+
}
|
|
141
|
+
async function launchdRestart() {
|
|
142
|
+
await launchdLoad();
|
|
143
|
+
}
|
|
144
|
+
async function launchdUninstall() {
|
|
145
|
+
await launchdStop();
|
|
146
|
+
await rm(launchdPlistPath(), { force: true });
|
|
147
|
+
}
|
|
148
|
+
async function launchdStatus() {
|
|
149
|
+
try {
|
|
150
|
+
const { stdout: stdout2 } = await run2("launchctl", ["print", `${guiDomain()}/${SERVICE_LABEL}`]);
|
|
151
|
+
const running = /state = running/.test(stdout2);
|
|
152
|
+
const pid = Number(/pid = (\d+)/.exec(stdout2)?.[1]);
|
|
153
|
+
return { loaded: true, running, pid: Number.isFinite(pid) && pid > 0 ? pid : void 0 };
|
|
154
|
+
} catch {
|
|
155
|
+
return { loaded: false, running: false };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/systemd.ts
|
|
160
|
+
import { mkdir as mkdir2, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
|
|
161
|
+
import { dirname as dirname2 } from "node:path";
|
|
162
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
163
|
+
import { promisify as promisify3 } from "node:util";
|
|
164
|
+
var run3 = promisify3(execFile3);
|
|
165
|
+
function quoteSystemdArg(value) {
|
|
166
|
+
const escaped = value.replaceAll("%", "%%").replaceAll('"', '\\"');
|
|
167
|
+
return /[\s"\\]/.test(value) ? `"${escaped}"` : escaped;
|
|
168
|
+
}
|
|
169
|
+
function generateUnit(def) {
|
|
170
|
+
return `[Unit]
|
|
171
|
+
Description=Octopus host proxy
|
|
172
|
+
After=network-online.target
|
|
173
|
+
Wants=network-online.target
|
|
174
|
+
StartLimitBurst=10
|
|
175
|
+
StartLimitIntervalSec=300
|
|
176
|
+
|
|
177
|
+
[Service]
|
|
178
|
+
Type=simple
|
|
179
|
+
WorkingDirectory=${quoteSystemdArg(dirname2(def.entryPath))}
|
|
180
|
+
Environment=OCTOPUS_PROXY_CONFIG=${quoteSystemdArg(def.configPath)}
|
|
181
|
+
ExecStart=${quoteSystemdArg(def.nodePath)} ${quoteSystemdArg(def.entryPath)}
|
|
182
|
+
KillMode=process
|
|
183
|
+
TimeoutStopSec=10
|
|
184
|
+
Restart=on-failure
|
|
185
|
+
RestartSec=5
|
|
186
|
+
RestartPreventExitStatus=78
|
|
187
|
+
UMask=0077
|
|
188
|
+
NoNewPrivileges=true
|
|
189
|
+
|
|
190
|
+
[Install]
|
|
191
|
+
WantedBy=default.target
|
|
192
|
+
`;
|
|
193
|
+
}
|
|
194
|
+
async function writeUnitFile(text) {
|
|
195
|
+
const path = systemdUnitPath();
|
|
196
|
+
await mkdir2(dirname2(path), { recursive: true });
|
|
197
|
+
await writeFile2(path, text, { mode: 420 });
|
|
198
|
+
return path;
|
|
199
|
+
}
|
|
200
|
+
async function systemctl(args) {
|
|
201
|
+
try {
|
|
202
|
+
const { stdout: stdout2 } = await run3("systemctl", ["--user", ...args]);
|
|
203
|
+
return stdout2;
|
|
204
|
+
} catch (error) {
|
|
205
|
+
const stderr = error instanceof Error ? error.stderr ?? error.message : String(error);
|
|
206
|
+
if (/Failed to connect to bus|D-Bus connection/i.test(String(stderr))) {
|
|
207
|
+
throw new Error(`systemctl --user ${args.join(" ")} failed: user session bus not reachable.
|
|
208
|
+
On a headless host run:
|
|
209
|
+
export XDG_RUNTIME_DIR=/run/user/$(id -u)
|
|
210
|
+
sudo loginctl enable-linger $USER`);
|
|
211
|
+
}
|
|
212
|
+
throw new Error(`systemctl --user ${args.join(" ")} failed: ${String(stderr).trim()}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async function ignoreFailure(action) {
|
|
216
|
+
try {
|
|
217
|
+
return await action();
|
|
218
|
+
} catch {
|
|
219
|
+
return void 0;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async function systemdReload() {
|
|
223
|
+
await systemctl(["daemon-reload"]);
|
|
224
|
+
}
|
|
225
|
+
async function systemdInstall() {
|
|
226
|
+
await systemdReload();
|
|
227
|
+
await systemctl(["enable", SYSTEMD_UNIT]);
|
|
228
|
+
await systemctl(["restart", SYSTEMD_UNIT]);
|
|
229
|
+
}
|
|
230
|
+
async function systemdStart() {
|
|
231
|
+
await systemctl(["start", SYSTEMD_UNIT]);
|
|
232
|
+
}
|
|
233
|
+
async function systemdStop() {
|
|
234
|
+
await systemctl(["stop", SYSTEMD_UNIT]);
|
|
235
|
+
}
|
|
236
|
+
async function systemdRestart() {
|
|
237
|
+
await systemctl(["restart", SYSTEMD_UNIT]);
|
|
238
|
+
}
|
|
239
|
+
async function systemdUninstall() {
|
|
240
|
+
await ignoreFailure(() => systemctl(["disable", "--now", SYSTEMD_UNIT]));
|
|
241
|
+
await rm2(systemdUnitPath(), { force: true });
|
|
242
|
+
await systemdReload();
|
|
243
|
+
}
|
|
244
|
+
async function systemdStatus() {
|
|
245
|
+
const active = await ignoreFailure(() => systemctl(["is-active", SYSTEMD_UNIT]));
|
|
246
|
+
const enabled = await ignoreFailure(() => systemctl(["is-enabled", SYSTEMD_UNIT]));
|
|
247
|
+
return { active: active?.trim() === "active", enabled: enabled?.trim() === "enabled" };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/service.ts
|
|
251
|
+
function serviceFilePath() {
|
|
252
|
+
return isMacOS() ? launchdPlistPath() : systemdUnitPath();
|
|
253
|
+
}
|
|
254
|
+
async function serviceFileExists() {
|
|
255
|
+
try {
|
|
256
|
+
await access(serviceFilePath());
|
|
257
|
+
return true;
|
|
258
|
+
} catch {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
async function readServiceFile() {
|
|
263
|
+
return readFile(serviceFilePath(), "utf8").catch(() => null);
|
|
264
|
+
}
|
|
265
|
+
async function buildServiceDefinition(opts) {
|
|
266
|
+
const config = await loadConfig(opts.configPath);
|
|
267
|
+
const resolution = await resolveNodePath({ pinned: opts.nodePath });
|
|
268
|
+
if (!resolution.nodePath) throw new Error("\u672A\u627E\u5230\u53EF\u7528\u7684 node \u53EF\u6267\u884C\u6587\u4EF6\uFF1B\u8BF7\u7528\u7CFB\u7EDF\u5305\u7BA1\u7406\u5668\u5B89\u88C5 node\uFF0C\u6216\u901A\u8FC7 --node-path \u6307\u5B9A\u7EDD\u5BF9\u8DEF\u5F84");
|
|
269
|
+
const opencodeDir = isAbsolute(config.tool.command) ? [dirname3(config.tool.command)] : [];
|
|
270
|
+
const pathEnv = [dirname3(resolution.nodePath), ...opencodeDir, "/usr/local/bin", "/usr/bin", "/bin"].filter((dir, index, all) => all.indexOf(dir) === index).join(":");
|
|
271
|
+
return { def: { nodePath: resolution.nodePath, entryPath: daemonEntryPath(), configPath: resolve(opts.configPath), pathEnv }, warning: resolution.warning };
|
|
272
|
+
}
|
|
273
|
+
async function installService(opts) {
|
|
274
|
+
const { def, warning } = await buildServiceDefinition(opts);
|
|
275
|
+
const expected = isMacOS() ? generatePlist(def) : generateUnit(def);
|
|
276
|
+
const path = serviceFilePath();
|
|
277
|
+
const existing = await readServiceFile();
|
|
278
|
+
const changed = existing !== expected || opts.force === true;
|
|
279
|
+
if (isMacOS()) {
|
|
280
|
+
if (changed) {
|
|
281
|
+
await writePlistFile(expected);
|
|
282
|
+
await launchdLoad();
|
|
283
|
+
} else await launchdStart();
|
|
284
|
+
} else {
|
|
285
|
+
if (changed) {
|
|
286
|
+
await writeUnitFile(expected);
|
|
287
|
+
await systemdInstall();
|
|
288
|
+
} else await systemdStart();
|
|
289
|
+
}
|
|
290
|
+
return { serviceFile: path, changed, nodePath: def.nodePath, warning };
|
|
291
|
+
}
|
|
292
|
+
async function uninstallService() {
|
|
293
|
+
const path = serviceFilePath();
|
|
294
|
+
if (isMacOS()) await launchdUninstall();
|
|
295
|
+
else await systemdUninstall();
|
|
296
|
+
return path;
|
|
297
|
+
}
|
|
298
|
+
async function controlService(action) {
|
|
299
|
+
if (!await serviceFileExists()) throw new Error("\u670D\u52A1\u5C1A\u672A\u5B89\u88C5\uFF1B\u8BF7\u5148\u8FD0\u884C octopus-proxy install");
|
|
300
|
+
if (isMacOS()) {
|
|
301
|
+
if (action === "start") await launchdStart();
|
|
302
|
+
else if (action === "stop") await launchdStop();
|
|
303
|
+
else await launchdRestart();
|
|
304
|
+
} else {
|
|
305
|
+
if (action === "start") await systemdStart();
|
|
306
|
+
else if (action === "stop") await systemdStop();
|
|
307
|
+
else await systemdRestart();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
async function serviceStatus() {
|
|
311
|
+
if (!await serviceFileExists()) return { installed: false, active: false };
|
|
312
|
+
if (isMacOS()) {
|
|
313
|
+
const state2 = await launchdStatus();
|
|
314
|
+
return { installed: true, active: state2.running, pid: state2.pid, serviceFile: launchdPlistPath() };
|
|
315
|
+
}
|
|
316
|
+
const state = await systemdStatus();
|
|
317
|
+
return { installed: true, active: state.active, enabled: state.enabled, serviceFile: systemdUnitPath() };
|
|
318
|
+
}
|
|
319
|
+
function logLocationHint() {
|
|
320
|
+
return isMacOS() ? launchdLogPath() : "journalctl --user -u octopus-proxy";
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// src/doctor.ts
|
|
324
|
+
var run4 = promisify4(execFile4);
|
|
325
|
+
async function runDoctor(opts) {
|
|
326
|
+
const results = [];
|
|
327
|
+
const record = (name, status, detail) => results.push({ name, status, detail });
|
|
328
|
+
let config;
|
|
329
|
+
try {
|
|
330
|
+
const info = await stat2(opts.configPath).catch((error) => {
|
|
331
|
+
if (error.code === "ENOENT") throw new Error(`\u914D\u7F6E\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${opts.configPath}\uFF1B\u5148\u8FD0\u884C octopus-proxy onboard`);
|
|
332
|
+
throw error;
|
|
333
|
+
});
|
|
334
|
+
if ((info.mode & 63) !== 0) {
|
|
335
|
+
if (opts.fix) {
|
|
336
|
+
await chmod(opts.configPath, 384);
|
|
337
|
+
record("\u914D\u7F6E\u6743\u9650", "ok", `\u5DF2\u4FEE\u590D\u4E3A 600\uFF08${opts.configPath}\uFF09`);
|
|
338
|
+
} else record("\u914D\u7F6E\u6743\u9650", "warn", `${opts.configPath} \u6743\u9650\u8D85\u8FC7 600\uFF1B\u8FD0\u884C doctor --fix \u4FEE\u590D`);
|
|
339
|
+
} else record("\u914D\u7F6E\u6743\u9650", "ok");
|
|
340
|
+
config = await loadConfig(opts.configPath);
|
|
341
|
+
record("\u914D\u7F6E\u6821\u9A8C", "ok", opts.configPath);
|
|
342
|
+
} catch (error) {
|
|
343
|
+
record("\u914D\u7F6E\u6821\u9A8C", "fail", error instanceof Error ? error.message : String(error));
|
|
344
|
+
}
|
|
345
|
+
if (config) {
|
|
346
|
+
try {
|
|
347
|
+
await mkdir3(config.stateDirectory, { recursive: true, mode: 448 });
|
|
348
|
+
await access2(config.stateDirectory, constants.W_OK);
|
|
349
|
+
record("\u72B6\u6001\u76EE\u5F55", "ok", config.stateDirectory);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
record("\u72B6\u6001\u76EE\u5F55", "fail", `\u65E0\u6CD5\u5199\u5165 ${config.stateDirectory}\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
352
|
+
}
|
|
353
|
+
const descriptor = describeTool(config.tool.kind);
|
|
354
|
+
const checkName = `${descriptor.label} \u547D\u4EE4`;
|
|
355
|
+
const command2 = config.tool.command;
|
|
356
|
+
if (!isAbsolute2(command2)) {
|
|
357
|
+
record(checkName, "warn", `\u5F53\u524D\u4E3A\u300C${command2}\u300D\uFF0C\u4F9D\u8D56 PATH \u67E5\u627E\uFF1B\u670D\u52A1\u73AF\u5883\u5EFA\u8BAE\u6539\u7528\u7EDD\u5BF9\u8DEF\u5F84\uFF08\u91CD\u65B0\u8FD0\u884C onboard \u6216\u624B\u5DE5\u4FEE\u6539\u914D\u7F6E\uFF09`);
|
|
358
|
+
} else if (!await access2(command2, constants.X_OK).then(() => true, () => false)) {
|
|
359
|
+
record(checkName, "fail", `${command2} \u4E0D\u5B58\u5728\u6216\u4E0D\u53EF\u6267\u884C`);
|
|
360
|
+
} else {
|
|
361
|
+
try {
|
|
362
|
+
const { stdout: stdout2 } = await run4(command2, [...descriptor.versionArgs], { timeout: 15e3 });
|
|
363
|
+
const version = stdout2.trim().split("\n")[0].trim();
|
|
364
|
+
if (version === descriptor.baseline) record(checkName, "ok", `${command2}\uFF08${version}\uFF09`);
|
|
365
|
+
else if (descriptor.versionPolicy === "exact") record(checkName, "warn", `${command2} \u7248\u672C\u4E3A ${version}\uFF0C\u5DF2\u9A8C\u8BC1\u57FA\u7EBF\u662F ${descriptor.baseline}\uFF08\u4EE3\u7406\u4F1A\u62D2\u7EDD\u6258\u7BA1\u8BE5\u7248\u672C\uFF09`);
|
|
366
|
+
else record(checkName, "warn", `${command2} \u7248\u672C\u4E3A ${version}\uFF0C\u5DF2\u9A8C\u8BC1\u57FA\u7EBF\u662F ${descriptor.baseline}\uFF1Bapp-server \u534F\u8BAE\u53EF\u80FD\u6F02\u79FB`);
|
|
367
|
+
} catch (error) {
|
|
368
|
+
record(checkName, "warn", `\u6267\u884C ${command2} ${descriptor.versionArgs.join(" ")} \u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
369
|
+
}
|
|
370
|
+
if (config.tool.kind === "codex") {
|
|
371
|
+
try {
|
|
372
|
+
await run4(command2, ["login", "status"], { timeout: 15e3 });
|
|
373
|
+
} catch {
|
|
374
|
+
record("Codex \u767B\u5F55", "warn", "codex login status \u672A\u901A\u8FC7\uFF1B\u65E0\u5934\u4E3B\u673A\u8BF7\u5148\u5B8C\u6210 codex \u767B\u5F55\uFF08--with-api-key \u6216\u590D\u5236 auth.json\uFF09");
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (config.tool.kind === "zcode") {
|
|
378
|
+
const credentials = join2(homedir2(), ".zcode", "v2", "credentials.json");
|
|
379
|
+
if (!await access2(credentials).then(() => true, () => false)) record("ZCode \u767B\u5F55", "warn", `\u672A\u627E\u5230 ${credentials}\uFF1B\u8BF7\u5148\u5728\u8BE5\u4E3B\u673A\u5B8C\u6210 zcode login`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
try {
|
|
384
|
+
await access2(daemonEntryPath(), constants.R_OK);
|
|
385
|
+
record("\u4EE3\u7406\u5165\u53E3", "ok", daemonEntryPath());
|
|
386
|
+
} catch {
|
|
387
|
+
record("\u4EE3\u7406\u5165\u53E3", "fail", `\u672A\u627E\u5230 ${daemonEntryPath()}\uFF1B\u8BF7\u91CD\u65B0\u5B89\u88C5 @skylandnpm/octopus-proxy\uFF08\u6E90\u7801\u6784\u5EFA\uFF1Apnpm --filter @skylandnpm/octopus-proxy build\uFF09`);
|
|
388
|
+
}
|
|
389
|
+
if (config) {
|
|
390
|
+
try {
|
|
391
|
+
const { def, warning } = await buildServiceDefinition({ configPath: opts.configPath });
|
|
392
|
+
if (warning) record("Node \u89E3\u6790", "warn", warning);
|
|
393
|
+
else record("Node \u89E3\u6790", "ok", def.nodePath);
|
|
394
|
+
if (isVersionManagedPath(def.nodePath)) record("Node \u8DEF\u5F84", "warn", `${def.nodePath} \u4F4D\u4E8E\u7248\u672C\u7BA1\u7406\u5668\u76EE\u5F55\uFF0C\u5347\u7EA7\u540E\u53EF\u80FD\u5931\u6548\uFF1B\u5EFA\u8BAE --node-path \u56FA\u5B9A\u4E3A\u7CFB\u7EDF node \u540E\u91CD\u65B0 install`);
|
|
395
|
+
const existing = await readServiceFile();
|
|
396
|
+
if (existing === null) record("\u670D\u52A1\u5B9A\u4E49", "warn", "\u5C1A\u672A\u5B89\u88C5\u670D\u52A1\uFF1B\u8FD0\u884C octopus-proxy install");
|
|
397
|
+
else {
|
|
398
|
+
const expected = isMacOS() ? generatePlist(def) : generateUnit(def);
|
|
399
|
+
if (existing === expected) record("\u670D\u52A1\u5B9A\u4E49", "ok");
|
|
400
|
+
else if (opts.fix) {
|
|
401
|
+
await installService({ ...opts, force: true });
|
|
402
|
+
record("\u670D\u52A1\u5B9A\u4E49", "ok", "\u5DF2\u6309\u5F53\u524D\u73AF\u5883\u91CD\u5199\u670D\u52A1\u5B9A\u4E49");
|
|
403
|
+
} else record("\u670D\u52A1\u5B9A\u4E49", "warn", "\u670D\u52A1\u5B9A\u4E49\u4E0E\u5F53\u524D\u73AF\u5883\u4E0D\u4E00\u81F4\uFF08node \u6216\u8DEF\u5F84\u53D8\u5316\uFF09\uFF1B\u8FD0\u884C doctor --fix \u6216 install --force \u66F4\u65B0");
|
|
404
|
+
}
|
|
405
|
+
} catch (error) {
|
|
406
|
+
record("\u670D\u52A1\u5B9A\u4E49", "fail", error instanceof Error ? error.message : String(error));
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const state = await serviceStatus();
|
|
410
|
+
if (state.active) record("\u670D\u52A1\u72B6\u6001", "ok", `\u8FD0\u884C\u4E2D${state.pid ? `\uFF08pid ${state.pid}\uFF09` : ""}`);
|
|
411
|
+
else if (!state.installed) record("\u670D\u52A1\u72B6\u6001", "warn", "\u5C1A\u672A\u5B89\u88C5\u670D\u52A1");
|
|
412
|
+
else if (opts.fix) {
|
|
413
|
+
try {
|
|
414
|
+
await controlService("start");
|
|
415
|
+
record("\u670D\u52A1\u72B6\u6001", "ok", "\u5DF2\u542F\u52A8");
|
|
416
|
+
} catch (error) {
|
|
417
|
+
record("\u670D\u52A1\u72B6\u6001", "fail", `\u542F\u52A8\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}\uFF1B\u53EF\u7528 octopus-proxy logs \u67E5\u770B\u65E5\u5FD7`);
|
|
418
|
+
}
|
|
419
|
+
} else record("\u670D\u52A1\u72B6\u6001", "warn", "\u672A\u5728\u8FD0\u884C\uFF1B\u8FD0\u884C octopus-proxy start \u6216 doctor --fix");
|
|
420
|
+
return results;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// src/onboard.ts
|
|
424
|
+
import { createInterface } from "node:readline/promises";
|
|
425
|
+
import { stdin, stdout } from "node:process";
|
|
426
|
+
import { mkdir as mkdir4, stat as stat3 } from "node:fs/promises";
|
|
427
|
+
import { isAbsolute as isAbsolute3 } from "node:path";
|
|
428
|
+
async function ask(rl, question, options = {}) {
|
|
429
|
+
for (; ; ) {
|
|
430
|
+
const suffix = options.default !== void 0 ? `\uFF08\u9ED8\u8BA4 ${options.default}\uFF09` : "";
|
|
431
|
+
const answer = (await rl.question(`${question}${suffix}: `)).trim();
|
|
432
|
+
const value = answer || options.default || "";
|
|
433
|
+
const problem = options.validate?.(value);
|
|
434
|
+
if (problem) {
|
|
435
|
+
console.log(` ${problem}`);
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
return value;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
function slug(name) {
|
|
442
|
+
return name.replaceAll(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+/, "").replace(/-+$/, "");
|
|
443
|
+
}
|
|
444
|
+
var validUrl = (value) => {
|
|
445
|
+
try {
|
|
446
|
+
const url = new URL(value);
|
|
447
|
+
return url.protocol === "https:" || url.hostname === "localhost" || url.hostname === "127.0.0.1" ? void 0 : "\u8FDC\u7A0B Hub \u5FC5\u987B\u4F7F\u7528 https";
|
|
448
|
+
} catch {
|
|
449
|
+
return "\u8BF7\u8F93\u5165\u5408\u6CD5 URL\uFF0C\u4F8B\u5982 https://hub.example.com";
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
var validId = (value) => /^[a-zA-Z0-9_-]+$/.test(value) ? void 0 : "\u4EC5\u9650\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF\u548C\u8FDE\u5B57\u7B26";
|
|
453
|
+
var validBotId = (value) => /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value) ? void 0 : "\u8BF7\u8F93\u5165\u673A\u5668\u4EBA UUID";
|
|
454
|
+
async function runOnboard(opts) {
|
|
455
|
+
let hubUrl = opts.hubUrl ?? "", botId = opts.botId ?? "", secretKey = opts.secretKey ?? "", stateDirectory = opts.stateDirectory ?? defaultStateDirectory(), toolCommand = opts.toolCommand ?? "", toolKind = opts.toolKind ?? "opencode", mode = "managed";
|
|
456
|
+
let projects = opts.projects.map((project) => ({ ...project, directory: expandHome(project.directory) }));
|
|
457
|
+
if (opts.interactive) {
|
|
458
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
459
|
+
console.log("Octopus Proxy \u521D\u59CB\u5316\u5411\u5BFC");
|
|
460
|
+
console.log(`\u914D\u7F6E\u5C06\u5199\u5165 ${opts.configPath}\uFF1B\u6587\u4EF6\u5305\u542B\u673A\u5668\u4EBA Secret\uFF0C\u4F1A\u4EE5 0600 \u6743\u9650\u4FDD\u5B58\u3002
|
|
461
|
+
`);
|
|
462
|
+
hubUrl = await ask(rl, "Hub \u5730\u5740", { default: hubUrl || void 0, validate: validUrl });
|
|
463
|
+
botId = await ask(rl, "\u673A\u5668\u4EBA ID\uFF08\u5728 Claw \u6216 Taskhub \u4E2D\u521B\u5EFA\u5E76\u590D\u5236\uFF09", { validate: validBotId });
|
|
464
|
+
secretKey = await ask(rl, "\u673A\u5668\u4EBA Secret\uFF08\u5728 Claw \u6216 Taskhub \u521B\u5EFA\uFF1B\u8F93\u5165\u4F1A\u56DE\u663E\u5728\u7EC8\u7AEF\uFF09", { validate: (value) => value.length >= 20 ? void 0 : "\u8BF7\u8F93\u5165\u5B8C\u6574\u7684\u673A\u5668\u4EBA Secret" });
|
|
465
|
+
stateDirectory = expandHome(await ask(rl, "\u72B6\u6001\u76EE\u5F55", { default: stateDirectory, validate: (value) => isAbsolute3(expandHome(value)) ? void 0 : "\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84" }));
|
|
466
|
+
toolKind = await ask(rl, `\u7F16\u7801\u5DE5\u5177\uFF08${TOOL_KINDS.join("/")}\uFF09`, { default: toolKind, validate: (value) => TOOL_KINDS.includes(value) ? void 0 : `\u8F93\u5165 ${TOOL_KINDS.join(" \u6216 ")}` });
|
|
467
|
+
const descriptor = describeTool(toolKind);
|
|
468
|
+
const found = await resolveCommandOnPath(toolKind, toolSearchDirs(toolKind));
|
|
469
|
+
toolCommand = await ask(rl, found ? `${descriptor.label} \u547D\u4EE4\uFF08\u63A2\u6D4B\u5230 ${found}\uFF09` : `${descriptor.label} \u547D\u4EE4\uFF08\u672A\u5728 PATH \u4E2D\u63A2\u6D4B\u5230\uFF0C\u8BF7\u8F93\u5165\u7EDD\u5BF9\u8DEF\u5F84\uFF09`, { default: found ?? void 0, validate: (value) => isAbsolute3(expandHome(value)) ? void 0 : "\u670D\u52A1\u73AF\u5883\u9700\u8981\u7EDD\u5BF9\u8DEF\u5F84" });
|
|
470
|
+
if (toolKind === "opencode") mode = await ask(rl, "OpenCode \u6A21\u5F0F\uFF08managed=\u7531\u4EE3\u7406\u6258\u7BA1\u542F\u52A8\uFF0Cexternal=\u4EC5\u8FDE\u63A5\u5DF2\u6709\u670D\u52A1\uFF09", { default: "managed", validate: (value) => value === "managed" || value === "external" ? void 0 : "\u8F93\u5165 managed \u6216 external" });
|
|
471
|
+
console.log("\n\u5F55\u5165\u6388\u6743\u9879\u76EE\uFF08\u540D\u79F0\u7559\u7A7A\u7ED3\u675F\uFF09\uFF1A");
|
|
472
|
+
for (; ; ) {
|
|
473
|
+
const name = (await rl.question(" \u9879\u76EE\u540D\u79F0: ")).trim();
|
|
474
|
+
if (!name) break;
|
|
475
|
+
let directory = "";
|
|
476
|
+
for (; ; ) {
|
|
477
|
+
directory = expandHome(await ask(rl, ` \u300C${name}\u300D\u76EE\u5F55\uFF08\u7EDD\u5BF9\u8DEF\u5F84\uFF09`));
|
|
478
|
+
if (!isAbsolute3(directory)) {
|
|
479
|
+
console.log(" \u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84");
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
if (!await stat3(directory).then(() => true, () => false)) {
|
|
483
|
+
console.log(" \u76EE\u5F55\u4E0D\u5B58\u5728");
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
const id = await ask(rl, " \u9879\u76EE ID", { default: slug(name), validate: validId });
|
|
489
|
+
projects.push({ id, name, directory });
|
|
490
|
+
}
|
|
491
|
+
rl.close();
|
|
492
|
+
if (!projects.length) throw new Error("\u81F3\u5C11\u9700\u8981\u5F55\u5165\u4E00\u4E2A\u6388\u6743\u9879\u76EE");
|
|
493
|
+
console.log("");
|
|
494
|
+
} else {
|
|
495
|
+
const missing = [];
|
|
496
|
+
if (!hubUrl) missing.push("--hub-url");
|
|
497
|
+
if (!botId) missing.push("--bot-id");
|
|
498
|
+
if (!secretKey) missing.push("--secret-key");
|
|
499
|
+
if (!projects.length) missing.push("--project");
|
|
500
|
+
if (missing.length) throw new Error(`\u975E\u4EA4\u4E92\u6A21\u5F0F\u7F3A\u5C11\u5FC5\u586B\u53C2\u6570\uFF1A${missing.join("\u3001")}`);
|
|
501
|
+
const urlProblem = validUrl(hubUrl);
|
|
502
|
+
if (urlProblem) throw new Error(`${urlProblem}\uFF08--hub-url\uFF09`);
|
|
503
|
+
if (validBotId(botId) || secretKey.length < 20) throw new Error("\u673A\u5668\u4EBA ID \u6216 Secret \u683C\u5F0F\u65E0\u6548");
|
|
504
|
+
projects = projects.map((project) => ({ ...project, id: project.id ?? slug(project.name) }));
|
|
505
|
+
const invalid = projects.find((project) => validId(project.id ?? "") !== void 0);
|
|
506
|
+
if (invalid) throw new Error(`\u9879\u76EE\u300C${invalid.name}\u300D\u65E0\u6CD5\u63A8\u5BFC\u51FA\u5408\u6CD5 ID\uFF08\u4EC5\u9650\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF\u548C\u8FDE\u5B57\u7B26\uFF09\uFF0C\u8BF7\u8C03\u6574\u540D\u79F0\u6216\u624B\u5DE5\u6307\u5B9A ID`);
|
|
507
|
+
if (projects.some((project) => !isAbsolute3(project.directory))) throw new Error("--project \u7684\u76EE\u5F55\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84");
|
|
508
|
+
const descriptor = describeTool(toolKind);
|
|
509
|
+
toolCommand = toolCommand ? expandHome(toolCommand) : await resolveCommandOnPath(toolKind, toolSearchDirs(toolKind)) ?? descriptor.defaultCommand;
|
|
510
|
+
if (!isAbsolute3(toolCommand)) console.log(`\u63D0\u793A\uFF1A\u672A\u5728 PATH \u4E2D\u63A2\u6D4B\u5230 ${descriptor.label}\uFF0C\u914D\u7F6E\u4FDD\u7559\u547D\u4EE4\u540D\u300C${toolCommand}\u300D\uFF1B\u670D\u52A1\u73AF\u5883\u5EFA\u8BAE\u7EDD\u5BF9\u8DEF\u5F84\uFF08\u53EF\u7528 --tool-command \u6307\u5B9A\uFF09`);
|
|
511
|
+
}
|
|
512
|
+
if (await stat3(opts.configPath).then(() => true, () => false)) {
|
|
513
|
+
if (opts.interactive) {
|
|
514
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
515
|
+
const answer = (await rl.question(`\u914D\u7F6E ${opts.configPath} \u5DF2\u5B58\u5728\uFF0C\u8986\u76D6\uFF1F(y/N): `)).trim();
|
|
516
|
+
rl.close();
|
|
517
|
+
if (!/^y(es)?$/i.test(answer)) {
|
|
518
|
+
console.log("\u5DF2\u53D6\u6D88\uFF0C\u672A\u5199\u5165\u4EFB\u4F55\u5185\u5BB9\u3002");
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
} else throw new Error(`\u914D\u7F6E\u5DF2\u5B58\u5728\uFF1A${opts.configPath}\uFF1B\u975E\u4EA4\u4E92\u6A21\u5F0F\u62D2\u7EDD\u8986\u76D6\uFF0C\u8BF7\u5148\u79FB\u9664\u6216\u7528 --config \u6307\u5B9A\u65B0\u8DEF\u5F84`);
|
|
522
|
+
}
|
|
523
|
+
const loopbackHttp = hubUrl.startsWith("http://");
|
|
524
|
+
if (loopbackHttp) console.log("\u63D0\u793A\uFF1AHub \u4E3A http \u5730\u5740\uFF0C\u914D\u7F6E\u5C06\u5199\u5165 allowInsecureLocalhost: true\uFF08\u4EC5\u9650\u672C\u673A\u56DE\u73AF\u8054\u8C03\uFF09");
|
|
525
|
+
await atomicJson(opts.configPath, {
|
|
526
|
+
hubUrl,
|
|
527
|
+
botId,
|
|
528
|
+
secretKey,
|
|
529
|
+
stateDirectory,
|
|
530
|
+
...loopbackHttp ? { allowInsecureLocalhost: true } : {},
|
|
531
|
+
projects: projects.map((project) => ({ id: project.id, name: project.name, directory: project.directory, specDirectories: ["specs", "docs"] })),
|
|
532
|
+
tool: { kind: toolKind, command: toolCommand || describeTool(toolKind).defaultCommand, ...toolKind === "opencode" ? { baseUrl: "http://127.0.0.1:4096", mode } : {}, autoStart: true }
|
|
533
|
+
});
|
|
534
|
+
await loadConfig(opts.configPath);
|
|
535
|
+
console.log(`\u5DF2\u5199\u5165\u914D\u7F6E\uFF1A${opts.configPath}\uFF080600\uFF09`);
|
|
536
|
+
stateDirectory = expandHome(stateDirectory);
|
|
537
|
+
await mkdir4(stateDirectory, { recursive: true, mode: 448 });
|
|
538
|
+
try {
|
|
539
|
+
const response = await fetch(hubUrl, { redirect: "manual", signal: AbortSignal.timeout(5e3) });
|
|
540
|
+
console.log(`Hub \u8FDE\u901A\u6027\uFF1AHTTP ${response.status}`);
|
|
541
|
+
} catch {
|
|
542
|
+
console.log("\u8B66\u544A\uFF1A\u6682\u65F6\u65E0\u6CD5\u8FDE\u63A5 Hub\uFF1B\u7A0D\u540E\u53EF\u7528 octopus-proxy doctor \u68C0\u67E5\u7F51\u7EDC\u4E0E\u8BC1\u4E66");
|
|
543
|
+
}
|
|
544
|
+
let install = opts.installServiceWanted ?? true;
|
|
545
|
+
if (opts.interactive && opts.installServiceWanted === void 0) {
|
|
546
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
547
|
+
const answer = (await rl.question("\u73B0\u5728\u5B89\u88C5\u5E76\u542F\u52A8\u7CFB\u7EDF\u670D\u52A1\uFF1F(Y/n): ")).trim();
|
|
548
|
+
rl.close();
|
|
549
|
+
install = !/^(n|no)$/i.test(answer);
|
|
550
|
+
}
|
|
551
|
+
if (install) {
|
|
552
|
+
const outcome = await installService({ configPath: opts.configPath });
|
|
553
|
+
console.log(outcome.changed ? `\u5DF2\u5B89\u88C5\u5E76\u542F\u52A8\u7CFB\u7EDF\u670D\u52A1\uFF1A${outcome.serviceFile}` : `\u670D\u52A1\u5B9A\u4E49\u4E0E\u914D\u7F6E\u4E00\u81F4\uFF0C\u5DF2\u786E\u4FDD\u8FD0\u884C\uFF1A${outcome.serviceFile}`);
|
|
554
|
+
console.log(`node\uFF1A${outcome.nodePath}`);
|
|
555
|
+
if (outcome.warning) console.log(`\u6CE8\u610F\uFF1A${outcome.warning}`);
|
|
556
|
+
console.log(`\u65E5\u5FD7\uFF1A${logLocationHint()}\uFF08octopus-proxy logs \u67E5\u770B\uFF09`);
|
|
557
|
+
} else console.log("\u5DF2\u8DF3\u8FC7\u670D\u52A1\u5B89\u88C5\uFF1B\u4E4B\u540E\u53EF\u8FD0\u884C octopus-proxy install");
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// src/cli.ts
|
|
561
|
+
var USAGE = `octopus-proxy \u2014 Octopus \u4E3B\u673A\u4EE3\u7406
|
|
562
|
+
|
|
563
|
+
\u7528\u6CD5: octopus-proxy <\u547D\u4EE4> [\u9009\u9879]
|
|
564
|
+
|
|
565
|
+
\u547D\u4EE4:
|
|
566
|
+
onboard \u4EA4\u4E92\u5F0F\u521D\u59CB\u5316\u914D\u7F6E\uFF0C\u5E76\u9ED8\u8BA4\u5B89\u88C5\u7CFB\u7EDF\u670D\u52A1
|
|
567
|
+
install \u6821\u9A8C\u914D\u7F6E\u540E\u5B89\u88C5/\u66F4\u65B0\u7CFB\u7EDF\u670D\u52A1\uFF08\u5E42\u7B49\uFF0C\u81EA\u52A8\u542F\u52A8\uFF09
|
|
568
|
+
uninstall \u505C\u6B62\u5E76\u79FB\u9664\u7CFB\u7EDF\u670D\u52A1\uFF08\u4FDD\u7559\u914D\u7F6E\u4E0E\u72B6\u6001\u6570\u636E\uFF09
|
|
569
|
+
start \u542F\u52A8\u670D\u52A1
|
|
570
|
+
stop \u505C\u6B62\u670D\u52A1
|
|
571
|
+
restart \u91CD\u542F\u670D\u52A1
|
|
572
|
+
status \u67E5\u770B\u670D\u52A1\u5B89\u88C5\u4E0E\u8FD0\u884C\u72B6\u6001
|
|
573
|
+
doctor \u68C0\u67E5\u914D\u7F6E\u3001OpenCode\u3001node \u4E0E\u670D\u52A1\u5065\u5EB7\uFF08--fix \u81EA\u52A8\u4FEE\u590D\uFF09
|
|
574
|
+
run \u524D\u53F0\u8FD0\u884C\u4EE3\u7406\uFF08\u8C03\u8BD5\u7528\uFF0CCtrl+C \u9000\u51FA\uFF09
|
|
575
|
+
logs \u67E5\u770B\u670D\u52A1\u65E5\u5FD7\uFF08-f \u6301\u7EED\u8DDF\u968F\uFF09
|
|
576
|
+
help \u663E\u793A\u672C\u5E2E\u52A9
|
|
577
|
+
|
|
578
|
+
\u901A\u7528\u9009\u9879:
|
|
579
|
+
--config <path> \u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84\uFF08\u9ED8\u8BA4 $OCTOPUS_PROXY_CONFIG \u6216 ~/.config/octopus/proxy.json\uFF09
|
|
580
|
+
|
|
581
|
+
onboard \u9009\u9879:
|
|
582
|
+
--non-interactive \u975E\u4EA4\u4E92\u6A21\u5F0F\uFF1B\u9700\u63D0\u4F9B Hub \u5730\u5740\u3001\u673A\u5668\u4EBA ID\u3001Secret \u548C\u9879\u76EE
|
|
583
|
+
--hub-url <url> Hub \u5730\u5740\uFF08https\uFF09
|
|
584
|
+
--bot-id <uuid> Claw \u6216 Taskhub \u4E2D\u521B\u5EFA\u7684\u673A\u5668\u4EBA ID
|
|
585
|
+
--secret-key <key> \u673A\u5668\u4EBA Secret\uFF08\u5728 Claw \u6216 Taskhub \u521B\u5EFA\uFF09
|
|
586
|
+
--state-dir <path> \u72B6\u6001\u76EE\u5F55\uFF08\u9ED8\u8BA4 ~/.local/state/octopus\uFF09
|
|
587
|
+
--project <name=path> \u6388\u6743\u9879\u76EE\uFF0C\u53EF\u91CD\u590D\u591A\u6B21
|
|
588
|
+
--tool <kind> \u7F16\u7801\u5DE5\u5177\uFF1Aopencode\uFF08\u9ED8\u8BA4\uFF09\u3001codex\u3001zcode \u6216 pi
|
|
589
|
+
--tool-command <p> \u7F16\u7801\u5DE5\u5177\u53EF\u6267\u884C\u6587\u4EF6\u7EDD\u5BF9\u8DEF\u5F84
|
|
590
|
+
--no-install-service \u53EA\u5199\u914D\u7F6E\uFF0C\u4E0D\u5B89\u88C5\u670D\u52A1
|
|
591
|
+
|
|
592
|
+
install \u9009\u9879:
|
|
593
|
+
--force \u91CD\u5199\u670D\u52A1\u5B9A\u4E49\u5E76\u91CD\u542F\uFF08\u914D\u7F6E\u6216\u8DEF\u5F84\u53D8\u5316\u540E\u4F7F\u7528\uFF09
|
|
594
|
+
--node-path <p> \u56FA\u5B9A\u670D\u52A1\u4F7F\u7528\u7684 node \u7EDD\u5BF9\u8DEF\u5F84
|
|
595
|
+
|
|
596
|
+
doctor / logs \u9009\u9879:
|
|
597
|
+
--fix \u81EA\u52A8\u4FEE\u590D\uFF08\u914D\u7F6E\u6743\u9650\u3001\u670D\u52A1\u5B9A\u4E49\u3001\u542F\u52A8\u670D\u52A1\uFF09
|
|
598
|
+
-f, --follow \u6301\u7EED\u8F93\u51FA\u65E5\u5FD7
|
|
599
|
+
|
|
600
|
+
\u9000\u51FA\u7801: 0 \u6210\u529F\uFF1B1 \u4E00\u822C\u9519\u8BEF\uFF1B78 \u914D\u7F6E\u65E0\u6548\uFF08\u670D\u52A1\u5355\u5143\u5DF2\u914D\u7F6E\u4E0D\u56E0\u6B64\u91CD\u542F\uFF09`;
|
|
601
|
+
var VALUE_FLAGS = /* @__PURE__ */ new Set(["--config", "--node-path", "--hub-url", "--bot-id", "--secret-key", "--state-dir", "--tool", "--tool-command", "--opencode-command", "--project"]);
|
|
602
|
+
function parse(argv) {
|
|
603
|
+
const flags2 = {}, projectArgs2 = [];
|
|
604
|
+
let command2;
|
|
605
|
+
for (let i = 0; i < argv.length; ) {
|
|
606
|
+
const arg = argv[i++];
|
|
607
|
+
if (arg === "-h" || arg === "--help") {
|
|
608
|
+
flags2["--help"] = "true";
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
if (arg === "-f") {
|
|
612
|
+
flags2["--follow"] = "true";
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
if (!arg.startsWith("--")) {
|
|
616
|
+
if (command2 === void 0) command2 = arg;
|
|
617
|
+
else throw new Error(`\u65E0\u6CD5\u8BC6\u522B\u7684\u53C2\u6570\uFF1A${arg}`);
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
const equals = arg.indexOf("=");
|
|
621
|
+
const name = equals === -1 ? arg : arg.slice(0, equals);
|
|
622
|
+
let value = equals === -1 ? void 0 : arg.slice(equals + 1);
|
|
623
|
+
if (value === void 0 && VALUE_FLAGS.has(name) && i < argv.length && !argv[i].startsWith("--")) value = argv[i++];
|
|
624
|
+
if (VALUE_FLAGS.has(name) && value === void 0) throw new Error(`\u9009\u9879 ${name} \u7F3A\u5C11\u503C`);
|
|
625
|
+
if (!VALUE_FLAGS.has(name) && value !== void 0) throw new Error(`\u9009\u9879 ${name} \u4E0D\u63A5\u53D7\u503C`);
|
|
626
|
+
if (name === "--project") {
|
|
627
|
+
projectArgs2.push(value);
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
flags2[name] = value ?? "true";
|
|
631
|
+
}
|
|
632
|
+
return { command: command2, flags: flags2, projectArgs: projectArgs2 };
|
|
633
|
+
}
|
|
634
|
+
async function showLogs(follow) {
|
|
635
|
+
const child = isMacOS() ? spawn("tail", ["-n", "200", ...follow ? ["-F"] : [], launchdLogPath()], { stdio: "inherit" }) : spawn("journalctl", ["--user", "-u", SYSTEMD_UNIT, "-n", "200", ...follow ? ["-f"] : ["--no-pager"]], { stdio: "inherit" });
|
|
636
|
+
if (isMacOS() && !await access3(launchdLogPath()).then(() => true, () => false)) {
|
|
637
|
+
console.error(`\u65E5\u5FD7\u6587\u4EF6\u5C1A\u672A\u751F\u6210\uFF1A${launchdLogPath()}`);
|
|
638
|
+
process.exit(1);
|
|
639
|
+
}
|
|
640
|
+
child.on("error", (error) => {
|
|
641
|
+
console.error(`\u65E0\u6CD5\u67E5\u770B\u65E5\u5FD7\uFF1A${error.message}\uFF08${logLocationHint()}\uFF09`);
|
|
642
|
+
process.exit(1);
|
|
643
|
+
});
|
|
644
|
+
for (const signal of ["SIGINT", "SIGTERM"]) process.on(signal, () => child.kill(signal));
|
|
645
|
+
const code = await new Promise((resolve2) => child.on("exit", (code2) => resolve2(code2 ?? 0)));
|
|
646
|
+
process.exit(code);
|
|
647
|
+
}
|
|
648
|
+
var { command, flags, projectArgs } = parse(process.argv.slice(2));
|
|
649
|
+
if (!command || command === "help" || flags["--help"]) {
|
|
650
|
+
console.log(USAGE);
|
|
651
|
+
process.exit(command || flags["--help"] ? 0 : 1);
|
|
652
|
+
}
|
|
653
|
+
if (!supportedPlatform()) {
|
|
654
|
+
console.error("octopus-proxy \u4EC5\u652F\u6301 macOS \u4E0E Linux");
|
|
655
|
+
process.exit(1);
|
|
656
|
+
}
|
|
657
|
+
var configPath = resolveConfigPath(flags["--config"]);
|
|
658
|
+
try {
|
|
659
|
+
switch (command) {
|
|
660
|
+
case "onboard": {
|
|
661
|
+
await runOnboard({
|
|
662
|
+
interactive: flags["--non-interactive"] !== "true",
|
|
663
|
+
configPath,
|
|
664
|
+
hubUrl: flags["--hub-url"],
|
|
665
|
+
botId: flags["--bot-id"],
|
|
666
|
+
secretKey: flags["--secret-key"],
|
|
667
|
+
stateDirectory: flags["--state-dir"],
|
|
668
|
+
projects: projectArgs.map((entry) => {
|
|
669
|
+
const equals = entry.indexOf("=");
|
|
670
|
+
if (equals === -1) throw new Error(`--project \u9700\u8981 name=path \u5F62\u5F0F\uFF1A${entry}`);
|
|
671
|
+
return { name: entry.slice(0, equals), directory: entry.slice(equals + 1) };
|
|
672
|
+
}),
|
|
673
|
+
toolKind: flags["--tool"],
|
|
674
|
+
toolCommand: flags["--tool-command"] ?? flags["--opencode-command"],
|
|
675
|
+
installServiceWanted: flags["--no-install-service"] === "true" ? false : void 0
|
|
676
|
+
});
|
|
677
|
+
break;
|
|
678
|
+
}
|
|
679
|
+
case "install": {
|
|
680
|
+
const outcome = await installService({ configPath, nodePath: flags["--node-path"], force: flags["--force"] === "true" });
|
|
681
|
+
console.log(outcome.changed ? `\u5DF2\u5B89\u88C5\u5E76\u542F\u52A8\u670D\u52A1\uFF1A${outcome.serviceFile}` : `\u670D\u52A1\u5B9A\u4E49\u4E0E\u914D\u7F6E\u4E00\u81F4\uFF0C\u5DF2\u786E\u4FDD\u8FD0\u884C\uFF1A${outcome.serviceFile}`);
|
|
682
|
+
console.log(`node\uFF1A${outcome.nodePath}`);
|
|
683
|
+
if (outcome.warning) console.log(`\u6CE8\u610F\uFF1A${outcome.warning}`);
|
|
684
|
+
console.log(`\u65E5\u5FD7\uFF1A${logLocationHint()}\uFF08octopus-proxy logs \u67E5\u770B\uFF09`);
|
|
685
|
+
break;
|
|
686
|
+
}
|
|
687
|
+
case "uninstall": {
|
|
688
|
+
const path = await uninstallService();
|
|
689
|
+
console.log(`\u5DF2\u505C\u6B62\u5E76\u79FB\u9664\u7CFB\u7EDF\u670D\u52A1\uFF08${path}\uFF09\uFF1B\u914D\u7F6E\u4E0E\u72B6\u6001\u6570\u636E\u5DF2\u4FDD\u7559`);
|
|
690
|
+
break;
|
|
691
|
+
}
|
|
692
|
+
case "start":
|
|
693
|
+
case "stop":
|
|
694
|
+
case "restart": {
|
|
695
|
+
await controlService(command);
|
|
696
|
+
console.log(`\u5DF2${command === "start" ? "\u542F\u52A8" : command === "stop" ? "\u505C\u6B62" : "\u91CD\u542F"}\u670D\u52A1`);
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
case "status": {
|
|
700
|
+
const state = await serviceStatus();
|
|
701
|
+
if (!state.installed) console.log("\u5C1A\u672A\u5B89\u88C5\u670D\u52A1\uFF1B\u8FD0\u884C octopus-proxy install\uFF08\u6216\u5148 onboard \u751F\u6210\u914D\u7F6E\uFF09");
|
|
702
|
+
else console.log([
|
|
703
|
+
`\u670D\u52A1\uFF1A\u5DF2\u5B89\u88C5\uFF08${state.serviceFile}\uFF09`,
|
|
704
|
+
`\u72B6\u6001\uFF1A${state.active ? `\u8FD0\u884C\u4E2D${state.pid ? `\uFF08pid ${state.pid}\uFF09` : ""}` : "\u672A\u8FD0\u884C"}`,
|
|
705
|
+
state.enabled === void 0 ? "" : `\u5F00\u673A\u81EA\u542F\uFF1A${state.enabled ? "\u5DF2\u542F\u7528" : "\u672A\u542F\u7528"}`,
|
|
706
|
+
`\u914D\u7F6E\uFF1A${configPath}`,
|
|
707
|
+
`\u65E5\u5FD7\uFF1A${logLocationHint()}`
|
|
708
|
+
].filter(Boolean).join("\n"));
|
|
709
|
+
break;
|
|
710
|
+
}
|
|
711
|
+
case "doctor": {
|
|
712
|
+
const results = await runDoctor({ configPath, fix: flags["--fix"] === "true" });
|
|
713
|
+
for (const result of results) console.log(`${result.status === "ok" ? "[OK] " : result.status === "warn" ? "[\u8B66\u544A]" : "[\u5931\u8D25]"} ${result.name}${result.detail ? ` \u2014 ${result.detail}` : ""}`);
|
|
714
|
+
if (results.some((result) => result.status === "fail")) {
|
|
715
|
+
console.log("\u5B58\u5728\u5931\u8D25\u9879\uFF1B\u6839\u636E\u63D0\u793A\u5904\u7406\u540E\u91CD\u8BD5 octopus-proxy doctor");
|
|
716
|
+
process.exit(1);
|
|
717
|
+
}
|
|
718
|
+
break;
|
|
719
|
+
}
|
|
720
|
+
case "run":
|
|
721
|
+
await runDaemon(configPath);
|
|
722
|
+
break;
|
|
723
|
+
case "logs":
|
|
724
|
+
await showLogs(flags["--follow"] === "true");
|
|
725
|
+
break;
|
|
726
|
+
default:
|
|
727
|
+
console.error(`\u672A\u77E5\u547D\u4EE4\uFF1A${command}`);
|
|
728
|
+
console.log(USAGE);
|
|
729
|
+
process.exit(1);
|
|
730
|
+
}
|
|
731
|
+
} catch (error) {
|
|
732
|
+
if (error instanceof ConfigError) {
|
|
733
|
+
console.error(error.message);
|
|
734
|
+
process.exit(78);
|
|
735
|
+
}
|
|
736
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
737
|
+
process.exit(1);
|
|
738
|
+
}
|