@hmharness/cli 0.6.2 → 0.6.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.
package/dist/main.js CHANGED
@@ -658,6 +658,16 @@ flags:
658
658
  if (sub === 'status') {
659
659
  const up = await hmhWebUp(Number.isFinite(port) ? port : 7788);
660
660
  stdout.write(up ? t.webRunning(readWebPid() || 0, port) + '\n' : t.webNotRunning + '\n');
661
+ // stale-daemon warning: the daemon is a code snapshot from spawn time;
662
+ // after an upgrade it keeps serving old code until restarted
663
+ const { webDaemonStale } = await import("./web-daemon.js");
664
+ const v = webDaemonStale();
665
+ if (up && v.stale) {
666
+ stdout.write(YELLOW(`⚠ daemon is running v${v.daemon} but CLI is v${v.cli} — stale code (new features missing). Run: hmh web stop && hmh web start\n`));
667
+ }
668
+ else if (up && v.daemon) {
669
+ stdout.write(DIM(`daemon v${v.daemon}\n`));
670
+ }
661
671
  return;
662
672
  }
663
673
  if (sub === 'start') {
@@ -10,8 +10,18 @@ export declare function stopWebDaemon(): boolean;
10
10
  * Idempotent: if our web UI is already up (pid file alive, or the port
11
11
  * answers as hmh), keep it; otherwise spawn it and wait up to ~6s for
12
12
  * readiness. Returns true when the web UI is usable.
13
+ *
14
+ * Version check: a daemon spawned by an older CLI keeps serving old code
15
+ * (the user sees missing features for days). We compare the recorded daemon
16
+ * version with the current CLI and restart on mismatch.
13
17
  */
14
18
  export declare function ensureWebDaemon(port?: number, entry?: string): Promise<boolean>;
19
+ /** Report whether the running daemon matches the current CLI version. */
20
+ export declare function webDaemonStale(): {
21
+ stale: boolean;
22
+ daemon: string;
23
+ cli: string;
24
+ };
15
25
  export declare function startWebDaemon(port?: number, entry?: string): {
16
26
  already: boolean;
17
27
  pid: number;
@@ -3,9 +3,16 @@
3
3
  * Shared by `hmh web start|stop|status` and the TUI auto-link (hmh tui
4
4
  * brings the web UI up unless --no-web). One pid file + one log file under
5
5
  * HMH_HOME; the daemon runs detached with no window and survives terminals.
6
+ *
7
+ * VERSION-AWARE (this bit me twice): a daemon is a code snapshot frozen at
8
+ * spawn time. After `npm i -g @hmharness/cli@X` the running daemon still
9
+ * serves the OLD code — new features silently missing, and the user sees
10
+ * stale behavior forever. `ensureWebDaemon` now compares the daemon's
11
+ * recorded version against the running CLI and auto-restarts on mismatch.
6
12
  */
7
13
  import { spawn } from 'node:child_process';
8
14
  import { execSync } from 'node:child_process';
15
+ import { createRequire } from 'node:module';
9
16
  import { openSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
10
17
  import { join } from 'node:path';
11
18
  import { homeDir } from '@hmharness/kernel';
@@ -90,7 +97,8 @@ export function stopWebDaemon() {
90
97
  }
91
98
  return killed;
92
99
  }
93
- /** Spawn the daemon (no window, detached). Returns the pid. */
100
+ /** Spawn the daemon (no window, detached). Returns the pid. Records the
101
+ * spawning CLI's version so a later upgrade can detect the stale snapshot. */
94
102
  function spawnWebDaemon(port, entry = process.argv[1]) {
95
103
  const home = homeDir();
96
104
  const log = openSync(join(home, 'web.log'), 'a');
@@ -102,21 +110,57 @@ function spawnWebDaemon(port, entry = process.argv[1]) {
102
110
  });
103
111
  child.unref();
104
112
  const pid = child.pid ?? 0;
105
- if (pid > 0)
113
+ if (pid > 0) {
106
114
  writeFileSync(join(home, 'web.pid'), String(pid));
115
+ try {
116
+ const v = createRequire(import.meta.url)('../package.json').version;
117
+ writeFileSync(join(home, 'web.version'), v);
118
+ }
119
+ catch { /* best effort */ }
120
+ }
107
121
  return pid;
108
122
  }
123
+ function daemonVersion() {
124
+ try {
125
+ return readFileSync(join(homeDir(), 'web.version'), 'utf8').trim();
126
+ }
127
+ catch {
128
+ return '';
129
+ }
130
+ }
131
+ function cliVersion() {
132
+ try {
133
+ return createRequire(import.meta.url)('../package.json').version;
134
+ }
135
+ catch {
136
+ return '';
137
+ }
138
+ }
109
139
  /**
110
140
  * Idempotent: if our web UI is already up (pid file alive, or the port
111
141
  * answers as hmh), keep it; otherwise spawn it and wait up to ~6s for
112
142
  * readiness. Returns true when the web UI is usable.
143
+ *
144
+ * Version check: a daemon spawned by an older CLI keeps serving old code
145
+ * (the user sees missing features for days). We compare the recorded daemon
146
+ * version with the current CLI and restart on mismatch.
113
147
  */
114
148
  export async function ensureWebDaemon(port = DEFAULT_WEB_PORT, entry = process.argv[1]) {
115
149
  const pid = readWebPid();
116
- if (pid && alive(pid))
117
- return true;
118
- if (await hmhWebUp(port))
119
- return true;
150
+ const daemonV = daemonVersion();
151
+ const myV = cliVersion();
152
+ const stale = daemonV !== '' && myV !== '' && daemonV !== myV;
153
+ if (!stale) {
154
+ if (pid && alive(pid))
155
+ return true;
156
+ if (await hmhWebUp(port))
157
+ return true;
158
+ }
159
+ else {
160
+ // stale daemon: kill it (it may hold the pid OR just the port)
161
+ stopWebDaemon();
162
+ await new Promise((r) => setTimeout(r, 600));
163
+ }
120
164
  spawnWebDaemon(port, entry);
121
165
  for (let i = 0; i < 12; i++) {
122
166
  await new Promise((r) => setTimeout(r, 500));
@@ -125,6 +169,12 @@ export async function ensureWebDaemon(port = DEFAULT_WEB_PORT, entry = process.a
125
169
  }
126
170
  return false;
127
171
  }
172
+ /** Report whether the running daemon matches the current CLI version. */
173
+ export function webDaemonStale() {
174
+ const d = daemonVersion();
175
+ const c = cliVersion();
176
+ return { stale: d !== '' && c !== '' && d !== c, daemon: d, cli: c };
177
+ }
128
178
  export function startWebDaemon(port = DEFAULT_WEB_PORT, entry = process.argv[1]) {
129
179
  const pid = readWebPid();
130
180
  if (pid && alive(pid))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hmharness/cli",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "hmharness command line: one-shot tasks, an interactive REPL, a fullscreen TUI, the web frontend, and direct tool invocation.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",
@@ -43,11 +43,11 @@
43
43
  "build": "tsc -p tsconfig.build.json"
44
44
  },
45
45
  "dependencies": {
46
- "@hmharness/kernel": "0.6.2",
47
- "@hmharness/evolution": "0.6.2",
48
- "@hmharness/domain-harmony": "0.6.2",
49
- "@hmharness/domain-ops": "0.6.2",
50
- "@hmharness/agent": "0.6.2",
51
- "@hmharness/web": "0.6.2"
46
+ "@hmharness/kernel": "0.6.4",
47
+ "@hmharness/evolution": "0.6.4",
48
+ "@hmharness/domain-harmony": "0.6.4",
49
+ "@hmharness/domain-ops": "0.6.4",
50
+ "@hmharness/agent": "0.6.4",
51
+ "@hmharness/web": "0.6.4"
52
52
  }
53
53
  }