linkshell-cli 0.2.65 → 0.2.67
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 +3 -0
- package/dist/cli/src/index.js +42 -2
- package/dist/cli/src/index.js.map +1 -1
- package/dist/cli/src/runtime/acp/acp-client.d.ts +41 -0
- package/dist/cli/src/runtime/acp/acp-client.js +102 -0
- package/dist/cli/src/runtime/acp/acp-client.js.map +1 -0
- package/dist/cli/src/runtime/acp/agent-session.d.ts +42 -0
- package/dist/cli/src/runtime/acp/agent-session.js +492 -0
- package/dist/cli/src/runtime/acp/agent-session.js.map +1 -0
- package/dist/cli/src/runtime/acp/json-rpc.d.ts +21 -0
- package/dist/cli/src/runtime/acp/json-rpc.js +150 -0
- package/dist/cli/src/runtime/acp/json-rpc.js.map +1 -0
- package/dist/cli/src/runtime/acp/provider-resolver.d.ts +13 -0
- package/dist/cli/src/runtime/acp/provider-resolver.js +22 -0
- package/dist/cli/src/runtime/acp/provider-resolver.js.map +1 -0
- package/dist/cli/src/runtime/bridge-session.d.ts +7 -0
- package/dist/cli/src/runtime/bridge-session.js +61 -0
- package/dist/cli/src/runtime/bridge-session.js.map +1 -1
- package/dist/cli/src/utils/daemon.d.ts +6 -0
- package/dist/cli/src/utils/daemon.js +22 -0
- package/dist/cli/src/utils/daemon.js.map +1 -1
- package/dist/cli/src/utils/keep-awake.d.ts +6 -0
- package/dist/cli/src/utils/keep-awake.js +48 -0
- package/dist/cli/src/utils/keep-awake.js.map +1 -0
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/dist/shared-protocol/src/index.d.ts +1076 -28
- package/dist/shared-protocol/src/index.js +108 -0
- package/dist/shared-protocol/src/index.js.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +49 -2
- package/src/runtime/acp/acp-client.ts +133 -0
- package/src/runtime/acp/agent-session.ts +589 -0
- package/src/runtime/acp/json-rpc.ts +177 -0
- package/src/runtime/acp/provider-resolver.ts +37 -0
- package/src/runtime/bridge-session.ts +72 -0
- package/src/utils/daemon.ts +28 -0
- package/src/utils/keep-awake.ts +61 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
2
|
+
import type { AgentFraming } from "./provider-resolver.js";
|
|
3
|
+
|
|
4
|
+
interface JsonRpcRequest {
|
|
5
|
+
jsonrpc: "2.0";
|
|
6
|
+
id: number | string;
|
|
7
|
+
method: string;
|
|
8
|
+
params?: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface JsonRpcResponse {
|
|
12
|
+
jsonrpc: "2.0";
|
|
13
|
+
id: number | string;
|
|
14
|
+
result?: unknown;
|
|
15
|
+
error?: { code: number; message: string; data?: unknown };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface JsonRpcNotification {
|
|
19
|
+
jsonrpc: "2.0";
|
|
20
|
+
method: string;
|
|
21
|
+
params?: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type JsonRpcMessage = JsonRpcRequest | JsonRpcResponse | JsonRpcNotification;
|
|
25
|
+
|
|
26
|
+
export class JsonRpcStdioTransport {
|
|
27
|
+
private child: ChildProcessWithoutNullStreams | undefined;
|
|
28
|
+
private nextId = 1;
|
|
29
|
+
private pending = new Map<
|
|
30
|
+
number | string,
|
|
31
|
+
{
|
|
32
|
+
resolve: (value: unknown) => void;
|
|
33
|
+
reject: (error: Error) => void;
|
|
34
|
+
timer: ReturnType<typeof setTimeout>;
|
|
35
|
+
}
|
|
36
|
+
>();
|
|
37
|
+
private buffer = "";
|
|
38
|
+
|
|
39
|
+
constructor(
|
|
40
|
+
private readonly command: string,
|
|
41
|
+
private readonly framing: AgentFraming,
|
|
42
|
+
private readonly onNotification: (method: string, params: unknown) => void,
|
|
43
|
+
private readonly onRequest: (method: string, params: unknown) => Promise<unknown> | unknown,
|
|
44
|
+
private readonly onExit: (message: string) => void,
|
|
45
|
+
) {}
|
|
46
|
+
|
|
47
|
+
start(cwd: string): void {
|
|
48
|
+
if (this.child) return;
|
|
49
|
+
this.child = spawn(this.command, {
|
|
50
|
+
cwd,
|
|
51
|
+
shell: true,
|
|
52
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
53
|
+
env: process.env,
|
|
54
|
+
});
|
|
55
|
+
this.child.stdout.setEncoding("utf8");
|
|
56
|
+
this.child.stderr.setEncoding("utf8");
|
|
57
|
+
this.child.stdout.on("data", (chunk: string) => this.read(chunk));
|
|
58
|
+
this.child.stderr.on("data", (chunk: string) => {
|
|
59
|
+
const trimmed = chunk.trim();
|
|
60
|
+
if (trimmed) process.stderr.write(`[agent:stderr] ${trimmed}\n`);
|
|
61
|
+
});
|
|
62
|
+
this.child.on("error", (error) => this.failAll(error.message));
|
|
63
|
+
this.child.on("exit", (code, signal) => {
|
|
64
|
+
this.failAll(`ACP agent exited (code=${code ?? "null"}, signal=${signal ?? "null"})`);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
request(method: string, params?: unknown, timeoutMs = 30_000): Promise<unknown> {
|
|
69
|
+
if (!this.child || this.child.stdin.destroyed) {
|
|
70
|
+
return Promise.reject(new Error("ACP agent is not running"));
|
|
71
|
+
}
|
|
72
|
+
const id = this.nextId++;
|
|
73
|
+
const message: JsonRpcRequest = { jsonrpc: "2.0", id, method, params };
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
const timer = setTimeout(() => {
|
|
76
|
+
this.pending.delete(id);
|
|
77
|
+
reject(new Error(`ACP request timed out: ${method}`));
|
|
78
|
+
}, timeoutMs);
|
|
79
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
80
|
+
this.write(message);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
notify(method: string, params?: unknown): void {
|
|
85
|
+
this.write({ jsonrpc: "2.0", method, params });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
stop(): void {
|
|
89
|
+
const child = this.child;
|
|
90
|
+
this.child = undefined;
|
|
91
|
+
if (child && !child.killed) child.kill("SIGTERM");
|
|
92
|
+
this.failAll("ACP transport stopped");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private write(message: JsonRpcMessage): void {
|
|
96
|
+
const raw = JSON.stringify(message);
|
|
97
|
+
if (this.framing === "newline") {
|
|
98
|
+
this.child?.stdin.write(`${raw}\n`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
this.child?.stdin.write(`Content-Length: ${Buffer.byteLength(raw, "utf8")}\r\n\r\n${raw}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private read(chunk: string): void {
|
|
105
|
+
this.buffer += chunk;
|
|
106
|
+
while (this.buffer.length > 0) {
|
|
107
|
+
const contentLengthMatch = this.buffer.match(/^Content-Length:\s*(\d+)\r?\n/i);
|
|
108
|
+
if (contentLengthMatch) {
|
|
109
|
+
const headerEnd = this.buffer.indexOf("\r\n\r\n");
|
|
110
|
+
const altHeaderEnd = this.buffer.indexOf("\n\n");
|
|
111
|
+
const end = headerEnd >= 0 ? headerEnd + 4 : altHeaderEnd >= 0 ? altHeaderEnd + 2 : -1;
|
|
112
|
+
if (end < 0) return;
|
|
113
|
+
const length = Number(contentLengthMatch[1]);
|
|
114
|
+
if (this.buffer.length < end + length) return;
|
|
115
|
+
const raw = this.buffer.slice(end, end + length);
|
|
116
|
+
this.buffer = this.buffer.slice(end + length);
|
|
117
|
+
this.dispatch(raw);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const newline = this.buffer.indexOf("\n");
|
|
122
|
+
if (newline < 0) return;
|
|
123
|
+
const raw = this.buffer.slice(0, newline).trim();
|
|
124
|
+
this.buffer = this.buffer.slice(newline + 1);
|
|
125
|
+
if (raw) this.dispatch(raw);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private dispatch(raw: string): void {
|
|
130
|
+
let message: JsonRpcMessage;
|
|
131
|
+
try {
|
|
132
|
+
message = JSON.parse(raw) as JsonRpcMessage;
|
|
133
|
+
} catch {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if ("id" in message && ("result" in message || "error" in message)) {
|
|
138
|
+
const pending = this.pending.get(message.id);
|
|
139
|
+
if (!pending) return;
|
|
140
|
+
this.pending.delete(message.id);
|
|
141
|
+
clearTimeout(pending.timer);
|
|
142
|
+
const response = message as JsonRpcResponse;
|
|
143
|
+
if (response.error) pending.reject(new Error(response.error.message));
|
|
144
|
+
else pending.resolve(response.result);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if ("method" in message && "id" in message) {
|
|
149
|
+
Promise.resolve(this.onRequest(message.method, message.params))
|
|
150
|
+
.then((result) => this.write({ jsonrpc: "2.0", id: message.id, result }))
|
|
151
|
+
.catch((error) => {
|
|
152
|
+
this.write({
|
|
153
|
+
jsonrpc: "2.0",
|
|
154
|
+
id: message.id,
|
|
155
|
+
error: {
|
|
156
|
+
code: -32000,
|
|
157
|
+
message: error instanceof Error ? error.message : String(error),
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if ("method" in message) {
|
|
165
|
+
this.onNotification(message.method, message.params);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private failAll(message: string): void {
|
|
170
|
+
this.onExit(message);
|
|
171
|
+
for (const [, pending] of this.pending) {
|
|
172
|
+
clearTimeout(pending.timer);
|
|
173
|
+
pending.reject(new Error(message));
|
|
174
|
+
}
|
|
175
|
+
this.pending.clear();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type AgentProvider = "codex" | "claude" | "custom";
|
|
2
|
+
export type AgentProtocol = "acp" | "codex-app-server";
|
|
3
|
+
export type AgentFraming = "content-length" | "newline";
|
|
4
|
+
|
|
5
|
+
export interface AgentCommandConfig {
|
|
6
|
+
command: string;
|
|
7
|
+
provider: AgentProvider;
|
|
8
|
+
protocol: AgentProtocol;
|
|
9
|
+
framing: AgentFraming;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function resolveAgentCommand(input: {
|
|
13
|
+
provider: AgentProvider;
|
|
14
|
+
command?: string;
|
|
15
|
+
}): AgentCommandConfig | null {
|
|
16
|
+
const explicit = input.command?.trim();
|
|
17
|
+
if (explicit) {
|
|
18
|
+
const isCodexAppServer = /\bcodex\b/.test(explicit) && /\bapp-server\b/.test(explicit);
|
|
19
|
+
return {
|
|
20
|
+
provider: input.provider,
|
|
21
|
+
command: explicit,
|
|
22
|
+
protocol: isCodexAppServer ? "codex-app-server" : "acp",
|
|
23
|
+
framing: isCodexAppServer ? "newline" : "content-length",
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (input.provider === "codex") {
|
|
28
|
+
return {
|
|
29
|
+
provider: "codex",
|
|
30
|
+
command: "codex app-server --listen stdio://",
|
|
31
|
+
protocol: "codex-app-server",
|
|
32
|
+
framing: "newline",
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
@@ -18,6 +18,9 @@ import { ScrollbackBuffer } from "./scrollback.js";
|
|
|
18
18
|
import { ScreenFallback } from "./screen-fallback.js";
|
|
19
19
|
import { ScreenShare } from "./screen-share.js";
|
|
20
20
|
import { getLanIp } from "../utils/lan-ip.js";
|
|
21
|
+
import { startKeepAwake, type KeepAwakeHandle } from "../utils/keep-awake.js";
|
|
22
|
+
import { AgentSessionProxy } from "./acp/agent-session.js";
|
|
23
|
+
import type { AgentProvider } from "./acp/provider-resolver.js";
|
|
21
24
|
|
|
22
25
|
export interface BridgeSessionOptions {
|
|
23
26
|
gatewayUrl: string;
|
|
@@ -32,6 +35,10 @@ export interface BridgeSessionOptions {
|
|
|
32
35
|
screen?: boolean;
|
|
33
36
|
providerConfig: ProviderConfig;
|
|
34
37
|
authToken?: string;
|
|
38
|
+
keepAwake?: boolean;
|
|
39
|
+
agentUi?: boolean;
|
|
40
|
+
agentProvider?: AgentProvider;
|
|
41
|
+
agentCommand?: string;
|
|
35
42
|
}
|
|
36
43
|
|
|
37
44
|
const HEARTBEAT_INTERVAL = 15_000;
|
|
@@ -126,6 +133,13 @@ function resolvePairingGateway(
|
|
|
126
133
|
}
|
|
127
134
|
}
|
|
128
135
|
|
|
136
|
+
function normalizeAgentProvider(provider: unknown): AgentProvider {
|
|
137
|
+
if (provider === "claude" || provider === "custom") {
|
|
138
|
+
return provider;
|
|
139
|
+
}
|
|
140
|
+
return "codex";
|
|
141
|
+
}
|
|
142
|
+
|
|
129
143
|
export class BridgeSession {
|
|
130
144
|
private readonly options: BridgeSessionOptions;
|
|
131
145
|
private socket: WebSocket | undefined;
|
|
@@ -150,6 +164,8 @@ export class BridgeSession {
|
|
|
150
164
|
private screenCapture: ScreenFallback | undefined;
|
|
151
165
|
private screenShare: ScreenShare | undefined;
|
|
152
166
|
private tunnelSockets = new Map<string, WebSocket>();
|
|
167
|
+
private keepAwake: KeepAwakeHandle | undefined;
|
|
168
|
+
private agentSession: AgentSessionProxy | undefined;
|
|
153
169
|
|
|
154
170
|
constructor(options: BridgeSessionOptions) {
|
|
155
171
|
this.options = options;
|
|
@@ -169,6 +185,25 @@ export class BridgeSession {
|
|
|
169
185
|
if (!this.sessionId) {
|
|
170
186
|
await this.createPairing();
|
|
171
187
|
}
|
|
188
|
+
if (this.options.keepAwake) {
|
|
189
|
+
this.keepAwake = startKeepAwake();
|
|
190
|
+
} else {
|
|
191
|
+
process.stderr.write("[bridge] keep-awake disabled\n");
|
|
192
|
+
}
|
|
193
|
+
if (this.options.agentUi) {
|
|
194
|
+
const agentProvider = normalizeAgentProvider(
|
|
195
|
+
this.options.agentProvider ?? this.options.providerConfig.provider,
|
|
196
|
+
);
|
|
197
|
+
this.agentSession = new AgentSessionProxy({
|
|
198
|
+
sessionId: this.sessionId,
|
|
199
|
+
cwd: process.cwd(),
|
|
200
|
+
provider: agentProvider,
|
|
201
|
+
command: this.options.agentCommand,
|
|
202
|
+
verbose: this.options.verbose,
|
|
203
|
+
send: (envelope) => this.send(envelope),
|
|
204
|
+
});
|
|
205
|
+
process.stderr.write("[bridge] agent GUI channel enabled\n");
|
|
206
|
+
}
|
|
172
207
|
await this.spawnTerminal(DEFAULT_TERMINAL_ID, process.cwd());
|
|
173
208
|
this.connectGateway();
|
|
174
209
|
}
|
|
@@ -509,6 +544,39 @@ export class BridgeSession {
|
|
|
509
544
|
this.screenShare?.handleIceCandidate(p.candidate, p.sdpMid, p.sdpMLineIndex);
|
|
510
545
|
break;
|
|
511
546
|
}
|
|
547
|
+
case "agent.initialize":
|
|
548
|
+
case "agent.session.new":
|
|
549
|
+
case "agent.session.load":
|
|
550
|
+
case "agent.session.list":
|
|
551
|
+
case "agent.prompt":
|
|
552
|
+
case "agent.cancel":
|
|
553
|
+
case "agent.permission.response": {
|
|
554
|
+
if (!this.agentSession) {
|
|
555
|
+
this.send(
|
|
556
|
+
createEnvelope({
|
|
557
|
+
type: "agent.capabilities",
|
|
558
|
+
sessionId: this.sessionId,
|
|
559
|
+
payload: {
|
|
560
|
+
enabled: false,
|
|
561
|
+
provider: normalizeAgentProvider(
|
|
562
|
+
this.options.agentProvider ?? this.options.providerConfig.provider,
|
|
563
|
+
),
|
|
564
|
+
error: "Agent GUI is not enabled. Start CLI with --agent-ui.",
|
|
565
|
+
supportsSessionList: false,
|
|
566
|
+
supportsSessionLoad: false,
|
|
567
|
+
supportsImages: false,
|
|
568
|
+
supportsAudio: false,
|
|
569
|
+
supportsPermission: false,
|
|
570
|
+
supportsPlan: false,
|
|
571
|
+
supportsCancel: false,
|
|
572
|
+
},
|
|
573
|
+
}),
|
|
574
|
+
);
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
await this.agentSession.handleEnvelope(envelope);
|
|
578
|
+
break;
|
|
579
|
+
}
|
|
512
580
|
case "file.upload": {
|
|
513
581
|
const p = parseTypedPayload("file.upload", envelope.payload);
|
|
514
582
|
const ext = p.filename.split(".").pop() || "png";
|
|
@@ -1622,6 +1690,10 @@ export class BridgeSession {
|
|
|
1622
1690
|
this.exited = true;
|
|
1623
1691
|
this.stopHeartbeat();
|
|
1624
1692
|
this.stopScreenCapture();
|
|
1693
|
+
this.agentSession?.stop();
|
|
1694
|
+
this.agentSession = undefined;
|
|
1695
|
+
this.keepAwake?.stop();
|
|
1696
|
+
this.keepAwake = undefined;
|
|
1625
1697
|
if (this.reconnectTimer) {
|
|
1626
1698
|
clearTimeout(this.reconnectTimer);
|
|
1627
1699
|
this.reconnectTimer = undefined;
|
package/src/utils/daemon.ts
CHANGED
|
@@ -7,6 +7,11 @@ const LINKSHELL_DIR = join(homedir(), ".linkshell");
|
|
|
7
7
|
|
|
8
8
|
type ServiceName = "gateway" | "bridge";
|
|
9
9
|
|
|
10
|
+
export interface ServiceMetadata {
|
|
11
|
+
keepAwake?: boolean;
|
|
12
|
+
startedAt?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
function pidFile(service: ServiceName): string {
|
|
11
16
|
return join(LINKSHELL_DIR, `${service}.pid`);
|
|
12
17
|
}
|
|
@@ -15,6 +20,10 @@ function logFile(service: ServiceName): string {
|
|
|
15
20
|
return join(LINKSHELL_DIR, `${service}.log`);
|
|
16
21
|
}
|
|
17
22
|
|
|
23
|
+
function metadataFile(service: ServiceName): string {
|
|
24
|
+
return join(LINKSHELL_DIR, `${service}.json`);
|
|
25
|
+
}
|
|
26
|
+
|
|
18
27
|
export function savePid(service: ServiceName, pid: number): void {
|
|
19
28
|
mkdirSync(LINKSHELL_DIR, { recursive: true });
|
|
20
29
|
writeFileSync(pidFile(service), String(pid), "utf8");
|
|
@@ -40,6 +49,7 @@ export function readPid(service: ServiceName): number | null {
|
|
|
40
49
|
|
|
41
50
|
export function removePid(service: ServiceName): void {
|
|
42
51
|
try { unlinkSync(pidFile(service)); } catch {}
|
|
52
|
+
try { unlinkSync(metadataFile(service)); } catch {}
|
|
43
53
|
}
|
|
44
54
|
|
|
45
55
|
export function getLogFile(service: ServiceName): string {
|
|
@@ -50,6 +60,24 @@ export function getPidFile(service: ServiceName): string {
|
|
|
50
60
|
return pidFile(service);
|
|
51
61
|
}
|
|
52
62
|
|
|
63
|
+
export function saveMetadata(
|
|
64
|
+
service: ServiceName,
|
|
65
|
+
metadata: ServiceMetadata,
|
|
66
|
+
): void {
|
|
67
|
+
mkdirSync(LINKSHELL_DIR, { recursive: true });
|
|
68
|
+
writeFileSync(metadataFile(service), JSON.stringify(metadata), "utf8");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function readMetadata(service: ServiceName): ServiceMetadata | null {
|
|
72
|
+
try {
|
|
73
|
+
const file = metadataFile(service);
|
|
74
|
+
if (!existsSync(file)) return null;
|
|
75
|
+
return JSON.parse(readFileSync(file, "utf8")) as ServiceMetadata;
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
53
81
|
export function spawnDaemon(service: ServiceName, args: string[]): number {
|
|
54
82
|
mkdirSync(LINKSHELL_DIR, { recursive: true });
|
|
55
83
|
const log = logFile(service);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
|
+
import { platform } from "node:os";
|
|
3
|
+
|
|
4
|
+
export interface KeepAwakeHandle {
|
|
5
|
+
enabled: boolean;
|
|
6
|
+
stop: () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function shouldKeepAwake(optionEnabled: boolean | undefined): boolean {
|
|
10
|
+
if (process.env.LINKSHELL_KEEP_AWAKE === "0") return false;
|
|
11
|
+
if (platform() !== "darwin") return false;
|
|
12
|
+
return optionEnabled !== false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function startKeepAwake(): KeepAwakeHandle {
|
|
16
|
+
if (platform() !== "darwin") {
|
|
17
|
+
return { enabled: false, stop: () => {} };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let child: ChildProcess | undefined;
|
|
21
|
+
let stopping = false;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
child = spawn("caffeinate", ["-i", "-w", String(process.pid)], {
|
|
25
|
+
stdio: "ignore",
|
|
26
|
+
detached: false,
|
|
27
|
+
});
|
|
28
|
+
} catch (error) {
|
|
29
|
+
process.stderr.write(
|
|
30
|
+
`[bridge] keep-awake unavailable: ${error instanceof Error ? error.message : String(error)}\n`,
|
|
31
|
+
);
|
|
32
|
+
return { enabled: false, stop: () => {} };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
child.on("error", (error) => {
|
|
36
|
+
if (stopping) return;
|
|
37
|
+
process.stderr.write(`[bridge] keep-awake unavailable: ${error.message}\n`);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
child.on("exit", (code, signal) => {
|
|
41
|
+
if (stopping) return;
|
|
42
|
+
process.stderr.write(
|
|
43
|
+
`[bridge] keep-awake stopped unexpectedly (code=${code ?? "null"}, signal=${signal ?? "null"})\n`,
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
process.stderr.write(
|
|
48
|
+
"[bridge] keep-awake enabled (macOS idle sleep prevention)\n",
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
enabled: true,
|
|
53
|
+
stop: () => {
|
|
54
|
+
stopping = true;
|
|
55
|
+
if (child && !child.killed) {
|
|
56
|
+
child.kill("SIGTERM");
|
|
57
|
+
}
|
|
58
|
+
child = undefined;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|