@hayasaka7/haya-pet 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.
Files changed (131) hide show
  1. package/.gitattributes +34 -0
  2. package/.github/workflows/release.yml +61 -0
  3. package/LICENSE +21 -0
  4. package/README.md +247 -0
  5. package/apps/cli/src/haya-pet.js +395 -0
  6. package/apps/cli/test/haya-pet.test.mjs +339 -0
  7. package/apps/companion/README.md +83 -0
  8. package/apps/companion/package.json +17 -0
  9. package/apps/companion/src/main/display-manager.js +71 -0
  10. package/apps/companion/src/main/index.js +349 -0
  11. package/apps/companion/src/main/lock-file.js +52 -0
  12. package/apps/companion/src/main/panel-placement.js +45 -0
  13. package/apps/companion/src/main/pet-loader.js +2 -0
  14. package/apps/companion/src/main/position-store.js +3 -0
  15. package/apps/companion/src/main/preload.cjs +13 -0
  16. package/apps/companion/src/main/state-file.js +2 -0
  17. package/apps/companion/src/main/terminal-helper-client.js +79 -0
  18. package/apps/companion/src/main/terminal-locator.js +44 -0
  19. package/apps/companion/src/main/tray-menu.js +79 -0
  20. package/apps/companion/src/main/window-options.js +66 -0
  21. package/apps/companion/src/renderer/index.html +18 -0
  22. package/apps/companion/src/renderer/interaction-controller.js +114 -0
  23. package/apps/companion/src/renderer/pet-window.js +275 -0
  24. package/apps/companion/src/renderer/session-bubbles.js +138 -0
  25. package/apps/companion/src/renderer/styles.css +225 -0
  26. package/apps/companion/src/renderer/task-talk-window.js +141 -0
  27. package/apps/companion/test/display-manager.test.mjs +48 -0
  28. package/apps/companion/test/interaction-controller.test.mjs +107 -0
  29. package/apps/companion/test/panel-placement.test.mjs +60 -0
  30. package/apps/companion/test/position-store.test.mjs +54 -0
  31. package/apps/companion/test/state-file.test.mjs +52 -0
  32. package/apps/companion/test/terminal-helper-client.test.mjs +68 -0
  33. package/apps/companion/test/terminal-locator.test.mjs +35 -0
  34. package/apps/companion/test/tray-menu.test.mjs +45 -0
  35. package/apps/companion/test/window-options.test.mjs +62 -0
  36. package/apps/pet-preview/index.html +42 -0
  37. package/apps/pet-preview/src/preview-app.js +123 -0
  38. package/apps/pet-preview/src/preview-state.js +70 -0
  39. package/apps/pet-preview/src/preview.css +125 -0
  40. package/apps/pet-preview/test/preview-state.test.mjs +62 -0
  41. package/assets/fallback-pet/README.md +16 -0
  42. package/assets/fallback-pet/pet.json +13 -0
  43. package/docs/architecture.md +144 -0
  44. package/docs/known-issues.md +49 -0
  45. package/docs/publishing.md +48 -0
  46. package/docs/screenshots/README.md +7 -0
  47. package/docs/screenshots/folder-collapsed.png +0 -0
  48. package/docs/screenshots/hero.png +0 -0
  49. package/docs/screenshots/pet-overlay.png +0 -0
  50. package/docs/screenshots/session-bubbles.png +0 -0
  51. package/docs/screenshots/tray-menu.png +0 -0
  52. package/docs/troubleshooting.md +36 -0
  53. package/native/README.md +80 -0
  54. package/native/linux-window-helper/README.md +29 -0
  55. package/native/mac-window-helper/README.md +30 -0
  56. package/native/win-window-helper/Program.cs +312 -0
  57. package/native/win-window-helper/README.md +53 -0
  58. package/native/win-window-helper/win-window-helper.csproj +12 -0
  59. package/package.json +35 -0
  60. package/packages/adapters/src/adapter-info.js +61 -0
  61. package/packages/adapters/src/capabilities.js +39 -0
  62. package/packages/adapters/src/heuristics.js +114 -0
  63. package/packages/adapters/src/output-observer.js +164 -0
  64. package/packages/adapters/src/routing.js +86 -0
  65. package/packages/adapters/test/adapter-info.test.mjs +35 -0
  66. package/packages/adapters/test/capabilities.test.mjs +44 -0
  67. package/packages/adapters/test/heuristics.test.mjs +42 -0
  68. package/packages/adapters/test/output-observer.test.mjs +142 -0
  69. package/packages/adapters/test/routing.test.mjs +93 -0
  70. package/packages/app-state/src/state-file.js +53 -0
  71. package/packages/app-state/src/state.js +80 -0
  72. package/packages/app-state/test/state.test.mjs +36 -0
  73. package/packages/cli-core/src/companion-launcher.js +69 -0
  74. package/packages/cli-core/src/pty-runner.js +96 -0
  75. package/packages/cli-core/src/run-command.js +353 -0
  76. package/packages/cli-core/src/strip-ansi.js +16 -0
  77. package/packages/cli-core/test/companion-launcher.test.mjs +98 -0
  78. package/packages/cli-core/test/run-command.test.mjs +177 -0
  79. package/packages/cli-core/test/strip-ansi.test.mjs +27 -0
  80. package/packages/daemon-core/src/daemon-runtime.js +49 -0
  81. package/packages/daemon-core/src/ipc-server.js +180 -0
  82. package/packages/daemon-core/src/ipc-transport.js +70 -0
  83. package/packages/daemon-core/src/singleton.js +46 -0
  84. package/packages/daemon-core/test/daemon-runtime.test.mjs +65 -0
  85. package/packages/daemon-core/test/ipc-server.test.mjs +70 -0
  86. package/packages/daemon-core/test/ipc-transport.test.mjs +72 -0
  87. package/packages/daemon-core/test/singleton.test.mjs +32 -0
  88. package/packages/pet-core/src/animation-state.js +84 -0
  89. package/packages/pet-core/src/animator.js +26 -0
  90. package/packages/pet-core/src/atlas.js +81 -0
  91. package/packages/pet-core/src/discovery.js +90 -0
  92. package/packages/pet-core/src/manifest.js +112 -0
  93. package/packages/pet-core/src/validation.js +43 -0
  94. package/packages/pet-core/test/animation-state.test.mjs +47 -0
  95. package/packages/pet-core/test/animator.test.mjs +31 -0
  96. package/packages/pet-core/test/atlas.test.mjs +81 -0
  97. package/packages/pet-core/test/discovery.test.mjs +93 -0
  98. package/packages/pet-core/test/manifest.test.mjs +93 -0
  99. package/packages/pet-core/test/validation.test.mjs +69 -0
  100. package/packages/platform-core/src/capabilities.js +49 -0
  101. package/packages/platform-core/src/paths.js +75 -0
  102. package/packages/platform-core/src/platform.js +15 -0
  103. package/packages/platform-core/test/platform.test.mjs +84 -0
  104. package/packages/protocol/src/messages.js +156 -0
  105. package/packages/protocol/test/messages.test.mjs +112 -0
  106. package/packages/session-core/src/bubble-linger.js +47 -0
  107. package/packages/session-core/src/bubble-view.js +79 -0
  108. package/packages/session-core/src/pet-state.js +56 -0
  109. package/packages/session-core/src/priority.js +56 -0
  110. package/packages/session-core/src/registry.js +144 -0
  111. package/packages/session-core/src/summaries.js +54 -0
  112. package/packages/session-core/test/bubble-linger.test.mjs +96 -0
  113. package/packages/session-core/test/bubble-view.test.mjs +79 -0
  114. package/packages/session-core/test/pet-state.test.mjs +118 -0
  115. package/packages/session-core/test/priority.test.mjs +53 -0
  116. package/packages/session-core/test/registry.test.mjs +161 -0
  117. package/packages/session-core/test/summaries.test.mjs +38 -0
  118. package/packages/task-core/src/approvals.js +91 -0
  119. package/packages/task-core/src/controls.js +61 -0
  120. package/packages/task-core/src/replies.js +80 -0
  121. package/packages/task-core/src/task-events.js +101 -0
  122. package/packages/task-core/src/task-status.js +93 -0
  123. package/packages/task-core/src/task-store.js +74 -0
  124. package/packages/task-core/test/approvals.test.mjs +61 -0
  125. package/packages/task-core/test/controls.test.mjs +61 -0
  126. package/packages/task-core/test/replies.test.mjs +51 -0
  127. package/packages/task-core/test/task-events.test.mjs +67 -0
  128. package/packages/task-core/test/task-status.test.mjs +49 -0
  129. package/packages/task-core/test/task-store.test.mjs +65 -0
  130. package/test/harness.mjs +22 -0
  131. package/test/run-tests.mjs +47 -0
