dsh-git-badge 0.19.0 → 0.20.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 +149 -0
- package/README.md +15 -7
- package/lib/client.js +826 -722
- package/lib/index.js +272 -176
- package/package.json +5 -11
- package/seam/checkout.js +1 -1
- package/seam/cli.js +31 -0
- package/seam/skill.js +3 -3
- package/seam/store.js +15 -74
- package/skill/gh/SKILL.md +20 -7
- package/seam/anchors.js +0 -105
- package/seam/apply.js +0 -243
- package/seam/patch.js +0 -181
- package/seam/postinstall.js +0 -82
- package/seam/stub-index.js +0 -11
package/seam/patch.js
DELETED
|
@@ -1,181 +0,0 @@
|
|
|
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 = "e784543780fb8ac7c38c853afd26b9b1713c6907eec2b07eff37461e07bd3be9";
|
|
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
|
-
* What `revert` WOULD do, without doing it — the ONE decision the `status`
|
|
125
|
-
* report and the revert branch both read, so they cannot drift apart. They did:
|
|
126
|
-
* `revert: would REFUSE` was documented in SEAM.md and never implemented, which
|
|
127
|
-
* is the same shape of bug as the hole this closes.
|
|
128
|
-
*
|
|
129
|
-
* A legitimate backup is the bytes as found before patching, so the CURRENT
|
|
130
|
-
* anchors must still resolve against it (`backupState` is "patchable"). A store
|
|
131
|
-
* whose backup predates a DSH update satisfies neither condition: it holds an
|
|
132
|
-
* OLDER upstream, so restoring it would downgrade the package AND leave a build
|
|
133
|
-
* this patch cannot re-apply.
|
|
134
|
-
*
|
|
135
|
-
* That store is easy to acquire by accident, because `apply` patches whichever
|
|
136
|
-
* store it is pointed at: the shipped patcher defaults to
|
|
137
|
-
* `$DSH_HOME/git-badge-seam` while this repo's wrapper keeps its own under
|
|
138
|
-
* `seam/`. Apply through one and the other's backup stays behind.
|
|
139
|
-
*
|
|
140
|
-
* @returns {{ action: "restore" | "noop" | "refuse", reason: string }}
|
|
141
|
-
*/
|
|
142
|
-
export function revertPlan(paths) {
|
|
143
|
-
if (!existsSync(paths.backupClient))
|
|
144
|
-
return { action: "refuse", reason: "no backup in this store, so the original bytes are unknown" };
|
|
145
|
-
const r = inspect(paths);
|
|
146
|
-
if (r.hash === sha256(readText(paths.backupClient)))
|
|
147
|
-
return { action: "noop", reason: "installed client.js already matches the backup" };
|
|
148
|
-
if (r.state !== "ours-current" && r.state !== "ours-stale" && r.state !== "ours-corrupt")
|
|
149
|
-
return {
|
|
150
|
-
action: "refuse",
|
|
151
|
-
reason: `the installed client.js is not this repo's artifact (state: ${r.state})`
|
|
152
|
-
};
|
|
153
|
-
const bstate = backupState(paths);
|
|
154
|
-
if (bstate !== "patchable")
|
|
155
|
-
return {
|
|
156
|
-
action: "refuse",
|
|
157
|
-
reason: `the backup is '${bstate}' — a different upstream generation, not a build this patch targets`
|
|
158
|
-
};
|
|
159
|
-
return { action: "restore", reason: "the backup is a build this patch targets" };
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Backup the bytes AS FOUND, patch client.js in place, install the no-op host
|
|
164
|
-
* half, and record the restart marker (`reason` names the actor: apply |
|
|
165
|
-
* postinstall | revert). Callers gate on inspect() first — this writes
|
|
166
|
-
* unconditionally.
|
|
167
|
-
*/
|
|
168
|
-
export function doPatch(paths, reason = "apply") {
|
|
169
|
-
mkdirSync(dataDir(), { recursive: true });
|
|
170
|
-
copyFileSync(paths.client, paths.backupClient);
|
|
171
|
-
copyFileSync(paths.index, paths.backupIndex);
|
|
172
|
-
writeFileSync(paths.client, applyPatch(readText(paths.client)));
|
|
173
|
-
copyFileSync(paths.stub, paths.index);
|
|
174
|
-
writeRestartMarker(reason);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/** Restore the backup over the installed files. The caller has already gated. */
|
|
178
|
-
export function doRestore(paths) {
|
|
179
|
-
writeFileSync(paths.client, readText(paths.backupClient));
|
|
180
|
-
copyFileSync(paths.backupIndex, paths.index);
|
|
181
|
-
}
|
package/seam/postinstall.js
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* postinstall hook — best-effort seam apply at plugin-install time.
|
|
4
|
-
*
|
|
5
|
-
* WHY THIS EXISTS: a market install activates the input chip immediately, but
|
|
6
|
-
* the sidebar session-row badges additionally need the seam patch on the DSH
|
|
7
|
-
* package itself, and nothing in the install flow told the user so. When pnpm
|
|
8
|
-
* allows this script (the user approves it under `allowBuilds` — DSH's plugin
|
|
9
|
-
* installer prints exactly that instruction when pnpm blocks it), the seam is
|
|
10
|
-
* applied right here; otherwise the plugin's boot log and the chip tooltip name
|
|
11
|
-
* the same one manual command.
|
|
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
|
-
*
|
|
18
|
-
* GUARDRAILS — this script may never break an install:
|
|
19
|
-
* - it runs ONLY when the installed DSH client.js is pristine upstream AND
|
|
20
|
-
* every anchor resolves exactly once (`patchable`); any other state —
|
|
21
|
-
* already patched, drifted, upstream-landed, DSH not found — just prints
|
|
22
|
-
* - it never throws: every failure path prints the manual command and exits 0
|
|
23
|
-
* - it cannot restart dsh web; the printed message says to
|
|
24
|
-
*
|
|
25
|
-
* The restart is deliberately manual: the running dsh web process rebuilds its
|
|
26
|
-
* client bundle at boot, and an install script restarting the host that is
|
|
27
|
-
* installing it is not safe to automate.
|
|
28
|
-
*/
|
|
29
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
30
|
-
import { firstFailure } from "./anchors.js";
|
|
31
|
-
import { doPatch, installPaths, inspect } from "./patch.js";
|
|
32
|
-
import { installSkill } from "./skill.js";
|
|
33
|
-
|
|
34
|
-
const HINT = "Sidebar session-row badges need one manual step: `npx dsh-git-badge apply`, then restart dsh web.";
|
|
35
|
-
|
|
36
|
-
function main() {
|
|
37
|
-
// The skill does not depend on the DSH install — install it before any
|
|
38
|
-
// client-shaped early return, so a market install always gets /gh.
|
|
39
|
-
console.log(`[dsh-git-badge] gh skill: ${installSkill()} — /gh in the input's commands menu.`);
|
|
40
|
-
const paths = installPaths();
|
|
41
|
-
if (paths === null || !existsSync(paths.client)) {
|
|
42
|
-
console.log(`[dsh-git-badge] DSH install not found; skipping seam setup. ${HINT}`);
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
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}`);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
main();
|
|
78
|
-
} catch (error) {
|
|
79
|
-
// An install hook must never fail the install over the seam.
|
|
80
|
-
console.log(`[dsh-git-badge] seam setup skipped (${String(error && error.message ? error.message : error)}). ${HINT}`);
|
|
81
|
-
}
|
|
82
|
-
process.exit(0);
|
package/seam/stub-index.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
//#region lib/types/index.js
|
|
2
|
-
/**
|
|
3
|
-
* Workspace picker plugin, node half. Pure UI plugin: the empty apply exists
|
|
4
|
-
* so the plugin appears in the host cordis.yml / Loader (load and lifecycle
|
|
5
|
-
* follow the host; the browser half ships via exports["./client"], discovered
|
|
6
|
-
* through the package.json dsh.client declaration).
|
|
7
|
-
*/
|
|
8
|
-
/** Host plugin body — no host-side behavior for the workspace picker plugin. */
|
|
9
|
-
function apply() {}
|
|
10
|
-
//#endregion
|
|
11
|
-
export { apply };
|