dsh-git-badge 0.8.0 → 0.9.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 +102 -1
- package/README.md +9 -3
- package/lib/client.js +220 -100
- package/lib/index.js +395 -7
- package/package.json +9 -8
- package/seam/anchors.js +138 -0
- package/seam/apply.js +304 -0
- package/seam/stub-index.js +11 -0
package/seam/anchors.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The seam patch as anchor pairs — the single definition of the patch.
|
|
3
|
+
*
|
|
4
|
+
* Every entry is [name, old, new]: `old` must occur EXACTLY ONCE in the target
|
|
5
|
+
* client.js, and is replaced by `new`. apply.js runs these against the INSTALLED
|
|
6
|
+
* file in place; the repo's seam/make-patch.sh runs them against the pinned
|
|
7
|
+
* pristine snapshot to regenerate patched-client.js for the upstream-PR diff
|
|
8
|
+
* only.
|
|
9
|
+
*
|
|
10
|
+
* This replaces the old whole-file hash guard: a DSH update that changes
|
|
11
|
+
* anything outside these blocks no longer invalidates the patch. Only a change
|
|
12
|
+
* that moves one of the anchors does — and the failure names the anchor that
|
|
13
|
+
* moved instead of reporting an opaque hash mismatch.
|
|
14
|
+
*
|
|
15
|
+
* Tabs matter: the target is a bundled, tab-indented file. The strings below
|
|
16
|
+
* use \t escapes so a reader cannot mistake them for spaces. When upstream
|
|
17
|
+
* moves an anchor, edit that entry's `old` (and `new`, if the insertion point
|
|
18
|
+
* changed) to the new upstream text — the drift report names which one.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export const MARKER = "dsh-git-badge:seam-patch";
|
|
22
|
+
// Bumped whenever the anchor set changes shape. An installed file carrying the
|
|
23
|
+
// MARKER without this revision is OUR older artifact: apply restores its backup
|
|
24
|
+
// and re-patches rather than refusing.
|
|
25
|
+
export const PATCH_REV = "2";
|
|
26
|
+
export const REV_COMMENT = `${MARKER} rev ${PATCH_REV}`;
|
|
27
|
+
|
|
28
|
+
// The seam strings upstream would declare if the patch were merged. Present in
|
|
29
|
+
// a file that is NOT ours => upstream landed; the patch is a no-op.
|
|
30
|
+
export const SEAM_STRINGS = [
|
|
31
|
+
'"sidebar.workspaces.sessionRow"',
|
|
32
|
+
'"sidebar.workspaces.sessionRow.detail"',
|
|
33
|
+
'"sidebar.workspaces.row"', // the pre-session-row seam, also upstream's if landed
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export const PATCHES = [
|
|
37
|
+
// --- 1. Session-row seam helper, inserted with the row component's props ---
|
|
38
|
+
[
|
|
39
|
+
"SessionNodeItem header + seam helpers",
|
|
40
|
+
"\t\tfunction SessionNodeItem({ node, currentId, now, onOpen, onRename, onFork, onArchive, onReveal, drag, flat = false, t }) {",
|
|
41
|
+
"\t\t/* dsh-git-badge:seam-patch rev 2 — seam/apply.sh recognises its own artifact by this\n\t\t * marker, which upstream would never carry. */\n\t\t/**\n\t\t* Seam entry boundary. The host wraps each registered ENTRY in its own error\n\t\t* boundary, but the outlet a row renders is NOT covered by it, so a throw\n\t\t* anywhere in an occupant's render path would propagate into the workspace\n\t\t* browser itself — and the shell ABDICATES that browser entry, blanking the\n\t\t* whole sidebar. A seam must not be able to do that: this keeps the blast\n\t\t* radius at \"no badge\", and logs what happened instead of swallowing it.\n\t\t*/\n\t\tclass SeamBoundary extends react.Component {\n\t\t\tconstructor(props) {\n\t\t\t\tsuper(props);\n\t\t\t\tthis.state = { failed: false };\n\t\t\t}\n\t\t\tstatic getDerivedStateFromError() {\n\t\t\t\treturn { failed: true };\n\t\t\t}\n\t\t\tcomponentDidCatch(error) {\n\t\t\t\tconsole.error(\"[dsh-git-badge] seam entry failed; badge omitted:\", error);\n\t\t\t}\n\t\t\trender() {\n\t\t\t\treturn this.state.failed ? null : this.props.children;\n\t\t\t}\n\t\t}\n\t\t/** Seam entry bodies, so the boundary above ENCLOSES the renderSlot call. */\n\t\tfunction SessionRowSeam({ renderSlot, sessionId, workspaceId, label }) {\n\t\t\treturn renderSlot(\"sidebar.workspaces.sessionRow\", { sessionId, workspaceId, label });\n\t\t}\n\t\tfunction SessionRowDetailSeam({ renderSlot, sessionId, workspaceId, label }) {\n\t\t\treturn renderSlot(\"sidebar.workspaces.sessionRow.detail\", { sessionId, workspaceId, label });\n\t\t}\n\t\t/**\n\t\t* Session-row seam (sidebar.workspaces.sessionRow): renders the additive\n\t\t* list-slot entries for one session row with the row owner share\n\t\t* ({ sessionId, workspaceId, label }).\n\t\t*\n\t\t* Returns null — not a fallback element — when no plugin occupies the seam,\n\t\t* so a pristine install renders exactly as upstream. `workspaceId` is only\n\t\t* ever passed from the TREE call site: the flat \"all sessions\" list and the\n\t\t* search-result list render this component without it, which is what keeps\n\t\t* badges off those lists without either of them needing a guard.\n\t\t*/\n\t\tfunction renderSessionRowSeam(renderSlot, sessionId, workspaceId, label) {\n\t\t\tif (renderSlot === void 0 || workspaceId === void 0) return null;\n\t\t\treturn (0, react_jsx_runtime.jsx)(SeamBoundary, {\n\t\t\t\tchildren: (0, react_jsx_runtime.jsx)(SessionRowSeam, { renderSlot, sessionId, workspaceId, label })\n\t\t\t});\n\t\t}\n\t\tfunction SessionNodeItem({ node, currentId, now, onOpen, onRename, onFork, onArchive, onReveal, drag, flat = false, t, renderSlot, workspaceId }) {",
|
|
42
|
+
],
|
|
43
|
+
// --- 2. The badge sits with the title, before the schedule/time cluster ---
|
|
44
|
+
[
|
|
45
|
+
"title span + badge render",
|
|
46
|
+
'\t\t\t\t\t\t(0, react_jsx_runtime.jsx)("span", {\n\t\t\t\t\t\t\tclassName: Rows_module_css_default.title,\n\t\t\t\t\t\t\tchildren: title\n\t\t\t\t\t\t}),',
|
|
47
|
+
'\t\t\t\t\t\t(0, react_jsx_runtime.jsx)("span", {\n\t\t\t\t\t\t\tclassName: Rows_module_css_default.title,\n\t\t\t\t\t\t\tchildren: title\n\t\t\t\t\t\t}),\n\t\t\t\t\t\trenderSessionRowSeam(renderSlot, node.id, workspaceId, title),',
|
|
48
|
+
],
|
|
49
|
+
// --- 3. The hover card gains the detail seam ---
|
|
50
|
+
[
|
|
51
|
+
"SessionHoverContent call-site props",
|
|
52
|
+
'\t\t\t\tcontent: (0, react_jsx_runtime.jsx)(SessionHoverContent, {\n\t\t\t\t\tnode,\n\t\t\t\t\tnow,\n\t\t\t\t\tt\n\t\t\t\t}),',
|
|
53
|
+
'\t\t\t\tcontent: (0, react_jsx_runtime.jsx)(SessionHoverContent, {\n\t\t\t\t\tnode,\n\t\t\t\t\tnow,\n\t\t\t\t\tt,\n\t\t\t\t\trenderSlot,\n\t\t\t\t\tworkspaceId\n\t\t\t\t}),',
|
|
54
|
+
],
|
|
55
|
+
[
|
|
56
|
+
"SessionHoverContent header",
|
|
57
|
+
"\t\tfunction SessionHoverContent({ node, now, t }) {",
|
|
58
|
+
"\t\tfunction SessionHoverContent({ node, now, t, renderSlot, workspaceId }) {",
|
|
59
|
+
],
|
|
60
|
+
[
|
|
61
|
+
"hover card detail row",
|
|
62
|
+
"\t\t\t\t\t}, status.label))\n\t\t\t\t]\n\t\t\t});\n\t\t}",
|
|
63
|
+
'\t\t\t\t\t}, status.label)),\n\t\t\t\t\trenderSlot !== void 0 && workspaceId !== void 0 ? (0, react_jsx_runtime.jsx)("div", {\n\t\t\t\t\t\tclassName: Rows_module_css_default.hoverStatus,\n\t\t\t\t\t\tchildren: (0, react_jsx_runtime.jsx)(SeamBoundary, {\n\t\t\t\t\t\t\tchildren: (0, react_jsx_runtime.jsx)(SessionRowDetailSeam, { renderSlot, sessionId: node.id, workspaceId, label: displayTitle(node, t) })\n\t\t\t\t\t\t})\n\t\t\t\t\t}) : null\n\t\t\t\t]\n\t\t\t});\n\t\t}',
|
|
64
|
+
],
|
|
65
|
+
// --- 4. Thread renderSlot: WorkspaceBrowser -> SessionTree ---
|
|
66
|
+
// A bare `renderSlot` at the SessionNodeItem call site resolves in THIS scope.
|
|
67
|
+
// The first cut of this patch threaded it into SessionNodeItem but not into
|
|
68
|
+
// SessionTree, so the row render threw `ReferenceError: renderSlot is not
|
|
69
|
+
// defined` the moment sessions appeared — and the shell abdicated the whole
|
|
70
|
+
// sidebar with it. Any prop a patched call site reads by shorthand must arrive
|
|
71
|
+
// through every intermediate scope.
|
|
72
|
+
[
|
|
73
|
+
"SessionTree header",
|
|
74
|
+
"\t\tfunction SessionTree({ useSessions, useSessionPendingInteraction, startSession, open, forkSession, workspaces, archivedSessionIds, workspaceReady, usePanelInfo, onRenameRequest, onDeleteRequest, onSessionRename, onSessionArchive, insertWorkspaceBefore, insertSessionBefore, orderBy, groupExpansion, setGroupExpanded, sessionOrderByAccount, sessionUpdatedAtByAccount, syncSessionOrderAccount, setSessionOrder, home, t, revealSessionId, onSessionRevealed }) {",
|
|
75
|
+
"\t\tfunction SessionTree({ useSessions, useSessionPendingInteraction, startSession, open, forkSession, workspaces, archivedSessionIds, workspaceReady, usePanelInfo, onRenameRequest, onDeleteRequest, onSessionRename, onSessionArchive, insertWorkspaceBefore, insertSessionBefore, orderBy, groupExpansion, setGroupExpanded, sessionOrderByAccount, sessionUpdatedAtByAccount, syncSessionOrderAccount, setSessionOrder, home, t, renderSlot, revealSessionId, onSessionRevealed }) {",
|
|
76
|
+
],
|
|
77
|
+
[
|
|
78
|
+
"SessionTree call site",
|
|
79
|
+
"(0, react_jsx_runtime.jsx)(SessionTree, {\n\t\t\t\t\t\t\tusePanelInfo,",
|
|
80
|
+
"(0, react_jsx_runtime.jsx)(SessionTree, {\n\t\t\t\t\t\t\trenderSlot,\n\t\t\t\t\t\t\tusePanelInfo,",
|
|
81
|
+
],
|
|
82
|
+
// --- 5. Thread renderSlot + the owning workspace id from the TREE call site only ---
|
|
83
|
+
[
|
|
84
|
+
"SessionNodeItem call site",
|
|
85
|
+
"\t\t\t\t\t\t\t\t\t\treturn (0, react_jsx_runtime.jsx)(SessionNodeItem, {\n\t\t\t\t\t\t\t\t\t\t\tnode,\n\t\t\t\t\t\t\t\t\t\t\tcurrentId: current,",
|
|
86
|
+
"\t\t\t\t\t\t\t\t\t\treturn (0, react_jsx_runtime.jsx)(SessionNodeItem, {\n\t\t\t\t\t\t\t\t\t\t\tnode,\n\t\t\t\t\t\t\t\t\t\t\tcurrentId: current,\n\t\t\t\t\t\t\t\t\t\t\trenderSlot,\n\t\t\t\t\t\t\t\t\t\t\t// only the tree knows which workspace a row belongs to; the\n\t\t\t\t\t\t\t\t\t\t\t// flat and search lists pass neither prop, so they stay bare\n\t\t\t\t\t\t\t\t\t\t\tworkspaceId: group.workspaceId,",
|
|
87
|
+
],
|
|
88
|
+
// --- 6. Declare the two seam children on the sidebar.workspaces registration ---
|
|
89
|
+
[
|
|
90
|
+
"seam children registration",
|
|
91
|
+
'\t\t\t\tchildren: { "sidebar.workspaces.directoryFlow": {\n\t\t\t\t\tkind: "single",\n\t\t\t\t\tscope: "root"\n\t\t\t\t} },',
|
|
92
|
+
'\t\t\t\tchildren: { "sidebar.workspaces.directoryFlow": {\n\t\t\t\t\tkind: "single",\n\t\t\t\t\tscope: "root"\n\t\t\t\t}, "sidebar.workspaces.sessionRow": {\n\t\t\t\t\tkind: "list",\n\t\t\t\t\tscope: "root"\n\t\t\t\t}, "sidebar.workspaces.sessionRow.detail": {\n\t\t\t\t\tkind: "list",\n\t\t\t\t\tscope: "root"\n\t\t\t\t} },',
|
|
93
|
+
],
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
export function anchorCounts(text) {
|
|
97
|
+
return Object.fromEntries(PATCHES.map(([name, old]) => [name, text.split(old).length - 1]));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function firstFailure(text) {
|
|
101
|
+
for (const [name, old] of PATCHES) {
|
|
102
|
+
const n = text.split(old).length - 1;
|
|
103
|
+
if (n !== 1) return { name, count: n };
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function applyPatch(text) {
|
|
109
|
+
for (const [name, old] of PATCHES) {
|
|
110
|
+
const n = text.split(old).length - 1;
|
|
111
|
+
if (n !== 1) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`anchor ${JSON.stringify(name)} found ${n} times (need exactly 1) — upstream drift; nothing was patched`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
for (const [, old, replacement] of PATCHES) text = text.replace(old, replacement);
|
|
118
|
+
return text;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function isOurs(text) {
|
|
122
|
+
return text.includes(MARKER);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function isCurrentRev(text) {
|
|
126
|
+
return text.includes(REV_COMMENT);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function seamPresent(text) {
|
|
130
|
+
return SEAM_STRINGS.some((s) => text.includes(s));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A patched file's applied signature (the `old` anchor forms are gone — that is the point). */
|
|
134
|
+
export function appliedSignature(text) {
|
|
135
|
+
return (
|
|
136
|
+
seamPresent(text) && text.includes("function renderSessionRowSeam") && text.includes("class SeamBoundary")
|
|
137
|
+
);
|
|
138
|
+
}
|
package/seam/apply.js
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* dsh-git-badge-seam — apply / revert / inspect the sidebar session-row seam
|
|
4
|
+
* patch on an installed DSH package.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS EXISTS: the sidebar session-row badges need a slot the DSH workspace
|
|
7
|
+
* browser does not declare upstream yet (proposed upstream — see PR.md in the
|
|
8
|
+
* plugin's repository). The patch is SEAM-ONLY; all badge logic lives in the
|
|
9
|
+
* dsh-git-badge plugin itself. This tool is shipped IN the package, so a market
|
|
10
|
+
* install needs no clone:
|
|
11
|
+
*
|
|
12
|
+
* dsh plugin --profile web add dsh-git-badge
|
|
13
|
+
* npx dsh-git-badge apply # then restart the dsh web process
|
|
14
|
+
*
|
|
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.
|
|
21
|
+
*
|
|
22
|
+
* Safety:
|
|
23
|
+
* - anchor assertion: every anchor must match exactly once; anything else is
|
|
24
|
+
* drift and NOTHING is written
|
|
25
|
+
* - own-artifact detection: every artifact carries the dsh-git-badge:seam-patch
|
|
26
|
+
* marker (with a rev number), so our stale patch can be upgraded in place
|
|
27
|
+
* while a genuine upstream landing (seam declared, no marker) is left alone
|
|
28
|
+
* - backup of the bytes as found: revert restores exactly what was installed
|
|
29
|
+
* before WE patched, guarded against downgrading a newer upstream build
|
|
30
|
+
* - the pinned KNOWN_GOOD_HASH is ADVISORY only: status reports whether the
|
|
31
|
+
* installed build is the one the anchors were verified against, but apply
|
|
32
|
+
* proceeds on any build whose anchors resolve
|
|
33
|
+
*
|
|
34
|
+
* Environment:
|
|
35
|
+
* DSH_INSTALL the DSH package root. Default: `<global npm root>/@deepseek-ai/dsh`.
|
|
36
|
+
* SEAM_DATA_DIR where revert backups are written. Default: this script's
|
|
37
|
+
* directory (inside the installed package).
|
|
38
|
+
*
|
|
39
|
+
* Usage: apply.js apply | apply.js revert | apply.js status
|
|
40
|
+
*/
|
|
41
|
+
import { execSync } from "node:child_process";
|
|
42
|
+
import { createHash } from "node:crypto";
|
|
43
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
44
|
+
import { dirname, join } from "node:path";
|
|
45
|
+
import { fileURLToPath } from "node:url";
|
|
46
|
+
import {
|
|
47
|
+
MARKER,
|
|
48
|
+
applyPatch,
|
|
49
|
+
appliedSignature,
|
|
50
|
+
firstFailure,
|
|
51
|
+
isCurrentRev,
|
|
52
|
+
isOurs,
|
|
53
|
+
seamPresent,
|
|
54
|
+
} from "./anchors.js";
|
|
55
|
+
|
|
56
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
57
|
+
const DATA_DIR = process.env.SEAM_DATA_DIR || HERE;
|
|
58
|
+
|
|
59
|
+
// sha256 of the upstream build the anchors were last verified against. ADVISORY:
|
|
60
|
+
// status prints it for orientation; apply does NOT require it.
|
|
61
|
+
const KNOWN_GOOD_HASH = "383b9ef779366c13d818500b6488896328b189f156addbaa480c835e902edd5f";
|
|
62
|
+
|
|
63
|
+
/** Locate the installed DSH package root; null when it cannot be found. */
|
|
64
|
+
function dshRoot() {
|
|
65
|
+
if (process.env.DSH_INSTALL) return process.env.DSH_INSTALL;
|
|
66
|
+
try {
|
|
67
|
+
return join(execSync("npm root -g", { encoding: "utf8" }).trim(), "@deepseek-ai", "dsh");
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const DSH = dshRoot();
|
|
74
|
+
if (!DSH) {
|
|
75
|
+
console.error("ERROR: cannot locate the DSH package. Set DSH_INSTALL to its root");
|
|
76
|
+
console.error("(e.g. the directory containing node_modules/@deepseek-ai/dsh).");
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
const PKG = join(DSH, "node_modules", "@deepseek-ai", "dsh-client-ui-workspace");
|
|
80
|
+
const CLIENT = join(PKG, "lib", "client.js");
|
|
81
|
+
const INDEX = join(PKG, "lib", "index.js");
|
|
82
|
+
const STUB_INDEX = join(HERE, "stub-index.js"); // no-op host half; copied over index.js on apply
|
|
83
|
+
const BACKUP_CLIENT = join(DATA_DIR, "backup-client.js");
|
|
84
|
+
const BACKUP_INDEX = join(DATA_DIR, "backup-index.js");
|
|
85
|
+
|
|
86
|
+
const sha256 = (text) => createHash("sha256").update(text, "utf8").digest("hex");
|
|
87
|
+
|
|
88
|
+
/** Inspect the installed client.js. Returns the same key:value report apply.sh printed. */
|
|
89
|
+
function inspect() {
|
|
90
|
+
const report = {};
|
|
91
|
+
let text;
|
|
92
|
+
try {
|
|
93
|
+
text = readClient();
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error.code === "ENOENT") {
|
|
96
|
+
report.state = "missing";
|
|
97
|
+
return report;
|
|
98
|
+
}
|
|
99
|
+
report.state = "unreadable";
|
|
100
|
+
report.error = String(error.message ?? error);
|
|
101
|
+
return report;
|
|
102
|
+
}
|
|
103
|
+
report.hash = sha256(text);
|
|
104
|
+
report.known_good_match = report.hash === KNOWN_GOOD_HASH ? "yes" : "no";
|
|
105
|
+
report.backup = existsSync(BACKUP_CLIENT) ? "yes" : "no";
|
|
106
|
+
report.marker = isOurs(text) ? "yes" : "no";
|
|
107
|
+
report.rev_current = isCurrentRev(text) ? "yes" : "no";
|
|
108
|
+
report.seam_strings = seamPresent(text) ? "yes" : "no";
|
|
109
|
+
|
|
110
|
+
const fail = firstFailure(text);
|
|
111
|
+
if (fail === null) report.anchors = "all-resolve";
|
|
112
|
+
else {
|
|
113
|
+
report.anchors = "drift";
|
|
114
|
+
report.anchor_fail = `${fail.name}:${fail.count}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (isOurs(text)) {
|
|
118
|
+
// A PATCHED file no longer contains the `old` anchor forms — that is the
|
|
119
|
+
// point — so it is verified by its applied signature instead.
|
|
120
|
+
report.applied_signature = appliedSignature(text) ? "yes" : "no";
|
|
121
|
+
if (!isCurrentRev(text)) report.state = "ours-stale";
|
|
122
|
+
else if (appliedSignature(text)) report.state = "ours-current";
|
|
123
|
+
else report.state = "ours-corrupt";
|
|
124
|
+
} else if (seamPresent(text)) report.state = "upstream-landed";
|
|
125
|
+
else if (fail === null) report.state = "patchable";
|
|
126
|
+
else report.state = "drift";
|
|
127
|
+
return report;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function readClient() {
|
|
131
|
+
return readText(CLIENT);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function readText(path) {
|
|
135
|
+
return readFileSync(path, "utf8");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Backup the bytes AS FOUND, patch client.js in place, install the no-op host half. */
|
|
139
|
+
function doPatch() {
|
|
140
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
141
|
+
copyFileSync(CLIENT, BACKUP_CLIENT);
|
|
142
|
+
copyFileSync(INDEX, BACKUP_INDEX);
|
|
143
|
+
const patched = applyPatch(readClient());
|
|
144
|
+
writeText(CLIENT, patched);
|
|
145
|
+
copyFileSync(STUB_INDEX, INDEX);
|
|
146
|
+
console.log("patched lib/client.js in place (anchors applied); host half stubbed.");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function writeText(path, text) {
|
|
150
|
+
writeFileSync(path, text, "utf8");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const VERDICT_EXIT_1 = new Set(["drift", "unreadable", "missing", "ours-corrupt"]);
|
|
154
|
+
|
|
155
|
+
const verb = process.argv[2];
|
|
156
|
+
if (!verb || !["apply", "revert", "status"].includes(verb)) {
|
|
157
|
+
console.error("usage: apply.js apply|revert|status");
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (verb === "status") {
|
|
162
|
+
const r = inspect();
|
|
163
|
+
console.log(`installed: ${CLIENT}`);
|
|
164
|
+
console.log(`hash: ${r.hash ?? r.error ?? "-"}`);
|
|
165
|
+
if (r.known_good_match === "yes") console.log("pinned: KNOWN-GOOD MATCH — the build the anchors were verified against");
|
|
166
|
+
else console.log(`pinned: ${KNOWN_GOOD_HASH} — differs (advisory only; anchors are the guard)`);
|
|
167
|
+
console.log(`anchors: ${r.anchors ?? "-"}`);
|
|
168
|
+
if (r.anchors === "drift") console.log(` ${r.anchor_fail}`);
|
|
169
|
+
console.log(`marker: ${r.marker ?? "-"} (rev current: ${r.rev_current ?? "-"}, applied signature: ${r.applied_signature ?? "n/a"})`);
|
|
170
|
+
console.log(`backup: ${r.backup ?? "-"}`);
|
|
171
|
+
console.log(`state: ${r.state}`);
|
|
172
|
+
switch (r.state) {
|
|
173
|
+
case "ours-current":
|
|
174
|
+
console.log("verdict: PATCHED — sidebar session-row badges come from this repo's patch.");
|
|
175
|
+
break;
|
|
176
|
+
case "ours-stale":
|
|
177
|
+
console.log("verdict: PATCHED, OUT OF DATE — our older artifact; 'apply' upgrades in place.");
|
|
178
|
+
break;
|
|
179
|
+
case "ours-corrupt":
|
|
180
|
+
console.log("verdict: BROKEN ARTIFACT — marker present but anchors/seam inconsistent. Inspect by hand.");
|
|
181
|
+
break;
|
|
182
|
+
case "upstream-landed":
|
|
183
|
+
console.log("verdict: UPSTREAM LANDED — 'apply' no-ops; the patch can be retired.");
|
|
184
|
+
break;
|
|
185
|
+
case "patchable":
|
|
186
|
+
console.log("verdict: PATCHABLE — 'apply' will patch it in place.");
|
|
187
|
+
break;
|
|
188
|
+
case "drift":
|
|
189
|
+
console.log("verdict: DRIFT — an anchor moved; update seam/anchors.js for the new upstream.");
|
|
190
|
+
break;
|
|
191
|
+
case "missing":
|
|
192
|
+
console.log("verdict: INSTALLED PACKAGE NOT FOUND — check DSH_INSTALL.");
|
|
193
|
+
break;
|
|
194
|
+
default:
|
|
195
|
+
console.log(`verdict: ${r.state.toUpperCase()} — see error above.`);
|
|
196
|
+
}
|
|
197
|
+
process.exit(VERDICT_EXIT_1.has(r.state) ? 1 : 0);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (verb === "apply") {
|
|
201
|
+
const r = inspect();
|
|
202
|
+
for (const [k, v] of Object.entries(r)) {
|
|
203
|
+
if (k !== "state") console.log(` ${k}=${v}`);
|
|
204
|
+
}
|
|
205
|
+
console.log(`installed: ${CLIENT}`);
|
|
206
|
+
switch (r.state) {
|
|
207
|
+
case "ours-current":
|
|
208
|
+
console.log("verdict: PATCHED (rev current) — nothing to do.");
|
|
209
|
+
break;
|
|
210
|
+
case "patchable":
|
|
211
|
+
doPatch();
|
|
212
|
+
console.log("verdict: PATCHED.");
|
|
213
|
+
break;
|
|
214
|
+
case "ours-stale": {
|
|
215
|
+
// Our older artifact. The re-patch target is the BACKUP (the bytes as we
|
|
216
|
+
// found them pre-patch). Validate THAT before touching the installed
|
|
217
|
+
// file: restoring a backup whose anchors no longer resolve would
|
|
218
|
+
// overwrite the installed build with an unpatchable older one.
|
|
219
|
+
if (!existsSync(BACKUP_CLIENT)) {
|
|
220
|
+
console.error("REFUSING: installed file is an older artifact of ours but no backup");
|
|
221
|
+
console.error("exists, so the original bytes are unknown. Reinstall the package or");
|
|
222
|
+
console.error("restore the backup by hand, then re-run apply.");
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
const backupText = readText(BACKUP_CLIENT);
|
|
226
|
+
const bstate = isOurs(backupText)
|
|
227
|
+
? "ours"
|
|
228
|
+
: seamPresent(backupText)
|
|
229
|
+
? "upstream-landed"
|
|
230
|
+
: firstFailure(backupText) === null
|
|
231
|
+
? "patchable"
|
|
232
|
+
: "drift";
|
|
233
|
+
if (bstate !== "patchable") {
|
|
234
|
+
console.error(`REFUSING: the backup (bytes as found before patching) is '${bstate}',`);
|
|
235
|
+
console.error("not patchable — the anchors no longer match the backed-up upstream.");
|
|
236
|
+
console.error("Nothing was changed. Reinstall the DSH package, then re-run apply.");
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
writeText(CLIENT, backupText);
|
|
240
|
+
copyFileSync(BACKUP_INDEX, INDEX);
|
|
241
|
+
doPatch();
|
|
242
|
+
console.log("verdict: UPGRADED — older artifact replaced with the current patch.");
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
case "upstream-landed":
|
|
246
|
+
console.error("verdict: UPSTREAM LANDED — the installed client.js declares the seam");
|
|
247
|
+
console.error("and is NOT this repo's artifact. Patch skipped (plugin only); the patch");
|
|
248
|
+
console.error("can be retired.");
|
|
249
|
+
break;
|
|
250
|
+
case "drift": {
|
|
251
|
+
const [name, count] = r.anchor_fail.split(":");
|
|
252
|
+
console.error("REFUSING: anchor drift — nothing was written.");
|
|
253
|
+
console.error(` anchor: ${name} (found ${count} times, need exactly 1)`);
|
|
254
|
+
console.error("Upstream changed code this patch anchors on. Update the matching entry");
|
|
255
|
+
console.error("in anchors.js, then re-run: dsh-git-badge-seam status && dsh-git-badge-seam apply");
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
case "ours-corrupt":
|
|
259
|
+
console.error("REFUSING: the installed file carries this repo's marker but is neither");
|
|
260
|
+
console.error("the current revision nor a restorable state. Inspect by hand:");
|
|
261
|
+
console.error(` grep -n '${MARKER}' "${CLIENT}"`);
|
|
262
|
+
process.exit(1);
|
|
263
|
+
break;
|
|
264
|
+
default:
|
|
265
|
+
console.error(`REFUSING: installed client.js is '${r.state}'.`);
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
console.log(`now install the plugin: dsh plugin --profile web add dsh-git-badge`);
|
|
269
|
+
} else {
|
|
270
|
+
// revert
|
|
271
|
+
if (existsSync(BACKUP_CLIENT)) {
|
|
272
|
+
const r = inspect();
|
|
273
|
+
const installedHash = r.hash;
|
|
274
|
+
const backupHash = sha256(readText(BACKUP_CLIENT));
|
|
275
|
+
if (installedHash === backupHash) {
|
|
276
|
+
// Idempotent re-run: the installed bytes already ARE the backup.
|
|
277
|
+
console.log("already reverted — installed client.js matches the backup; nothing to do.");
|
|
278
|
+
} else if (r.state === "ours-current" || r.state === "ours-stale" || r.state === "ours-corrupt") {
|
|
279
|
+
writeText(CLIENT, readText(BACKUP_CLIENT));
|
|
280
|
+
copyFileSync(BACKUP_INDEX, INDEX);
|
|
281
|
+
console.log("reverted client.js/index.js to the bytes as found before patching.");
|
|
282
|
+
} else {
|
|
283
|
+
// Installed file is not ours => upstream replaced it since patching.
|
|
284
|
+
console.error("REFUSING to revert: the installed client.js is not this repo's");
|
|
285
|
+
console.error(`artifact (state: ${r.state}), so upstream replaced it since the patch was`);
|
|
286
|
+
console.error("applied. Restoring the backup would DOWNGRADE the package.");
|
|
287
|
+
console.error("Nothing was changed. If the downgrade is really what you want, restore");
|
|
288
|
+
console.error(`${BACKUP_CLIENT} by hand.`);
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
} else {
|
|
292
|
+
const r = inspect();
|
|
293
|
+
if (r.state === "ours-current" || r.state === "ours-stale" || r.state === "ours-corrupt") {
|
|
294
|
+
console.error("REFUSING: the installed file is patched but no backup exists, so the");
|
|
295
|
+
console.error("original bytes are unknown. Reinstall the package to clear the patch:");
|
|
296
|
+
console.error(" npm rebuild -g @deepseek-ai/dsh # or your package manager's equivalent");
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
console.log("no backup present and nothing of ours installed; client.js left untouched.");
|
|
300
|
+
}
|
|
301
|
+
// restore the no-op host half if no backup of the original exists
|
|
302
|
+
if (!existsSync(BACKUP_INDEX)) copyFileSync(STUB_INDEX, INDEX);
|
|
303
|
+
console.log("remember to remove the plugin: dsh plugin --profile web remove dsh-git-badge");
|
|
304
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
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 };
|