seshmux 0.1.3 → 0.1.4

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.
@@ -4710,11 +4710,143 @@ var init_search2 = __esm({
4710
4710
  }
4711
4711
  });
4712
4712
 
4713
+ // server/lib/update.ts
4714
+ function compareVersions(a, b) {
4715
+ const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
4716
+ const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
4717
+ const len = Math.max(pa.length, pb.length);
4718
+ for (let i = 0; i < len; i++) {
4719
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
4720
+ if (d !== 0) return d < 0 ? -1 : 1;
4721
+ }
4722
+ return 0;
4723
+ }
4724
+ function isDaemonStale(daemonVersion, serverVersion) {
4725
+ if (!daemonVersion || !serverVersion) return false;
4726
+ return compareVersions(serverVersion, daemonVersion) > 0;
4727
+ }
4728
+ function detectInstallMethod(opts) {
4729
+ const p = opts.argvRealPath;
4730
+ if (p.includes("/_npx/") || p.includes("\\_npx\\")) return "npx";
4731
+ const prefix = realpath2(opts.globalPrefix);
4732
+ if (prefix && realpath2(p).startsWith(prefix)) return "global";
4733
+ return "local";
4734
+ }
4735
+ function realpath2(p) {
4736
+ if (!p) return "";
4737
+ try {
4738
+ return (0, import_node_fs9.realpathSync)(p);
4739
+ } catch {
4740
+ return p;
4741
+ }
4742
+ }
4743
+ function resolveArgvRealPath() {
4744
+ const raw = process.argv[1] ?? "";
4745
+ try {
4746
+ return (0, import_node_fs9.realpathSync)(raw);
4747
+ } catch {
4748
+ return raw;
4749
+ }
4750
+ }
4751
+ function defaultExec(cmd, args) {
4752
+ return new Promise((resolve, reject) => {
4753
+ (0, import_node_child_process7.execFile)(cmd, args, { timeout: 12e4, maxBuffer: 8 * 1024 * 1024 }, (err, stdout, stderr) => {
4754
+ if (err) reject(Object.assign(err, { stdout, stderr }));
4755
+ else resolve({ stdout, stderr });
4756
+ });
4757
+ });
4758
+ }
4759
+ async function defaultGlobalPrefix(exec) {
4760
+ try {
4761
+ const { stdout } = await exec("npm", ["prefix", "-g"]);
4762
+ return stdout.trim();
4763
+ } catch {
4764
+ return "";
4765
+ }
4766
+ }
4767
+ async function checkUpdate(deps = {}) {
4768
+ const current = deps.current ?? process.env.SESHMUX_VERSION ?? process.env.npm_package_version ?? "0.0.0";
4769
+ const fetchFn = deps.fetchFn ?? defaultFetch;
4770
+ const exec = deps.exec ?? defaultExec;
4771
+ const argvRealPath = deps.argvRealPath ?? resolveArgvRealPath();
4772
+ const globalPrefix = deps.globalPrefix ?? await defaultGlobalPrefix(exec);
4773
+ const installMethod = detectInstallMethod({ argvRealPath, globalPrefix });
4774
+ let latest = current;
4775
+ if (cache && Date.now() - cache.ts < CACHE_TTL_MS) {
4776
+ latest = cache.latest;
4777
+ } else {
4778
+ latest = await fetchLatest(fetchFn, current);
4779
+ cache = { ts: Date.now(), latest };
4780
+ }
4781
+ return {
4782
+ current,
4783
+ latest,
4784
+ updateAvailable: compareVersions(latest, current) > 0,
4785
+ installMethod
4786
+ };
4787
+ }
4788
+ async function fetchLatest(fetchFn, current) {
4789
+ const controller = new AbortController();
4790
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
4791
+ try {
4792
+ const res = await fetchFn(REGISTRY_URL, { signal: controller.signal });
4793
+ if (!res.ok) return current;
4794
+ const body = await res.json();
4795
+ return typeof body.version === "string" ? body.version : current;
4796
+ } catch {
4797
+ return current;
4798
+ } finally {
4799
+ clearTimeout(timer);
4800
+ }
4801
+ }
4802
+ async function applyUpdate(deps) {
4803
+ if (deps.installMethod === "npx") {
4804
+ throw new Error("cannot self-update an npx invocation \u2014 run `npx seshmux@latest` next time");
4805
+ }
4806
+ const exec = deps.exec ?? defaultExec;
4807
+ const previous = deps.current;
4808
+ const version2 = /^\d+\.\d+\.\d+[A-Za-z0-9.\-+]*$/.test(deps.target ?? "") ? deps.target : null;
4809
+ const spec = `seshmux@${version2 ?? "latest"}`;
4810
+ try {
4811
+ const { stdout, stderr } = await exec("npm", ["i", "-g", spec, "--prefer-online"]);
4812
+ return { ok: true, log: [stdout, stderr].filter(Boolean).join("\n"), previous };
4813
+ } catch (e) {
4814
+ const err = e;
4815
+ const log = [err.stdout, err.stderr, err.message].filter(Boolean).join("\n");
4816
+ return { ok: false, log, previous };
4817
+ }
4818
+ }
4819
+ var import_node_child_process7, import_node_fs9, REGISTRY_URL, FETCH_TIMEOUT_MS, CACHE_TTL_MS, defaultFetch, cache;
4820
+ var init_update = __esm({
4821
+ "server/lib/update.ts"() {
4822
+ "use strict";
4823
+ import_node_child_process7 = require("node:child_process");
4824
+ import_node_fs9 = require("node:fs");
4825
+ REGISTRY_URL = "https://registry.npmjs.org/seshmux/latest";
4826
+ FETCH_TIMEOUT_MS = 5e3;
4827
+ CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
4828
+ defaultFetch = (url, init) => fetch(url, init);
4829
+ cache = null;
4830
+ }
4831
+ });
4832
+
4713
4833
  // server/routes/env.ts
