claude-yes 1.71.0 → 1.72.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/dist/{SUPPORTED_CLIS-DtYo1wxO.js → SUPPORTED_CLIS-jR_I2op4.js} +2 -2
- package/dist/cli.js +6 -2
- package/dist/index.js +1 -1
- package/dist/{tray-BzSS0v-i.js → tray-Dyiihcrq.js} +83 -12
- package/package.json +1 -1
- package/ts/cli.ts +6 -0
- package/ts/tray.spec.ts +128 -21
- package/ts/tray.ts +98 -11
|
@@ -815,7 +815,7 @@ function tryCatch(catchFn, fn) {
|
|
|
815
815
|
//#endregion
|
|
816
816
|
//#region package.json
|
|
817
817
|
var name = "agent-yes";
|
|
818
|
-
var version = "1.
|
|
818
|
+
var version = "1.72.0";
|
|
819
819
|
|
|
820
820
|
//#endregion
|
|
821
821
|
//#region ts/pty-fix.ts
|
|
@@ -1895,4 +1895,4 @@ const SUPPORTED_CLIS = Object.keys(CLIS_CONFIG);
|
|
|
1895
1895
|
|
|
1896
1896
|
//#endregion
|
|
1897
1897
|
export { AgentContext as a, PidStore as c, config as i, removeControlCharacters as l, CLIS_CONFIG as n, name as o, agentYes as r, version as s, SUPPORTED_CLIS as t };
|
|
1898
|
-
//# sourceMappingURL=SUPPORTED_CLIS-
|
|
1898
|
+
//# sourceMappingURL=SUPPORTED_CLIS-jR_I2op4.js.map
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { c as PidStore, o as name, s as version, t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-
|
|
2
|
+
import { c as PidStore, o as name, s as version, t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-jR_I2op4.js";
|
|
3
3
|
import { t as logger } from "./logger-CX77vJDA.js";
|
|
4
4
|
import { argv } from "process";
|
|
5
5
|
import { spawn } from "child_process";
|
|
@@ -481,7 +481,7 @@ function buildRustArgs(argv, cliFromScript, supportedClis) {
|
|
|
481
481
|
const updateCheckPromise = checkAndAutoUpdate();
|
|
482
482
|
const config = parseCliArgs(process.argv);
|
|
483
483
|
if (config.tray) {
|
|
484
|
-
const { startTray } = await import("./tray-
|
|
484
|
+
const { startTray } = await import("./tray-Dyiihcrq.js");
|
|
485
485
|
await startTray();
|
|
486
486
|
await new Promise(() => {});
|
|
487
487
|
}
|
|
@@ -572,6 +572,10 @@ if (config.verbose) {
|
|
|
572
572
|
console.log(config);
|
|
573
573
|
console.log(argv);
|
|
574
574
|
}
|
|
575
|
+
{
|
|
576
|
+
const { ensureTray } = await import("./tray-Dyiihcrq.js");
|
|
577
|
+
ensureTray();
|
|
578
|
+
}
|
|
575
579
|
const { default: cliYes } = await import("./index.js");
|
|
576
580
|
const { exitCode } = await cliYes({
|
|
577
581
|
...config,
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as AgentContext, i as config, l as removeControlCharacters, n as CLIS_CONFIG, r as agentYes } from "./SUPPORTED_CLIS-
|
|
1
|
+
import { a as AgentContext, i as config, l as removeControlCharacters, n as CLIS_CONFIG, r as agentYes } from "./SUPPORTED_CLIS-jR_I2op4.js";
|
|
2
2
|
import "./logger-CX77vJDA.js";
|
|
3
3
|
|
|
4
4
|
export { AgentContext, CLIS_CONFIG, config, agentYes as default, removeControlCharacters };
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { n as getRunningAgentCount } from "./runningLock-BBI_URhR.js";
|
|
2
|
+
import { mkdir, readFile, unlink, writeFile } from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { existsSync } from "fs";
|
|
2
6
|
|
|
3
7
|
//#region ts/tray.ts
|
|
4
8
|
const POLL_INTERVAL = 2e3;
|
|
9
|
+
const getTrayDir = () => path.join(process.env.CLAUDE_YES_HOME || homedir(), ".claude-yes");
|
|
10
|
+
const getTrayPidFile = () => path.join(getTrayDir(), "tray.pid");
|
|
5
11
|
const ICON_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAjklEQVQ4T2NkoBAwUqifgWoGMDIyNjAyMv5nYGBYQMgVjMgCQM0LGBkZHYDYAY8BDUBxByB2wGcAyAUOQOwAxPYMDAyOeCzAbwBIMyMjowNQsz0ely8ACjng8wJeA0CaGRgY7IHYAZ8hQHEHfF7AawBYMwODPZABRHsBpwEgzUDN9kDsgM8lQHEHfC4gJhwAAM3hMBGq3cNNAAAAAElFTkSuQmCC";
|
|
6
12
|
function buildMenuItems(tasks) {
|
|
7
13
|
const items = [];
|
|
@@ -43,15 +49,83 @@ function buildMenuItems(tasks) {
|
|
|
43
49
|
});
|
|
44
50
|
return items;
|
|
45
51
|
}
|
|
52
|
+
function isDesktopOS() {
|
|
53
|
+
return process.platform === "darwin" || process.platform === "win32";
|
|
54
|
+
}
|
|
55
|
+
function isTrayProcessRunning(pid) {
|
|
56
|
+
try {
|
|
57
|
+
process.kill(pid, 0);
|
|
58
|
+
return true;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Write the current process PID to the tray PID file
|
|
65
|
+
*/
|
|
66
|
+
async function writeTrayPid() {
|
|
67
|
+
await mkdir(getTrayDir(), { recursive: true });
|
|
68
|
+
await writeFile(getTrayPidFile(), String(process.pid), "utf8");
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Remove the tray PID file
|
|
72
|
+
*/
|
|
73
|
+
async function removeTrayPid() {
|
|
74
|
+
try {
|
|
75
|
+
await unlink(getTrayPidFile());
|
|
76
|
+
} catch {}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Check if a tray process is already running
|
|
80
|
+
*/
|
|
81
|
+
async function isTrayRunning() {
|
|
82
|
+
try {
|
|
83
|
+
const pidFile = getTrayPidFile();
|
|
84
|
+
if (!existsSync(pidFile)) return false;
|
|
85
|
+
const pid = parseInt(await readFile(pidFile, "utf8"), 10);
|
|
86
|
+
if (isNaN(pid)) return false;
|
|
87
|
+
return isTrayProcessRunning(pid);
|
|
88
|
+
} catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Auto-spawn a tray process in the background if not already running.
|
|
94
|
+
* Only spawns on desktop OS (macOS/Windows).
|
|
95
|
+
* Silently does nothing if systray2 is not installed or on non-desktop OS.
|
|
96
|
+
*/
|
|
97
|
+
async function ensureTray() {
|
|
98
|
+
if (!isDesktopOS()) return;
|
|
99
|
+
if (await isTrayRunning()) return;
|
|
100
|
+
try {
|
|
101
|
+
const cliPath = new URL("./cli.ts", import.meta.url).pathname;
|
|
102
|
+
const { spawn } = await import("child_process");
|
|
103
|
+
spawn(process.execPath, [
|
|
104
|
+
cliPath,
|
|
105
|
+
"--tray",
|
|
106
|
+
"--no-rust"
|
|
107
|
+
], {
|
|
108
|
+
detached: true,
|
|
109
|
+
stdio: "ignore",
|
|
110
|
+
env: { ...process.env }
|
|
111
|
+
}).unref();
|
|
112
|
+
} catch {}
|
|
113
|
+
}
|
|
46
114
|
async function startTray() {
|
|
47
|
-
if (
|
|
115
|
+
if (!isDesktopOS()) {
|
|
48
116
|
console.error("Tray icon is only supported on macOS and Windows.");
|
|
49
117
|
return;
|
|
50
118
|
}
|
|
119
|
+
if (await isTrayRunning()) {
|
|
120
|
+
console.error("Tray is already running.");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
await writeTrayPid();
|
|
51
124
|
let SysTray;
|
|
52
125
|
try {
|
|
53
126
|
SysTray = (await import("systray2")).default;
|
|
54
127
|
} catch {
|
|
128
|
+
await removeTrayPid();
|
|
55
129
|
console.error("systray2 is not installed. Install it with: npm install systray2");
|
|
56
130
|
return;
|
|
57
131
|
}
|
|
@@ -71,7 +145,7 @@ async function startTray() {
|
|
|
71
145
|
systray.onClick((action) => {
|
|
72
146
|
if (action.item.title === "Quit Tray") {
|
|
73
147
|
systray.kill(false);
|
|
74
|
-
process.exit(0);
|
|
148
|
+
removeTrayPid().finally(() => process.exit(0));
|
|
75
149
|
}
|
|
76
150
|
});
|
|
77
151
|
let lastCount = count;
|
|
@@ -92,18 +166,15 @@ async function startTray() {
|
|
|
92
166
|
}
|
|
93
167
|
} catch {}
|
|
94
168
|
}, POLL_INTERVAL);
|
|
95
|
-
|
|
169
|
+
const cleanup = () => {
|
|
96
170
|
clearInterval(interval);
|
|
97
171
|
systray.kill(false);
|
|
98
|
-
process.exit(0);
|
|
99
|
-
}
|
|
100
|
-
process.on("
|
|
101
|
-
|
|
102
|
-
systray.kill(false);
|
|
103
|
-
process.exit(0);
|
|
104
|
-
});
|
|
172
|
+
removeTrayPid().finally(() => process.exit(0));
|
|
173
|
+
};
|
|
174
|
+
process.on("SIGINT", cleanup);
|
|
175
|
+
process.on("SIGTERM", cleanup);
|
|
105
176
|
}
|
|
106
177
|
|
|
107
178
|
//#endregion
|
|
108
|
-
export { startTray };
|
|
109
|
-
//# sourceMappingURL=tray-
|
|
179
|
+
export { ensureTray, startTray };
|
|
180
|
+
//# sourceMappingURL=tray-Dyiihcrq.js.map
|
package/package.json
CHANGED
package/ts/cli.ts
CHANGED
|
@@ -142,6 +142,12 @@ if (config.verbose) {
|
|
|
142
142
|
console.log(argv);
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
// Auto-spawn tray icon in background on desktop OS (best-effort, silent failure)
|
|
146
|
+
{
|
|
147
|
+
const { ensureTray } = await import("./tray.ts");
|
|
148
|
+
ensureTray(); // fire-and-forget, don't await
|
|
149
|
+
}
|
|
150
|
+
|
|
145
151
|
const { default: cliYes } = await import("./index.ts");
|
|
146
152
|
const { exitCode } = await cliYes({ ...config, autoYes: config.autoYes });
|
|
147
153
|
|
package/ts/tray.spec.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
2
|
|
|
3
3
|
// Mock systray2 before imports
|
|
4
4
|
const mockSysTray = vi.hoisted(() => {
|
|
@@ -8,12 +8,10 @@ const mockSysTray = vi.hoisted(() => {
|
|
|
8
8
|
sendAction: vi.fn(),
|
|
9
9
|
kill: vi.fn(),
|
|
10
10
|
};
|
|
11
|
-
const MockClass = vi.fn().mockImplementation(function (this: any
|
|
11
|
+
const MockClass = vi.fn().mockImplementation(function (this: any) {
|
|
12
12
|
Object.assign(this, instance);
|
|
13
|
-
(MockClass as any).__lastOpts = opts;
|
|
14
13
|
return this;
|
|
15
14
|
}) as any;
|
|
16
|
-
MockClass.__lastOpts = null;
|
|
17
15
|
return {
|
|
18
16
|
instance,
|
|
19
17
|
MockClass,
|
|
@@ -33,10 +31,33 @@ vi.mock("./runningLock.ts", () => ({
|
|
|
33
31
|
getRunningAgentCount: mockGetRunningAgentCount,
|
|
34
32
|
}));
|
|
35
33
|
|
|
34
|
+
// Mock fs for PID file operations
|
|
35
|
+
const mockFs = vi.hoisted(() => ({
|
|
36
|
+
existsSync: vi.fn().mockReturnValue(false),
|
|
37
|
+
}));
|
|
38
|
+
const mockFsPromises = vi.hoisted(() => ({
|
|
39
|
+
mkdir: vi.fn().mockResolvedValue(undefined),
|
|
40
|
+
readFile: vi.fn().mockResolvedValue(""),
|
|
41
|
+
writeFile: vi.fn().mockResolvedValue(undefined),
|
|
42
|
+
unlink: vi.fn().mockResolvedValue(undefined),
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
vi.mock("fs", () => ({ existsSync: mockFs.existsSync }));
|
|
46
|
+
vi.mock("fs/promises", () => mockFsPromises);
|
|
47
|
+
|
|
48
|
+
// Mock child_process for ensureTray
|
|
49
|
+
const mockSpawn = vi.hoisted(() => {
|
|
50
|
+
const child = { unref: vi.fn() };
|
|
51
|
+
return { spawn: vi.fn().mockReturnValue(child), child };
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
vi.mock("child_process", () => ({ spawn: mockSpawn.spawn }));
|
|
55
|
+
|
|
36
56
|
describe("tray", () => {
|
|
37
57
|
beforeEach(() => {
|
|
38
58
|
vi.clearAllMocks();
|
|
39
59
|
mockGetRunningAgentCount.mockResolvedValue({ count: 0, tasks: [] });
|
|
60
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
40
61
|
});
|
|
41
62
|
|
|
42
63
|
describe("startTray", () => {
|
|
@@ -57,6 +78,8 @@ describe("tray", () => {
|
|
|
57
78
|
);
|
|
58
79
|
expect(mockSysTray.instance.ready).toHaveBeenCalled();
|
|
59
80
|
expect(mockSysTray.instance.onClick).toHaveBeenCalled();
|
|
81
|
+
// Should write PID file
|
|
82
|
+
expect(mockFsPromises.writeFile).toHaveBeenCalled();
|
|
60
83
|
|
|
61
84
|
Object.defineProperty(process, "platform", { value: originalPlatform });
|
|
62
85
|
});
|
|
@@ -94,15 +117,12 @@ describe("tray", () => {
|
|
|
94
117
|
expect(menuArg.menu.title).toBe("AY: 2");
|
|
95
118
|
expect(menuArg.menu.tooltip).toBe("agent-yes: 2 running");
|
|
96
119
|
|
|
97
|
-
// Should have: header, separator, 2 agent items, separator, quit
|
|
98
120
|
const items = menuArg.menu.items;
|
|
99
121
|
expect(items[0].title).toBe("Running agents: 2");
|
|
100
122
|
expect(items[2].title).toContain("[1234]");
|
|
101
123
|
expect(items[2].title).toContain("project-a");
|
|
102
124
|
expect(items[3].title).toContain("[5678]");
|
|
103
125
|
expect(items[3].title).toContain("project-b");
|
|
104
|
-
|
|
105
|
-
// Last item should be "Quit Tray"
|
|
106
126
|
expect(items[items.length - 1].title).toBe("Quit Tray");
|
|
107
127
|
|
|
108
128
|
Object.defineProperty(process, "platform", { value: originalPlatform });
|
|
@@ -111,20 +131,17 @@ describe("tray", () => {
|
|
|
111
131
|
it("should handle quit menu click", async () => {
|
|
112
132
|
const originalPlatform = process.platform;
|
|
113
133
|
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
114
|
-
|
|
115
134
|
const mockExit = vi.spyOn(process, "exit").mockImplementation(() => undefined as never);
|
|
116
135
|
|
|
117
136
|
const { startTray } = await import("./tray.ts");
|
|
118
137
|
await startTray();
|
|
119
138
|
|
|
120
|
-
// Get the onClick callback
|
|
121
139
|
const onClickCb = mockSysTray.instance.onClick.mock.calls[0][0];
|
|
122
|
-
|
|
123
|
-
// Simulate "Quit Tray" click
|
|
124
140
|
onClickCb({ item: { title: "Quit Tray" } });
|
|
125
141
|
|
|
126
142
|
expect(mockSysTray.instance.kill).toHaveBeenCalledWith(false);
|
|
127
|
-
|
|
143
|
+
// removeTrayPid is called (unlink)
|
|
144
|
+
await vi.waitFor(() => expect(mockExit).toHaveBeenCalledWith(0));
|
|
128
145
|
|
|
129
146
|
mockExit.mockRestore();
|
|
130
147
|
Object.defineProperty(process, "platform", { value: originalPlatform });
|
|
@@ -133,13 +150,11 @@ describe("tray", () => {
|
|
|
133
150
|
it("should update tray when agent count changes", async () => {
|
|
134
151
|
const originalPlatform = process.platform;
|
|
135
152
|
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
136
|
-
|
|
137
153
|
vi.useFakeTimers();
|
|
138
154
|
|
|
139
155
|
const { startTray } = await import("./tray.ts");
|
|
140
156
|
await startTray();
|
|
141
157
|
|
|
142
|
-
// Now simulate agent count change on next poll
|
|
143
158
|
mockGetRunningAgentCount.mockResolvedValue({
|
|
144
159
|
count: 3,
|
|
145
160
|
tasks: [
|
|
@@ -170,15 +185,12 @@ describe("tray", () => {
|
|
|
170
185
|
],
|
|
171
186
|
});
|
|
172
187
|
|
|
173
|
-
// Advance timer past poll interval
|
|
174
188
|
await vi.advanceTimersByTimeAsync(2100);
|
|
175
189
|
|
|
176
190
|
expect(mockSysTray.instance.sendAction).toHaveBeenCalledWith(
|
|
177
191
|
expect.objectContaining({
|
|
178
192
|
type: "update-menu",
|
|
179
|
-
menu: expect.objectContaining({
|
|
180
|
-
title: "AY: 3",
|
|
181
|
-
}),
|
|
193
|
+
menu: expect.objectContaining({ title: "AY: 3" }),
|
|
182
194
|
}),
|
|
183
195
|
);
|
|
184
196
|
|
|
@@ -189,15 +201,12 @@ describe("tray", () => {
|
|
|
189
201
|
it("should not update tray when agent count stays the same", async () => {
|
|
190
202
|
const originalPlatform = process.platform;
|
|
191
203
|
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
192
|
-
|
|
193
204
|
vi.useFakeTimers();
|
|
194
205
|
|
|
195
206
|
const { startTray } = await import("./tray.ts");
|
|
196
207
|
await startTray();
|
|
197
208
|
|
|
198
|
-
// Same count on next poll
|
|
199
209
|
mockGetRunningAgentCount.mockResolvedValue({ count: 0, tasks: [] });
|
|
200
|
-
|
|
201
210
|
await vi.advanceTimersByTimeAsync(2100);
|
|
202
211
|
|
|
203
212
|
expect(mockSysTray.instance.sendAction).not.toHaveBeenCalled();
|
|
@@ -229,5 +238,103 @@ describe("tray", () => {
|
|
|
229
238
|
|
|
230
239
|
Object.defineProperty(process, "platform", { value: originalPlatform });
|
|
231
240
|
});
|
|
241
|
+
|
|
242
|
+
it("should skip if tray already running", async () => {
|
|
243
|
+
const originalPlatform = process.platform;
|
|
244
|
+
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
245
|
+
|
|
246
|
+
// Simulate existing tray PID file with a live process (our own PID)
|
|
247
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
248
|
+
mockFsPromises.readFile.mockResolvedValue(String(process.pid));
|
|
249
|
+
|
|
250
|
+
const { startTray } = await import("./tray.ts");
|
|
251
|
+
await startTray();
|
|
252
|
+
|
|
253
|
+
// Should NOT create systray because one is already running
|
|
254
|
+
expect(mockSysTray.MockClass).not.toHaveBeenCalled();
|
|
255
|
+
|
|
256
|
+
Object.defineProperty(process, "platform", { value: originalPlatform });
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe("isTrayRunning", () => {
|
|
261
|
+
it("should return false when no PID file exists", async () => {
|
|
262
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
263
|
+
|
|
264
|
+
const { isTrayRunning } = await import("./tray.ts");
|
|
265
|
+
expect(await isTrayRunning()).toBe(false);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("should return false when PID file has invalid content", async () => {
|
|
269
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
270
|
+
mockFsPromises.readFile.mockResolvedValue("not-a-number");
|
|
271
|
+
|
|
272
|
+
const { isTrayRunning } = await import("./tray.ts");
|
|
273
|
+
expect(await isTrayRunning()).toBe(false);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("should return true when PID file points to a running process", async () => {
|
|
277
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
278
|
+
mockFsPromises.readFile.mockResolvedValue(String(process.pid));
|
|
279
|
+
|
|
280
|
+
const { isTrayRunning } = await import("./tray.ts");
|
|
281
|
+
expect(await isTrayRunning()).toBe(true);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("should return false when PID file points to a dead process", async () => {
|
|
285
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
286
|
+
mockFsPromises.readFile.mockResolvedValue("999999999");
|
|
287
|
+
|
|
288
|
+
const { isTrayRunning } = await import("./tray.ts");
|
|
289
|
+
expect(await isTrayRunning()).toBe(false);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
describe("ensureTray", () => {
|
|
294
|
+
it("should spawn tray on macOS when not running", async () => {
|
|
295
|
+
const originalPlatform = process.platform;
|
|
296
|
+
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
297
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
298
|
+
|
|
299
|
+
const { ensureTray } = await import("./tray.ts");
|
|
300
|
+
await ensureTray();
|
|
301
|
+
|
|
302
|
+
expect(mockSpawn.spawn).toHaveBeenCalledWith(
|
|
303
|
+
process.execPath,
|
|
304
|
+
expect.arrayContaining(["--tray", "--no-rust"]),
|
|
305
|
+
expect.objectContaining({ detached: true, stdio: "ignore" }),
|
|
306
|
+
);
|
|
307
|
+
expect(mockSpawn.child.unref).toHaveBeenCalled();
|
|
308
|
+
|
|
309
|
+
Object.defineProperty(process, "platform", { value: originalPlatform });
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("should not spawn on Linux", async () => {
|
|
313
|
+
const originalPlatform = process.platform;
|
|
314
|
+
Object.defineProperty(process, "platform", { value: "linux" });
|
|
315
|
+
|
|
316
|
+
const { ensureTray } = await import("./tray.ts");
|
|
317
|
+
await ensureTray();
|
|
318
|
+
|
|
319
|
+
expect(mockSpawn.spawn).not.toHaveBeenCalled();
|
|
320
|
+
|
|
321
|
+
Object.defineProperty(process, "platform", { value: originalPlatform });
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("should not spawn if tray already running", async () => {
|
|
325
|
+
const originalPlatform = process.platform;
|
|
326
|
+
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
327
|
+
|
|
328
|
+
// Simulate existing tray
|
|
329
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
330
|
+
mockFsPromises.readFile.mockResolvedValue(String(process.pid));
|
|
331
|
+
|
|
332
|
+
const { ensureTray } = await import("./tray.ts");
|
|
333
|
+
await ensureTray();
|
|
334
|
+
|
|
335
|
+
expect(mockSpawn.spawn).not.toHaveBeenCalled();
|
|
336
|
+
|
|
337
|
+
Object.defineProperty(process, "platform", { value: originalPlatform });
|
|
338
|
+
});
|
|
232
339
|
});
|
|
233
340
|
});
|
package/ts/tray.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { mkdir, readFile, writeFile, unlink } from "fs/promises";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import path from "path";
|
|
1
5
|
import { getRunningAgentCount, type Task } from "./runningLock.ts";
|
|
2
6
|
|
|
3
7
|
const POLL_INTERVAL = 2000;
|
|
4
8
|
|
|
9
|
+
const getTrayDir = () => path.join(process.env.CLAUDE_YES_HOME || homedir(), ".claude-yes");
|
|
10
|
+
const getTrayPidFile = () => path.join(getTrayDir(), "tray.pid");
|
|
11
|
+
|
|
5
12
|
// Minimal 16x16 white circle PNG as base64 (used as tray icon)
|
|
6
13
|
const ICON_BASE64 =
|
|
7
14
|
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAA" +
|
|
@@ -40,17 +47,100 @@ function buildMenuItems(tasks: Task[]) {
|
|
|
40
47
|
return items;
|
|
41
48
|
}
|
|
42
49
|
|
|
50
|
+
function isDesktopOS(): boolean {
|
|
51
|
+
return process.platform === "darwin" || process.platform === "win32";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isTrayProcessRunning(pid: number): boolean {
|
|
55
|
+
try {
|
|
56
|
+
process.kill(pid, 0);
|
|
57
|
+
return true;
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Write the current process PID to the tray PID file
|
|
65
|
+
*/
|
|
66
|
+
async function writeTrayPid(): Promise<void> {
|
|
67
|
+
const dir = getTrayDir();
|
|
68
|
+
await mkdir(dir, { recursive: true });
|
|
69
|
+
await writeFile(getTrayPidFile(), String(process.pid), "utf8");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Remove the tray PID file
|
|
74
|
+
*/
|
|
75
|
+
async function removeTrayPid(): Promise<void> {
|
|
76
|
+
try {
|
|
77
|
+
await unlink(getTrayPidFile());
|
|
78
|
+
} catch {
|
|
79
|
+
// ignore
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if a tray process is already running
|
|
85
|
+
*/
|
|
86
|
+
export async function isTrayRunning(): Promise<boolean> {
|
|
87
|
+
try {
|
|
88
|
+
const pidFile = getTrayPidFile();
|
|
89
|
+
if (!existsSync(pidFile)) return false;
|
|
90
|
+
const pid = parseInt(await readFile(pidFile, "utf8"), 10);
|
|
91
|
+
if (isNaN(pid)) return false;
|
|
92
|
+
return isTrayProcessRunning(pid);
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Auto-spawn a tray process in the background if not already running.
|
|
100
|
+
* Only spawns on desktop OS (macOS/Windows).
|
|
101
|
+
* Silently does nothing if systray2 is not installed or on non-desktop OS.
|
|
102
|
+
*/
|
|
103
|
+
export async function ensureTray(): Promise<void> {
|
|
104
|
+
if (!isDesktopOS()) return;
|
|
105
|
+
if (await isTrayRunning()) return;
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
// Resolve the CLI entry point (dist/cli.js or ts/cli.ts)
|
|
109
|
+
const cliPath = new URL("./cli.ts", import.meta.url).pathname;
|
|
110
|
+
const { spawn } = await import("child_process");
|
|
111
|
+
|
|
112
|
+
// Spawn detached tray process
|
|
113
|
+
const child = spawn(process.execPath, [cliPath, "--tray", "--no-rust"], {
|
|
114
|
+
detached: true,
|
|
115
|
+
stdio: "ignore",
|
|
116
|
+
env: { ...process.env },
|
|
117
|
+
});
|
|
118
|
+
child.unref();
|
|
119
|
+
} catch {
|
|
120
|
+
// Silently fail — tray is best-effort
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
43
124
|
export async function startTray(): Promise<void> {
|
|
44
|
-
|
|
45
|
-
if (process.platform !== "darwin" && process.platform !== "win32") {
|
|
125
|
+
if (!isDesktopOS()) {
|
|
46
126
|
console.error("Tray icon is only supported on macOS and Windows.");
|
|
47
127
|
return;
|
|
48
128
|
}
|
|
49
129
|
|
|
130
|
+
// Check if another tray is already running
|
|
131
|
+
if (await isTrayRunning()) {
|
|
132
|
+
console.error("Tray is already running.");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Register our PID
|
|
137
|
+
await writeTrayPid();
|
|
138
|
+
|
|
50
139
|
let SysTray: typeof import("systray2").default;
|
|
51
140
|
try {
|
|
52
141
|
SysTray = (await import("systray2")).default;
|
|
53
142
|
} catch {
|
|
143
|
+
await removeTrayPid();
|
|
54
144
|
console.error("systray2 is not installed. Install it with: npm install systray2");
|
|
55
145
|
return;
|
|
56
146
|
}
|
|
@@ -75,7 +165,7 @@ export async function startTray(): Promise<void> {
|
|
|
75
165
|
systray.onClick((action) => {
|
|
76
166
|
if (action.item.title === "Quit Tray") {
|
|
77
167
|
systray.kill(false);
|
|
78
|
-
process.exit(0);
|
|
168
|
+
removeTrayPid().finally(() => process.exit(0));
|
|
79
169
|
}
|
|
80
170
|
});
|
|
81
171
|
|
|
@@ -105,14 +195,11 @@ export async function startTray(): Promise<void> {
|
|
|
105
195
|
}, POLL_INTERVAL);
|
|
106
196
|
|
|
107
197
|
// Cleanup on exit
|
|
108
|
-
|
|
198
|
+
const cleanup = () => {
|
|
109
199
|
clearInterval(interval);
|
|
110
200
|
systray.kill(false);
|
|
111
|
-
process.exit(0);
|
|
112
|
-
}
|
|
113
|
-
process.on("
|
|
114
|
-
|
|
115
|
-
systray.kill(false);
|
|
116
|
-
process.exit(0);
|
|
117
|
-
});
|
|
201
|
+
removeTrayPid().finally(() => process.exit(0));
|
|
202
|
+
};
|
|
203
|
+
process.on("SIGINT", cleanup);
|
|
204
|
+
process.on("SIGTERM", cleanup);
|
|
118
205
|
}
|