dsh-git-badge 0.17.0 → 0.17.1

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/seam/apply.js CHANGED
@@ -13,13 +13,12 @@
13
13
  * npx dsh-git-badge apply # then restart the dsh web process
14
14
  *
15
15
  * HOW PATCHING WORKS — anchors, not hashes. The patch is defined once in
16
- * anchors.js as [name, old, new] triples. `apply` locates each `old` block in
17
- * the INSTALLED client.js (each must occur exactly once) and replaces it in
18
- * place. A DSH update that changes anything else in the file no longer matters;
19
- * only a change that moves one of the anchors does, and the failure names the
20
- * anchor that moved instead of reporting an opaque hash mismatch.
16
+ * anchors.js as [name, old, new] triples; the mechanics (locating the install,
17
+ * inspecting its state, writing the patch and its backup) live once in
18
+ * patch.js, shared with the postinstall hook. This file is the CLI: it decides
19
+ * what to print and which exit codes to take, per the states inspect() names.
21
20
  *
22
- * Safety:
21
+ * Safety (enforced in patch.js + anchors.js, surfaced here):
23
22
  * - anchor assertion: every anchor must match exactly once; anything else is
24
23
  * drift and NOTHING is written
25
24
  * - own-artifact detection: every artifact carries the dsh-git-badge:seam-patch
@@ -31,154 +30,42 @@
31
30
  * installed build is the one the anchors were verified against, but apply
32
31
  * proceeds on any build whose anchors resolve
33
32
  *
34
- * Environment:
33
+ * Environment (read by patch.js):
35
34
  * DSH_INSTALL the DSH package root. Default: `<global npm root>/@deepseek-ai/dsh`.
36
35
  * SEAM_DATA_DIR where revert backups are written. Default:
37
36
  * `$DSH_HOME/git-badge-seam` (else `~/.dsh/git-badge-seam`) —
38
37
  * a stable, user-level store shared by every way this tool
39
- * can run. See the note at the definition below.
38
+ * can run. See seam/store.js for the restart marker.
40
39
  *
41
40
  * Usage: apply.js apply | apply.js revert | apply.js status
42
41
  */
43
- import { execSync } from "node:child_process";
44
- import { createHash } from "node:crypto";
45
- import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
46
- import { dirname, join } from "node:path";
47
- import { fileURLToPath } from "node:url";
48
- import {
49
- MARKER,
50
- applyPatch,
51
- appliedSignature,
52
- firstFailure,
53
- isCurrentRev,
54
- isOurs,
55
- seamPresent,
56
- } from "./anchors.js";
57
-
42
+ import { copyFileSync, existsSync, readFileSync } from "node:fs";
43
+ import { MARKER } from "./anchors.js";
44
+ import { backupState, doPatch, doRestore, installPaths, inspect, KNOWN_GOOD_HASH, sha256 } from "./patch.js";
58
45
  import { installSkill } from "./skill.js";
59
- import { dataDir, writeRestartMarker } from "./store.js";
60
-
61
- const HERE = dirname(fileURLToPath(import.meta.url));
62
- /**
63
- * The stable backup store — shared with the postinstall hook and any future
64
- * invocation path, so an apply→revert round trip works across npx cache
65
- * entries. SEAM_DATA_DIR overrides (the test suite does, to stay hermetic).
66
- * See seam/store.js for the full rationale and the restart marker.
67
- */
68
- const DATA_DIR = dataDir();
69
-
70
- // sha256 of the upstream build the anchors were last verified against. ADVISORY:
71
- // status prints it for orientation; apply does NOT require it.
72
- const KNOWN_GOOD_HASH = "383b9ef779366c13d818500b6488896328b189f156addbaa480c835e902edd5f";
73
-
74
- /** Locate the installed DSH package root; null when it cannot be found. */
75
- function dshRoot() {
76
- if (process.env.DSH_INSTALL) return process.env.DSH_INSTALL;
77
- try {
78
- return join(execSync("npm root -g", { encoding: "utf8" }).trim(), "@deepseek-ai", "dsh");
79
- } catch {
80
- return null;
81
- }
82
- }
46
+ import { writeRestartMarker } from "./store.js";
83
47
 
