dsh-git-badge 0.10.1 → 0.12.0

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/CHANGELOG.md CHANGED
@@ -10,6 +10,38 @@ npm. 0.6.0 is the first release recorded here.
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [0.12.0] - 2026-09-15
14
+
15
+ ### Removed
16
+
17
+ - **The restart notice and one-click Restart button are gone from the input
18
+ chip.** The inline text — *restart dsh web to activate the sidebar badges* —
19
+ and its Restart button (`POST /dsh-market/api/v1/restart`) could outlive the
20
+ restart it asked for when the marker survived a boot, and the nag outweighed
21
+ the convenience. The restart-pending marker pipeline is intact server-side
22
+ (`seam/store.js`, `seamRestartPending` on the status response) for a future
23
+ marketplace-surface notification; the chip simply no longer renders it.
24
+
25
+ ## [0.11.0] - 2026-09-14
26
+
27
+ ### Added
28
+
29
+ - **A one-click restart for the change the marketplace cannot see.** The
30
+ postinstall/apply patches DSH's own files, which is invisible to dshmarket's
31
+ restart bookkeeping — a market install could complete "restart-free" while
32
+ the running bundles no longer matched the disk. Now: the installer writes a
33
+ `restart-pending` marker (`seam/store.js`); the plugin's status response
34
+ carries it; and while it is pending the input chip shows a one-line
35
+ explanation — *the installer patched DSH, client bundles are composed at
36
+ boot* — plus a **Restart** button calling dshmarket's PUBLIC, documented v1
37
+ restart endpoint (`POST /dsh-market/api/v1/restart`, feature-detected via
38
+ `capabilities`; hosts that delegate restart — desktop/supervised — get the
39
+ manual instruction instead, never a hand-rolled process-control path). The
40
+ button polls until the host is serving again, then reloads into the
41
+ recomposed boot. The marker is cleared by the node half at the next boot, so
42
+ it self-expires exactly when the restart it asks for has happened. Applies
43
+ to `revert` too — removing the patch equally needs a restart to take effect.
44
+
13
45
  ## [0.10.1] - 2026-09-14
14
46
 
15
47
  ### Fixed
package/lib/client.js CHANGED
@@ -785,7 +785,7 @@ window.__ModuleLoader__.load({
785
785
  onFocus: () => setLinkHover(true),
786
786
  onBlur: () => setLinkHover(false),
787
787
  children: prToken
788
- })
788
+ }),
789
789
  ]
790
790
  });
791
791
  // No Tooltip primitive (a shell that does not seed it): render the chip
package/lib/index.js CHANGED
@@ -69,6 +69,7 @@
69
69
  import { execFile } from "node:child_process";
70
70
  import { watch, existsSync, readFileSync } from "node:fs";
71
71
  import { basename, isAbsolute, join, resolve } from "node:path";
72
+ import { clearRestartMarker, readRestartMarker } from "../seam/store.js";
72
73
 
73
74
  const inject = ["webServer", "workspaceRegistry"];
74
75
  const name = "dsh-git-badge";
