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/package.json CHANGED
@@ -1,12 +1,11 @@
1
1
  {
2
2
  "name": "dsh-git-badge",
3
- "version": "0.19.0",
4
- "description": "Git status badges for DeepSeek Harness: an input-row chip on every install, plus sidebar session-row badges (git status, PR/CI action tokens) after one manual step: `npx dsh-git-badge apply`, then restart dsh web. Event-driven freshness over SSE.",
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/apply.js",
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/patch.js",
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 apply.js. The legacy bin name
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
- export const SKILL_NAME = "gh";
20
+ const SKILL_NAME = "gh";
21
21
 
22
22
  /** The user-level skills catalog — DSH_HOME honours the harness override. */
23
- export function skillsDir() {
23
+ function skillsDir() {
24
24
  return join(process.env.DSH_HOME || join(process.env.HOME || "", ".dsh"), "skills");
25
25
  }
26
26
 
27
- export function skillSource() {
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
- * Two out-of-band stores live here, both keyed files under dataDir():
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, rmSync, writeFileSync } from "node:fs";
11
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
34
12
  import { join } from "node:path";
35
13
 
36
- /** The stable backup store — same default apply.js uses; SEAM_DATA_DIR overrides. */
37
- export function dataDir() {
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
- writeFileSync(file, JSON.stringify({ schema: "dsh-git-badge/session-checkout/v1", sessions }), "utf8");
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 true;
79
+ if (!(sessionId in sessions)) return;
134
80
  delete sessions[sessionId];
135
- writeFileSync(
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
- return false;
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; `git push` when
34
- ahead; stage and commit when dirty (`git commit` if work is already staged,
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
- - **`/gh push`** — `git push`. If there is no upstream, `git push -u origin
40
- <branch>`. If the push is rejected as non-fast-forward, STOP and ask: offer
41
- `--force-with-lease` as the option, never plain `--force`, and never without
42
- an explicit yes.
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. Never invent categories. The older
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
- }