@dylanpiercey/work 0.0.12 → 0.0.13

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 (2) hide show
  1. package/bin/work.mjs +107 -0
  2. package/package.json +1 -1
package/bin/work.mjs CHANGED
@@ -208,7 +208,114 @@ function serviceEnv() {
208
208
  return env;
209
209
  }
210
210
 
211
+ /** True when `cmd` is a work *server* (not `work setup` / `work update`). */
212
+ function isWorkServerCmd(cmd) {
213
+ if (/\bwork(\.mjs)?\s+(setup|update|version)\b/.test(cmd)) return false;
214
+ return (
215
+ /\bwork\.mjs\s+start\b/.test(cmd) ||
216
+ /\bwork\s+start\b/.test(cmd) ||
217
+ /run-prod\.sh/.test(cmd) ||
218
+ /work server is running/.test(cmd) ||
219
+ (/@dylanpiercey\/work\//.test(cmd) && !/\bnpm\b/.test(cmd)) ||
220
+ /\/work\/\S*\/dist\/index\.mjs/.test(cmd)
221
+ );
222
+ }
223
+
224
+ function pidsListening(port) {
225
+ const lsof = spawnSync(
226
+ "lsof",
227
+ ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"],
228
+ { encoding: "utf8" },
229
+ );
230
+ if (lsof.stdout) {
231
+ return [
232
+ ...new Set(
233
+ lsof.stdout
234
+ .trim()
235
+ .split(/\s+/)
236
+ .map(Number)
237
+ .filter((n) => n > 1 && n !== process.pid),
238
+ ),
239
+ ];
240
+ }
241
+ const ss = spawnSync("ss", ["-ltnp", `sport = :${port}`], { encoding: "utf8" });
242
+ const pids = [];
243
+ for (const m of (ss.stdout || "").matchAll(/pid=(\d+)/g)) {
244
+ const pid = Number(m[1]);
245
+ if (pid > 1 && pid !== process.pid) pids.push(pid);
246
+ }
247
+ return [...new Set(pids)];
248
+ }
249
+
250
+ function workServerPids() {
251
+ const out = spawnSync("ps", ["-ax", "-o", "pid=,command="], {
252
+ encoding: "utf8",
253
+ });
254
+ const pids = [];
255
+ for (const line of (out.stdout || "").split("\n")) {
256
+ const m = line.trim().match(/^(\d+)\s+(.*)$/);
257
+ if (!m) continue;
258
+ const pid = Number(m[1]);
259
+ if (pid === process.pid || pid === process.ppid) continue;
260
+ if (isWorkServerCmd(m[2])) pids.push(pid);
261
+ }
262
+ return pids;
263
+ }
264
+
265
+ function killPids(pids, signal) {
266
+ for (const pid of pids) {
267
+ try {
268
+ process.kill(pid, signal);
269
+ } catch {}
270
+ }
271
+ }
272
+
273
+ /** Stop the managed service *and* any leftover checkout / `work start`
274
+ * process still bound to the port. A pre-npm install often wasn't the
275
+ * current unit (or was a bare node), so restarting work.service left it
276
+ * sitting in front of the new one. */
277
+ function stopOldCopies(port) {
278
+ log("stopping any previous work server");
279
+ if (process.platform === "darwin") {
280
+ const uid = process.getuid();
281
+ spawnSync("launchctl", ["bootout", `gui/${uid}/dev.work.app`], {
282
+ stdio: "ignore",
283
+ });
284
+ const agents = path.join(os.homedir(), "Library/LaunchAgents");
285
+ try {
286
+ for (const name of fs.readdirSync(agents)) {
287
+ if (!name.endsWith(".plist")) continue;
288
+ const file = path.join(agents, name);
289
+ const text = fs.readFileSync(file, "utf8");
290
+ if (!/dev\.work\.app|work\.mjs|run-prod\.sh|@dylanpiercey\/work/.test(text))
291
+ continue;
292
+ const label = text.match(
293
+ /<key>Label<\/key>\s*<string>([^<]+)<\/string>/,
294
+ )?.[1];
295
+ if (label) {
296
+ spawnSync("launchctl", ["bootout", `gui/${uid}/${label}`], {
297
+ stdio: "ignore",
298
+ });
299
+ }
300
+ }
301
+ } catch {}
302
+ } else if (have("systemctl")) {
303
+ spawnSync("systemctl", ["--user", "stop", "work.service"], {
304
+ stdio: "ignore",
305
+ });
306
+ }
307
+ const seen = new Set([...pidsListening(port), ...workServerPids()]);
308
+ if (seen.size) {
309
+ log(`stopping ${seen.size} leftover work process(es)`);
310
+ killPids(seen, "SIGTERM");
311
+ spawnSync("sleep", ["0.4"], { stdio: "ignore" });
312
+ }
313
+ killPids(pidsListening(port), "SIGKILL");
314
+ }
315
+
211
316
  function installService() {
317
+ const port = serviceEnv().PORT;
318
+ stopOldCopies(port);
212
319
  const cliPath = fileURLToPath(import.meta.url);
213
320
  if (process.platform === "darwin") {
214
321
  const plist = serviceFile();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dylanpiercey/work",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "type": "module",
5
5
  "description": "Web GUI for agentic dev workspaces",
6
6
  "license": "MIT",