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/CHANGELOG.md +41 -7
- package/README.md +1 -1
- package/lib/client.js +91 -67
- package/lib/index.js +98 -120
- package/package.json +2 -4
- package/seam/apply.js +34 -154
- package/seam/checkout.js +3 -5
- package/seam/patch.js +142 -0
- package/seam/postinstall.js +38 -50
- package/seam/store.js +8 -4
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
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
|
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 {
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
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 {
|
|
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
|
|
85
|
-
if (
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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(
|
|
291
|
-
const r = inspect();
|
|
171
|
+
if (existsSync(paths.backupClient)) {
|
|
172
|
+
const r = inspect(paths);
|
|
292
173
|
const installedHash = r.hash;
|
|
293
|
-
const backupHash = sha256(
|
|
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
|
-
|
|
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(`${
|
|
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(
|
|
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(
|
|
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
|
+
}
|
package/seam/postinstall.js
CHANGED
|
@@ -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 {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
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
|
|
49
|
-
if (
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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(
|
|
92
|
-
const raw = JSON.parse(readFileSync(
|
|
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
|
: {};
|