super-backlog 1.3.3 → 1.3.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.
@@ -8,6 +8,7 @@ import { startHubServer } from '../dashboard/hub.js';
8
8
  import { renderDashboard } from '../dashboard/render.js';
9
9
  import { DASHBOARD_PORT } from '../dashboard/server.js';
10
10
  import { atomicWrite } from '../lib/atomic.js';
11
+ import { defaultBuildFingerprint } from '../lib/build-fingerprint.js';
11
12
  import { clearHubState, isPidAlive, newHubToken, readHubState, writeHubState } from '../lib/hub-state.js';
12
13
  import { projectSlug } from '../lib/slug.js';
13
14
  import { KIT_VERSION } from '../lib/version.js';
@@ -157,6 +158,8 @@ export async function runDashboard(cwd, args, deps = {}) {
157
158
  const isAlive = deps.isAlive ?? isPidAlive;
158
159
  const killPid = deps.killPid ?? ((p) => { process.kill(p); });
159
160
  const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
161
+ const buildFingerprint = deps.buildFingerprint ?? defaultBuildFingerprint;
162
+ const myFingerprint = buildFingerprint();
160
163
  let slugResult;
161
164
  try {
162
165
  slugResult = projectSlug(cwd);
@@ -180,7 +183,14 @@ export async function runDashboard(cwd, args, deps = {}) {
180
183
  : undefined;
181
184
  const liveVersion = typeof statusJson?.version === 'string' ? statusJson.version : undefined;
182
185
  const effectiveVersion = liveVersion ?? state.version;
183
- if (effectiveVersion === KIT_VERSION) {
186
+ const liveFingerprint = typeof statusJson?.fingerprint === 'string' ? statusJson.fingerprint : undefined;
187
+ // Same version AND (proven identical build OR build cannot be
188
+ // assessed locally) -> attach. A missing live fingerprint from a
189
+ // same-version hub means it was started by an older CLI and cannot
190
+ // see the current build, so it is restarted exactly once.
191
+ const buildMatches = myFingerprint !== null && liveFingerprint === myFingerprint;
192
+ const buildUnassessable = myFingerprint === null;
193
+ if (effectiveVersion === KIT_VERSION && (buildMatches || buildUnassessable)) {
184
194
  if (values['port'] !== undefined && port !== state.port) {
185
195
  console.error(`error: a hub is already running on ${state.port}`);
186
196
  return 1;
@@ -194,7 +204,12 @@ export async function runDashboard(cwd, args, deps = {}) {
194
204
  noOpen,
195
205
  });
196
206
  }
197
- console.error(`hub v${effectiveVersion ?? 'unknown'} does not match this CLI (v${KIT_VERSION}) — restarting it`);
207
+ if (effectiveVersion === KIT_VERSION) {
208
+ console.error(`hub build changed (fingerprint mismatch, same version v${KIT_VERSION}) — restarting it`);
209
+ }
210
+ else {
211
+ console.error(`hub v${effectiveVersion ?? 'unknown'} does not match this CLI (v${KIT_VERSION}) — restarting it`);
212
+ }
198
213
  killPid(state.pid);
199
214
  let dead = !isAlive(state.pid);
200
215
  for (let attempt = 0; !dead && attempt < STOP_POLL_MAX_ATTEMPTS; attempt++) {
@@ -234,7 +249,7 @@ export async function runDashboard(cwd, args, deps = {}) {
234
249
  console.error(`error: dashboard serve failed (${err instanceof Error ? err.message : String(err)})`);
235
250
  return 1;
236
251
  }
237
- writeHubState(home, { pid, port: hub.port, token, version: KIT_VERSION });
252
+ writeHubState(home, { pid, port: hub.port, token, version: KIT_VERSION, fingerprint: myFingerprint ?? undefined });
238
253
  const result = hub.register({ cwd, file: outPath, regenerate });
239
254
  if (!result.ok) {
240
255
  await hub.close();
@@ -9,6 +9,7 @@ import { collectDashboardData } from './data.js';
9
9
  import { renderDashboard } from './render.js';
10
10
  import { createDebouncedReloader, createReloadBroker, DASHBOARD_PORT, recursiveWatchSupported, } from './server.js';
11
11
  import { atomicWrite } from '../lib/atomic.js';
12
+ import { defaultBuildFingerprint } from '../lib/build-fingerprint.js';
12
13
  import { projectSlug, realpathKey } from '../lib/slug.js';
13
14
  import { KIT_VERSION } from '../lib/version.js';
14
15
  import { createModelApiHandler } from '../models/dashboard-api.js';
@@ -63,6 +64,7 @@ function generateDashboard(cwd, file) {
63
64
  export async function startHubServer(opts) {
64
65
  const projects = new Map();
65
66
  const token = opts.token;
67
+ const fingerprint = defaultBuildFingerprint();
66
68
  let port = 0;
67
69
  let watchWarned = false;
68
70
  function watchBacklog(cwd, reloader) {
@@ -166,7 +168,7 @@ export async function startHubServer(opts) {
166
168
  sendText(res, 401, 'unauthorized');
167
169
  return;
168
170
  }
169
- sendJson(res, 200, { pid: process.pid, port, version: KIT_VERSION });
171
+ sendJson(res, 200, { pid: process.pid, port, version: KIT_VERSION, fingerprint: fingerprint ?? undefined });
170
172
  return;
171
173
  }
172
174
  if (pathname === '/api/hub/register' && method === 'POST') {
@@ -0,0 +1,50 @@
1
+ // src/lib/build-fingerprint.ts
2
+ import { createHash } from 'node:crypto';
3
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
4
+ import { createRequire } from 'node:module';
5
+ import { dirname, join } from 'node:path';
6
+ const require = createRequire(import.meta.url);
7
+ function distRoot() {
8
+ // Resolves from src/lib (dev/tests) and dist/lib (runtime) alike: both sit
9
+ // two levels below the package root that owns dist/.
10
+ const pkgPath = require.resolve('../../package.json');
11
+ return join(dirname(pkgPath), 'dist');
12
+ }
13
+ function walk(dir, out) {
14
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
15
+ const full = join(dir, entry.name);
16
+ if (entry.isDirectory())
17
+ walk(full, out);
18
+ else
19
+ out.push(full);
20
+ }
21
+ }
22
+ /**
23
+ * Stable content hash over the built CLI. Two builds of the same source
24
+ * produce the same fingerprint; any changed or added file changes it.
25
+ * Returns null when the build directory is missing.
26
+ */
27
+ export function computeBuildFingerprint(root = distRoot()) {
28
+ if (!existsSync(root))
29
+ return null;
30
+ const files = [];
31
+ walk(root, files);
32
+ if (files.length === 0)
33
+ return null;
34
+ files.sort();
35
+ const hash = createHash('sha256');
36
+ for (const file of files) {
37
+ hash.update(file.slice(root.length + 1).replaceAll('\\', '/'));
38
+ hash.update('\0');
39
+ hash.update(readFileSync(file));
40
+ hash.update('\0');
41
+ }
42
+ return hash.digest('hex').slice(0, 16);
43
+ }
44
+ let cachedDefault;
45
+ /** Process-lifetime cached fingerprint of the running build. */
46
+ export function defaultBuildFingerprint() {
47
+ if (cachedDefault === undefined)
48
+ cachedDefault = computeBuildFingerprint();
49
+ return cachedDefault;
50
+ }
@@ -16,8 +16,14 @@ export function readHubState(home) {
16
16
  typeof parsed.token !== 'string') {
17
17
  return null;
18
18
  }
19
- const { pid, port, token, version } = parsed;
20
- return typeof version === 'string' ? { pid, port, token, version } : { pid, port, token };
19
+ const { pid, port, token, version, fingerprint } = parsed;
20
+ return {
21
+ pid,
22
+ port,
23
+ token,
24
+ ...(typeof version === 'string' ? { version } : {}),
25
+ ...(typeof fingerprint === 'string' ? { fingerprint } : {}),
26
+ };
21
27
  }
22
28
  catch {
23
29
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "super-backlog",
3
- "version": "1.3.3",
3
+ "version": "1.3.4",
4
4
  "description": "One command to equip any project with Backlog.md + Superpowers, plus a Project Dashboard.",
5
5
  "license": "MIT",
6
6
  "repository": {