@@ -1218,6 +1219,11 @@ async function effectiveTarget(target, bySession, wantPr, notify) {
1218
1219
 
1219
1220
  /** Host plugin body — register the status route and the SSE change feed. */
1220
1221
  function apply(ctx) {
1222
+ // A fresh boot composed the files on disk into the new bundles — any
1223
+ // restart-pending marker was written by an install/apply against the PREVIOUS
1224
+ // boot and its change is now live. Clearing here is what makes the marker
1225
+ // self-expiring: it survives exactly until the restart it asks for happens.
1226
+ clearRestartMarker();
1221
1227
  ctx.effect(() => ctx.webServer.register({
1222
1228
  kind: "exact",
1223
1229
  path: "/api/git-badge",
@@ -1230,6 +1236,7 @@ function apply(ctx) {
1230
1236
  res.end(JSON.stringify({ git: false, error: target.error }));
1231
1237
  return;
1232
1238
  }
1239
+ const pending = readRestartMarker();
1233
1240
  const detail = url.searchParams.get("detail") === "1";
1234
1241
  const pr = url.searchParams.get("pr") === "1";
1235
1242
  // WHO asked decides whether a linked worktree may stand in for the
@@ -1268,6 +1275,12 @@ function apply(ctx) {
1268
1275
  // tell whether an event belongs to it — without this the input chip
1269
1276
  // only ever refreshed on remount or the 60s poll.
1270
1277
  res.writeHead(200, { "content-type": "application/json" });
1278
+ if (pending !== null) {
1279
+ // dshmarket cannot see postinstall/apply host-file changes; this
1280
+ // flag is how the running UI learns a restart is pending and
1281
+ // offers its own Restart button (dshmarket v1 restart endpoint).
1282
+ body.seamRestartPending = { by: pending.by, at: pending.at };
1283
+ }
1271
1284
  res.end(JSON.stringify(body));
1272
1285
  } catch (error) {
1273
1286
  res.writeHead(400, { "content-type": "application/json" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-git-badge",
3
- "version": "0.10.1",
3
+ "version": "0.12.0",
4
4
  "description": "Git status badges for DeepSeek Harness: an input-row chip on every install, plus sidebar session-row badges (git status, PR/CI action tokens) after one manual step: `npx dsh-git-badge apply`, then restart dsh web. Event-driven freshness over SSE.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -52,6 +52,7 @@
52
52
  "seam/apply.js",
53
53
  "seam/anchors.js",
54
54
  "seam/postinstall.js",
55
+ "seam/store.js",
55
56
  "seam/stub-index.js",
56
57
  "cordis.patch.yml",
57
58
  "package.json",
package/seam/apply.js CHANGED
@@ -55,18 +55,16 @@ import {
55
55
  seamPresent,
56
56
  } from "./anchors.js";
57
57
 
58
+ import { dataDir, writeRestartMarker } from "./store.js";
59
+
58
60
  const HERE = dirname(fileURLToPath(import.meta.url));
59
61
  /**
60
- * Where revert backups live. A STABLE, user-level directory by default — NOT
61
- * this script's own directory. For an npx run that directory is a cache entry
62
- * keyed by package version: a backup written by one `npx dsh-git-badge apply`
63
- * was invisible to the next `npx dsh-git-badge revert`, which then had to
64
- * refuse ("patched but no backup exists") even though the bytes were on disk
65
- * somewhere. Sharing one store across npx runs, the profile bin and the repo
66
- * wrapper makes the apply→revert round trip reliable for everyone. Override
67
- * with SEAM_DATA_DIR (the test suite does, to stay hermetic).
62
+ * The stable backup store — shared with the postinstall hook and any future
63
+ * invocation path, so an apply→revert round trip works across npx cache
64
+ * entries. SEAM_DATA_DIR overrides (the test suite does, to stay hermetic).
65
+ * See seam/store.js for the full rationale and the restart marker.
68
66
  */
69
- const DATA_DIR = process.env.SEAM_DATA_DIR || join(process.env.DSH_HOME || join(process.env.HOME || "", ".dsh"), "git-badge-seam");
67
+ const DATA_DIR = dataDir();
70
68
 
71
69
  // sha256 of the upstream build the anchors were last verified against. ADVISORY:
72
70
  // status prints it for orientation; apply does NOT require it.
@@ -156,6 +154,9 @@ function doPatch() {
156
154
  writeText(CLIENT, patched);
157
155
  copyFileSync(STUB_INDEX, INDEX);
158
156
  console.log("patched lib/client.js in place (anchors applied); host half stubbed.");
157
+ // dshmarket cannot see this host-file change; the plugin surfaces the
158
+ // restart itself (marker → status response → chip button).
159
+ writeRestartMarker("apply");
159
160
  }
160
161
 
161
162
  function writeText(path, text) {
@@ -291,6 +292,8 @@ if (verb === "apply") {
291
292
  writeText(CLIENT, readText(BACKUP_CLIENT));
292
293
  copyFileSync(BACKUP_INDEX, INDEX);
293
294
  console.log("reverted client.js/index.js to the bytes as found before patching.");
295
+ // A revert also changes bytes the running bundles were composed from.
296
+ writeRestartMarker("revert");
294
297
  } else {
295
298
  // Installed file is not ours => upstream replaced it since patching.
296
299
  console.error("REFUSING to revert: the installed client.js is not this repo's");
@@ -26,15 +26,10 @@ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from
26
26
  import { dirname, join } from "node:path";
27
27
  import { fileURLToPath } from "node:url";
28
28
  import { applyPatch, firstFailure, isOurs, seamPresent } from "./anchors.js";
29
+ import { dataDir, writeRestartMarker } from "./store.js";
29
30
 
30
31
  const HERE = dirname(fileURLToPath(import.meta.url));
31
32
  const HINT = "Sidebar session-row badges need one manual step: `npx dsh-git-badge apply`, then restart dsh web.";
32
- /**
33
- * Same stable backup store apply.js defaults to — the backup this hook takes
34
- * must be findable by a LATER `npx dsh-git-badge revert` running from a
35
- * different cache entry. SEAM_DATA_DIR overrides (test suite).
36
- */
37
- const DATA_DIR = process.env.SEAM_DATA_DIR || join(process.env.DSH_HOME || join(process.env.HOME || "", ".dsh"), "git-badge-seam");
38
33
 
39
34
  function dshClientPath() {
40
35
  try {
@@ -74,12 +69,16 @@ function main() {
74
69
  return;
75
70
  }
76
71
  // The one state we act on: pristine upstream, anchors resolve.
77
- mkdirSync(DATA_DIR, { recursive: true });
78
- copyFileSync(client, join(DATA_DIR, "backup-client.js"));
79
- copyFileSync(index, join(DATA_DIR, "backup-index.js"));
72
+ mkdirSync(dataDir(), { recursive: true });
73
+ copyFileSync(client, join(dataDir(), "backup-client.js"));
74
+ copyFileSync(index, join(dataDir(), "backup-index.js"));
80
75
  writeFileSync(client, applyPatch(text));
81
76
  copyFileSync(join(HERE, "stub-index.js"), index);
82
77
  console.log("[dsh-git-badge] seam applied — RESTART dsh web to get sidebar session-row badges.");
78
+ // Tell the running UI: dshmarket cannot see this host-file change, so the
79
+ // plugin offers the Restart button itself (marker → status response → chip).
80
+ // Read/cleared by the plugin halves — see seam/store.js.
81
+ writeRestartMarker("postinstall");
83
82
  }
84
83
 
85
84
  try {
package/seam/store.js ADDED
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The restart-pending marker — the bridge from install-time (node side) to the
3
+ * running UI.
4
+ *
5
+ * WHY IT EXISTS: dshmarket's pending-restart banner tracks its own package
6
+ * bookkeeping. A postinstall that patches the DSH HOST package
7
+ * (dsh-client-ui-workspace) mutates files outside that bookkeeping, so the
8
+ * market can complete "restart-free" while the host's composed client bundles
9
+ * no longer match the files on disk. The marker is the out-of-band signal:
10
+ *
11
+ * postinstall / apply / revert → write the marker (seam changed on disk)
12
+ * plugin node half, at boot → clear the marker (fresh boot composed the
13
+ * new bytes; the change is live)
14
+ * plugin client half → while the marker is served on the status
15
+ * response, offer a Restart button calling
16
+ * dshmarket's public v1 restart endpoint
17
+ *
18
+ * The hot-mount path is what makes this work on a FIRST market install: the
19
+ * freshly installed plugin's client half mounts into the running shell, reads
20
+ * the marker through the status response, and can restart the host — even
21
+ * though the process that installed the patch is gone.
22
+ */
23
+
24
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
25
+ import { join } from "node:path";
26
+
27
+ /** The stable backup store — same default apply.js uses; SEAM_DATA_DIR overrides. */
28
+ export function dataDir() {
29
+ if (process.env.SEAM_DATA_DIR) return process.env.SEAM_DATA_DIR;
30
+ return join(process.env.DSH_HOME || join(process.env.HOME || "", ".dsh"), "git-badge-seam");
31
+ }
32
+
33
+ function markerPath() {
34
+ return join(dataDir(), "restart-pending.json");
35
+ }
36
+
37
+ /**
38
+ * Record that DSH files were changed on disk and a restart is needed to
39
+ * compose them. `reason` is a one-word actor (postinstall | apply | revert).
40
+ */
41
+ export function writeRestartMarker(reason) {
42
+ try {
43
+ mkdirSync(dataDir(), { recursive: true });
44
+ writeFileSync(
45
+ markerPath(),
46
+ JSON.stringify({ schema: "dsh-git-badge/restart-pending/v1", by: reason, at: new Date().toISOString() }),
47
+ "utf8",
48
+ );
49
+ } catch {
50
+ // The marker is a UX nicety; never fail the operation that earned it.
51
+ }
52
+ }
53
+
54
+ /** The pending marker, or null. Never throws. */
55
+ export function readRestartMarker() {
56
+ try {
57
+ if (!existsSync(markerPath())) return null;
58
+ return JSON.parse(readFileSync(markerPath(), "utf8"));
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+
64
+ /** The change is live — a new boot composed it. Clear the pending state. */
65
+ export function clearRestartMarker() {
66
+ try {
67
+ rmSync(markerPath(), { force: true });
68
+ } catch {
69
+ // Absent is the goal; any error leaves the marker to be cleared later.
70
+ }
71
+ }