84
- const DSH = dshRoot();
85
- if (!DSH) {
48
+ const paths = installPaths();
49
+ if (paths === null) {
86
50
  console.error("ERROR: cannot locate the DSH package. Set DSH_INSTALL to its root");
87
51
  console.error("(e.g. the directory containing node_modules/@deepseek-ai/dsh).");
88
52
  process.exit(1);
89
53
  }
90
- const PKG = join(DSH, "node_modules", "@deepseek-ai", "dsh-client-ui-workspace");
91
- const CLIENT = join(PKG, "lib", "client.js");
92
- const INDEX = join(PKG, "lib", "index.js");
93
- const STUB_INDEX = join(HERE, "stub-index.js"); // no-op host half; copied over index.js on apply
94
- const BACKUP_CLIENT = join(DATA_DIR, "backup-client.js");
95
- const BACKUP_INDEX = join(DATA_DIR, "backup-index.js");
96
-
97
- const sha256 = (text) => createHash("sha256").update(text, "utf8").digest("hex");
98
-
99
- /** Inspect the installed client.js. Returns the same key:value report apply.sh printed. */
100
- function inspect() {
101
- const report = {};
102
- let text;
103
- try {
104
- text = readClient();
105
- } catch (error) {
106
- if (error.code === "ENOENT") {
107
- report.state = "missing";
108
- return report;
109
- }
110
- report.state = "unreadable";
111
- report.error = String(error.message ?? error);
112
- return report;
113
- }
114
- report.hash = sha256(text);
115
- report.known_good_match = report.hash === KNOWN_GOOD_HASH ? "yes" : "no";
116
- report.backup = existsSync(BACKUP_CLIENT) ? "yes" : "no";
117
- report.marker = isOurs(text) ? "yes" : "no";
118
- report.rev_current = isCurrentRev(text) ? "yes" : "no";
119
- report.seam_strings = seamPresent(text) ? "yes" : "no";
120
-
121
- const fail = firstFailure(text);
122
- if (fail === null) report.anchors = "all-resolve";
123
- else {
124
- report.anchors = "drift";
125
- report.anchor_fail = `${fail.name}:${fail.count}`;
126
- }
127
-
128
- if (isOurs(text)) {
129
- // A PATCHED file no longer contains the `old` anchor forms — that is the
130
- // point — so it is verified by its applied signature instead.
131
- report.applied_signature = appliedSignature(text) ? "yes" : "no";
132
- if (!isCurrentRev(text)) report.state = "ours-stale";
133
- else if (appliedSignature(text)) report.state = "ours-current";
134
- else report.state = "ours-corrupt";
135
- } else if (seamPresent(text)) report.state = "upstream-landed";
136
- else if (fail === null) report.state = "patchable";
137
- else report.state = "drift";
138
- return report;
139
- }
140
-
141
- function readClient() {
142
- return readText(CLIENT);
143
- }
144
-
145
- function readText(path) {
146
- return readFileSync(path, "utf8");
147
- }
148
-
149
- /** Backup the bytes AS FOUND, patch client.js in place, install the no-op host half. */
150
- function doPatch() {
151
- mkdirSync(DATA_DIR, { recursive: true });
152
- copyFileSync(CLIENT, BACKUP_CLIENT);
153
- copyFileSync(INDEX, BACKUP_INDEX);
154
- const patched = applyPatch(readClient());
155
- writeText(CLIENT, patched);
156
- copyFileSync(STUB_INDEX, INDEX);
157
- console.log("patched lib/client.js in place (anchors applied); host half stubbed.");
158
- // dshmarket cannot see this host-file change; the plugin surfaces the
159
- // restart itself (marker → status response → chip button).
160
- writeRestartMarker("apply");
161
- }
162
-
163
- function writeText(path, text) {
164
- writeFileSync(path, text, "utf8");
165
- }
54
+ const CLIENT = paths.client;
166
55
 
167
56
  const VERDICT_EXIT_1 = new Set(["drift", "unreadable", "missing", "ours-corrupt"]);
168
57
 
169
- // keep the /gh skill in step with the package on every patcher run
170
- if (["apply", "revert", "status"].includes(process.argv[2])) {
171
- console.log(`[dsh-git-badge] gh skill: ${installSkill()}`);
172
- }
173
-
174
58
  const verb = process.argv[2];
175
59
  if (!verb || !["apply", "revert", "status"].includes(verb)) {
176
60
  console.error("usage: apply.js apply|revert|status");
177
61
  process.exit(1);
178
62
  }
179
63
 
64
+ // keep the /gh skill in step with the package on every patcher run
65
+ console.log(`[dsh-git-badge] gh skill: ${installSkill()}`);
66
+
180
67
  if (verb === "status") {
181
- const r = inspect();
68
+ const r = inspect(paths);
182
69
  console.log(`installed: ${CLIENT}`);
183
70
  console.log(`hash: ${r.hash ?? r.error ?? "-"}`);
184
71
  if (r.known_good_match === "yes") console.log("pinned: KNOWN-GOOD MATCH — the build the anchors were verified against");
@@ -217,7 +104,7 @@ if (verb === "status") {
217
104
  }
218
105
 
219
106
  if (verb === "apply") {
220
- const r = inspect();
107
+ const r = inspect(paths);
221
108
  for (const [k, v] of Object.entries(r)) {
222
109
  if (k !== "state") console.log(` ${k}=${v}`);
223
110
  }
@@ -227,7 +114,8 @@ if (verb === "apply") {
227
114
  console.log("verdict: PATCHED (rev current) — nothing to do.");
228
115
  break;
229
116
  case "patchable":
230
- doPatch();
117
+ doPatch(paths, "apply");
118
+ console.log("patched lib/client.js in place (anchors applied); host half stubbed.");
231
119
  console.log("verdict: PATCHED.");
232
120
  break;
233
121
  case "ours-stale": {
@@ -235,29 +123,22 @@ if (verb === "apply") {
235
123
  // found them pre-patch). Validate THAT before touching the installed
236
124
  // file: restoring a backup whose anchors no longer resolve would
237
125
  // overwrite the installed build with an unpatchable older one.
238
- if (!existsSync(BACKUP_CLIENT)) {
126
+ if (!existsSync(paths.backupClient)) {
239
127
  console.error("REFUSING: installed file is an older artifact of ours but no backup");
240
128
  console.error("exists, so the original bytes are unknown. Reinstall the package or");
241
129
  console.error("restore the backup by hand, then re-run apply.");
242
130
  process.exit(1);
243
131
  }
244
- const backupText = readText(BACKUP_CLIENT);
245
- const bstate = isOurs(backupText)
246
- ? "ours"
247
- : seamPresent(backupText)
248
- ? "upstream-landed"
249
- : firstFailure(backupText) === null
250
- ? "patchable"
251
- : "drift";
132
+ const bstate = backupState(paths);
252
133
  if (bstate !== "patchable") {
253
134
  console.error(`REFUSING: the backup (bytes as found before patching) is '${bstate}',`);
254
135
  console.error("not patchable — the anchors no longer match the backed-up upstream.");
255
136
  console.error("Nothing was changed. Reinstall the DSH package, then re-run apply.");
256
137
  process.exit(1);
257
138
  }
258
- writeText(CLIENT, backupText);
259
- copyFileSync(BACKUP_INDEX, INDEX);
260
- doPatch();
139
+ doRestore(paths);
140
+ doPatch(paths, "apply");
141
+ console.log("patched lib/client.js in place (anchors applied); host half stubbed.");
261
142
  console.log("verdict: UPGRADED — older artifact replaced with the current patch.");
262
143
  break;
263
144
  }
@@ -287,16 +168,15 @@ if (verb === "apply") {
287
168
  console.log(`now install the plugin: dsh plugin --profile web add dsh-git-badge`);
288
169
  } else {
289
170
  // revert
290
- if (existsSync(BACKUP_CLIENT)) {
291
- const r = inspect();
171
+ if (existsSync(paths.backupClient)) {
172
+ const r = inspect(paths);
292
173
  const installedHash = r.hash;
293
- const backupHash = sha256(readText(BACKUP_CLIENT));
174
+ const backupHash = sha256(readFileSync(paths.backupClient, "utf8"));
294
175
  if (installedHash === backupHash) {
295
176
  // Idempotent re-run: the installed bytes already ARE the backup.
296
177
  console.log("already reverted — installed client.js matches the backup; nothing to do.");
297
178
  } else if (r.state === "ours-current" || r.state === "ours-stale" || r.state === "ours-corrupt") {
298
- writeText(CLIENT, readText(BACKUP_CLIENT));
299
- copyFileSync(BACKUP_INDEX, INDEX);
179
+ doRestore(paths);
300
180
  console.log("reverted client.js/index.js to the bytes as found before patching.");
301
181
  // A revert also changes bytes the running bundles were composed from.
302
182
  writeRestartMarker("revert");
@@ -306,14 +186,14 @@ if (verb === "apply") {
306
186
  console.error(`artifact (state: ${r.state}), so upstream replaced it since the patch was`);
307
187
  console.error("applied. Restoring the backup would DOWNGRADE the package.");
308
188
  console.error("Nothing was changed. If the downgrade is really what you want, restore");
309
- console.error(`${BACKUP_CLIENT} by hand.`);
189
+ console.error(`${paths.backupClient} by hand.`);
310
190
  process.exit(1);
311
191
  }
312
192
  } else {
313
- const r = inspect();
193
+ const r = inspect(paths);
314
194
  if (r.state === "ours-current" || r.state === "ours-stale" || r.state === "ours-corrupt") {
315
195
  console.error("REFUSING: the installed file is patched but no backup exists in");
316
- console.error(`${DATA_DIR}, so the original bytes are unknown here. Options:`);
196
+ console.error("the seam data dir, so the original bytes are unknown here. Options:");
317
197
  console.error(" 1. If you applied from a clone of the plugin's repository, run its");
318
198
  console.error(" wrapper instead — it keeps its own backup:");
319
199
  console.error(" <clone>/seam/apply.sh revert");
@@ -324,6 +204,6 @@ if (verb === "apply") {
324
204
  console.log("no backup present and nothing of ours installed; client.js left untouched.");
325
205
  }
326
206
  // restore the no-op host half if no backup of the original exists
327
- if (!existsSync(BACKUP_INDEX)) copyFileSync(STUB_INDEX, INDEX);
207
+ if (!existsSync(paths.backupIndex)) copyFileSync(paths.stub, paths.index);
328
208
  console.log("remember to remove the plugin: dsh plugin --profile web remove dsh-git-badge");
329
209
  }
package/seam/checkout.js CHANGED
@@ -21,6 +21,7 @@
21
21
  */
22
22
  import { execFileSync } from "node:child_process";
23
23
  import { basename, resolve } from "node:path";
24
+ import { parseWorktreeList } from "../lib/index.js";
24
25
  import { clearSessionCheckout, writeSessionCheckout } from "./store.js";
25
26
 
26
27
  function usage(message) {
@@ -28,13 +29,10 @@ function usage(message) {
28
29
  console.error("usage: dsh-git-badge-checkout <path> | --clear");
29
30
  }
30
31
 
31
- /** Worktree paths git lists for the repo containing `dir`. */
32
+ /** Worktree paths git lists for the repo containing `dir` — the node half's parser. */
32
33
  function worktreePaths(dir) {
33
34
  const out = execFileSync("git", ["-C", dir, "worktree", "list", "--porcelain"], { encoding: "utf8" });
34
- return out
35
- .split("\n")
36
- .filter((line) => line.startsWith("worktree "))
37
- .map((line) => resolve(line.slice("worktree ".length).trim()));
35
+ return parseWorktreeList(out).map((entry) => resolve(entry.path));
38
36
  }
39
37
 
40
38
  function branchOf(dir) {
package/seam/patch.js ADDED
@@ -0,0 +1,142 @@
1
+ /**
2
+ * The seam-patch engine — the ONE copy of the mechanics both patch entry
3
+ * points share: seam/apply.js (the shipped CLI) and seam/postinstall.js (the
4
+ * install hook). Locating the install, inspecting its state, and writing the
5
+ * patch each used to exist twice, once per entry point, with the guardrails
6
+ * duplicated between them; now the entry points only decide what to SAY and
7
+ * which exits to take. This module returns facts and never prints.
8
+ *
9
+ * The patch itself is defined once in anchors.js; see its header for the
10
+ * anchor-not-hash design and the safety rails (own-artifact detection, backup
11
+ * of the bytes as found, downgrade-guarded revert).
12
+ */
13
+ import { execSync } from "node:child_process";
14
+ import { createHash } from "node:crypto";
15
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
16
+ import { dirname, join } from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+ import { appliedSignature, applyPatch, firstFailure, isCurrentRev, isOurs, seamPresent } from "./anchors.js";
19
+ import { dataDir, writeRestartMarker } from "./store.js";
20
+
21
+ const HERE = dirname(fileURLToPath(import.meta.url));
22
+
23
+ // sha256 of the upstream build the anchors were last verified against. ADVISORY:
24
+ // status prints it for orientation; apply does NOT require it.
25
+ export const KNOWN_GOOD_HASH = "383b9ef779366c13d818500b6488896328b189f156addbaa480c835e902edd5f";
26
+
27
+ /** Locate the installed DSH package root; null when it cannot be found. */
28
+ export function dshRoot() {
29
+ if (process.env.DSH_INSTALL) return process.env.DSH_INSTALL;
30
+ try {
31
+ return join(execSync("npm root -g", { encoding: "utf8" }).trim(), "@deepseek-ai", "dsh");
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * The files one patch acts on, resolved against `root` (default: the located
39
+ * DSH install). Null when there is no root — the entry points print their own
40
+ * "cannot locate" advice. Backup paths resolve per call, so SEAM_DATA_DIR (and
41
+ * DSH_INSTALL) are honoured whenever the process environment says so.
42
+ */
43
+ export function installPaths(root = dshRoot()) {
44
+ if (!root) return null;
45
+ const pkg = join(root, "node_modules", "@deepseek-ai", "dsh-client-ui-workspace");
46
+ const store = dataDir();
47
+ return {
48
+ root,
49
+ client: join(pkg, "lib", "client.js"),
50
+ index: join(pkg, "lib", "index.js"),
51
+ stub: join(HERE, "stub-index.js"),
52
+ backupClient: join(store, "backup-client.js"),
53
+ backupIndex: join(store, "backup-index.js")
54
+ };
55
+ }
56
+
57
+ export const sha256 = (text) => createHash("sha256").update(text, "utf8").digest("hex");
58
+
59
+ const readText = (path) => readFileSync(path, "utf8");
60
+
61
+ /**
62
+ * Inspect the installed client.js. Returns the key:value report the CLI's
63
+ * `status` prints, with `state` one of: missing | unreadable | ours-current |
64
+ * ours-stale | ours-corrupt | upstream-landed | patchable | drift.
65
+ */
66
+ export function inspect(paths = installPaths()) {
67
+ const report = {};
68
+ let text;
69
+ try {
70
+ text = readText(paths.client);
71
+ } catch (error) {
72
+ if (error.code === "ENOENT") {
73
+ report.state = "missing";
74
+ return report;
75
+ }
76
+ report.state = "unreadable";
77
+ report.error = String(error.message ?? error);
78
+ return report;
79
+ }
80
+ report.hash = sha256(text);
81
+ report.known_good_match = report.hash === KNOWN_GOOD_HASH ? "yes" : "no";
82
+ report.backup = existsSync(paths.backupClient) ? "yes" : "no";
83
+ report.marker = isOurs(text) ? "yes" : "no";
84
+ report.rev_current = isCurrentRev(text) ? "yes" : "no";
85
+ report.seam_strings = seamPresent(text) ? "yes" : "no";
86
+
87
+ const fail = firstFailure(text);
88
+ if (fail === null) report.anchors = "all-resolve";
89
+ else {
90
+ report.anchors = "drift";
91
+ report.anchor_fail = `${fail.name}:${fail.count}`;
92
+ }
93
+
94
+ if (isOurs(text)) {
95
+ // A PATCHED file no longer contains the `old` anchor forms — that is the
96
+ // point — so it is verified by its applied signature instead.
97
+ report.applied_signature = appliedSignature(text) ? "yes" : "no";
98
+ if (!isCurrentRev(text)) report.state = "ours-stale";
99
+ else if (appliedSignature(text)) report.state = "ours-current";
100
+ else report.state = "ours-corrupt";
101
+ } else if (seamPresent(text)) report.state = "upstream-landed";
102
+ else if (fail === null) report.state = "patchable";
103
+ else report.state = "drift";
104
+ return report;
105
+ }
106
+
107
+ /**
108
+ * The backup's own patchability — the upgrade path validates it BEFORE
109
+ * restoring, so re-patching can never overwrite the installed build with an
110
+ * unpatchable older one. Returns "ours" | "upstream-landed" | "patchable" | "drift".
111
+ */
112
+ export function backupState(paths) {
113
+ const text = readText(paths.backupClient);
114
+ return isOurs(text)
115
+ ? "ours"
116
+ : seamPresent(text)
117
+ ? "upstream-landed"
118
+ : firstFailure(text) === null
119
+ ? "patchable"
120
+ : "drift";
121
+ }
122
+
123
+ /**
124
+ * Backup the bytes AS FOUND, patch client.js in place, install the no-op host
125
+ * half, and record the restart marker (`reason` names the actor: apply |
126
+ * postinstall | revert). Callers gate on inspect() first — this writes
127
+ * unconditionally.
128
+ */
129
+ export function doPatch(paths, reason = "apply") {
130
+ mkdirSync(dataDir(), { recursive: true });
131
+ copyFileSync(paths.client, paths.backupClient);
132
+ copyFileSync(paths.index, paths.backupIndex);
133
+ writeFileSync(paths.client, applyPatch(readText(paths.client)));
134
+ copyFileSync(paths.stub, paths.index);
135
+ writeRestartMarker(reason);
136
+ }
137
+
138
+ /** Restore the backup over the installed files. The caller has already gated. */
139
+ export function doRestore(paths) {
140
+ writeFileSync(paths.client, readText(paths.backupClient));
141
+ copyFileSync(paths.backupIndex, paths.index);
142
+ }
@@ -10,6 +10,11 @@
10
10
  * applied right here; otherwise the plugin's boot log and the chip tooltip name
11
11
  * the same one manual command.
12
12
  *
13
+ * The mechanics live once in patch.js, shared with the apply CLI — the state
14
+ * gate below reads the same inspect() report the CLI acts on, so the hook and
15
+ * the tool cannot disagree about what a file's state is. This file only decides
16
+ * what to print for each state at install time.
17
+ *
13
18
  * GUARDRAILS — this script may never break an install:
14
19
  * - it runs ONLY when the installed DSH client.js is pristine upstream AND
15
20
  * every anchor resolves exactly once (`patchable`); any other state —
@@ -21,68 +26,51 @@
21
26
  * client bundle at boot, and an install script restarting the host that is
22
27
  * installing it is not safe to automate.
23
28
  */
24
- import { execSync } from "node:child_process";
25
- import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
26
- import { dirname, join } from "node:path";
27
- import { fileURLToPath } from "node:url";
28
- import { applyPatch, firstFailure, isOurs, seamPresent } from "./anchors.js";
29
+ import { existsSync, readFileSync } from "node:fs";
30
+ import { firstFailure } from "./anchors.js";
31
+ import { doPatch, installPaths, inspect } from "./patch.js";
29
32
  import { installSkill } from "./skill.js";
30
- import { dataDir, writeRestartMarker } from "./store.js";
31
33
 
32
- const HERE = dirname(fileURLToPath(import.meta.url));
33
34
  const HINT = "Sidebar session-row badges need one manual step: `npx dsh-git-badge apply`, then restart dsh web.";
34
35
 
35
- function dshClientPath() {
36
- try {
37
- const root = process.env.DSH_INSTALL || join(execSync("npm root -g", { encoding: "utf8" }).trim(), "@deepseek-ai", "dsh");
38
- return join(root, "node_modules", "@deepseek-ai", "dsh-client-ui-workspace", "lib", "client.js");
39
- } catch {
40
- return null;
41
- }
42
- }
43
-
44
36
  function main() {
45
37
  // The skill does not depend on the DSH install — install it before any
46
38
  // client-shaped early return, so a market install always gets /gh.
47
39
  console.log(`[dsh-git-badge] gh skill: ${installSkill()} — /gh in the input's commands menu.`);
48
- const client = dshClientPath();
49
- if (client === null || !existsSync(client)) {
40
+ const paths = installPaths();
41
+ if (paths === null || !existsSync(paths.client)) {
50
42
  console.log(`[dsh-git-badge] DSH install not found; skipping seam setup. ${HINT}`);
51
43
  return;
52
44
  }
53
- // the index stub is copied with the client on apply (see apply.js)
54
- const index = join(dirname(client), "index.js");
55
- const text = readFileSync(client, "utf8");
56
-
57
- // Already ours (any rev): nothing to do here; apply.js upgrade rules govern.
58
- if (isOurs(text)) {
59
- console.log("[dsh-git-badge] seam already applied — sidebar session-row badges active after restart.");
60
- return;
61
- }
62
- // Upstream landed the seam: the patch is retired.
63
- if (seamPresent(text)) {
64
- console.log("[dsh-git-badge] DSH declares the seam itself — no patch needed.");
65
- return;
66
- }
67
- // Pristine, but the anchors no longer resolve: a DSH update moved anchored
68
- // code. Never patch half of anything — print, and let a patched anchors.js
69
- // do it.
70
- const fail = firstFailure(text);
71
- if (fail !== null) {
72
- console.log(`[dsh-git-badge] DSH build drifted from the patch anchors (${fail.name}); skipping. ${HINT}`);
73
- return;
45
+ const state = inspect(paths).state;
46
+ switch (state) {
47
+ // Already ours (any rev): nothing to do here; apply.js upgrade rules govern.
48
+ case "ours-current":
49
+ case "ours-stale":
50
+ case "ours-corrupt":
51
+ console.log("[dsh-git-badge] seam already applied — sidebar session-row badges active after restart.");
52
+ return;
53
+ // Upstream landed the seam: the patch is retired.
54
+ case "upstream-landed":
55
+ console.log("[dsh-git-badge] DSH declares the seam itself — no patch needed.");
56
+ return;
57
+ // Pristine, but the anchors no longer resolve: a DSH update moved anchored
58
+ // code. Never patch half of anything — print, and let a patched anchors.js
59
+ // do it.
60
+ case "drift": {
61
+ const fail = firstFailure(readFileSync(paths.client, "utf8"));
62
+ console.log(`[dsh-git-badge] DSH build drifted from the patch anchors (${fail?.name}); skipping. ${HINT}`);
63
+ return;
64
+ }
65
+ case "patchable":
66
+ // The one state we act on: pristine upstream, anchors resolve.
67
+ doPatch(paths, "postinstall");
68
+ console.log("[dsh-git-badge] seam applied — RESTART dsh web to get sidebar session-row badges.");
69
+ return;
70
+ default:
71
+ // missing / unreadable / ours-corrupt: nothing this hook can do safely.
72
+ console.log(`[dsh-git-badge] DSH install state '${state}'; skipping seam setup. ${HINT}`);
74
73
  }
75
- // The one state we act on: pristine upstream, anchors resolve.
76
- mkdirSync(dataDir(), { recursive: true });
77
- copyFileSync(client, join(dataDir(), "backup-client.js"));
78
- copyFileSync(index, join(dataDir(), "backup-index.js"));
79
- writeFileSync(client, applyPatch(text));
80
- copyFileSync(join(HERE, "stub-index.js"), index);
81
- console.log("[dsh-git-badge] seam applied — RESTART dsh web to get sidebar session-row badges.");
82
- // Tell the running UI: dshmarket cannot see this host-file change, so the
83
- // plugin offers the Restart button itself (marker → status response → chip).
84
- // Read/cleared by the plugin halves — see seam/store.js.
85
- writeRestartMarker("postinstall");
86
74
  }
87
75
 
88
76
  try {
package/seam/store.js CHANGED
@@ -85,11 +85,15 @@ function sessionCheckoutsPath() {
85
85
  return join(dataDir(), "session-checkouts.json");
86
86
  }
87
87
 
88
- /** Every registration, `{ sessionId: { path, at } }`. Never throws. */
89
- export function readSessionCheckouts() {
88
+ /** Every registration, `{ sessionId: { path, at } }`. Never throws.
89
+ *
90
+ * `file` overrides the store path — the test seam lib/index.js uses (its
91
+ * `config.sessionCheckoutsFile`) — so the file's shape is parsed in ONE place.
92
+ */
93
+ export function readSessionCheckouts(file = sessionCheckoutsPath()) {
90
94
  try {
91
- if (!existsSync(sessionCheckoutsPath())) return {};
92
- const raw = JSON.parse(readFileSync(sessionCheckoutsPath(), "utf8"));
95
+ if (!existsSync(file)) return {};
96
+ const raw = JSON.parse(readFileSync(file, "utf8"));
93
97
  return raw !== null && typeof raw === "object" && typeof raw.sessions === "object" && raw.sessions !== null
94
98
  ? raw.sessions
95
99
  : {};