4714
4834
  var env_exports = {};
4715
4835
  __export(env_exports, {
4716
4836
  default: () => envRoutes
4717
4837
  });
4838
+ async function realDaemonVersion() {
4839
+ let conn = null;
4840
+ try {
4841
+ conn = await dial();
4842
+ const { version: version2 } = await conn.hello();
4843
+ return version2 || null;
4844
+ } catch {
4845
+ return null;
4846
+ } finally {
4847
+ conn?.close();
4848
+ }
4849
+ }
4718
4850
  function commandPreview(commands) {
4719
4851
  return {
4720
4852
  fresh: commands.fresh("").join(" "),
@@ -4725,12 +4857,16 @@ function commandPreview(commands) {
4725
4857
  }
4726
4858
  async function envRoutes(f, deps = {}) {
4727
4859
  const bridgeStatus2 = deps.bridgeStatus ?? (() => bridgeStatus());
4860
+ const daemonVersion = deps.daemonVersion ?? realDaemonVersion;
4861
+ const serverVersion = deps.serverVersion ?? (() => process.env.SESHMUX_VERSION ?? "");
4728
4862
  f.get("/api/env", async () => {
4729
- const [env, bridge, providers] = await Promise.all([
4863
+ const [env, bridge, providers, dVersion] = await Promise.all([
4730
4864
  detectEnv(),
4731
4865
  bridgeStatus2().catch(() => ({ claude: false, codex: false })),
4732
- getProviders()
4866
+ getProviders(),
4867
+ daemonVersion().catch(() => null)
4733
4868
  ]);
4869
+ const sVersion = serverVersion();
4734
4870
  const commands = {};
4735
4871
  const teams = {};
4736
4872
  for (const p of providers) {
@@ -4744,7 +4880,14 @@ async function envRoutes(f, deps = {}) {
4744
4880
  codex: { registered: bridge.codex }
4745
4881
  },
4746
4882
  commands,
4747
- teams
4883
+ teams,
4884
+ // The daemon outlives server updates by design — tell the UI when it's older than us
4885
+ // so the user can be nudged to fully restart. Never auto-restart: that kills live PTYs.
4886
+ daemon: {
4887
+ version: dVersion,
4888
+ serverVersion: sVersion || null,
4889
+ stale: isDaemonStale(dVersion, sVersion)
4890
+ }
4748
4891
  };
4749
4892
  });
4750
4893
  }
@@ -4754,6 +4897,8 @@ var init_env = __esm({
4754
4897
  init_detect();
4755
4898
  init_registry();
4756
4899
  init_types();
4900
+ init_daemon_client();
4901
+ init_update();
4757
4902
  }
4758
4903
  });
4759
4904
 
@@ -5016,10 +5161,11 @@ async function termRoutes(f, deps = {}) {
5016
5161
  try {
5017
5162
  conn = await (deps.dialFn ?? dial)();
5018
5163
  const { data } = await withTimeout(conn.history(req.params.ptyId, lines), 5e3, "history timed out");
5019
- return reply.send({ data });
5164
+ return reply.send({ supported: true, data });
5020
5165
  } catch (e) {
5021
5166
  const msg = e.message || String(e);
5022
- return reply.code(msg.includes("unknown method") ? 501 : 500).send({ error: msg });
5167
+ if (msg.includes("unknown method")) return reply.send({ supported: false, data: "" });
5168
+ return reply.code(500).send({ error: msg });
5023
5169
  } finally {
5024
5170
  conn?.close();
5025
5171
  }
@@ -5602,7 +5748,7 @@ async function notifyRoutes(f) {
5602
5748
  return { ok: true, delivered: false, reason: "disabled" };
5603
5749
  }
5604
5750
  await new Promise((resolve) => {
5605
- const child = (0, import_node_child_process7.execFile)(
5751
+ const child = (0, import_node_child_process8.execFile)(
5606
5752
  "osascript",
5607
5753
  [
5608
5754
  "-e",
@@ -5620,11 +5766,11 @@ async function notifyRoutes(f) {
5620
5766
  return { ok: true, delivered: true };
5621
5767
  });
5622
5768
  }
5623
- var import_node_child_process7, import_promises18, import_node_path22, import_node_os10;
5769
+ var import_node_child_process8, import_promises18, import_node_path22, import_node_os10;
5624
5770
  var init_notify = __esm({
5625
5771
  "server/routes/notify.ts"() {
5626
5772
  "use strict";
5627
- import_node_child_process7 = require("node:child_process");
5773
+ import_node_child_process8 = require("node:child_process");
5628
5774
  import_promises18 = require("node:fs/promises");
5629
5775
  import_node_path22 = __toESM(require("node:path"));
5630
5776
  import_node_os10 = __toESM(require("node:os"));
@@ -5656,122 +5802,6 @@ var init_approval = __esm({
5656
5802
  }
5657
5803
  });
5658
5804
 
5659
- // server/lib/update.ts
5660
- function compareVersions(a, b) {
5661
- const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
5662
- const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
5663
- const len = Math.max(pa.length, pb.length);
5664
- for (let i = 0; i < len; i++) {
5665
- const d = (pa[i] ?? 0) - (pb[i] ?? 0);
5666
- if (d !== 0) return d < 0 ? -1 : 1;
5667
- }
5668
- return 0;
5669
- }
5670
- function detectInstallMethod(opts) {
5671
- const p = opts.argvRealPath;
5672
- if (p.includes("/_npx/") || p.includes("\\_npx\\")) return "npx";
5673
- const prefix = realpath2(opts.globalPrefix);
5674
- if (prefix && realpath2(p).startsWith(prefix)) return "global";
5675
- return "local";
5676
- }
5677
- function realpath2(p) {
5678
- if (!p) return "";
5679
- try {
5680
- return (0, import_node_fs9.realpathSync)(p);
5681
- } catch {
5682
- return p;
5683
- }
5684
- }
5685
- function resolveArgvRealPath() {
5686
- const raw = process.argv[1] ?? "";
5687
- try {
5688
- return (0, import_node_fs9.realpathSync)(raw);
5689
- } catch {
5690
- return raw;
5691
- }
5692
- }
5693
- function defaultExec(cmd, args) {
5694
- return new Promise((resolve, reject) => {
5695
- (0, import_node_child_process8.execFile)(cmd, args, { timeout: 12e4, maxBuffer: 8 * 1024 * 1024 }, (err, stdout, stderr) => {
5696
- if (err) reject(Object.assign(err, { stdout, stderr }));
5697
- else resolve({ stdout, stderr });
5698
- });
5699
- });
5700
- }
5701
- async function defaultGlobalPrefix(exec) {
5702
- try {
5703
- const { stdout } = await exec("npm", ["prefix", "-g"]);
5704
- return stdout.trim();
5705
- } catch {
5706
- return "";
5707
- }
5708
- }
5709
- async function checkUpdate(deps = {}) {
5710
- const current = deps.current ?? process.env.SESHMUX_VERSION ?? process.env.npm_package_version ?? "0.0.0";
5711
- const fetchFn = deps.fetchFn ?? defaultFetch;
5712
- const exec = deps.exec ?? defaultExec;
5713
- const argvRealPath = deps.argvRealPath ?? resolveArgvRealPath();
5714
- const globalPrefix = deps.globalPrefix ?? await defaultGlobalPrefix(exec);
5715
- const installMethod = detectInstallMethod({ argvRealPath, globalPrefix });
5716
- let latest = current;
5717
- if (cache && Date.now() - cache.ts < CACHE_TTL_MS) {
5718
- latest = cache.latest;
5719
- } else {
5720
- latest = await fetchLatest(fetchFn, current);
5721
- cache = { ts: Date.now(), latest };
5722
- }
5723
- return {
5724
- current,
5725
- latest,
5726
- updateAvailable: compareVersions(latest, current) > 0,
5727
- installMethod
5728
- };
5729
- }
5730
- async function fetchLatest(fetchFn, current) {
5731
- const controller = new AbortController();
5732
- const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
5733
- try {
5734
- const res = await fetchFn(REGISTRY_URL, { signal: controller.signal });
5735
- if (!res.ok) return current;
5736
- const body = await res.json();
5737
- return typeof body.version === "string" ? body.version : current;
5738
- } catch {
5739
- return current;
5740
- } finally {
5741
- clearTimeout(timer);
5742
- }
5743
- }
5744
- async function applyUpdate(deps) {
5745
- if (deps.installMethod === "npx") {
5746
- throw new Error("cannot self-update an npx invocation \u2014 run `npx seshmux@latest` next time");
5747
- }
5748
- const exec = deps.exec ?? defaultExec;
5749
- const previous = deps.current;
5750
- const version2 = /^\d+\.\d+\.\d+[A-Za-z0-9.\-+]*$/.test(deps.target ?? "") ? deps.target : null;
5751
- const spec = `seshmux@${version2 ?? "latest"}`;
5752
- try {
5753
- const { stdout, stderr } = await exec("npm", ["i", "-g", spec, "--prefer-online"]);
5754
- return { ok: true, log: [stdout, stderr].filter(Boolean).join("\n"), previous };
5755
- } catch (e) {
5756
- const err = e;
5757
- const log = [err.stdout, err.stderr, err.message].filter(Boolean).join("\n");
5758
- return { ok: false, log, previous };
5759
- }
5760
- }
5761
- var import_node_child_process8, import_node_fs9, REGISTRY_URL, FETCH_TIMEOUT_MS, CACHE_TTL_MS, defaultFetch, cache;
5762
- var init_update = __esm({
5763
- "server/lib/update.ts"() {
5764
- "use strict";
5765
- import_node_child_process8 = require("node:child_process");
5766
- import_node_fs9 = require("node:fs");
5767
- REGISTRY_URL = "https://registry.npmjs.org/seshmux/latest";
5768
- FETCH_TIMEOUT_MS = 5e3;
5769
- CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
5770
- defaultFetch = (url, init) => fetch(url, init);
5771
- cache = null;
5772
- }
5773
- });
5774
-
5775
5805
  // server/routes/update.ts
5776
5806
  var update_exports = {};
5777
5807
  __export(update_exports, {
package/bin/seshmux.js CHANGED
@@ -18,7 +18,17 @@ const http = require('node:http');
18
18
  const { randomBytes } = require('node:crypto');
19
19
  const path = require('node:path');
20
20
  const fs = require('node:fs');
21
- const { ensureDaemon } = require('../daemon/ensure');
21
+ const { ensureDaemon, pidAlive, paths, configDir } = require('../daemon/ensure');
22
+
23
+ // The daemon's pid, from the pidfile it writes in the config dir. null when it isn't running.
24
+ function readDaemonPid() {
25
+ try {
26
+ const pid = Number(fs.readFileSync(paths(configDir()).pid, 'utf8').trim());
27
+ return Number.isInteger(pid) && pid > 0 && pidAlive(pid) ? pid : null;
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
22
32
 
23
33
  // Absolute path to THIS cli entry, inherited by the server child. The MCP
24
34
  // bridge registration writes it into agent configs (`node <bin> mcp-bridge`) —
@@ -88,12 +98,17 @@ function parseArgs(argv) {
88
98
  // $PORT is honoured because server/index.ts already does — without this the CLI
89
99
  // silently ignored it and booted on 4700 anyway. --port still wins over the env.
90
100
  const envPort = Number(process.env.PORT);
91
- const args = { port: Number.isInteger(envPort) && envPort > 0 ? envPort : 4700, noOpen: false };
101
+ const args = {
102
+ port: Number.isInteger(envPort) && envPort > 0 ? envPort : 4700,
103
+ noOpen: false,
104
+ restartDaemon: false,
105
+ };
92
106
  for (let i = 0; i < argv.length; i++) {
93
107
  const a = argv[i];
94
108
  if (a === '--port' || a === '-p') args.port = Number(argv[++i]);
95
109
  else if (a.startsWith('--port=')) args.port = Number(a.slice(7));
96
110
  else if (a === '--no-open') args.noOpen = true;
111
+ else if (a === '--restart-daemon') args.restartDaemon = true;
97
112
  }
98
113
  if (!Number.isInteger(args.port) || args.port <= 0) args.port = 4700;
99
114
  return args;
@@ -143,6 +158,35 @@ async function main() {
143
158
 
144
159
  const args = parseArgs(argv);
145
160
 
161
+ // --restart-daemon: the ONLY way to upgrade the daemon. ensureDaemon() treats any daemon that
162
+ // answers hello as 'ok' and reuses it (daemon/ensure.js classify()), which is what keeps your
163
+ // sessions alive across server updates — but it also means a daemon started months ago runs
164
+ // forever, missing every RPC added since. Restarting seshmux does NOT replace it. This does.
165
+ //
166
+ // Destructive on purpose, so it is explicit and never automatic: tmux-tier sessions rehydrate
167
+ // from `tmux ls` and survive, but PLAIN-tier PTYs die with the daemon. Hence a flag the user
168
+ // types, not something the update flow does behind their back.
169
+ if (args.restartDaemon) {
170
+ const before = readDaemonPid();
171
+ if (before) {
172
+ console.log(`[seshmux] stopping daemon ${before} — tmux-backed sessions survive; any non-tmux PTYs will end`);
173
+ try {
174
+ process.kill(before, 'SIGTERM');
175
+ } catch {
176
+ /* already gone */
177
+ }
178
+ for (let i = 0; i < 40 && pidAlive(before); i++) await new Promise((r) => setTimeout(r, 100));
179
+ if (pidAlive(before)) {
180
+ console.error(`[seshmux] daemon ${before} did not stop; not starting a second one`);
181
+ process.exit(1);
182
+ }
183
+ } else {
184
+ console.log('[seshmux] no daemon running');
185
+ }
186
+ const { spawned } = await ensureDaemon();
187
+ console.log(`[seshmux] daemon ${spawned ? 'restarted' : 'already up'} (pid ${readDaemonPid() ?? '?'})`);
188
+ }
189
+
146
190
  // If a healthy seshmux already runs on the requested port range, just open the
147
191
  // browser to it — no duplicate server, no port fight.
148
192
  const reuse = await resolvePort(args.port);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "seshmux",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Local-first mission control for AI coding agents (Claude Code + Codex)",
5
5
  "bin": {
6
6
  "seshmux": "bin/seshmux.js"
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "scripts": {
12
12
  "predev": "node -e \"require('./daemon/ensure').ensureDaemon()\"",
13
- "dev": "tsx server/index.ts",
13
+ "dev": "PORT=${PORT:-4800} tsx server/index.ts",
14
14
  "build": "bash scripts/build-standalone.sh",
15
15
  "start": "node bin/seshmux.js",
16
16
  "lint:styles": "bash scripts/lint-styles.sh",