kablan 0.9.0 → 0.9.2

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/bin/cli.js CHANGED
@@ -148,10 +148,12 @@ async function main() {
148
148
  const args = process.argv.slice(2);
149
149
  const isMcpMode = args.includes("--mcp");
150
150
 
151
- // `--install` puts a double-clickable app in ~/Applications and exits; it never starts a
152
- // server itself, so re-running it to update is safe while Kablan is open.
151
+ // `--install` puts a double-clickable app in ~/Applications and exits. It stops a running
152
+ // Kablan first: the swap would otherwise leave the old process serving, and the update would
153
+ // look like it had not happened.
153
154
  if (args.includes("--uninstall")) {
154
- const removed = uninstallMacApp();
155
+ const { app: removed, stoppedPid } = uninstallMacApp();
156
+ if (stoppedPid) console.log(`Stopped the running Kablan (pid ${stoppedPid})`);
155
157
  console.log(removed ? `Removed ${removed}` : `Nothing installed at ${appDir()}`);
156
158
  console.log("Cached binaries are still in ~/.kablan/bin; delete that to reclaim the space.");
157
159
  return;
@@ -159,7 +161,10 @@ async function main() {
159
161
 
160
162
  if (args.includes("--install")) {
161
163
  await extractAndRun("kablan", (bin) => {
162
- const app = installMacApp(bin, CLI_VERSION);
164
+ const { app, stoppedPid } = installMacApp(bin, CLI_VERSION);
165
+ if (stoppedPid) {
166
+ console.log(`\nStopped the running Kablan (pid ${stoppedPid}) so this version replaces it`);
167
+ }
163
168
  console.log(`\nInstalled ${app}`);
164
169
  console.log("Open it from Launchpad or Spotlight — it starts Kablan in the background");
165
170
  console.log("and opens your browser. Logs: ~/Library/Logs/Kablan/kablan.log");
@@ -111,9 +111,92 @@ function infoPlist(version) {
111
111
  `;
112
112
  }
113
113
 
114
+
115
+ /**
116
+ * Stop a Kablan that is already running, and say whether one was.
117
+ *
118
+ * Replacing the bundle under a live process does nothing to that process: it keeps serving, the
119
+ * launcher finds its port file and simply opens a browser at it, and the update looks like it
120
+ * failed — the app still reports the version it was started with. So the install stops it first.
121
+ *
122
+ * The port file is the only handle we have on it. Whoever is listening on that port has to look
123
+ * like Kablan before it is signalled, because a stale port file plus a recycled port would
124
+ * otherwise point at some unrelated process. SIGTERM first so the server can close its database
125
+ * cleanly; SIGKILL only if it is still there after a grace period.
126
+ *
127
+ * @returns the pid stopped, or null when nothing was running
128
+ */
129
+ function stopRunningApp() {
130
+ const bin = APP_NAME.toLowerCase();
131
+ const tmp = process.env.TMPDIR || "/tmp";
132
+ const portFile = path.join(tmp, bin, `${bin}.port`);
133
+
134
+ let port;
135
+ try {
136
+ port = fs.readFileSync(portFile, "utf8").trim();
137
+ } catch {
138
+ return null;
139
+ }
140
+ if (!/^\d+$/.test(port)) return null;
141
+
142
+ let pids;
143
+ try {
144
+ pids = execFileSync("/usr/sbin/lsof", ["-ti", `tcp:${port}`, "-sTCP:LISTEN"], {
145
+ encoding: "utf8",
146
+ stdio: ["ignore", "pipe", "ignore"],
147
+ })
148
+ .split("\n")
149
+ .map((line) => line.trim())
150
+ .filter(Boolean);
151
+ } catch {
152
+ // Nothing listening: the port file outlived the process it described.
153
+ return null;
154
+ }
155
+
156
+ for (const pid of pids) {
157
+ let command = "";
158
+ try {
159
+ command = execFileSync("/bin/ps", ["-o", "command=", "-p", pid], {
160
+ encoding: "utf8",
161
+ stdio: ["ignore", "pipe", "ignore"],
162
+ }).trim();
163
+ } catch {
164
+ continue;
165
+ }
166
+ // Ours: either a cached release binary or the one inside the bundle.
167
+ if (!command.includes(`.${bin}/bin/`) && !command.includes(`${bin}-server`)) continue;
168
+
169
+ try {
170
+ process.kill(Number(pid), "SIGTERM");
171
+ } catch {
172
+ continue;
173
+ }
174
+
175
+ // Give it a moment to close its database before resorting to SIGKILL.
176
+ const deadline = Date.now() + 5000;
177
+ while (Date.now() < deadline) {
178
+ try {
179
+ process.kill(Number(pid), 0);
180
+ } catch {
181
+ return pid; // gone
182
+ }
183
+ execFileSync("/bin/sleep", ["0.2"]);
184
+ }
185
+ try {
186
+ process.kill(Number(pid), "SIGKILL");
187
+ } catch {
188
+ // Raced us to exit.
189
+ }
190
+ return pid;
191
+ }
192
+
193
+ return null;
194
+ }
195
+
114
196
  /**
115
197
  * @param binPath the extracted server binary to install
116
198
  * @param version the wrapper's version, shown in Finder's Get Info
199
+ * @returns {{ app: string, stoppedPid: string | null }}
117
200
  */
118
201
  function installMacApp(binPath, version) {
119
202
  if (process.platform !== "darwin") {
@@ -123,6 +206,10 @@ function installMacApp(binPath, version) {
123
206
  );
124
207
  }
125
208
 
209
+ // Before anything is replaced: a running copy would survive the swap and keep serving the
210
+ // version it started with.
211
+ const stoppedPid = stopRunningApp();
212
+
126
213
  const app = appDir();
127
214
  const macos = path.join(app, "Contents", "MacOS");
128
215
  const resources = path.join(app, "Contents", "Resources");
@@ -160,14 +247,16 @@ function installMacApp(binPath, version) {
160
247
  // Cosmetic only — the app works either way.
161
248
  }
162
249
 
163
- return app;
250
+ return { app, stoppedPid };
164
251
  }
165
252
 
166
253
  function uninstallMacApp() {
254
+ // Removing the bundle from under a running server leaves it running with no way back to it.
255
+ const stoppedPid = stopRunningApp();
167
256
  const app = appDir();
168
- if (!fs.existsSync(app)) return null;
257
+ if (!fs.existsSync(app)) return { app: null, stoppedPid };
169
258
  fs.rmSync(app, { recursive: true, force: true });
170
- return app;
259
+ return { app, stoppedPid };
171
260
  }
172
261
 
173
- module.exports = { installMacApp, uninstallMacApp, appDir, APP_NAME };
262
+ module.exports = { installMacApp, uninstallMacApp, stopRunningApp, appDir, APP_NAME };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kablan",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "NPX wrapper around the Kablan server and its MCP task server",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Shaked Amar",