@@ -0,0 +1,98 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "../../../test/harness.mjs";
3
+ import { ensureCompanionConnection } from "../src/companion-launcher.js";
4
+
5
+ const noSleep = async () => {};
6
+
7
+ test("connects without launching when the companion is already running", async () => {
8
+ let launched = 0;
9
+ const client = { id: "existing" };
10
+ const result = await ensureCompanionConnection({
11
+ connect: async () => client,
12
+ launch: async () => { launched += 1; },
13
+ sleep: noSleep
14
+ });
15
+ assert.equal(result.client, client);
16
+ assert.equal(result.started, false);
17
+ assert.equal(launched, 0);
18
+ });
19
+
20
+ test("launches the companion then connects after it comes up", async () => {
21
+ let launched = 0;
22
+ let attempts = 0;
23
+ const client = { id: "fresh" };
24
+ const result = await ensureCompanionConnection({
25
+ // First check fails (not running); after launch, the 3rd connect succeeds.
26
+ connect: async () => {
27
+ attempts += 1;
28
+ if (attempts <= 3) {
29
+ throw new Error("ECONNREFUSED");
30
+ }
31
+ return client;
32
+ },
33
+ launch: async () => { launched += 1; },
34
+ attempts: 10,
35
+ intervalMs: 1,
36
+ sleep: noSleep
37
+ });
38
+ assert.equal(result.client, client);
39
+ assert.equal(result.started, true);
40
+ assert.equal(launched, 1, "launches exactly once");
41
+ });
42
+
43
+ test("does not launch when autoStart is disabled", async () => {
44
+ let launched = 0;
45
+ const result = await ensureCompanionConnection({
46
+ connect: async () => { throw new Error("no daemon"); },
47
+ launch: async () => { launched += 1; },
48
+ autoStart: false,
49
+ sleep: noSleep
50
+ });
51
+ assert.equal(result.client, null);
52
+ assert.equal(result.started, false);
53
+ assert.equal(launched, 0);
54
+ });
55
+
56
+ test("times out gracefully when the companion never comes up", async () => {
57
+ let launched = 0;
58
+ const result = await ensureCompanionConnection({
59
+ connect: async () => { throw new Error("never listening"); },
60
+ launch: async () => { launched += 1; },
61
+ attempts: 4,
62
+ intervalMs: 1,
63
+ sleep: noSleep
64
+ });
65
+ assert.equal(result.client, null);
66
+ assert.equal(result.started, false);
67
+ assert.equal(result.timedOut, true);
68
+ assert.equal(launched, 1);
69
+ });
70
+
71
+ test("degrades gracefully when launching throws (e.g. electron missing)", async () => {
72
+ const result = await ensureCompanionConnection({
73
+ connect: async () => { throw new Error("no daemon"); },
74
+ launch: async () => { throw new Error("electron not installed"); },
75
+ sleep: noSleep
76
+ });
77
+ assert.equal(result.client, null);
78
+ assert.equal(result.started, false);
79
+ assert.ok(result.error instanceof Error);
80
+ });
81
+
82
+ test("stops polling as soon as a connection succeeds", async () => {
83
+ let connectCalls = 0;
84
+ const client = { id: "x" };
85
+ await ensureCompanionConnection({
86
+ connect: async () => {
87
+ connectCalls += 1;
88
+ if (connectCalls === 1) throw new Error("not yet"); // initial check
89
+ if (connectCalls === 2) return client; // first poll succeeds
90
+ throw new Error("should not poll again");
91
+ },
92
+ launch: async () => {},
93
+ attempts: 10,
94
+ intervalMs: 1,
95
+ sleep: noSleep
96
+ });
97
+ assert.equal(connectCalls, 2);
98
+ });
@@ -0,0 +1,177 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { basename, join } from "node:path";
5
+ import { test } from "../../../test/harness.mjs";
6
+ import { runGenericCommand } from "../src/run-command.js";
7
+
8
+ function createClock(start = 1000, step = 10) {
9
+ let current = start;
10
+ return () => {
11
+ const value = current;
12
+ current += step;
13
+ return value;
14
+ };
15
+ }
16
+
17
+ test("emits generic command lifecycle messages and preserves exit code", async () => {
18
+ const messages = [];
19
+ const cwd = process.cwd();
20
+
21
+ const result = await runGenericCommand({
22
+ command: process.execPath,
23
+ args: ["-e", "setTimeout(() => process.exit(7), 40)"],
24
+ cwd,
25
+ clientId: "generic",
26
+ clientDisplayName: "Generic",
27
+ sessionId: "sess_test",
28
+ heartbeatIntervalMs: 5,
29
+ now: createClock(),
30
+ stdio: "ignore",
31
+ send: async (message) => {
32
+ messages.push(message);
33
+ }
34
+ });
35
+
36
+ assert.equal(result.exitCode, 7);
37
+ assert.equal(result.sessionId, "sess_test");
38
+
39
+ assert.equal(messages[0].type, "register");
40
+ assert.equal(messages[0].sessionId, "sess_test");
41
+ assert.equal(messages[0].clientId, "generic");
42
+ assert.equal(messages[0].clientDisplayName, "Generic");
43
+ assert.equal(messages[0].cwd, cwd);
44
+ assert.equal(messages[0].projectName, basename(cwd));
45
+ assert.equal(messages[0].pid, result.pid);
46
+
47
+ const stateMessage = messages.find((message) => message.type === "state");
48
+ assert.equal(stateMessage.state, "idle");
49
+ assert.equal(stateMessage.source, "wrapper");
50
+ assert.equal(stateMessage.summary, "session started");
51
+
52
+ assert.ok(messages.some((message) => message.type === "heartbeat"));
53
+
54
+ const unregister = messages[messages.length - 1];
55
+ assert.equal(unregister.type, "unregister");
56
+ assert.equal(unregister.sessionId, "sess_test");
57
+ assert.equal(unregister.exitCode, 7);
58
+ });
59
+
60
+ test("launches a Windows .cmd shim via the shell and reports a valid pid", async () => {
61
+ if (process.platform !== "win32") {
62
+ return; // shell resolution of .cmd shims is Windows-specific
63
+ }
64
+
65
+ const dir = mkdtempSync(join(tmpdir(), "haya-pet-cmd-"));
66
+ const cmdPath = join(dir, "fake-cli.cmd");
67
+ writeFileSync(cmdPath, "@echo off\r\nexit /b 5\r\n");
68
+
69
+ try {
70
+ const messages = [];
71
+ const result = await runGenericCommand({
72
+ command: cmdPath,
73
+ args: [],
74
+ cwd: process.cwd(),
75
+ sessionId: "sess_cmd",
76
+ heartbeatIntervalMs: 50,
77
+ now: createClock(),
78
+ stdio: "ignore",
79
+ send: async (message) => messages.push(message)
80
+ });
81
+
82
+ assert.equal(result.exitCode, 5);
83
+ const register = messages.find((message) => message.type === "register");
84
+ assert.ok(register, "should send a register message");
85
+ assert.ok(Number.isInteger(register.pid) && register.pid > 0, "pid must be a positive integer");
86
+ } finally {
87
+ rmSync(dir, { recursive: true, force: true });
88
+ }
89
+ });
90
+
91
+ test("observe mode infers state from PTY output via the observer", async () => {
92
+ const messages = [];
93
+ const ESC = String.fromCharCode(27);
94
+
95
+ const result = await runGenericCommand({
96
+ command: "claude",
97
+ args: [],
98
+ cwd: process.cwd(),
99
+ clientId: "claude-code",
100
+ clientDisplayName: "Claude Code",
101
+ sessionId: "sess_obs",
102
+ heartbeatIntervalMs: 1000,
103
+ now: createClock(),
104
+ observe: true,
105
+ send: async (message) => messages.push(message),
106
+ // Injected PTY: emits a tool-use line (with ANSI noise), then exits 0.
107
+ spawnPty: async ({ onData }) => {
108
+ onData(`${ESC}[mRunning tests${ESC}[0m\r\n`);
109
+ return {
110
+ pid: 4321,
111
+ exit: new Promise((resolve) => setTimeout(() => resolve({ exitCode: 0 }), 10)),
112
+ write() {},
113
+ kill() {}
114
+ };
115
+ }
116
+ });
117
+
118
+ assert.equal(result.exitCode, 0);
119
+ assert.equal(result.pid, 4321);
120
+
121
+ const register = messages.find((m) => m.type === "register");
122
+ assert.equal(register.pid, 4321);
123
+
124
+ const inferred = messages.find((m) => m.type === "state" && m.source === "pty_output");
125
+ assert.ok(inferred, "should emit a state inferred from PTY output");
126
+ assert.equal(inferred.state, "running_tool");
127
+
128
+ assert.equal(messages.at(-1).type, "unregister");
129
+ });
130
+
131
+ test("maps successful generic command exit to a success state before unregister", async () => {
132
+ const messages = [];
133
+
134
+ const result = await runGenericCommand({
135
+ command: process.execPath,
136
+ args: ["-e", "process.exit(0)"],
137
+ cwd: process.cwd(),
138
+ sessionId: "sess_success",
139
+ heartbeatIntervalMs: 50,
140
+ now: createClock(),
141
+ stdio: "ignore",
142
+ send: async (message) => {
143
+ messages.push(message);
144
+ }
145
+ });
146
+
147
+ assert.equal(result.exitCode, 0);
148
+
149
+ const finalState = messages.filter((message) => message.type === "state").at(-1);
150
+ assert.equal(finalState.state, "success");
151
+ assert.equal(finalState.summary, "process exited successfully");
152
+ assert.equal(messages.at(-1).type, "unregister");
153
+ });
154
+
155
+ test("maps failed generic command exit to a failed state before unregister", async () => {
156
+ const messages = [];
157
+
158
+ const result = await runGenericCommand({
159
+ command: process.execPath,
160
+ args: ["-e", "process.exit(3)"],
161
+ cwd: process.cwd(),
162
+ sessionId: "sess_failed",
163
+ heartbeatIntervalMs: 50,
164
+ now: createClock(),
165
+ stdio: "ignore",
166
+ send: async (message) => {
167
+ messages.push(message);
168
+ }
169
+ });
170
+
171
+ assert.equal(result.exitCode, 3);
172
+
173
+ const finalState = messages.filter((message) => message.type === "state").at(-1);
174
+ assert.equal(finalState.state, "failed");
175
+ assert.equal(finalState.summary, "process exited with code 3");
176
+ assert.equal(messages.at(-1).exitCode, 3);
177
+ });
@@ -0,0 +1,27 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "../../../test/harness.mjs";
3
+ import { stripAnsi } from "../src/strip-ansi.js";
4
+
5
+ const ESC = String.fromCharCode(27); // 
6
+ const BEL = String.fromCharCode(7); // 
7
+
8
+ test("strips CSI cursor/clear sequences", () => {
9
+ assert.equal(stripAnsi(`${ESC}[2J${ESC}[H${ESC}[mrunning tests\r\n`), "running tests\r\n");
10
+ });
11
+
12
+ test("strips OSC title sequences", () => {
13
+ assert.equal(stripAnsi(`${ESC}]0;C:\\cmd.exe${BEL}plain text`), "plain text");
14
+ });
15
+
16
+ test("strips color codes but keeps the words", () => {
17
+ assert.equal(stripAnsi(`${ESC}[31mError:${ESC}[0m boom`), "Error: boom");
18
+ });
19
+
20
+ test("leaves plain text untouched", () => {
21
+ assert.equal(stripAnsi("waiting for approval"), "waiting for approval");
22
+ });
23
+
24
+ test("handles real ConPTY-style output", () => {
25
+ const raw = `${ESC}[?25l${ESC}[2J${ESC}[mApplying patch to src/index.js\r\n${ESC}]0;cmd${BEL}${ESC}[?25h`;
26
+ assert.ok(stripAnsi(raw).includes("Applying patch to src/index.js"));
27
+ });
@@ -0,0 +1,49 @@
1
+ import { createSessionRegistry } from "../../session-core/src/registry.js";
2
+ import { attachProtocolStream } from "./ipc-transport.js";
3
+
4
+ export function createDaemonRuntime(options = {}) {
5
+ const registry = options.registry ?? createSessionRegistry(options.registryOptions);
6
+ const onSessionChanged = options.onSessionChanged ?? noop;
7
+ const onProtocolError = options.onProtocolError ?? noop;
8
+
9
+ return {
10
+ registry,
11
+
12
+ handleMessage(message) {
13
+ const session = registry.applyMessage(message);
14
+ onSessionChanged(session);
15
+ return session;
16
+ },
17
+
18
+ attachStream(stream) {
19
+ return attachProtocolStream(stream, {
20
+ onMessage: (message) => {
21
+ this.handleMessage(message);
22
+ },
23
+ onError: onProtocolError
24
+ });
25
+ },
26
+
27
+ getSession(sessionId) {
28
+ return registry.getSession(sessionId);
29
+ },
30
+
31
+ listSessions() {
32
+ return registry.listSessions();
33
+ },
34
+
35
+ getPrioritySession(priorityOptions = {}) {
36
+ return registry.getPrioritySession(priorityOptions);
37
+ },
38
+
39
+ markStaleSessions(now) {
40
+ const staleSessions = registry.markStaleSessions(now);
41
+ for (const session of staleSessions) {
42
+ onSessionChanged(session);
43
+ }
44
+ return staleSessions;
45
+ }
46
+ };
47
+ }
48
+
49
+ function noop() {}
@@ -0,0 +1,180 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import net from "node:net";
3
+ import { dirname } from "node:path";
4
+ import { attachProtocolStream, writeProtocolMessage } from "./ipc-transport.js";
5
+
6
+ export async function createIpcServer(options = {}) {
7
+ const {
8
+ endpoint,
9
+ platform = process.platform,
10
+ onMessage,
11
+ onProtocolError = noop
12
+ } = options;
13
+
14
+ if (typeof onMessage !== "function") {
15
+ throw new TypeError("onMessage must be a function");
16
+ }
17
+
18
+ const sockets = new Set();
19
+ const server = net.createServer((socket) => {
20
+ sockets.add(socket);
21
+ socket.on("close", () => sockets.delete(socket));
22
+ attachProtocolStream(socket, {
23
+ onMessage,
24
+ onError: onProtocolError
25
+ });
26
+ });
27
+
28
+ const listenOptions = await resolveListenOptions(endpoint, platform);
29
+ await listen(server, listenOptions);
30
+
31
+ return {
32
+ endpoint: getServerEndpoint(server, listenOptions),
33
+ server,
34
+
35
+ async close() {
36
+ for (const socket of sockets) {
37
+ socket.destroy();
38
+ }
39
+
40
+ await closeServer(server);
41
+ }
42
+ };
43
+ }
44
+
45
+ export async function createIpcClient(options = {}) {
46
+ const { endpoint } = options;
47
+ const socket = net.createConnection(toConnectionOptions(endpoint));
48
+ await waitForConnect(socket);
49
+
50
+ return {
51
+ socket,
52
+
53
+ async send(message) {
54
+ if (!writeProtocolMessage(socket, message)) {
55
+ await once(socket, "drain");
56
+ }
57
+ },
58
+
59
+ async close() {
60
+ if (socket.destroyed) {
61
+ return;
62
+ }
63
+
64
+ socket.end();
65
+ await once(socket, "close");
66
+ }
67
+ };
68
+ }
69
+
70
+ async function resolveListenOptions(endpoint, platform) {
71
+ if (platform === "test") {
72
+ return {
73
+ type: "tcp",
74
+ host: "127.0.0.1",
75
+ port: 0,
76
+ name: endpoint
77
+ };
78
+ }
79
+
80
+ if (typeof endpoint !== "string" || endpoint.trim() === "") {
81
+ throw new Error("endpoint must be a non-empty string");
82
+ }
83
+
84
+ if (!endpoint.startsWith("\\\\.\\")) {
85
+ await mkdir(dirname(endpoint), { recursive: true });
86
+ }
87
+
88
+ return {
89
+ type: "path",
90
+ path: endpoint
91
+ };
92
+ }
93
+
94
+ function getServerEndpoint(server, listenOptions) {
95
+ if (listenOptions.type === "tcp") {
96
+ const address = server.address();
97
+ return {
98
+ type: "tcp",
99
+ host: address.address,
100
+ port: address.port
101
+ };
102
+ }
103
+
104
+ return listenOptions.path;
105
+ }
106
+
107
+ function toConnectionOptions(endpoint) {
108
+ if (typeof endpoint === "string") {
109
+ return { path: endpoint };
110
+ }
111
+
112
+ if (endpoint?.type === "tcp") {
113
+ return {
114
+ host: endpoint.host,
115
+ port: endpoint.port
116
+ };
117
+ }
118
+
119
+ if (Number.isInteger(endpoint?.port)) {
120
+ return endpoint;
121
+ }
122
+
123
+ throw new Error("Unsupported IPC endpoint");
124
+ }
125
+
126
+ function listen(server, listenOptions) {
127
+ return new Promise((resolve, reject) => {
128
+ server.once("error", reject);
129
+ server.listen(toServerListenOptions(listenOptions), () => {
130
+ server.off("error", reject);
131
+ resolve();
132
+ });
133
+ });
134
+ }
135
+
136
+ function toServerListenOptions(listenOptions) {
137
+ if (listenOptions.type === "tcp") {
138
+ return {
139
+ host: listenOptions.host,
140
+ port: listenOptions.port
141
+ };
142
+ }
143
+
144
+ return listenOptions.path;
145
+ }
146
+
147
+ function waitForConnect(socket) {
148
+ return new Promise((resolve, reject) => {
149
+ socket.once("error", reject);
150
+ socket.once("connect", () => {
151
+ socket.off("error", reject);
152
+ resolve();
153
+ });
154
+ });
155
+ }
156
+
157
+ function closeServer(server) {
158
+ return new Promise((resolve, reject) => {
159
+ if (!server.listening) {
160
+ resolve();
161
+ return;
162
+ }
163
+
164
+ server.close((error) => {
165
+ if (error) {
166
+ reject(error);
167
+ } else {
168
+ resolve();
169
+ }
170
+ });
171
+ });
172
+ }
173
+
174
+ function once(emitter, eventName) {
175
+ return new Promise((resolve) => {
176
+ emitter.once(eventName, resolve);
177
+ });
178
+ }
179
+
180
+ function noop() {}
@@ -0,0 +1,70 @@
1
+ import { assertProtocolMessage } from "../../protocol/src/messages.js";
2
+
3
+ export function encodeProtocolMessage(message) {
4
+ assertProtocolMessage(message);
5
+ return `${JSON.stringify(message)}\n`;
6
+ }
7
+
8
+ export function writeProtocolMessage(writable, message) {
9
+ return writable.write(encodeProtocolMessage(message));
10
+ }
11
+
12
+ export function createProtocolMessageReader({ onMessage, onError = defaultErrorHandler } = {}) {
13
+ if (typeof onMessage !== "function") {
14
+ throw new TypeError("onMessage must be a function");
15
+ }
16
+
17
+ let buffer = "";
18
+
19
+ return {
20
+ push(chunk) {
21
+ buffer += chunk.toString("utf8");
22
+
23
+ let newlineIndex = buffer.indexOf("\n");
24
+ while (newlineIndex !== -1) {
25
+ const line = buffer.slice(0, newlineIndex);
26
+ buffer = buffer.slice(newlineIndex + 1);
27
+ readFrame(line, onMessage, onError);
28
+ newlineIndex = buffer.indexOf("\n");
29
+ }
30
+ },
31
+
32
+ flush() {
33
+ if (buffer.trim() !== "") {
34
+ readFrame(buffer, onMessage, onError);
35
+ }
36
+ buffer = "";
37
+ }
38
+ };
39
+ }
40
+
41
+ export function attachProtocolStream(readable, options) {
42
+ const reader = createProtocolMessageReader(options);
43
+ readable.on("data", (chunk) => reader.push(chunk));
44
+ readable.on("end", () => reader.flush());
45
+ return reader;
46
+ }
47
+
48
+ function readFrame(line, onMessage, onError) {
49
+ if (line.trim() === "") {
50
+ return;
51
+ }
52
+
53
+ let message;
54
+ try {
55
+ message = JSON.parse(line);
56
+ } catch (error) {
57
+ onError(new Error(`Invalid protocol JSON frame: ${error.message}`));
58
+ return;
59
+ }
60
+
61
+ try {
62
+ onMessage(assertProtocolMessage(message));
63
+ } catch (error) {
64
+ onError(error);
65
+ }
66
+ }
67
+
68
+ function defaultErrorHandler(error) {
69
+ throw error;
70
+ }
@@ -0,0 +1,46 @@
1
+ // Daemon singleton enforcement helpers (product plan section 39). The pure
2
+ // decision logic here lets the Electron main process and tests share the same
3
+ // stale-lock detection without touching the filesystem.
4
+
5
+ export function serializeLock({ pid, startedAt, endpoint }) {
6
+ return `${JSON.stringify({ pid, startedAt, endpoint })}\n`;
7
+ }
8
+
9
+ export function parseLock(text) {
10
+ let parsed;
11
+ try {
12
+ parsed = JSON.parse(text);
13
+ } catch {
14
+ return undefined;
15
+ }
16
+
17
+ if (!isPlainObject(parsed)) {
18
+ return undefined;
19
+ }
20
+
21
+ if (!Number.isInteger(parsed.pid) || parsed.pid <= 0) {
22
+ return undefined;
23
+ }
24
+
25
+ if (!Number.isFinite(parsed.startedAt) || typeof parsed.endpoint !== "string") {
26
+ return undefined;
27
+ }
28
+
29
+ return { pid: parsed.pid, startedAt: parsed.startedAt, endpoint: parsed.endpoint };
30
+ }
31
+
32
+ export function resolveSingletonAction({ lock, isAlive }) {
33
+ if (typeof isAlive !== "function") {
34
+ throw new TypeError("isAlive must be a function");
35
+ }
36
+
37
+ if (!lock) {
38
+ return "acquire";
39
+ }
40
+
41
+ return isAlive(lock.pid) ? "forward" : "reclaim";
42
+ }
43
+
44
+ function isPlainObject(value) {
45
+ return typeof value === "object" && value !== null && !Array.isArray(value);
46
+ }