@caoruhua/open-claude-remote 0.1.6 → 0.2.3
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 +514 -11
- package/dist/backend/src/api/config-routes.d.ts +2 -2
- package/dist/backend/src/api/config-routes.d.ts.map +1 -1
- package/dist/backend/src/api/config-routes.js +21 -6
- package/dist/backend/src/api/config-routes.js.map +1 -1
- package/dist/backend/src/api/health-routes.d.ts +4 -1
- package/dist/backend/src/api/health-routes.d.ts.map +1 -1
- package/dist/backend/src/api/health-routes.js +20 -2
- package/dist/backend/src/api/health-routes.js.map +1 -1
- package/dist/backend/src/api/hook-routes.d.ts +2 -2
- package/dist/backend/src/api/hook-routes.d.ts.map +1 -1
- package/dist/backend/src/api/hook-routes.js +10 -3
- package/dist/backend/src/api/hook-routes.js.map +1 -1
- package/dist/backend/src/api/instance-routes.d.ts +5 -3
- package/dist/backend/src/api/instance-routes.d.ts.map +1 -1
- package/dist/backend/src/api/instance-routes.js +48 -45
- package/dist/backend/src/api/instance-routes.js.map +1 -1
- package/dist/backend/src/api/router.d.ts +4 -12
- package/dist/backend/src/api/router.d.ts.map +1 -1
- package/dist/backend/src/api/router.js +28 -20
- package/dist/backend/src/api/router.js.map +1 -1
- package/dist/backend/src/api/status-routes.d.ts +2 -2
- package/dist/backend/src/api/status-routes.d.ts.map +1 -1
- package/dist/backend/src/api/status-routes.js +11 -6
- package/dist/backend/src/api/status-routes.js.map +1 -1
- package/dist/backend/src/attach.d.ts +1 -1
- package/dist/backend/src/attach.d.ts.map +1 -1
- package/dist/backend/src/attach.js +25 -71
- package/dist/backend/src/attach.js.map +1 -1
- package/dist/backend/src/cli-utils.d.ts +24 -1
- package/dist/backend/src/cli-utils.d.ts.map +1 -1
- package/dist/backend/src/cli-utils.js +73 -37
- package/dist/backend/src/cli-utils.js.map +1 -1
- package/dist/backend/src/cli.d.ts +22 -2
- package/dist/backend/src/cli.d.ts.map +1 -1
- package/dist/backend/src/cli.js +328 -23
- package/dist/backend/src/cli.js.map +1 -1
- package/dist/backend/src/config.d.ts +48 -15
- package/dist/backend/src/config.d.ts.map +1 -1
- package/dist/backend/src/config.js +126 -27
- package/dist/backend/src/config.js.map +1 -1
- package/dist/backend/src/daemon/daemon-client.d.ts +69 -0
- package/dist/backend/src/daemon/daemon-client.d.ts.map +1 -0
- package/dist/backend/src/daemon/daemon-client.js +209 -0
- package/dist/backend/src/daemon/daemon-client.js.map +1 -0
- package/dist/backend/src/daemon/daemon-entry.d.ts +8 -0
- package/dist/backend/src/daemon/daemon-entry.d.ts.map +1 -0
- package/dist/backend/src/daemon/daemon-entry.js +37 -0
- package/dist/backend/src/daemon/daemon-entry.js.map +1 -0
- package/dist/backend/src/daemon/daemon-launcher.d.ts +13 -0
- package/dist/backend/src/daemon/daemon-launcher.d.ts.map +1 -0
- package/dist/backend/src/daemon/daemon-launcher.js +62 -0
- package/dist/backend/src/daemon/daemon-launcher.js.map +1 -0
- package/dist/backend/src/index.d.ts.map +1 -1
- package/dist/backend/src/index.js +115 -251
- package/dist/backend/src/index.js.map +1 -1
- package/dist/backend/src/instance/index.d.ts +4 -0
- package/dist/backend/src/instance/index.d.ts.map +1 -0
- package/dist/backend/src/instance/index.js +3 -0
- package/dist/backend/src/instance/index.js.map +1 -0
- package/dist/backend/src/instance/instance-manager.d.ts +62 -0
- package/dist/backend/src/instance/instance-manager.d.ts.map +1 -0
- package/dist/backend/src/instance/instance-manager.js +194 -0
- package/dist/backend/src/instance/instance-manager.js.map +1 -0
- package/dist/backend/src/instance/instance-session.d.ts +87 -0
- package/dist/backend/src/instance/instance-session.d.ts.map +1 -0
- package/dist/backend/src/instance/instance-session.js +359 -0
- package/dist/backend/src/instance/instance-session.js.map +1 -0
- package/dist/backend/src/instance/types.d.ts +39 -0
- package/dist/backend/src/instance/types.d.ts.map +1 -0
- package/dist/backend/src/instance/types.js +2 -0
- package/dist/backend/src/instance/types.js.map +1 -0
- package/dist/backend/src/notification/notification-manager.js +1 -1
- package/dist/backend/src/notification/notification-manager.js.map +1 -1
- package/dist/backend/src/notification/notification-service-factory.js +1 -1
- package/dist/backend/src/notification/notification-service-factory.js.map +1 -1
- package/dist/backend/src/pty/virtual-pty.d.ts.map +1 -1
- package/dist/backend/src/pty/virtual-pty.js +6 -0
- package/dist/backend/src/pty/virtual-pty.js.map +1 -1
- package/dist/backend/src/registry/shared-token.d.ts +2 -2
- package/dist/backend/src/registry/shared-token.js +11 -11
- package/dist/backend/src/registry/shared-token.js.map +1 -1
- package/dist/backend/src/registry/stop-instances.d.ts +0 -25
- package/dist/backend/src/registry/stop-instances.d.ts.map +1 -1
- package/dist/backend/src/registry/stop-instances.js +6 -206
- package/dist/backend/src/registry/stop-instances.js.map +1 -1
- package/dist/backend/src/skills/index.d.ts +4 -0
- package/dist/backend/src/skills/index.d.ts.map +1 -0
- package/dist/backend/src/skills/index.js +4 -0
- package/dist/backend/src/skills/index.js.map +1 -0
- package/dist/backend/src/skills/skill-command-merger.d.ts +32 -0
- package/dist/backend/src/skills/skill-command-merger.d.ts.map +1 -0
- package/dist/backend/src/skills/skill-command-merger.js +78 -0
- package/dist/backend/src/skills/skill-command-merger.js.map +1 -0
- package/dist/backend/src/skills/skill-commands.d.ts +25 -0
- package/dist/backend/src/skills/skill-commands.d.ts.map +1 -0
- package/dist/backend/src/skills/skill-commands.js +56 -0
- package/dist/backend/src/skills/skill-commands.js.map +1 -0
- package/dist/backend/src/skills/skill-scanner.d.ts +36 -0
- package/dist/backend/src/skills/skill-scanner.d.ts.map +1 -0
- package/dist/backend/src/skills/skill-scanner.js +143 -0
- package/dist/backend/src/skills/skill-scanner.js.map +1 -0
- package/dist/backend/src/terminal/terminal-relay.d.ts +1 -0
- package/dist/backend/src/terminal/terminal-relay.d.ts.map +1 -1
- package/dist/backend/src/terminal/terminal-relay.js +6 -0
- package/dist/backend/src/terminal/terminal-relay.js.map +1 -1
- package/dist/backend/src/update.d.ts.map +1 -1
- package/dist/backend/src/update.js +46 -1
- package/dist/backend/src/update.js.map +1 -1
- package/dist/backend/src/utils/banner.d.ts +15 -0
- package/dist/backend/src/utils/banner.d.ts.map +1 -0
- package/dist/backend/src/utils/banner.js +95 -0
- package/dist/backend/src/utils/banner.js.map +1 -0
- package/dist/backend/src/ws/ws-server.d.ts +13 -54
- package/dist/backend/src/ws/ws-server.d.ts.map +1 -1
- package/dist/backend/src/ws/ws-server.js +57 -155
- package/dist/backend/src/ws/ws-server.js.map +1 -1
- package/dist/shared/constants.d.ts +2 -1
- package/dist/shared/constants.d.ts.map +1 -1
- package/dist/shared/constants.js +2 -1
- package/dist/shared/constants.js.map +1 -1
- package/dist/shared/defaults.d.ts.map +1 -1
- package/dist/shared/defaults.js +1 -5
- package/dist/shared/defaults.js.map +1 -1
- package/dist/shared/instance.d.ts +4 -9
- package/dist/shared/instance.d.ts.map +1 -1
- package/dist/shared/instance.js +0 -2
- package/dist/shared/instance.js.map +1 -1
- package/frontend-dist/assets/index-CM2xfmS8.js +152 -0
- package/frontend-dist/index.html +1 -1
- package/package.json +2 -2
- package/frontend-dist/assets/index-Cfhr3h3e.js +0 -152
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Daemon 子进程入口点
|
|
4
|
+
*
|
|
5
|
+
* 由 daemon-launcher.ts 通过 fork() 启动。
|
|
6
|
+
* 以 daemonMode 运行 startServer(不创建 firstSession,不接管 stdin/stdout)。
|
|
7
|
+
* 启动成功后通过 IPC 通知父进程,然后断开 IPC。
|
|
8
|
+
*/
|
|
9
|
+
// Daemon 不需要 CLI 模式的 logger 配置
|
|
10
|
+
delete process.env.CLI_MODE;
|
|
11
|
+
// 标记为无终端模式
|
|
12
|
+
process.env.NO_TERMINAL = 'true';
|
|
13
|
+
void (async () => {
|
|
14
|
+
try {
|
|
15
|
+
// 从环境变量读取 CLI overrides
|
|
16
|
+
const overridesJson = process.env.DAEMON_OVERRIDES;
|
|
17
|
+
const overrides = overridesJson ? JSON.parse(overridesJson) : {};
|
|
18
|
+
// 强制 daemon 模式
|
|
19
|
+
overrides.daemonMode = true;
|
|
20
|
+
const { startServer } = await import('../index.js');
|
|
21
|
+
await startServer(overrides);
|
|
22
|
+
// 服务启动成功,通知父进程
|
|
23
|
+
if (process.send) {
|
|
24
|
+
process.send({ type: 'ready', pid: process.pid });
|
|
25
|
+
// 断开 IPC 通道,让父进程可以独立退出
|
|
26
|
+
process.disconnect();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
31
|
+
if (process.send) {
|
|
32
|
+
process.send({ type: 'error', message });
|
|
33
|
+
}
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
})();
|
|
37
|
+
//# sourceMappingURL=daemon-entry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daemon-entry.js","sourceRoot":"","sources":["../../../../backend/src/daemon/daemon-entry.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;AAEH,+BAA+B;AAC/B,OAAO,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC;AAC5B,WAAW;AACX,OAAO,CAAC,GAAG,CAAC,WAAW,GAAG,MAAM,CAAC;AAEjC,KAAK,CAAC,KAAK,IAAI,EAAE;IACf,IAAI,CAAC;QACH,wBAAwB;QACxB,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;QACnD,MAAM,SAAS,GAAG,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAEjE,eAAe;QACf,SAAS,CAAC,UAAU,GAAG,IAAI,CAAC;QAE5B,MAAM,EAAE,WAAW,EAAE,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,CAAC;QACpD,MAAM,WAAW,CAAC,SAAS,CAAC,CAAC;QAE7B,eAAe;QACf,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;YACjB,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;YAClD,uBAAuB;YACvB,OAAO,CAAC,UAAU,EAAE,CAAC;QACvB,CAAC;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;YACjB,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;QAC3C,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC,CAAC,EAAE,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CliOverrides } from '../config.js';
|
|
2
|
+
export interface DaemonLaunchResult {
|
|
3
|
+
pid: number;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Fork 一个 daemon 子进程并等待其就绪
|
|
7
|
+
*
|
|
8
|
+
* @param overrides CLI 覆盖参数(传给 daemon 的 startServer)
|
|
9
|
+
* @returns daemon 进程的 PID
|
|
10
|
+
* @throws 启动超时或 daemon 报错
|
|
11
|
+
*/
|
|
12
|
+
export declare function launchDaemon(overrides?: Omit<CliOverrides, 'daemonMode'>): Promise<DaemonLaunchResult>;
|
|
13
|
+
//# sourceMappingURL=daemon-launcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daemon-launcher.d.ts","sourceRoot":"","sources":["../../../../backend/src/daemon/daemon-launcher.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAOjD,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,SAAS,GAAE,IAAI,CAAC,YAAY,EAAE,YAAY,CAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAgDhH"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon 启动器
|
|
3
|
+
*
|
|
4
|
+
* 使用 fork() 启动 daemon-entry.ts 作为独立子进程。
|
|
5
|
+
* 等待 IPC "ready" 消息确认启动成功,然后 unref 子进程让 CLI 可以独立退出。
|
|
6
|
+
*/
|
|
7
|
+
import { fork } from 'node:child_process';
|
|
8
|
+
import { resolve, dirname } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
/** daemon 启动超时(毫秒) */
|
|
12
|
+
const DAEMON_READY_TIMEOUT_MS = 15_000;
|
|
13
|
+
/**
|
|
14
|
+
* Fork 一个 daemon 子进程并等待其就绪
|
|
15
|
+
*
|
|
16
|
+
* @param overrides CLI 覆盖参数(传给 daemon 的 startServer)
|
|
17
|
+
* @returns daemon 进程的 PID
|
|
18
|
+
* @throws 启动超时或 daemon 报错
|
|
19
|
+
*/
|
|
20
|
+
export async function launchDaemon(overrides = {}) {
|
|
21
|
+
const entryPath = resolve(__dirname, 'daemon-entry.js');
|
|
22
|
+
// 判断是否运行在 tsx/ts-node 下(开发模式),使用 .ts 后缀
|
|
23
|
+
const isDevMode = process.execArgv.some(arg => arg.includes('tsx') || arg.includes('ts-node') || arg.includes('loader'));
|
|
24
|
+
const actualEntry = isDevMode
|
|
25
|
+
? resolve(__dirname, 'daemon-entry.ts')
|
|
26
|
+
: entryPath;
|
|
27
|
+
const child = fork(actualEntry, [], {
|
|
28
|
+
detached: true,
|
|
29
|
+
stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
|
|
30
|
+
env: {
|
|
31
|
+
...process.env,
|
|
32
|
+
DAEMON_OVERRIDES: JSON.stringify(overrides),
|
|
33
|
+
CLI_MODE: undefined, // daemon 不是 CLI 模式
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
return new Promise((promiseResolve, reject) => {
|
|
37
|
+
const timeout = setTimeout(() => {
|
|
38
|
+
child.kill();
|
|
39
|
+
reject(new Error(`Daemon failed to start within ${DAEMON_READY_TIMEOUT_MS / 1000}s`));
|
|
40
|
+
}, DAEMON_READY_TIMEOUT_MS);
|
|
41
|
+
child.on('message', (msg) => {
|
|
42
|
+
clearTimeout(timeout);
|
|
43
|
+
if (msg.type === 'ready') {
|
|
44
|
+
// Daemon 启动成功,解除引用让 CLI 可以退出
|
|
45
|
+
child.unref();
|
|
46
|
+
promiseResolve({ pid: msg.pid });
|
|
47
|
+
}
|
|
48
|
+
else if (msg.type === 'error') {
|
|
49
|
+
reject(new Error(`Daemon startup failed: ${msg.message}`));
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
child.on('error', (err) => {
|
|
53
|
+
clearTimeout(timeout);
|
|
54
|
+
reject(new Error(`Failed to fork daemon: ${err.message}`));
|
|
55
|
+
});
|
|
56
|
+
child.on('exit', (code) => {
|
|
57
|
+
clearTimeout(timeout);
|
|
58
|
+
reject(new Error(`Daemon exited unexpectedly with code ${code}`));
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=daemon-launcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daemon-launcher.js","sourceRoot":"","sources":["../../../../backend/src/daemon/daemon-launcher.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAGzC,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE1D,sBAAsB;AACtB,MAAM,uBAAuB,GAAG,MAAM,CAAC;AAMvC;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,YAA8C,EAAE;IACjF,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,EAAE,iBAAiB,CAAC,CAAC;IAExD,wCAAwC;IACxC,MAAM,SAAS,GAAG,OAAO,CAAC,QAAQ,CAAC,IAAI,CACrC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAChF,CAAC;IACF,MAAM,WAAW,GAAG,SAAS;QAC3B,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,iBAAiB,CAAC;QACvC,CAAC,CAAC,SAAS,CAAC;IAEd,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,EAAE,EAAE,EAAE;QAClC,QAAQ,EAAE,IAAI;QACd,KAAK,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,CAAC;QAC5C,GAAG,EAAE;YACH,GAAG,OAAO,CAAC,GAAG;YACd,gBAAgB,EAAE,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC;YAC3C,QAAQ,EAAE,SAAS,EAAE,mBAAmB;SACzC;KACF,CAAC,CAAC;IAEH,OAAO,IAAI,OAAO,CAAqB,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE;QAChE,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE;YAC9B,KAAK,CAAC,IAAI,EAAE,CAAC;YACb,MAAM,CAAC,IAAI,KAAK,CAAC,iCAAiC,uBAAuB,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC;QACxF,CAAC,EAAE,uBAAuB,CAAC,CAAC;QAE5B,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,GAAQ,EAAE,EAAE;YAC/B,YAAY,CAAC,OAAO,CAAC,CAAC;YACtB,IAAI,GAAG,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;gBACzB,6BAA6B;gBAC7B,KAAK,CAAC,KAAK,EAAE,CAAC;gBACd,cAAc,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC;YACnC,CAAC;iBAAM,IAAI,GAAG,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;gBAChC,MAAM,CAAC,IAAI,KAAK,CAAC,0BAA0B,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;YAC7D,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YACxB,YAAY,CAAC,OAAO,CAAC,CAAC;YACtB,MAAM,CAAC,IAAI,KAAK,CAAC,0BAA0B,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;QAC7D,CAAC,CAAC,CAAC;QAEH,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;YACxB,YAAY,CAAC,OAAO,CAAC,CAAC;YACtB,MAAM,CAAC,IAAI,KAAK,CAAC,wCAAwC,IAAI,EAAE,CAAC,CAAC,CAAC;QACpE,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../backend/src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../backend/src/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAAuC,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAgBrF,wBAAsB,WAAW,CAAC,YAAY,GAAE,YAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmQhF"}
|
|
@@ -1,70 +1,47 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
2
|
import { resolve, dirname } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { existsSync
|
|
5
|
-
import { homedir } from 'node:os';
|
|
6
|
-
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
7
5
|
import express from 'express';
|
|
8
6
|
import cors from 'cors';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
7
|
+
import { loadConfig, ensureDefaultUserConfig } from './config.js';
|
|
8
|
+
import { PortInUseError } from './cli-utils.js';
|
|
11
9
|
import { AuthModule } from './auth/auth-middleware.js';
|
|
12
|
-
import { PtyManager } from './pty/pty-manager.js';
|
|
13
10
|
import { WsServer } from './ws/ws-server.js';
|
|
14
|
-
import {
|
|
15
|
-
import { SessionController } from './session/session-controller.js';
|
|
11
|
+
import { InstanceManager } from './instance/instance-manager.js';
|
|
16
12
|
import { TerminalRelay } from './terminal/terminal-relay.js';
|
|
17
13
|
import { createApiRouter } from './api/router.js';
|
|
14
|
+
import { setDaemonStartTime } from './api/health-routes.js';
|
|
18
15
|
import { PushService } from './push/push-service.js';
|
|
19
16
|
import { createNotificationManager } from './notification/notification-manager.js';
|
|
20
17
|
import { createNotificationServiceFactory } from './notification/notification-service-factory.js';
|
|
21
18
|
import { logger, setInstanceContext } from './logger/logger.js';
|
|
22
19
|
import { getOrCreateSharedToken } from './registry/shared-token.js';
|
|
23
|
-
import { findAvailablePort } from './registry/port-finder.js';
|
|
24
|
-
import { InstanceRegistryManager } from './registry/instance-registry.js';
|
|
25
|
-
import { InstanceSpawner } from './registry/instance-spawner.js';
|
|
26
20
|
import { IpMonitor } from './utils/ip-monitor.js';
|
|
27
|
-
import {
|
|
28
|
-
import { getCurrentVersion } from './update.js';
|
|
21
|
+
import { printBanner } from './utils/banner.js';
|
|
29
22
|
export async function startServer(cliOverrides = {}) {
|
|
30
|
-
// 1. Load configuration
|
|
23
|
+
// 1. Load configuration (global + workdir merged)
|
|
31
24
|
const config = loadConfig(cliOverrides);
|
|
32
|
-
const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
|
|
33
25
|
// 1.5. Ensure default shortcuts/commands in user config
|
|
34
26
|
await ensureDefaultUserConfig();
|
|
35
27
|
// 2. Shared config directory and token
|
|
36
|
-
const
|
|
37
|
-
const { token, source: tokenSource } = getOrCreateSharedToken(
|
|
38
|
-
// 3.
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const actualPort = await findAvailablePort(config.port, config.host);
|
|
43
|
-
// Update config to reflect the actual port (cookie name must match actual port
|
|
44
|
-
// to prevent cross-instance cookie collision when port auto-increments)
|
|
45
|
-
if (actualPort !== config.port) {
|
|
46
|
-
config.sessionCookieName = createSessionCookieName(actualPort);
|
|
47
|
-
config.port = actualPort;
|
|
48
|
-
logger.info({
|
|
49
|
-
actualPort,
|
|
50
|
-
sessionCookieName: config.sessionCookieName,
|
|
51
|
-
}, 'Config updated for actual port');
|
|
52
|
-
}
|
|
53
|
-
// 设置日志实例上下文,后续所有日志自动包含 instancePort 字段
|
|
54
|
-
setInstanceContext(actualPort);
|
|
28
|
+
const homeConfigDir = resolve(process.env.HOME ?? process.env.USERPROFILE ?? '', '.claude-remote');
|
|
29
|
+
const { token, source: tokenSource } = getOrCreateSharedToken(homeConfigDir, config.token ?? undefined);
|
|
30
|
+
// 3. Fixed port (8866)
|
|
31
|
+
const port = config.port; // DEFAULT_PORT
|
|
32
|
+
// 设置日志实例上下文
|
|
33
|
+
setInstanceContext(port);
|
|
55
34
|
// 5. Create Express app
|
|
56
35
|
const app = express();
|
|
57
36
|
app.use(express.json());
|
|
58
37
|
app.use(cors({
|
|
59
38
|
origin: (origin, callback) => {
|
|
60
|
-
// Allow same-origin requests (origin is undefined) and requests from LAN
|
|
61
39
|
if (!origin) {
|
|
62
40
|
callback(null, true);
|
|
63
41
|
return;
|
|
64
42
|
}
|
|
65
43
|
try {
|
|
66
44
|
const url = new URL(origin);
|
|
67
|
-
// 允许同一 host 的任意端口(多实例跨端口访问)
|
|
68
45
|
const allowedHosts = [config.displayIp, 'localhost', '127.0.0.1'];
|
|
69
46
|
if (allowedHosts.includes(url.hostname)) {
|
|
70
47
|
callback(null, true);
|
|
@@ -80,49 +57,45 @@ export async function startServer(cliOverrides = {}) {
|
|
|
80
57
|
}));
|
|
81
58
|
// 6. Create HTTP server
|
|
82
59
|
const httpServer = createServer(app);
|
|
83
|
-
// 7. Setup auth module
|
|
60
|
+
// 7. Setup auth module (fixed cookie name: session_id)
|
|
84
61
|
const authModule = new AuthModule({
|
|
85
62
|
token,
|
|
86
63
|
sessionTtlMs: config.sessionTtlMs,
|
|
87
64
|
rateLimitPerMinute: config.authRateLimit,
|
|
88
65
|
cookieName: config.sessionCookieName,
|
|
89
66
|
});
|
|
90
|
-
// 8. Setup
|
|
91
|
-
const
|
|
92
|
-
//
|
|
93
|
-
const pushService = new PushService(sharedConfigDir);
|
|
94
|
-
// 9.1. Create NotificationManager for dynamic enabled status checking
|
|
67
|
+
// 8. Setup Push service
|
|
68
|
+
const pushService = new PushService(homeConfigDir);
|
|
69
|
+
// 8.1. Create NotificationManager for dynamic enabled status checking
|
|
95
70
|
const notificationManager = createNotificationManager();
|
|
96
|
-
//
|
|
71
|
+
// 8.2. Create NotificationServiceFactory for lazy-loading notification services
|
|
97
72
|
const notificationServiceFactory = createNotificationServiceFactory();
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
73
|
+
// 9. InstanceManager (replaces SessionController)
|
|
74
|
+
const instanceManager = new InstanceManager();
|
|
75
|
+
instanceManager.setSharedServices({
|
|
76
|
+
pushService,
|
|
77
|
+
notificationManager,
|
|
78
|
+
notificationServiceFactory,
|
|
79
|
+
displayIp: config.displayIp,
|
|
80
|
+
});
|
|
81
|
+
// 10. Create WebSocket server (with InstanceManager routing)
|
|
103
82
|
const wsServer = new WsServer(httpServer, authModule);
|
|
104
|
-
|
|
83
|
+
wsServer.setInstanceManager(instanceManager);
|
|
84
|
+
// 11. Mount REST API
|
|
105
85
|
app.use('/api', createApiRouter({
|
|
106
86
|
authModule,
|
|
107
|
-
|
|
108
|
-
getController: () => sessionController,
|
|
87
|
+
instanceManager,
|
|
109
88
|
pushService,
|
|
110
|
-
listInstances: () => registry.list(),
|
|
111
|
-
currentInstanceId: instanceId,
|
|
112
|
-
instanceSpawner,
|
|
113
89
|
notificationManager,
|
|
114
90
|
notificationServiceFactory,
|
|
115
|
-
|
|
91
|
+
onShutdown: () => shutdown(0),
|
|
116
92
|
}));
|
|
117
|
-
//
|
|
93
|
+
// 12. Serve frontend static files (if built)
|
|
118
94
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
119
|
-
// 开发模式 (tsx backend/src/index.ts): __dirname = backend/src/ → ../../frontend-dist
|
|
120
|
-
// 生产构建 (node dist/backend/src/index.js): __dirname = dist/backend/src/ → ../../../frontend-dist
|
|
121
95
|
const isDistBuild = __dirname.includes('/dist/');
|
|
122
96
|
const frontendDist = resolve(__dirname, isDistBuild ? '../../../frontend-dist' : '../../frontend-dist');
|
|
123
97
|
if (existsSync(frontendDist)) {
|
|
124
98
|
app.use(express.static(frontendDist));
|
|
125
|
-
// SPA fallback — skip /api and /ws paths to avoid hijacking backend routes
|
|
126
99
|
app.get('*', (req, res, next) => {
|
|
127
100
|
if (req.path.startsWith('/api') || req.path.startsWith('/ws')) {
|
|
128
101
|
return next();
|
|
@@ -134,139 +107,86 @@ export async function startServer(cliOverrides = {}) {
|
|
|
134
107
|
else {
|
|
135
108
|
logger.warn('Frontend dist not found, skipping static file serving');
|
|
136
109
|
}
|
|
137
|
-
//
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
finalArgs = [...config.claudeArgs, '--settings', settingsPath];
|
|
164
|
-
logger.info({ port: actualPort, savedSettingsPath: settingsPath }, 'Generated Claude settings with instance-specific hook URL');
|
|
165
|
-
}
|
|
166
|
-
ptyManager.on('error', (err) => {
|
|
167
|
-
logger.error({ err }, 'PTY process error');
|
|
168
|
-
process.stderr.write(`\n[ERROR] Failed to start Claude CLI: ${err.message}\n`);
|
|
169
|
-
registry.unregister(instanceId);
|
|
170
|
-
process.exit(1);
|
|
171
|
-
});
|
|
172
|
-
ptyManager.spawn({
|
|
173
|
-
command: config.claudeCommand,
|
|
174
|
-
args: finalArgs,
|
|
175
|
-
cwd: config.claudeCwd,
|
|
176
|
-
});
|
|
177
|
-
// Start relay after spawn (if not headless)
|
|
178
|
-
if (relay) {
|
|
179
|
-
relay.start();
|
|
180
|
-
}
|
|
181
|
-
sessionController.setStatus('running');
|
|
182
|
-
// 18. Register instance in shared registry
|
|
183
|
-
registry.register({
|
|
184
|
-
instanceId,
|
|
185
|
-
name: config.instanceName,
|
|
186
|
-
host: config.displayIp,
|
|
187
|
-
port: actualPort,
|
|
188
|
-
pid: process.pid,
|
|
189
|
-
cwd: config.claudeCwd,
|
|
190
|
-
startedAt: new Date().toISOString(),
|
|
191
|
-
headless: noTerminal,
|
|
192
|
-
claudeArgs: config.claudeArgs.length > 0 ? config.claudeArgs : undefined,
|
|
193
|
-
});
|
|
194
|
-
// 18.5. Clean up stale settings files for dead instances
|
|
195
|
-
try {
|
|
196
|
-
const settingsDir = resolve(sharedConfigDir, SETTINGS_DIR);
|
|
197
|
-
if (existsSync(settingsDir)) {
|
|
198
|
-
const alivePorts = new Set((await registry.list()).map(i => i.port));
|
|
199
|
-
for (const file of readdirSync(settingsDir)) {
|
|
200
|
-
const match = file.match(/^(\d+)\.json$/);
|
|
201
|
-
if (match) {
|
|
202
|
-
const port = parseInt(match[1], 10);
|
|
203
|
-
if (!alivePorts.has(port)) {
|
|
204
|
-
try {
|
|
205
|
-
unlinkSync(resolve(settingsDir, file));
|
|
206
|
-
logger.info({ file, port }, 'Cleaned up stale settings file');
|
|
207
|
-
}
|
|
208
|
-
catch {
|
|
209
|
-
// 静默处理:清理失败不影响启动
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
110
|
+
// 13. Create first instance (skip in daemon mode — instances created via API)
|
|
111
|
+
const daemonMode = cliOverrides.daemonMode === true;
|
|
112
|
+
let firstSession;
|
|
113
|
+
let relay;
|
|
114
|
+
let ptyDataHandler;
|
|
115
|
+
if (!daemonMode) {
|
|
116
|
+
const noTerminal = process.env.NO_TERMINAL === 'true';
|
|
117
|
+
firstSession = instanceManager.createInstance({
|
|
118
|
+
cwd: config.claudeCwd,
|
|
119
|
+
name: config.instanceName,
|
|
120
|
+
claudeCommand: config.claudeCommand,
|
|
121
|
+
claudeArgs: config.claudeArgs,
|
|
122
|
+
headless: noTerminal,
|
|
123
|
+
});
|
|
124
|
+
// 14. Terminal Relay (条件启动,headless 模式跳过)
|
|
125
|
+
// PTY output → stdout: InstanceSession 只广播给 WS 客户端,CLI 需要额外管道
|
|
126
|
+
const isTTY = !noTerminal && process.stdin.isTTY;
|
|
127
|
+
if (isTTY) {
|
|
128
|
+
ptyDataHandler = (data) => {
|
|
129
|
+
process.stdout.write(data);
|
|
130
|
+
};
|
|
131
|
+
firstSession.ptyManager.on('data', ptyDataHandler);
|
|
132
|
+
}
|
|
133
|
+
relay = isTTY ? new TerminalRelay(firstSession.ptyManager) : undefined;
|
|
134
|
+
if (relay) {
|
|
135
|
+
relay.start();
|
|
214
136
|
}
|
|
215
137
|
}
|
|
216
|
-
|
|
217
|
-
logger.debug({ err }, 'Settings cleanup skipped');
|
|
218
|
-
}
|
|
219
|
-
// 18.6. Start IP monitor
|
|
138
|
+
// 15. IP monitor
|
|
220
139
|
const ipMonitor = new IpMonitor((newIp, oldIp) => {
|
|
221
|
-
|
|
222
|
-
logger.info({ oldIp, newIp, newUrl }, 'IP change detected');
|
|
223
|
-
// Update config
|
|
140
|
+
logger.info({ oldIp, newIp }, 'IP change detected');
|
|
224
141
|
config.displayIp = newIp;
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
// Update instance URL in session controller
|
|
228
|
-
sessionController?.setInstanceUrl(newUrl);
|
|
229
|
-
// Broadcast to all connected clients
|
|
230
|
-
wsServer.broadcast({
|
|
231
|
-
type: 'ip_changed',
|
|
232
|
-
oldIp,
|
|
233
|
-
newIp,
|
|
234
|
-
newUrl,
|
|
235
|
-
});
|
|
236
|
-
}, 30000, 2); // 30s interval, 2 consecutive detections required
|
|
142
|
+
instanceManager.updateDisplayIp(newIp);
|
|
143
|
+
}, 30000, 2);
|
|
237
144
|
ipMonitor.start(config.displayIp);
|
|
238
|
-
//
|
|
145
|
+
// 16. Unified graceful shutdown
|
|
239
146
|
let shuttingDown = false;
|
|
240
147
|
const shutdown = (exitCode = 0) => {
|
|
241
148
|
if (shuttingDown)
|
|
242
149
|
return;
|
|
243
150
|
shuttingDown = true;
|
|
244
151
|
logger.info({ exitCode }, 'Shutting down...');
|
|
245
|
-
// Unregister from shared registry
|
|
246
|
-
registry.unregister(instanceId);
|
|
247
152
|
if (relay) {
|
|
248
153
|
relay.stop();
|
|
249
154
|
}
|
|
250
155
|
ipMonitor.stop();
|
|
251
|
-
// Pause stdin to remove it as an active event loop handle
|
|
252
156
|
if (!process.stdin.isTTY) {
|
|
253
157
|
process.stdin.pause();
|
|
254
158
|
}
|
|
255
|
-
|
|
159
|
+
instanceManager.destroyAll();
|
|
256
160
|
wsServer.destroy();
|
|
257
161
|
authModule.destroy();
|
|
258
162
|
httpServer.close(() => {
|
|
259
163
|
process.exit(exitCode);
|
|
260
164
|
});
|
|
261
|
-
// Force exit if httpServer.close hangs (ref'd to guarantee it fires)
|
|
262
165
|
setTimeout(() => process.exit(exitCode), 2000);
|
|
263
166
|
};
|
|
264
|
-
// Handle PTY exit
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
167
|
+
// Handle first instance PTY exit (only in non-daemon mode)
|
|
168
|
+
if (firstSession) {
|
|
169
|
+
const session = firstSession;
|
|
170
|
+
session.on('exit', (exitCode) => {
|
|
171
|
+
logger.info({ exitCode, instanceId: session.instanceId }, 'First instance PTY exited');
|
|
172
|
+
// 打印提示后 CLI 退出,daemon 后台运行
|
|
173
|
+
// 注意:先打印提示再停止 relay,确保输出可见
|
|
174
|
+
process.stderr.write(`\nInstance "${session.name}" exited with code ${exitCode}.\n`);
|
|
175
|
+
process.stderr.write('Daemon is still running. Use "claude-remote stop" to shut down.\n\n');
|
|
176
|
+
// 清理 PTY 输出监听器(避免事件循环句柄残留)
|
|
177
|
+
if (ptyDataHandler) {
|
|
178
|
+
session.ptyManager.removeListener('data', ptyDataHandler);
|
|
179
|
+
}
|
|
180
|
+
// 停止 terminal relay(恢复终端状态)
|
|
181
|
+
if (relay) {
|
|
182
|
+
relay.stop();
|
|
183
|
+
}
|
|
184
|
+
// CLI 退出,daemon 继续后台运行
|
|
185
|
+
// 注意:不调用 shutdown(),只移除 CLI 相关的事件监听并退出进程
|
|
186
|
+
process.exitCode = exitCode;
|
|
187
|
+
process.exit(exitCode);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
270
190
|
process.on('SIGINT', () => {
|
|
271
191
|
logger.info('SIGINT received');
|
|
272
192
|
shutdown(0);
|
|
@@ -275,23 +195,16 @@ export async function startServer(cliOverrides = {}) {
|
|
|
275
195
|
logger.info('SIGTERM received');
|
|
276
196
|
shutdown(0);
|
|
277
197
|
});
|
|
278
|
-
// When running via pnpm dev (stdin is a pipe, not TTY)
|
|
279
|
-
|
|
280
|
-
// Also handle Ctrl+C which may come through stdin in pipe mode
|
|
281
|
-
if (!process.stdin.isTTY) {
|
|
198
|
+
// When running via pnpm dev (stdin is a pipe, not TTY) — skip in daemon mode
|
|
199
|
+
if (!daemonMode && !process.stdin.isTTY) {
|
|
282
200
|
process.stdin.resume();
|
|
283
|
-
// Kitty keyboard protocol CSI u variants for Ctrl+C:
|
|
284
|
-
// Match press/repeat (event type 1 or 2) but not release (3)
|
|
285
201
|
const CTRL_C = '\x03';
|
|
286
202
|
const KITTY_CTRL_C_RE = /\x1b\[99;5(?::(?:[12]))?(?:;\d+)*u/;
|
|
287
|
-
// Use 'readable' event for immediate reads without waiting for newline
|
|
288
|
-
// In pipe mode, 'data' event is line-buffered and waits for Enter
|
|
289
203
|
process.stdin.on('readable', () => {
|
|
290
204
|
let chunk;
|
|
291
205
|
while ((chunk = process.stdin.read()) !== null) {
|
|
292
206
|
const str = chunk.toString();
|
|
293
207
|
logger.info({ hex: chunk.toString('hex'), len: chunk.length, str: str.replace(/\x1b/g, 'ESC') }, 'stdin data received in non-TTY mode');
|
|
294
|
-
// Single Ctrl+C (classic ETX or Kitty protocol) → shutdown immediately
|
|
295
208
|
if (str === CTRL_C || KITTY_CTRL_C_RE.test(str)) {
|
|
296
209
|
logger.info('Ctrl+C detected in pipe mode, initiating shutdown');
|
|
297
210
|
shutdown(0);
|
|
@@ -303,86 +216,37 @@ export async function startServer(cliOverrides = {}) {
|
|
|
303
216
|
shutdown(0);
|
|
304
217
|
});
|
|
305
218
|
}
|
|
306
|
-
//
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
process.exit(1);
|
|
314
|
-
});
|
|
315
|
-
// 21. Start listening
|
|
316
|
-
httpServer.listen(actualPort, config.host, () => {
|
|
317
|
-
const url = `http://${config.displayIp}:${actualPort}`;
|
|
318
|
-
const qrUrl = `${url}?token=${token}`;
|
|
319
|
-
const qrLines = generateQRCodeLines(qrUrl);
|
|
320
|
-
// Version (with fallback)
|
|
321
|
-
let version = '';
|
|
322
|
-
try {
|
|
323
|
-
version = getCurrentVersion();
|
|
324
|
-
}
|
|
325
|
-
catch { /* ignore */ }
|
|
326
|
-
// Left column: instance info + commands
|
|
327
|
-
const leftLines = [];
|
|
328
|
-
leftLines.push(`Instance: ${config.instanceName}`);
|
|
329
|
-
leftLines.push(`URL: ${url}`);
|
|
330
|
-
leftLines.push(`PID: ${process.pid}`);
|
|
331
|
-
leftLines.push(`Logs: ${config.logDir}`);
|
|
332
|
-
leftLines.push('');
|
|
333
|
-
leftLines.push('Commands:');
|
|
334
|
-
leftLines.push(` attach: claude-remote attach ${actualPort}`);
|
|
335
|
-
leftLines.push(' update: claude-remote update');
|
|
336
|
-
// Right column: QR lines + blank + centered label
|
|
337
|
-
const qrLabel = 'Scan QR to connect';
|
|
338
|
-
const rightLines = [...qrLines, ''];
|
|
339
|
-
const targetHeight = Math.max(leftLines.length, rightLines.length + 1);
|
|
340
|
-
while (rightLines.length < targetHeight - 1) {
|
|
341
|
-
rightLines.push('');
|
|
342
|
-
}
|
|
343
|
-
rightLines.push(qrLabel);
|
|
344
|
-
// Width calculations
|
|
345
|
-
const qrWidth = Math.max(qrLines[0]?.length || 0, qrLabel.length);
|
|
346
|
-
const leftWidth = Math.max(...leftLines.map(l => l.length), 35);
|
|
347
|
-
const totalWidth = leftWidth + qrWidth + 6;
|
|
348
|
-
// Border components
|
|
349
|
-
const topBorder = '╔' + '═'.repeat(totalWidth - 2) + '╗';
|
|
350
|
-
const title = version ? `Claude Code Remote v${version}` : 'Claude Code Remote';
|
|
351
|
-
const titleLine = '║' + title.padStart(Math.floor((totalWidth - 2 + title.length) / 2)).padEnd(totalWidth - 2) + '║';
|
|
352
|
-
const sepLine = '╠' + '═'.repeat(leftWidth + 1) + '╤' + '═'.repeat(qrWidth + 2) + '╣';
|
|
353
|
-
const midSep = '╠' + '═'.repeat(leftWidth + 1) + '╧' + '═'.repeat(qrWidth + 2) + '╣';
|
|
354
|
-
const tokenLine = '║ ' + `Token: ${token}`.padEnd(totalWidth - 4) + ' ║';
|
|
355
|
-
const bottomBorder = '╚' + '═'.repeat(totalWidth - 2) + '╝';
|
|
356
|
-
// Render three-section banner
|
|
357
|
-
process.stderr.write('\n');
|
|
358
|
-
process.stderr.write(topBorder + '\n');
|
|
359
|
-
process.stderr.write(titleLine + '\n');
|
|
360
|
-
process.stderr.write(sepLine + '\n');
|
|
361
|
-
const maxLines = Math.max(leftLines.length, rightLines.length);
|
|
362
|
-
for (let i = 0; i < maxLines; i++) {
|
|
363
|
-
const left = (leftLines[i] || '').padEnd(leftWidth);
|
|
364
|
-
let right;
|
|
365
|
-
if (i < qrLines.length && rightLines[i]) {
|
|
366
|
-
right = ` ${rightLines[i]} `.padEnd(qrWidth + 2);
|
|
367
|
-
}
|
|
368
|
-
else if (rightLines[i]) {
|
|
369
|
-
const centered = rightLines[i].padStart(Math.floor((qrWidth + rightLines[i].length) / 2)).padEnd(qrWidth);
|
|
370
|
-
right = ` ${centered} `;
|
|
219
|
+
// 17. Start listening (awaitable: resolves on success, rejects on error)
|
|
220
|
+
await new Promise((resolve, reject) => {
|
|
221
|
+
httpServer.on('error', (err) => {
|
|
222
|
+
if (err.code === 'EADDRINUSE') {
|
|
223
|
+
logger.warn({ port, host: config.host }, 'Port is already in use, will attempt fallback');
|
|
224
|
+
reject(new PortInUseError(port));
|
|
225
|
+
return;
|
|
371
226
|
}
|
|
372
|
-
|
|
373
|
-
|
|
227
|
+
logger.error({ err }, 'HTTP server error');
|
|
228
|
+
reject(err);
|
|
229
|
+
});
|
|
230
|
+
httpServer.listen(port, config.host, () => {
|
|
231
|
+
// Record daemon start time
|
|
232
|
+
setDaemonStartTime(new Date().toISOString());
|
|
233
|
+
const url = `http://${config.displayIp}:${port}`;
|
|
234
|
+
if (!daemonMode) {
|
|
235
|
+
// CLI 直接启动模式(pnpm dev 等):打印 banner
|
|
236
|
+
printBanner({
|
|
237
|
+
url,
|
|
238
|
+
token,
|
|
239
|
+
instanceName: config.instanceName,
|
|
240
|
+
logDir: config.logDir,
|
|
241
|
+
pid: process.pid,
|
|
242
|
+
});
|
|
374
243
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
process.stderr.write(tokenLine + '\n');
|
|
379
|
-
process.stderr.write(bottomBorder + '\n');
|
|
380
|
-
process.stderr.write('\n');
|
|
381
|
-
logger.info({ url, host: config.host, port: actualPort, instanceName: config.instanceName, tokenSource }, 'Server started');
|
|
244
|
+
logger.info({ url, host: config.host, port, instanceName: config.instanceName, tokenSource, daemonMode }, 'Server started');
|
|
245
|
+
resolve();
|
|
246
|
+
});
|
|
382
247
|
});
|
|
383
248
|
}
|
|
384
249
|
// Auto-start when run directly (non-CLI mode)
|
|
385
|
-
// CLI mode: cli.ts imports startServer() directly
|
|
386
250
|
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
387
251
|
startServer().catch((err) => {
|
|
388
252
|
logger.error({ err }, 'Failed to start');
|