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/package.json
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-git-badge",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Git status badges for DeepSeek Harness: an input-row chip
|
|
3
|
+
"version": "0.20.1",
|
|
4
|
+
"description": "Git status badges for DeepSeek Harness: an input-row chip and a sidebar session-row action glyph, on every install — no patch and no manual step. Event-driven freshness over SSE.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"bin": {
|
|
8
|
-
"dsh-git-badge": "seam/
|
|
9
|
-
"dsh-git-badge-seam": "seam/apply.js",
|
|
8
|
+
"dsh-git-badge": "seam/cli.js",
|
|
10
9
|
"dsh-git-badge-checkout": "seam/checkout.js"
|
|
11
10
|
},
|
|
12
11
|
"exports": {
|
|
@@ -15,8 +14,7 @@
|
|
|
15
14
|
"./package.json": "./package.json"
|
|
16
15
|
},
|
|
17
16
|
"scripts": {
|
|
18
|
-
"test": "node --test"
|
|
19
|
-
"postinstall": "node seam/postinstall.js"
|
|
17
|
+
"test": "node --test"
|
|
20
18
|
},
|
|
21
19
|
"dsh": {
|
|
22
20
|
"bundle": {
|
|
@@ -53,14 +51,10 @@
|
|
|
53
51
|
"lib/client.js",
|
|
54
52
|
"lib/index.js",
|
|
55
53
|
"package.json",
|
|
56
|
-
"seam/anchors.js",
|
|
57
|
-
"seam/apply.js",
|
|
58
54
|
"seam/checkout.js",
|
|
59
|
-
"seam/
|
|
60
|
-
"seam/postinstall.js",
|
|
55
|
+
"seam/cli.js",
|
|
61
56
|
"seam/skill.js",
|
|
62
57
|
"seam/store.js",
|
|
63
|
-
"seam/stub-index.js",
|
|
64
58
|
"skill/gh/SKILL.md"
|
|
65
59
|
],
|
|
66
60
|
"dependencies": {
|
package/seam/checkout.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* npx dsh-git-badge checkout --clear forget this session
|
|
18
18
|
*
|
|
19
19
|
* (npx can only reach the bin named after the package; the checkout verb
|
|
20
|
-
* dispatches to this file from
|
|
20
|
+
* dispatches to this file from cli.js. The legacy bin name
|
|
21
21
|
* dsh-git-badge-checkout still works when invoked directly.)
|
|
22
22
|
*
|
|
23
23
|
* The path is validated as a real worktree of its repository before it is
|
package/seam/cli.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* dsh-git-badge CLI — the worktree-checkout registration command.
|
|
4
|
+
*
|
|
5
|
+
* The badge needs no patch, so this tool has ONE job: register the checkout a
|
|
6
|
+
* session is working in. That is the one fact DSH cannot supply — a session's
|
|
7
|
+
* cwd is immutable creation metadata and is always the main checkout, so a
|
|
8
|
+
* session doing its work in a linked `git worktree` is invisible without an
|
|
9
|
+
* explicit registration.
|
|
10
|
+
*
|
|
11
|
+
* The plugin ships this file as the package-name bin, because npx only auto-runs
|
|
12
|
+
* a bin whose name matches the package:
|
|
13
|
+
*
|
|
14
|
+
* npx dsh-git-badge checkout <path>
|
|
15
|
+
* npx dsh-git-badge checkout --clear
|
|
16
|
+
*
|
|
17
|
+
* The `dsh-git-badge-checkout` bin invokes checkout.js directly, for callers
|
|
18
|
+
* that want the verb-less form. The `/gh` skill is installed by the plugin
|
|
19
|
+
* itself at boot (see lib/index.js), not here.
|
|
20
|
+
*
|
|
21
|
+
* Usage: dsh-git-badge checkout <path|--clear>
|
|
22
|
+
*/
|
|
23
|
+
const verb = process.argv[2];
|
|
24
|
+
if (verb === "checkout") {
|
|
25
|
+
// Strip the verb so checkout.js sees argv[2] = <path|--clear>.
|
|
26
|
+
process.argv.splice(2, 1);
|
|
27
|
+
await import("./checkout.js");
|
|
28
|
+
process.exit(0); // checkout's success path returns; every failure path exits inside
|
|
29
|
+
}
|
|
30
|
+
console.error("usage: dsh-git-badge checkout <path|--clear>");
|
|
31
|
+
process.exit(1);
|
package/seam/skill.js
CHANGED
|
@@ -17,14 +17,14 @@ import { fileURLToPath } from "node:url";
|
|
|
17
17
|
|
|
18
18
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
const SKILL_NAME = "gh";
|
|
21
21
|
|
|
22
22
|
/** The user-level skills catalog — DSH_HOME honours the harness override. */
|
|
23
|
-
|
|
23
|
+
function skillsDir() {
|
|
24
24
|
return join(process.env.DSH_HOME || join(process.env.HOME || "", ".dsh"), "skills");
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
function skillSource() {
|
|
28
28
|
return join(HERE, "..", "skill", "gh", "SKILL.md");
|
|
29
29
|
}
|
|
30
30
|
|
package/seam/store.js
CHANGED
|
@@ -1,83 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The session-checkout store — one keyed file under dataDir():
|
|
3
3
|
*
|
|
4
|
-
* restart-pending.json install/applied a host patch → the UI offers a restart
|
|
5
4
|
* session-checkouts.json which checkout a SESSION is working in — the one
|
|
6
5
|
* fact dsh cannot supply (a session's cwd is immutable
|
|
7
6
|
* creation metadata and always the main checkout, so a
|
|
8
7
|
* session working in a linked worktree is invisible
|
|
9
8
|
* without an explicit registration)
|
|
10
|
-
*
|
|
11
|
-
* The restart-pending marker — the bridge from install-time (node side) to the
|
|
12
|
-
* running UI.
|
|
13
|
-
*
|
|
14
|
-
* WHY IT EXISTS: dshmarket's pending-restart banner tracks its own package
|
|
15
|
-
* bookkeeping. A postinstall that patches the DSH HOST package
|
|
16
|
-
* (dsh-client-ui-workspace) mutates files outside that bookkeeping, so the
|
|
17
|
-
* market can complete "restart-free" while the host's composed client bundles
|
|
18
|
-
* no longer match the files on disk. The marker is the out-of-band signal:
|
|
19
|
-
*
|
|
20
|
-
* postinstall / apply / revert → write the marker (seam changed on disk)
|
|
21
|
-
* plugin node half, at boot → clear the marker (fresh boot composed the
|
|
22
|
-
* new bytes; the change is live)
|
|
23
|
-
* plugin client half → while the marker is served on the status
|
|
24
|
-
* response, offer a Restart button calling
|
|
25
|
-
* dshmarket's public v1 restart endpoint
|
|
26
|
-
*
|
|
27
|
-
* The hot-mount path is what makes this work on a FIRST market install: the
|
|
28
|
-
* freshly installed plugin's client half mounts into the running shell, reads
|
|
29
|
-
* the marker through the status response, and can restart the host — even
|
|
30
|
-
* though the process that installed the patch is gone.
|
|
31
9
|
*/
|
|
32
10
|
|
|
33
|
-
import { existsSync, mkdirSync, readFileSync,
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
34
12
|
import { join } from "node:path";
|
|
35
13
|
|
|
36
|
-
/** The stable
|
|
37
|
-
|
|
14
|
+
/** The stable store directory — SEAM_DATA_DIR overrides. */
|
|
15
|
+
function dataDir() {
|
|
38
16
|
if (process.env.SEAM_DATA_DIR) return process.env.SEAM_DATA_DIR;
|
|
39
17
|
return join(process.env.DSH_HOME || join(process.env.HOME || "", ".dsh"), "git-badge-seam");
|
|
40
18
|
}
|
|
41
19
|
|
|
42
|
-
function markerPath() {
|
|
43
|
-
return join(dataDir(), "restart-pending.json");
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Record that DSH files were changed on disk and a restart is needed to
|
|
48
|
-
* compose them. `reason` is a one-word actor (postinstall | apply | revert).
|
|
49
|
-
*/
|
|
50
|
-
export function writeRestartMarker(reason) {
|
|
51
|
-
try {
|
|
52
|
-
mkdirSync(dataDir(), { recursive: true });
|
|
53
|
-
writeFileSync(
|
|
54
|
-
markerPath(),
|
|
55
|
-
JSON.stringify({ schema: "dsh-git-badge/restart-pending/v1", by: reason, at: new Date().toISOString() }),
|
|
56
|
-
"utf8",
|
|
57
|
-
);
|
|
58
|
-
} catch {
|
|
59
|
-
// The marker is a UX nicety; never fail the operation that earned it.
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** The pending marker, or null. Never throws. */
|
|
64
|
-
export function readRestartMarker() {
|
|
65
|
-
try {
|
|
66
|
-
if (!existsSync(markerPath())) return null;
|
|
67
|
-
return JSON.parse(readFileSync(markerPath(), "utf8"));
|
|
68
|
-
} catch {
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** The change is live — a new boot composed it. Clear the pending state. */
|
|
74
|
-
export function clearRestartMarker() {
|
|
75
|
-
try {
|
|
76
|
-
rmSync(markerPath(), { force: true });
|
|
77
|
-
} catch {
|
|
78
|
-
// Absent is the goal; any error leaves the marker to be cleared later.
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
20
|
/** How long a session-checkout registration stays trustworthy. */
|
|
82
21
|
export const SESSION_CHECKOUT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
83
22
|
|
|
@@ -85,6 +24,13 @@ function sessionCheckoutsPath() {
|
|
|
85
24
|
return join(dataDir(), "session-checkouts.json");
|
|
86
25
|
}
|
|
87
26
|
|
|
27
|
+
const SCHEMA = "dsh-git-badge/session-checkout/v1";
|
|
28
|
+
|
|
29
|
+
/** Write the whole store in one place — the schema lives beside this call. */
|
|
30
|
+
function persist(sessions) {
|
|
31
|
+
writeFileSync(sessionCheckoutsPath(), JSON.stringify({ schema: SCHEMA, sessions }), "utf8");
|
|
32
|
+
}
|
|
33
|
+
|
|
88
34
|
/** Every registration, `{ sessionId: { path, at } }`. Never throws.
|
|
89
35
|
*
|
|
90
36
|
* `file` overrides the store path — the test seam lib/index.js uses (its
|
|
@@ -119,7 +65,7 @@ export function writeSessionCheckout(sessionId, path) {
|
|
|
119
65
|
mkdirSync(dataDir(), { recursive: true });
|
|
120
66
|
const sessions = readSessionCheckouts();
|
|
121
67
|
sessions[sessionId] = { path, at: new Date().toISOString() };
|
|
122
|
-
|
|
68
|
+
persist(sessions);
|
|
123
69
|
return { ok: true, file };
|
|
124
70
|
} catch (error) {
|
|
125
71
|
return { ok: false, file, error: String(error?.message ?? error), code: String(error?.code ?? "") };
|
|
@@ -130,15 +76,10 @@ export function writeSessionCheckout(sessionId, path) {
|
|
|
130
76
|
export function clearSessionCheckout(sessionId) {
|
|
131
77
|
try {
|
|
132
78
|
const sessions = readSessionCheckouts();
|
|
133
|
-
if (!(sessionId in sessions)) return
|
|
79
|
+
if (!(sessionId in sessions)) return;
|
|
134
80
|
delete sessions[sessionId];
|
|
135
|
-
|
|
136
|
-
sessionCheckoutsPath(),
|
|
137
|
-
JSON.stringify({ schema: "dsh-git-badge/session-checkout/v1", sessions }),
|
|
138
|
-
"utf8",
|
|
139
|
-
);
|
|
140
|
-
return true;
|
|
81
|
+
persist(sessions);
|
|
141
82
|
} catch {
|
|
142
|
-
|
|
83
|
+
// forgetting is best-effort: the registration expires on its own
|
|
143
84
|
}
|
|
144
85
|
}
|
package/skill/gh/SKILL.md
CHANGED
|
@@ -30,16 +30,27 @@ unstaged / untracked / unmerged), plus any in-progress operation marker
|
|
|
30
30
|
match wins: resume a paused operation (`git merge --continue`,
|
|
31
31
|
`git rebase --continue`, …; a squash merge concludes with `git commit`);
|
|
32
32
|
resolve unmerged files only WITH the user; `git pull --ff-only` when behind;
|
|
33
|
-
`git push -u origin <branch>` when there is no upstream
|
|
34
|
-
|
|
33
|
+
`git push -u origin <branch>` when there is no upstream OR the upstream names
|
|
34
|
+
a different branch (a feature branch still tracking `origin/main` is the common
|
|
35
|
+
case); `git push` when ahead of a same-named upstream; stage and commit when
|
|
36
|
+
dirty (`git commit` if work is already staged,
|
|
35
37
|
`git add -p && git commit` for unstaged edits, `git add -A && git commit`
|
|
36
38
|
only for untracked-only changes, which `add -p` cannot see); `gh pr checks
|
|
37
39
|
<n> --watch` when CI fails. Clean and synced → say so and stop.
|
|
38
40
|
|
|
39
|
-
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
Three first-match cases have no verb of their own and belong to the row's
|
|
42
|
+
triage: a pull request whose `mergeState` is `DIRTY` (base conflict),
|
|
43
|
+
`review` `CHANGES_REQUESTED` (address the review), or `review`
|
|
44
|
+
`REVIEW_REQUIRED` (wait for the reviewer). Report which PR it is and what
|
|
45
|
+
it needs; do not invent a command for it.
|
|
46
|
+
|
|
47
|
+
- **`/gh push`** — `git push` when the upstream shares the branch name. If there
|
|
48
|
+
is no upstream, OR the upstream names a different branch (a feature branch
|
|
49
|
+
tracking `origin/main` is the common case), run `git push -u origin <branch>`:
|
|
50
|
+
a bare `git push` is refused there under the default `push.default=simple`, and
|
|
51
|
+
`-u` publishes under the branch's own name and repairs the tracking. If the
|
|
52
|
+
push is rejected as non-fast-forward, STOP and ask: offer `--force-with-lease`
|
|
53
|
+
as the option, never plain `--force`, and never without an explicit yes.
|
|
43
54
|
|
|
44
55
|
- **`/gh pr`** — create a pull request for the current branch: push first if
|
|
45
56
|
needed, then `gh pr create` with a title from the branch's commits and a body
|
|
@@ -114,7 +125,9 @@ unstaged / untracked / unmerged), plus any in-progress operation marker
|
|
|
114
125
|
When the request comes through the agent instead, read the current value from
|
|
115
126
|
the settings document (`~/.dsh/settings.yaml`, the `git-badge:` section) and
|
|
116
127
|
change that `order` list — any subset, any order; unlisted categories rank
|
|
117
|
-
after in default order.
|
|
128
|
+
after in default order. The categories are `operation`, `unmerged`,
|
|
129
|
+
`conflict`, `review`, `sync`, `publish`, `merge`, `commit` and `checks`.
|
|
130
|
+
Never invent categories. The older
|
|
118
131
|
`~/.dsh/git-badge-next.json` is only a fallback for hosts without the
|
|
119
132
|
settings stack; do not edit it when the namespace is present. Typical
|
|
120
133
|
invocations: "/gh order commit first", "/gh order never suggest checks
|
package/seam/anchors.js
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
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
|
-
* ONE seam, not two (rev 3). Upstream 0.2.0-rc.2 threads `renderSlot` into
|
|
16
|
-
* SessionNodeItem and SessionHoverContent itself and declares its own
|
|
17
|
-
* `sidebar.session.row.hover` hover-card seat, so the badge's provenance line
|
|
18
|
-
* rides that upstream seat and needs no patch. The seat upstream still lacks is
|
|
19
|
-
* the always-visible row token after the title: these three anchors add it.
|
|
20
|
-
*
|
|
21
|
-
* Tabs matter: the target is a bundled, tab-indented file. The strings below
|
|
22
|
-
* use \t escapes so a reader cannot mistake them for spaces. When upstream
|
|
23
|
-
* moves an anchor, edit that entry's `old` (and `new`, if the insertion point
|
|
24
|
-
* changed) to the new upstream text — the drift report names which one.
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
export const MARKER = "dsh-git-badge:seam-patch";
|
|
28
|
-
// Bumped whenever the anchor set changes shape. An installed file carrying the
|
|
29
|
-
// MARKER without this revision is OUR older artifact: apply restores its backup
|
|
30
|
-
// and re-patches rather than refusing.
|
|
31
|
-
export const PATCH_REV = "3";
|
|
32
|
-
export const REV_COMMENT = `${MARKER} rev ${PATCH_REV}`;
|
|
33
|
-
|
|
34
|
-
// The seam strings upstream would declare if the patch were merged. Present in
|
|
35
|
-
// a file that is NOT ours => upstream landed; the patch is a no-op.
|
|
36
|
-
export const SEAM_STRINGS = [
|
|
37
|
-
'"sidebar.workspaces.sessionRow"',
|
|
38
|
-
'"sidebar.workspaces.sessionRow.detail"',
|
|
39
|
-
'"sidebar.workspaces.row"', // the pre-session-row seam, also upstream's if landed
|
|
40
|
-
];
|
|
41
|
-
|
|
42
|
-
export const PATCHES = [
|
|
43
|
-
// --- 1. Session-row seam helper, inserted with the row component's props ---
|
|
44
|
-
[
|
|
45
|
-
"SessionNodeItem header + seam helpers",
|
|
46
|
-
"\t\tfunction SessionNodeItem({ node, currentId, now, onOpen, onRenameRequest, renderSlot, onReveal, drag, t }) {",
|
|
47
|
-
"\t\t/* dsh-git-badge:seam-patch rev 3 — 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 body, so the boundary above ENCLOSES the renderSlot call. */\n\t\tfunction SessionRowSeam({ renderSlot, sessionId, label }) {\n\t\t\treturn renderSlot(\"sidebar.workspaces.sessionRow\", { sessionId, 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, 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.\n\t\t*/\n\t\tfunction renderSessionRowSeam(renderSlot, sessionId, label) {\n\t\t\tif (renderSlot === 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, label })\n\t\t\t});\n\t\t}\n\t\tfunction SessionNodeItem({ node, currentId, now, onOpen, onRenameRequest, renderSlot, onReveal, drag, t }) {"
|
|
48
|
-
],
|
|
49
|
-
// --- 2. The token sits with the title, before the time cluster ---
|
|
50
|
-
[
|
|
51
|
-
"title span + badge render",
|
|
52
|
-
"\t\t\t\t\t\t(0, react_jsx_runtime.jsx)(\"span\", {\n\t\t\t\t\t\t\tref: titleRef,\n\t\t\t\t\t\t\tclassName: Rows_module_css_default.title,\n\t\t\t\t\t\t\tonDoubleClick: row.blank ? void 0 : (e) => {\n\t\t\t\t\t\t\t\te.stopPropagation();\n\t\t\t\t\t\t\t\tonRenameRequest(node.id, row.title);\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tchildren: title\n\t\t\t\t\t\t}),",
|
|
53
|
-
"\t\t\t\t\t\t(0, react_jsx_runtime.jsx)(\"span\", {\n\t\t\t\t\t\t\tref: titleRef,\n\t\t\t\t\t\t\tclassName: Rows_module_css_default.title,\n\t\t\t\t\t\t\tonDoubleClick: row.blank ? void 0 : (e) => {\n\t\t\t\t\t\t\t\te.stopPropagation();\n\t\t\t\t\t\t\t\tonRenameRequest(node.id, row.title);\n\t\t\t\t\t\t\t},\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, title),"
|
|
54
|
-
],
|
|
55
|
-
// --- 3. Declare the one seam child on the sidebar.workspaces registration ---
|
|
56
|
-
[
|
|
57
|
-
"seam children registration",
|
|
58
|
-
"\t\t\t\t\t\"sidebar.workspaces.directoryFlow\": {\n\t\t\t\t\t\tkind: \"single\",\n\t\t\t\t\t\tscope: \"root\"\n\t\t\t\t\t},",
|
|
59
|
-
"\t\t\t\t\t\"sidebar.workspaces.directoryFlow\": {\n\t\t\t\t\t\tkind: \"single\",\n\t\t\t\t\t\tscope: \"root\"\n\t\t\t\t\t},\n\t\t\t\t\t\"sidebar.workspaces.sessionRow\": {\n\t\t\t\t\t\tkind: \"list\",\n\t\t\t\t\t\tscope: \"root\"\n\t\t\t\t\t},"
|
|
60
|
-
],
|
|
61
|
-
];
|
|
62
|
-
|
|
63
|
-
export function anchorCounts(text) {
|
|
64
|
-
return Object.fromEntries(PATCHES.map(([name, old]) => [name, text.split(old).length - 1]));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function firstFailure(text) {
|
|
68
|
-
for (const [name, old] of PATCHES) {
|
|
69
|
-
const n = text.split(old).length - 1;
|
|
70
|
-
if (n !== 1) return { name, count: n };
|
|
71
|
-
}
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function applyPatch(text) {
|
|
76
|
-
for (const [name, old] of PATCHES) {
|
|
77
|
-
const n = text.split(old).length - 1;
|
|
78
|
-
if (n !== 1) {
|
|
79
|
-
throw new Error(
|
|
80
|
-
`anchor ${JSON.stringify(name)} found ${n} times (need exactly 1) — upstream drift; nothing was patched`
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
for (const [, old, replacement] of PATCHES) text = text.replace(old, replacement);
|
|
85
|
-
return text;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function isOurs(text) {
|
|
89
|
-
return text.includes(MARKER);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export function isCurrentRev(text) {
|
|
93
|
-
return text.includes(REV_COMMENT);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function seamPresent(text) {
|
|
97
|
-
return SEAM_STRINGS.some((s) => text.includes(s));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** A patched file's applied signature (the `old` anchor forms are gone — that is the point). */
|
|
101
|
-
export function appliedSignature(text) {
|
|
102
|
-
return (
|
|
103
|
-
seamPresent(text) && text.includes("function renderSessionRowSeam") && text.includes("class SeamBoundary")
|
|
104
|
-
);
|
|
105
|
-
}
|
package/seam/apply.js
DELETED
|
@@ -1,243 +0,0 @@
|
|
|
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; 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.
|
|
20
|
-
*
|
|
21
|
-
* Safety (enforced in patch.js + anchors.js, surfaced here):
|
|
22
|
-
* - anchor assertion: every anchor must match exactly once; anything else is
|
|
23
|
-
* drift and NOTHING is written
|
|
24
|
-
* - own-artifact detection: every artifact carries the dsh-git-badge:seam-patch
|
|
25
|
-
* marker (with a rev number), so our stale patch can be upgraded in place
|
|
26
|
-
* while a genuine upstream landing (seam declared, no marker) is left alone
|
|
27
|
-
* - backup of the bytes as found: revert restores exactly what was installed
|
|
28
|
-
* before WE patched, guarded against downgrading a newer upstream build
|
|
29
|
-
* - the pinned KNOWN_GOOD_HASH is ADVISORY only: status reports whether the
|
|
30
|
-
* installed build is the one the anchors were verified against, but apply
|
|
31
|
-
* proceeds on any build whose anchors resolve
|
|
32
|
-
*
|
|
33
|
-
* Environment (read by patch.js):
|
|
34
|
-
* DSH_INSTALL the DSH package root. Default: `<global npm root>/@deepseek-ai/dsh`.
|
|
35
|
-
* SEAM_DATA_DIR where revert backups are written. Default:
|
|
36
|
-
* `$DSH_HOME/git-badge-seam` (else `~/.dsh/git-badge-seam`) —
|
|
37
|
-
* a stable, user-level store shared by every way this tool
|
|
38
|
-
* can run. See seam/store.js for the restart marker.
|
|
39
|
-
*
|
|
40
|
-
* The package also declares this file under the package's own name
|
|
41
|
-
* (bin "dsh-git-badge"), because npx only auto-runs a bin whose name matches
|
|
42
|
-
* the package name: `npx dsh-git-badge apply` resolves, while
|
|
43
|
-
* `npx dsh-git-badge-seam` 404s (no PACKAGE carries that name). The alias
|
|
44
|
-
* multiplexes the checkout bin too — npx cannot reach a second bin by name,
|
|
45
|
-
* so the checkout verb dispatches here:
|
|
46
|
-
*
|
|
47
|
-
* Usage: npx dsh-git-badge apply | revert | status | checkout <path|--clear>
|
|
48
|
-
* (equivalently apply.js <verb>; checkout delegates to checkout.js)
|
|
49
|
-
*/
|
|
50
|
-
import { copyFileSync, existsSync } from "node:fs";
|
|
51
|
-
import { MARKER } from "./anchors.js";
|
|
52
|
-
import { backupState, doPatch, doRestore, installPaths, inspect, KNOWN_GOOD_HASH, revertPlan } from "./patch.js";
|
|
53
|
-
import { installSkill } from "./skill.js";
|
|
54
|
-
import { writeRestartMarker } from "./store.js";
|
|
55
|
-
|
|
56
|
-
const VERDICT_EXIT_1 = new Set(["drift", "unreadable", "missing", "ours-corrupt"]);
|
|
57
|
-
|
|
58
|
-
const verb = process.argv[2];
|
|
59
|
-
if (verb === "checkout") {
|
|
60
|
-
// alias-bin multiplex: strip the verb so checkout.js sees argv[2] = <path|--clear>
|
|
61
|
-
process.argv.splice(2, 1);
|
|
62
|
-
await import("./checkout.js");
|
|
63
|
-
process.exit(0); // checkout's success path returns; every failure path exits inside
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const paths = installPaths();
|
|
67
|
-
if (paths === null) {
|
|
68
|
-
console.error("ERROR: cannot locate the DSH package. Set DSH_INSTALL to its root");
|
|
69
|
-
console.error("(e.g. the directory containing node_modules/@deepseek-ai/dsh).");
|
|
70
|
-
process.exit(1);
|
|
71
|
-
}
|
|
72
|
-
const CLIENT = paths.client;
|
|
73
|
-
|
|
74
|
-
if (!verb || !["apply", "revert", "status"].includes(verb)) {
|
|
75
|
-
console.error("usage: dsh-git-badge apply|revert|status (or: dsh-git-badge checkout <path|--clear>)");
|
|
76
|
-
process.exit(1);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// keep the /gh skill in step with the package on every patcher run
|
|
80
|
-
console.log(`[dsh-git-badge] gh skill: ${installSkill()}`);
|
|
81
|
-
|
|
82
|
-
if (verb === "status") {
|
|
83
|
-
const r = inspect(paths);
|
|
84
|
-
console.log(`installed: ${CLIENT}`);
|
|
85
|
-
console.log(`hash: ${r.hash ?? r.error ?? "-"}`);
|
|
86
|
-
if (r.known_good_match === "yes") console.log("pinned: KNOWN-GOOD MATCH — the build the anchors were verified against");
|
|
87
|
-
else console.log(`pinned: ${KNOWN_GOOD_HASH} — differs (advisory only; anchors are the guard)`);
|
|
88
|
-
console.log(`anchors: ${r.anchors ?? "-"}`);
|
|
89
|
-
if (r.anchors === "drift") console.log(` ${r.anchor_fail}`);
|
|
90
|
-
console.log(`marker: ${r.marker ?? "-"} (rev current: ${r.rev_current ?? "-"}, applied signature: ${r.applied_signature ?? "n/a"})`);
|
|
91
|
-
console.log(`backup: ${r.backup ?? "-"}`);
|
|
92
|
-
console.log(`state: ${r.state}`);
|
|
93
|
-
switch (r.state) {
|
|
94
|
-
case "ours-current":
|
|
95
|
-
console.log("verdict: PATCHED — sidebar session-row badges come from this repo's patch.");
|
|
96
|
-
break;
|
|
97
|
-
case "ours-stale":
|
|
98
|
-
console.log("verdict: PATCHED, OUT OF DATE — our older artifact; 'apply' upgrades in place.");
|
|
99
|
-
break;
|
|
100
|
-
case "ours-corrupt":
|
|
101
|
-
console.log("verdict: BROKEN ARTIFACT — marker present but anchors/seam inconsistent. Inspect by hand.");
|
|
102
|
-
break;
|
|
103
|
-
case "upstream-landed":
|
|
104
|
-
console.log("verdict: UPSTREAM LANDED — 'apply' no-ops; the patch can be retired.");
|
|
105
|
-
break;
|
|
106
|
-
case "patchable":
|
|
107
|
-
console.log("verdict: PATCHABLE — 'apply' will patch it in place.");
|
|
108
|
-
break;
|
|
109
|
-
case "drift":
|
|
110
|
-
console.log("verdict: DRIFT — an anchor moved; update seam/anchors.js for the new upstream.");
|
|
111
|
-
break;
|
|
112
|
-
case "missing":
|
|
113
|
-
console.log("verdict: INSTALLED PACKAGE NOT FOUND — check DSH_INSTALL.");
|
|
114
|
-
break;
|
|
115
|
-
default:
|
|
116
|
-
console.log(`verdict: ${r.state.toUpperCase()} — see error above.`);
|
|
117
|
-
}
|
|
118
|
-
// Whether `revert` would act or refuse, reported BEFORE anyone needs it. The
|
|
119
|
-
// refusal can be about a backup that lives in a different store than the one
|
|
120
|
-
// being inspected, so revert time is the wrong moment to learn about it.
|
|
121
|
-
const plan = revertPlan(paths);
|
|
122
|
-
const revertLine =
|
|
123
|
-
plan.action === "refuse"
|
|
124
|
-
? `would REFUSE — ${plan.reason}`
|
|
125
|
-
: plan.action === "noop"
|
|
126
|
-
? `nothing to do — ${plan.reason}`
|
|
127
|
-
: `would restore the bytes as found — ${plan.reason}`;
|
|
128
|
-
console.log(`revert: ${revertLine}`);
|
|
129
|
-
process.exit(VERDICT_EXIT_1.has(r.state) ? 1 : 0);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (verb === "apply") {
|
|
133
|
-
const r = inspect(paths);
|
|
134
|
-
for (const [k, v] of Object.entries(r)) {
|
|
135
|
-
if (k !== "state") console.log(` ${k}=${v}`);
|
|
136
|
-
}
|
|
137
|
-
console.log(`installed: ${CLIENT}`);
|
|
138
|
-
switch (r.state) {
|
|
139
|
-
case "ours-current":
|
|
140
|
-
console.log("verdict: PATCHED (rev current) — nothing to do.");
|
|
141
|
-
break;
|
|
142
|
-
case "patchable":
|
|
143
|
-
doPatch(paths, "apply");
|
|
144
|
-
console.log("patched lib/client.js in place (anchors applied); host half stubbed.");
|
|
145
|
-
console.log("verdict: PATCHED.");
|
|
146
|
-
break;
|
|
147
|
-
case "ours-stale": {
|
|
148
|
-
// Our older artifact. The re-patch target is the BACKUP (the bytes as we
|
|
149
|
-
// found them pre-patch). Validate THAT before touching the installed
|
|
150
|
-
// file: restoring a backup whose anchors no longer resolve would
|
|
151
|
-
// overwrite the installed build with an unpatchable older one.
|
|
152
|
-
if (!existsSync(paths.backupClient)) {
|
|
153
|
-
console.error("REFUSING: installed file is an older artifact of ours but no backup");
|
|
154
|
-
console.error("exists, so the original bytes are unknown. Reinstall the package or");
|
|
155
|
-
console.error("restore the backup by hand, then re-run apply.");
|
|
156
|
-
process.exit(1);
|
|
157
|
-
}
|
|
158
|
-
const bstate = backupState(paths);
|
|
159
|
-
if (bstate !== "patchable") {
|
|
160
|
-
console.error(`REFUSING: the backup (bytes as found before patching) is '${bstate}',`);
|
|
161
|
-
console.error("not patchable — the anchors no longer match the backed-up upstream.");
|
|
162
|
-
console.error("Nothing was changed. Reinstall the DSH package, then re-run apply.");
|
|
163
|
-
process.exit(1);
|
|
164
|
-
}
|
|
165
|
-
doRestore(paths);
|
|
166
|
-
doPatch(paths, "apply");
|
|
167
|
-
console.log("patched lib/client.js in place (anchors applied); host half stubbed.");
|
|
168
|
-
console.log("verdict: UPGRADED — older artifact replaced with the current patch.");
|
|
169
|
-
break;
|
|
170
|
-
}
|
|
171
|
-
case "upstream-landed":
|
|
172
|
-
console.error("verdict: UPSTREAM LANDED — the installed client.js declares the seam");
|
|
173
|
-
console.error("and is NOT this repo's artifact. Patch skipped (plugin only); the patch");
|
|
174
|
-
console.error("can be retired.");
|
|
175
|
-
break;
|
|
176
|
-
case "drift": {
|
|
177
|
-
const [name, count] = r.anchor_fail.split(":");
|
|
178
|
-
console.error("REFUSING: anchor drift — nothing was written.");
|
|
179
|
-
console.error(` anchor: ${name} (found ${count} times, need exactly 1)`);
|
|
180
|
-
console.error("Upstream changed code this patch anchors on. Update the matching entry");
|
|
181
|
-
console.error("in anchors.js, then re-run: dsh-git-badge status && dsh-git-badge apply");
|
|
182
|
-
process.exit(1);
|
|
183
|
-
}
|
|
184
|
-
case "ours-corrupt":
|
|
185
|
-
console.error("REFUSING: the installed file carries this repo's marker but is neither");
|
|
186
|
-
console.error("the current revision nor a restorable state. Inspect by hand:");
|
|
187
|
-
console.error(` grep -n '${MARKER}' "${CLIENT}"`);
|
|
188
|
-
process.exit(1);
|
|
189
|
-
break;
|
|
190
|
-
default:
|
|
191
|
-
console.error(`REFUSING: installed client.js is '${r.state}'.`);
|
|
192
|
-
process.exit(1);
|
|
193
|
-
}
|
|
194
|
-
console.log(`now install the plugin: dsh plugin --profile web add dsh-git-badge`);
|
|
195
|
-
} else {
|
|
196
|
-
// revert — ONE decision (revertPlan), shared with the `status` report, so the
|
|
197
|
-
// refusal a user is warned about is the refusal they actually get.
|
|
198
|
-
const plan = revertPlan(paths);
|
|
199
|
-
const r = inspect(paths);
|
|
200
|
-
const ours = r.state === "ours-current" || r.state === "ours-stale" || r.state === "ours-corrupt";
|
|
201
|
-
const hasBackup = existsSync(paths.backupClient);
|
|
202
|
-
|
|
203
|
-
if (plan.action === "restore") {
|
|
204
|
-
doRestore(paths);
|
|
205
|
-
console.log("reverted client.js/index.js to the bytes as found before patching.");
|
|
206
|
-
// A revert also changes bytes the running bundles were composed from.
|
|
207
|
-
writeRestartMarker("revert");
|
|
208
|
-
} else if (plan.action === "noop") {
|
|
209
|
-
// Idempotent re-run: the installed bytes already ARE the backup.
|
|
210
|
-
console.log(`already reverted — ${plan.reason}; nothing to do.`);
|
|
211
|
-
} else if (hasBackup && ours) {
|
|
212
|
-
// The dangerous case: WE patched this build, but the backup beside it is
|
|
213
|
-
// from another upstream generation. Restoring it downgrades the package.
|
|
214
|
-
console.error("REFUSING to revert: the backup in this store is not a build this patch");
|
|
215
|
-
console.error(`targets — ${plan.reason}.`);
|
|
216
|
-
console.error("Restoring it would replace the installed build with an older upstream that the");
|
|
217
|
-
console.error("current anchors cannot re-apply. Nothing was changed.");
|
|
218
|
-
console.error("Revert from the store that patched this build instead — its `status` prints");
|
|
219
|
-
console.error("`revert: would restore ...` — or reinstall the DSH package.");
|
|
220
|
-
process.exit(1);
|
|
221
|
-
} else if (hasBackup) {
|
|
222
|
-
// Installed file is not ours => upstream replaced it since patching.
|
|
223
|
-
console.error("REFUSING to revert: the installed client.js is not this repo's");
|
|
224
|
-
console.error(`artifact (state: ${r.state}). Restoring the backup would DOWNGRADE the`);
|
|
225
|
-
console.error("package. Nothing was changed. If the downgrade is really what you want,");
|
|
226
|
-
console.error(`restore ${paths.backupClient} by hand.`);
|
|
227
|
-
process.exit(1);
|
|
228
|
-
} else if (ours) {
|
|
229
|
-
console.error("REFUSING: the installed file is patched but no backup exists in");
|
|
230
|
-
console.error("the seam data dir, so the original bytes are unknown here. Options:");
|
|
231
|
-
console.error(" 1. If you applied from a clone of the plugin's repository, run its");
|
|
232
|
-
console.error(" wrapper instead — it keeps its own backup:");
|
|
233
|
-
console.error(" <clone>/seam/apply.sh revert");
|
|
234
|
-
console.error(" 2. Otherwise reinstall the package — pristine files, guaranteed:");
|
|
235
|
-
console.error(" npm i -g @deepseek-ai/dsh@latest");
|
|
236
|
-
process.exit(1);
|
|
237
|
-
} else {
|
|
238
|
-
console.log("no backup present and nothing of ours installed; client.js left untouched.");
|
|
239
|
-
}
|
|
240
|
-
// restore the no-op host half if no backup of the original exists
|
|
241
|
-
if (!existsSync(paths.backupIndex)) copyFileSync(paths.stub, paths.index);
|
|
242
|
-
console.log("remember to remove the plugin: dsh plugin --profile web remove dsh-git-badge");
|
|
243
|
-
}
|