@botcord/daemon 0.2.80 → 0.2.82

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.
@@ -37,3 +37,4 @@ export declare function writeCurrentPid(opts?: {
37
37
  currentPid?: number;
38
38
  }): void;
39
39
  export declare function removePidFile(pidPath?: string): void;
40
+ export declare function isBotCordDaemonStartCommand(command: string): boolean;
@@ -147,12 +147,30 @@ export function removePidFile(pidPath = PID_PATH) {
147
147
  // ignore
148
148
  }
149
149
  }
150
- function isBotCordDaemonStartCommand(command) {
150
+ export function isBotCordDaemonStartCommand(command) {
151
151
  if (!/\bstart\b/.test(command))
152
152
  return false;
153
- return (command.includes("botcord-daemon") ||
154
- /(?:^|\s)\S*botcord\S*\/daemon\/dist\/index\.js(?:\s|$)/.test(command) ||
155
- /(?:^|\s)\S*packages\/daemon\/dist\/index\.js(?:\s|$)/.test(command));
153
+ // Only treat a row as "the daemon" when argv[0] is the real entry point:
154
+ // either the node interpreter (running `dist/index.js`) or the resolved
155
+ // `botcord-daemon` bin shim. Reject shell wrappers (`sh`, `bash`, `npm`,
156
+ // `npx`, `timeout`, ...) — their argv mentions `botcord-daemon` only as a
157
+ // literal arg, not as the executable being run. Killing a wrapper takes
158
+ // out the actual daemon it started, which is exactly the bug we want
159
+ // to avoid in cloud sandboxes.
160
+ const exe = (command.trim().split(/\s+/, 1)[0] ?? "").split("/").pop() ?? "";
161
+ const isNode = /^node(\d.*)?$/.test(exe);
162
+ const isDaemonBin = exe === "botcord-daemon";
163
+ if (!isNode && !isDaemonBin)
164
+ return false;
165
+ return (
166
+ // node-resolved bin shim, e.g. .../node_modules/.bin/botcord-daemon
167
+ /\/\.?bin\/botcord-daemon(?:\s|$)/.test(command) ||
168
+ // direct bin invocation, e.g. /usr/local/bin/botcord-daemon or argv[0]=botcord-daemon
169
+ /(?:^|\s)\S*botcord-daemon(?:\s|$)/.test(command) ||
170
+ // node running the published daemon entry script
171
+ /\bbotcord\S*\/daemon\/dist\/index\.js(?:\s|$)/.test(command) ||
172
+ // node running the in-repo daemon entry script (dev / monorepo)
173
+ /\bpackages\/daemon\/dist\/index\.js(?:\s|$)/.test(command));
156
174
  }
157
175
  function delay(ms) {
158
176
  return new Promise((resolve) => setTimeout(resolve, ms));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botcord/daemon",
3
- "version": "0.2.80",
3
+ "version": "0.2.82",
4
4
  "description": "BotCord local daemon — bridges Hub inbox push to local Claude Code / Codex / Gemini CLIs",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@botcord/cli": "^0.1.7",
31
- "@botcord/protocol-core": "^0.2.10",
31
+ "@botcord/protocol-core": "^0.2.11",
32
32
  "@larksuiteoapi/node-sdk": "^1.63.1",
33
33
  "ws": "^8.20.1"
34
34
  },
@@ -5,6 +5,7 @@ import path from "node:path";
5
5
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
6
6
  import {
7
7
  ensureNoOtherDaemonFromPidFile,
8
+ isBotCordDaemonStartCommand,
8
9
  parseDaemonProcesses,
9
10
  readPid,
10
11
  removePidFile,
@@ -90,6 +91,38 @@ describe("daemon singleton pid helpers", () => {
90
91
  expect(out.map((p) => p.pid)).toEqual([111, 222]);
91
92
  });
92
93
 
94
+ it("does not match shell wrappers whose argv mentions botcord-daemon as a literal", () => {
95
+ // These are the wrapper command lines we observed in cloud sandboxes;
96
+ // they must NOT be classified as the daemon, otherwise the singleton
97
+ // check kills the wrapper and takes the actual daemon down with it.
98
+ const wrappers = [
99
+ "npm exec --yes --package @botcord/daemon@latest -- botcord-daemon start --foreground",
100
+ "npx --yes --package @botcord/daemon@latest -- botcord-daemon start --foreground",
101
+ "sh -c botcord-daemon start --foreground",
102
+ "/bin/bash -l -c export npm_config_cache=/tmp/c; npm exec --yes --package @botcord/daemon@latest -- botcord-daemon start --foreground",
103
+ "timeout 30 npm exec --yes --package @botcord/daemon@latest -- botcord-daemon start --foreground",
104
+ ];
105
+ for (const cmd of wrappers) {
106
+ expect(isBotCordDaemonStartCommand(cmd), `wrongly matched wrapper: ${cmd}`).toBe(false);
107
+ }
108
+ });
109
+
110
+ it("matches the actual daemon entry processes", () => {
111
+ const matches = [
112
+ // node running the published daemon (npx / npm exec resolution under _npx)
113
+ "node /tmp/botcord-npm-cache/_npx/abc123/node_modules/@botcord/daemon/dist/index.js start --foreground",
114
+ // node running the resolved bin shim
115
+ "node /Users/me/.botcord/daemon/node_modules/.bin/botcord-daemon start --foreground",
116
+ // direct invocation of the bin
117
+ "/usr/local/bin/botcord-daemon start --foreground",
118
+ // monorepo dev: node running packages/daemon/dist/index.js
119
+ "node /home/dev/botcord/packages/daemon/dist/index.js start --foreground",
120
+ ];
121
+ for (const cmd of matches) {
122
+ expect(isBotCordDaemonStartCommand(cmd), `failed to match daemon: ${cmd}`).toBe(true);
123
+ }
124
+ });
125
+
93
126
  it("terminates extra daemon processes discovered outside the pid file", async () => {
94
127
  const child = spawn(process.execPath, ["-e", "setInterval(() => {}, 1000)"], {
95
128
  stdio: "ignore",
@@ -193,12 +193,28 @@ export function removePidFile(pidPath = PID_PATH): void {
193
193
  }
194
194
  }
195
195
 
196
- function isBotCordDaemonStartCommand(command: string): boolean {
196
+ export function isBotCordDaemonStartCommand(command: string): boolean {
197
197
  if (!/\bstart\b/.test(command)) return false;
198
+ // Only treat a row as "the daemon" when argv[0] is the real entry point:
199
+ // either the node interpreter (running `dist/index.js`) or the resolved
200
+ // `botcord-daemon` bin shim. Reject shell wrappers (`sh`, `bash`, `npm`,
201
+ // `npx`, `timeout`, ...) — their argv mentions `botcord-daemon` only as a
202
+ // literal arg, not as the executable being run. Killing a wrapper takes
203
+ // out the actual daemon it started, which is exactly the bug we want
204
+ // to avoid in cloud sandboxes.
205
+ const exe = (command.trim().split(/\s+/, 1)[0] ?? "").split("/").pop() ?? "";
206
+ const isNode = /^node(\d.*)?$/.test(exe);
207
+ const isDaemonBin = exe === "botcord-daemon";
208
+ if (!isNode && !isDaemonBin) return false;
198
209
  return (
199
- command.includes("botcord-daemon") ||
200
- /(?:^|\s)\S*botcord\S*\/daemon\/dist\/index\.js(?:\s|$)/.test(command) ||
201
- /(?:^|\s)\S*packages\/daemon\/dist\/index\.js(?:\s|$)/.test(command)
210
+ // node-resolved bin shim, e.g. .../node_modules/.bin/botcord-daemon
211
+ /\/\.?bin\/botcord-daemon(?:\s|$)/.test(command) ||
212
+ // direct bin invocation, e.g. /usr/local/bin/botcord-daemon or argv[0]=botcord-daemon
213
+ /(?:^|\s)\S*botcord-daemon(?:\s|$)/.test(command) ||
214
+ // node running the published daemon entry script
215
+ /\bbotcord\S*\/daemon\/dist\/index\.js(?:\s|$)/.test(command) ||
216
+ // node running the in-repo daemon entry script (dev / monorepo)
217
+ /\bpackages\/daemon\/dist\/index\.js(?:\s|$)/.test(command)
202
218
  );
203
219
  }
204
220