dsh-git-badge 0.17.4 → 0.19.0
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 +75 -0
- package/README.md +1 -1
- package/lib/client.js +95 -26
- package/lib/index.js +11 -0
- package/package.json +1 -1
- package/seam/anchors.js +15 -48
- package/seam/apply.js +54 -35
- package/seam/checkout.js +13 -2
- package/seam/patch.js +40 -1
- package/seam/store.js +12 -8
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,81 @@ npm. 0.6.0 is the first release recorded here.
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.19.0] - 2026-10-12
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **`revert` refuses a backup from a different upstream generation.** `apply`
|
|
18
|
+
patches whichever `SEAM_DATA_DIR` it is pointed at, so the shipped default
|
|
19
|
+
(`$DSH_HOME/git-badge-seam`) and this repo's wrapper (`seam/`) each keep their
|
|
20
|
+
own backup — and applying through one leaves the other's behind. After a DSH
|
|
21
|
+
update that stale backup holds an OLDER upstream, and `revert` used to restore
|
|
22
|
+
it blindly: a silent downgrade of the installed package. `revert` now validates
|
|
23
|
+
the backup before restoring it — the current anchors must resolve against it —
|
|
24
|
+
and refuses with a clear message, changing nothing.
|
|
25
|
+
- **`status` reports what `revert` would do**, under a `revert:` line: restore,
|
|
26
|
+
no-op, or REFUSE and why. SEAM.md had documented that line from the beginning;
|
|
27
|
+
it was never implemented, so the refusal could arrive as a surprise at the one
|
|
28
|
+
moment it was least welcome.
|
|
29
|
+
- **A worktree registration that cannot be written now says WHY.** The store
|
|
30
|
+
swallowed the cause, so `npx dsh-git-badge checkout <path>` reported only
|
|
31
|
+
"could not write the registration" — no path, no reason, no recovery — and the
|
|
32
|
+
next reader had to rediscover that a file sandbox or a read-only home can
|
|
33
|
+
refuse the write. It now names the target file, the real errno, and the three
|
|
34
|
+
ways out.
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- **The chip's hover card names the linked worktrees a session is NOT
|
|
39
|
+
following.** That state used to be invisible: a registration the sandbox
|
|
40
|
+
refused left a badge describing the session's own checkout, which looks exactly
|
|
41
|
+
like a session with nothing to follow, while the agent's work sat in a tree the
|
|
42
|
+
UI never mentioned. The card now reads
|
|
43
|
+
`2 linked · none registered — npx dsh-git-badge checkout <path>`. It rides
|
|
44
|
+
`detail=1` like the rest of the card's extras, so nothing at rest pays for the
|
|
45
|
+
extra `git worktree list`.
|
|
46
|
+
|
|
47
|
+
## [0.18.0] - 2026-10-12
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- **The sidebar seam patch works again on DSH 0.2.0-rc.2.** That release rewrote
|
|
52
|
+
`dsh-client-ui-workspace/lib/client.js` (2821 → 4425 lines) and moved 8 of the
|
|
53
|
+
9 anchors the patcher pinned, so `npx dsh-git-badge status` reported `drift`
|
|
54
|
+
and `apply` refused to write. The anchor set is rewritten for the new upstream:
|
|
55
|
+
the patch is three anchors and **+48/−0** (was nine anchors and +74/−5).
|
|
56
|
+
|
|
57
|
+
### Changed
|
|
58
|
+
|
|
59
|
+
- **The session-row action token is a pill now.** It was grey text beside the
|
|
60
|
+
relative time, which made `push` — the one row that wants a decision — read as
|
|
61
|
+
metadata. It is now a severity-tinted pill with a leading glyph and a heavier
|
|
62
|
+
weight: `↑ push`, `↓ pull`, `✓ merge`, `✗ fix CI`, `! resolve`, `◆ review`.
|
|
63
|
+
Routine sync also moved off grey onto the app's business-blue token, so the
|
|
64
|
+
severity ramp is four steps (red, amber, green, blue) rather than three plus
|
|
65
|
+
silence. The glyph is `aria-hidden`, so a screen reader still reads the word
|
|
66
|
+
alone.
|
|
67
|
+
- **The patch is one seam now, not two.** DSH 0.2.0-rc.2 threads `renderSlot`
|
|
68
|
+
into `SessionNodeItem` and `SessionHoverContent` itself and declares
|
|
69
|
+
`sidebar.session.row.hover`, so the hover card's provenance line rides that
|
|
70
|
+
upstream seat. The patched `sidebar.workspaces.sessionRow.detail` seam, the
|
|
71
|
+
`SessionTree`/`SessionNodeItem` `renderSlot` threading, and the `workspaceId`
|
|
72
|
+
owner-share field are all retired. The one remaining patched seam is
|
|
73
|
+
`sidebar.workspaces.sessionRow`: the always-visible row token after the title,
|
|
74
|
+
which no upstream seat can host (`sidebar.workspaces.session.row.action` is
|
|
75
|
+
`display:none` until hover, and `sidebar.session.row.leading` is a 16px cell
|
|
76
|
+
that renders only while the row is idle).
|
|
77
|
+
- **The row token now shows in the flat grouping too.** The old patch passed
|
|
78
|
+
`workspaceId` from the tree call site only, which kept flat rows bare. Upstream
|
|
79
|
+
no longer passes a workspace id to `SessionNodeItem`, so the token renders
|
|
80
|
+
wherever a session row does. Search results are unaffected.
|
|
81
|
+
- **The hover-card provenance line works on an unpatched install.** It no longer
|
|
82
|
+
depends on this package's seam patch.
|
|
83
|
+
- `PATCH_REV` is now `3`, so an installed rev-2 artifact is recognised as stale
|
|
84
|
+
rather than skipped. Because the anchors moved with upstream's rewrite, a rev-2
|
|
85
|
+
install cannot be re-patched in place: `apply` refuses and tells you to
|
|
86
|
+
reinstall the DSH package first.
|
|
87
|
+
|
|
13
88
|
## [0.17.4] - 2026-09-18
|
|
14
89
|
|
|
15
90
|
### Fixed
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Git status badges for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek
|
|
|
6
6
|
- **Hover card** — rest a pointer on the chip for what won't fit: the file breakdown (`✎3` split into staged / unstaged / untracked), the last ten commits, and the stash. Fetched only on hover, so the everyday badge pays nothing for it. Works on every install.
|
|
7
7
|
- **PR / CI token** — `● main ↑0 ↓2 PR#142 ✗` when the branch has a GitHub pull request, read through your own `gh` CLI (the plugin never handles a credential). The token **links to the PR** — it opens in a new tab, and underlines when you hover or focus it. Invisible when `gh` cannot answer: not installed, not logged in, no GitHub remote, offline or rate-limited all render the unchanged chip, and a token with no URL to link to stays plain text.
|
|
8
8
|
- **Worktree-aware** — a conversation's directory is fixed when it is created, so a conversation that does its work in a linked `git worktree` would otherwise keep reporting the main checkout. The session registers its checkout (`npx dsh-git-badge checkout <path>`, run by the git-worktree and `/gh checkout` paths), and the badge describes **that worktree** instead — its branch, counts, tree mark and PR/CI token (`🌳 chore/global-skills-tiering ↑0 ↓2 PR#391 ✓` rather than `● main`). The hover card names the tree in a `worktree` row and adds a `checkout` row with the conversation's own directory, so the main checkout's branch and dirty state stay one hover away. Registrations are advisory: git's own worktree list validates the path, entries expire after 24h, and a workspace-targeted row always reports the checkout the registry owns.
|
|
9
|
-
- **Sidebar session-row action token** — each session row shows the ONE thing that conversation needs from you, or nothing at all: `merge`, `fix CI`, `review`, `resolve`, `pull`, `push`. It is triage rather than status: `merge` appears only when GitHub itself reports `mergeStateStatus: CLEAN`, and every waiting state (draft, blocked, behind, checks still running) stays silent. Hovering the row adds a line naming the checkout and the pull request the action refers to. The token floats to the right of the row, is set heavier than the timestamp beside it, and is coloured by how much it needs you — red for a conflict or a failing build, amber for a requested review, green for `merge`, quiet grey for routine sync. It appears only when the session is **idle** — while a turn is running the row stays quiet, because the checkout is still moving and a suggestion would describe a state that is about to change. Requires the `sidebar.workspaces.sessionRow` seam; without it the
|
|
9
|
+
- **Sidebar session-row action token** — each session row shows the ONE thing that conversation needs from you, or nothing at all: `merge`, `fix CI`, `review`, `resolve`, `pull`, `push`. It is triage rather than status: `merge` appears only when GitHub itself reports `mergeStateStatus: CLEAN`, and every waiting state (draft, blocked, behind, checks still running) stays silent. Hovering the row adds a line naming the checkout and the pull request the action refers to. The token floats to the right of the row, is set heavier than the timestamp beside it, and is coloured by how much it needs you — red for a conflict or a failing build, amber for a requested review, green for `merge`, quiet grey for routine sync. It appears only when the session is **idle** — while a turn is running the row stays quiet, because the checkout is still moving and a suggestion would describe a state that is about to change. Requires the `sidebar.workspaces.sessionRow` seam for the row token itself; without it the row stays bare and the plugin logs what's active to the browser console. The hover-card line does **not** need the patch — it renders through upstream's own `sidebar.session.row.hover` seat.
|
|
10
10
|
- **Nothing on workspace rows.** A workspace cannot know which worktree its conversations are using, so a per-workspace badge could only guess or report the main checkout. The seam is on the session row for that reason.
|
|
11
11
|
|
|
12
12
|
The leading **mark** belongs to the input chip: a circle filled with the status colour, or a **tree** when the checkout is a linked `git worktree`. The sidebar row deliberately carries no mark — see the row bullet above, where one word does the job a colour cannot. (`●`/`🌳` stand in for it here — it is a drawn SVG so that it can take the theme's status colours.)
|
package/lib/client.js
CHANGED
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
* and the PR/CI token, immediately after the session title. Deliberately shows
|
|
11
11
|
* NO branch and no worktree name: the row lists conversations, and which
|
|
12
12
|
* checkout the badge describes is the hover card's business.
|
|
13
|
-
* - sidebar.
|
|
14
|
-
* that checkout, rendered inside the card upstream ALREADY shows for a
|
|
15
|
-
* row rather than in a tooltip nested inside it.
|
|
13
|
+
* - sidebar.session.row.hover (upstream, no patch): the hover-card line that
|
|
14
|
+
* names that checkout, rendered inside the card upstream ALREADY shows for a
|
|
15
|
+
* session row rather than in a tooltip nested inside it.
|
|
16
16
|
* - conversation.input.left (upstream): chip — status mark + branch + a
|
|
17
17
|
* worktree's name + in-progress operation token + sync/dirty suffix + the
|
|
18
18
|
* PR/CI token, with a HOVER CARD carrying the breakdown the chip has no room
|
|
@@ -536,18 +536,35 @@ window.__ModuleLoader__.load({
|
|
|
536
536
|
/**
|
|
537
537
|
* Action -> colour. Colour here means SEVERITY — how much this needs you —
|
|
538
538
|
* rather than identity: a broken build and a conflict are the loud ones, a
|
|
539
|
-
* requested review is a nudge, `merge` is the all-clear, and routine sync
|
|
540
|
-
*
|
|
541
|
-
*
|
|
542
|
-
*
|
|
539
|
+
* requested review is a nudge, `merge` is the all-clear, and routine sync is
|
|
540
|
+
* the quiet end of the same ramp rather than outside it. That last move is
|
|
541
|
+
* deliberate: grey put `push` below the threshold of notice, so the one row
|
|
542
|
+
* that wanted a decision read as a timestamp. The WORD remains the channel
|
|
543
|
+
* (colour only reinforces it), and the tokens are the app's own, so both
|
|
544
|
+
* themes work.
|
|
543
545
|
*/
|
|
544
546
|
const ACTION_COLOUR = {
|
|
545
547
|
resolve: "var(--dsw-alias-state-error-primary, #e5484d)",
|
|
546
548
|
"fix CI": "var(--dsw-alias-state-error-primary, #e5484d)",
|
|
547
549
|
review: "var(--dsw-alias-state-warn-primary, #d29922)",
|
|
548
550
|
merge: "var(--dsw-alias-state-success-primary, #30a46c)",
|
|
549
|
-
pull: "var(--dsw-alias-
|
|
550
|
-
push: "var(--dsw-alias-
|
|
551
|
+
pull: "var(--dsw-alias-state-business-primary, #3b82f6)",
|
|
552
|
+
push: "var(--dsw-alias-state-business-primary, #3b82f6)"
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Action -> glyph. The SHAPE channel, beside the colour: a glyph makes the
|
|
557
|
+
* row readable before the word is parsed, and it survives a monochrome or
|
|
558
|
+
* colour-blind view where the tint alone would not. Keys mirror
|
|
559
|
+
* ACTION_COLOUR exactly; a missing entry degrades to the bare word.
|
|
560
|
+
*/
|
|
561
|
+
const ACTION_GLYPH = {
|
|
562
|
+
resolve: "!",
|
|
563
|
+
"fix CI": "\u2717",
|
|
564
|
+
review: "\u25C6",
|
|
565
|
+
merge: "\u2713",
|
|
566
|
+
pull: "\u2193",
|
|
567
|
+
push: "\u2191"
|
|
551
568
|
};
|
|
552
569
|
|
|
553
570
|
/**
|
|
@@ -556,10 +573,21 @@ window.__ModuleLoader__.load({
|
|
|
556
573
|
* the sidebar — that alignment is what makes the list scannable;
|
|
557
574
|
* - `marginRight` keeps it clear of the relative time beside it (the first cut
|
|
558
575
|
* sat flush and read `merge11m`);
|
|
559
|
-
* - weight
|
|
560
|
-
*
|
|
576
|
+
* - weight 600 plus a little padding make it a PILL, so the token reads as a
|
|
577
|
+
* tag rather than as loose text the eye can skip;
|
|
578
|
+
* - the radius is the app's own small one, so the pill belongs to the shell.
|
|
579
|
+
*
|
|
580
|
+
* The FILL is not set here: it is the action's own colour at low alpha, which
|
|
581
|
+
* only SessionGitBadge can build.
|
|
561
582
|
*/
|
|
562
|
-
const ACTION_STYLE = {
|
|
583
|
+
const ACTION_STYLE = {
|
|
584
|
+
flex: "none",
|
|
585
|
+
marginLeft: "auto",
|
|
586
|
+
marginRight: "8px",
|
|
587
|
+
fontWeight: 600,
|
|
588
|
+
padding: "0 6px",
|
|
589
|
+
borderRadius: "var(--dsw-radius-sm, 4px)"
|
|
590
|
+
};
|
|
563
591
|
|
|
564
592
|
/**
|
|
565
593
|
* The row's own run state, from the shell's standard kit.
|
|
@@ -598,9 +626,9 @@ window.__ModuleLoader__.load({
|
|
|
598
626
|
}
|
|
599
627
|
|
|
600
628
|
/**
|
|
601
|
-
* The action badge:
|
|
602
|
-
* only when the session is IDLE. A turn in flight is precisely when a
|
|
603
|
-
* suggestion is noise: the checkout is still moving and the agent has not
|
|
629
|
+
* The action badge: a severity-tinted pill carrying a glyph and the word — and
|
|
630
|
+
* shown only when the session is IDLE. A turn in flight is precisely when a
|
|
631
|
+
* git suggestion is noise: the checkout is still moving and the agent has not
|
|
604
632
|
* finished, so the action describes a state that is about to change. Nothing
|
|
605
633
|
* about the action itself changes; the gate is the whole feature.
|
|
606
634
|
*
|
|
@@ -616,17 +644,33 @@ window.__ModuleLoader__.load({
|
|
|
616
644
|
const action = actionToken(info);
|
|
617
645
|
if (action === "") return null;
|
|
618
646
|
const colour = ACTION_COLOUR[action];
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
647
|
+
const style = { ...META_STYLE, ...ACTION_STYLE };
|
|
648
|
+
if (colour !== void 0) {
|
|
649
|
+
style.color = colour;
|
|
650
|
+
// The pill's fill is the action's own colour at low alpha, so severity is
|
|
651
|
+
// a wash a row can carry rather than a shout. color-mix is a current-browser
|
|
652
|
+
// feature: where it is unsupported the pill keeps its geometry, colour,
|
|
653
|
+
// glyph and weight, and only the tint is lost.
|
|
654
|
+
style.background = `color-mix(in srgb, ${colour} 18%, transparent)`;
|
|
655
|
+
}
|
|
656
|
+
// The glyph is DECORATION: `aria-hidden` keeps the accessible name the bare
|
|
657
|
+
// word, and the trailing space in its own text separates the two on screen
|
|
658
|
+
// without a margin. A missing glyph entry degrades to the word alone.
|
|
659
|
+
const glyph = ACTION_GLYPH[action];
|
|
660
|
+
return react_jsx_runtime.jsxs("span", {
|
|
661
|
+
style,
|
|
662
|
+
children:
|
|
663
|
+
glyph === void 0
|
|
664
|
+
? action
|
|
665
|
+
: [react_jsx_runtime.jsx("span", { "aria-hidden": true, children: `${glyph} ` }, "glyph"), action]
|
|
622
666
|
});
|
|
623
667
|
}
|
|
624
668
|
|
|
625
669
|
/**
|
|
626
670
|
* Session-row hover-card line — everything the action word cannot say: which
|
|
627
671
|
* checkout the row's status describes, and which pull request the action
|
|
628
|
-
* refers to. It renders through the
|
|
629
|
-
* upstream already opens for a row.
|
|
672
|
+
* refers to. It renders through the upstream sidebar.session.row.hover
|
|
673
|
+
* seat, inside the card upstream already opens for a row.
|
|
630
674
|
*
|
|
631
675
|
* This matters MORE now that the row shows only an imperative: `merge` with no
|
|
632
676
|
* way to see merge WHAT would be a nag rather than a hint, so the line appears
|
|
@@ -716,6 +760,24 @@ window.__ModuleLoader__.load({
|
|
|
716
760
|
return name;
|
|
717
761
|
}
|
|
718
762
|
|
|
763
|
+
/**
|
|
764
|
+
* The `worktrees` row: how many LINKED worktrees this repository has while
|
|
765
|
+
* the badge follows none of them.
|
|
766
|
+
*
|
|
767
|
+
* It earns its place on one thing only — the failure it makes visible. A
|
|
768
|
+
* registration is written outside the repository, so a file sandbox or a
|
|
769
|
+
* read-only home can refuse it. When that happens the badge silently keeps
|
|
770
|
+
* describing the session's own checkout, and the agent's work sits in a tree
|
|
771
|
+
* the UI never names. That state is indistinguishable from a session with
|
|
772
|
+
* nothing to follow, so the card is the only place it can be said. The node
|
|
773
|
+
* half computes it under `detail=1` only, so nothing at rest pays for it.
|
|
774
|
+
*/
|
|
775
|
+
function linkedWorktreesDetail(info) {
|
|
776
|
+
const linked = info?.linkedWorktrees;
|
|
777
|
+
if (typeof linked !== "number" || linked <= 0) return void 0;
|
|
778
|
+
return linked + " linked \u00B7 none registered \u2014 npx dsh-git-badge checkout <path>";
|
|
779
|
+
}
|
|
780
|
+
|
|
719
781
|
/**
|
|
720
782
|
* ONE commit line, shared by the branch hover and the PR token's hover so
|
|
721
783
|
* the two cannot drift apart again: a fixed-width monospace hash cell, so
|
|
@@ -1032,6 +1094,11 @@ window.__ModuleLoader__.load({
|
|
|
1032
1094
|
// WHICH checkout the badge describes, when it is a worktree. The chip shows
|
|
1033
1095
|
// the branch alone, so this row is the only place the tree is named.
|
|
1034
1096
|
add("worktree", worktreeDetail(data));
|
|
1097
|
+
// The linked-but-unregistered state — the on-screen difference between
|
|
1098
|
+
// "nothing to follow" and "the registration could not be written". Rides
|
|
1099
|
+
// the detail payload, so it appears once the hover has fetched, exactly
|
|
1100
|
+
// like the `checkout` row below it.
|
|
1101
|
+
add("worktrees", linkedWorktreesDetail(data));
|
|
1035
1102
|
// The conversation's OWN directory, present only when the badge is
|
|
1036
1103
|
// describing a followed worktree — the chip's branch and counts are the
|
|
1037
1104
|
// tree's in that case, so this is where the checkout's own state stays
|
|
@@ -1765,11 +1832,13 @@ window.__ModuleLoader__.load({
|
|
|
1765
1832
|
}, SessionGitBadge);
|
|
1766
1833
|
});
|
|
1767
1834
|
// The row is ALREADY a HoverCard anchor upstream, so a badge that opened
|
|
1768
|
-
// its own tooltip would nest two cards. This
|
|
1769
|
-
//
|
|
1770
|
-
//
|
|
1771
|
-
|
|
1772
|
-
|
|
1835
|
+
// its own tooltip would nest two cards. This renders the provenance line
|
|
1836
|
+
// inside that card instead, through the hover seat upstream declares
|
|
1837
|
+
// itself (sidebar.session.row.hover) — no seam patch and no request of
|
|
1838
|
+
// its own. Upstream 0.2.0-rc.2 threads renderSlot into the card and adds
|
|
1839
|
+
// the seat; before that this needed a patched sessionRow.detail seam.
|
|
1840
|
+
ctx.slots.inject("sidebar.session.row.hover", () => ctx.slots.register({
|
|
1841
|
+
name: "sidebar.session.row.hover",
|
|
1773
1842
|
id: "git-badge-row-detail"
|
|
1774
1843
|
}, SessionGitDetail));
|
|
1775
1844
|
// Input-row chip: upstream additive slot rendered in the input bar's
|
|
@@ -1824,7 +1893,7 @@ window.__ModuleLoader__.load({
|
|
|
1824
1893
|
// Additive; the host reads apply/inject and ignores the rest. The suite
|
|
1825
1894
|
// drives these to assert the REQUEST contract — which surface asks for the
|
|
1826
1895
|
// expensive extras — without a browser, a fetch or a network.
|
|
1827
|
-
exports.__internals = { targetQuery, formatPrToken, formatFileBreakdown, formatPrDetail, formatCheckoutDetail, worktreeDetail, actionToken, ghSkillActions, cleanGate, moveCategory };
|
|
1896
|
+
exports.__internals = { targetQuery, formatPrToken, formatFileBreakdown, formatPrDetail, formatCheckoutDetail, worktreeDetail, linkedWorktreesDetail, actionToken, ghSkillActions, cleanGate, moveCategory };
|
|
1828
1897
|
return module.exports;
|
|
1829
1898
|
}
|
|
1830
1899
|
});
|
package/lib/index.js
CHANGED
|
@@ -1686,6 +1686,17 @@ function apply(ctx) {
|
|
|
1686
1686
|
}
|
|
1687
1687
|
}
|
|
1688
1688
|
}
|
|
1689
|
+
if (detail && info.git === true && session !== null && effective.worktree === void 0) {
|
|
1690
|
+
// No registration is in effect, so the badge describes the session's OWN
|
|
1691
|
+
// checkout. When this repository also has LINKED worktrees, that is the one
|
|
1692
|
+
// state worth naming: a registration write that failed (a file sandbox, a
|
|
1693
|
+
// read-only home) looks exactly like a session with nothing to follow, and
|
|
1694
|
+
// the agent's work then sits in a tree the badge never mentions. Gated on
|
|
1695
|
+
// detail=1 (a pointer resting on the chip), so nothing at rest pays for the
|
|
1696
|
+
// extra `git worktree list`.
|
|
1697
|
+
const { worktrees } = await worktreesFor(target.path);
|
|
1698
|
+
if (worktrees.length > 1) body.linkedWorktrees = worktrees.length - 1;
|
|
1699
|
+
}
|
|
1689
1700
|
// Echo the resolved workspace id. SSE events identify the workspace,
|
|
1690
1701
|
// not the session, so a session-targeted client has no other way to
|
|
1691
1702
|
// tell whether an event belongs to it — without this the input chip
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-git-badge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
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.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
package/seam/anchors.js
CHANGED
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
* that moves one of the anchors does — and the failure names the anchor that
|
|
13
13
|
* moved instead of reporting an opaque hash mismatch.
|
|
14
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
|
+
*
|
|
15
21
|
* Tabs matter: the target is a bundled, tab-indented file. The strings below
|
|
16
22
|
* use \t escapes so a reader cannot mistake them for spaces. When upstream
|
|
17
23
|
* moves an anchor, edit that entry's `old` (and `new`, if the insertion point
|
|
@@ -22,7 +28,7 @@ export const MARKER = "dsh-git-badge:seam-patch";
|
|
|
22
28
|
// Bumped whenever the anchor set changes shape. An installed file carrying the
|
|
23
29
|
// MARKER without this revision is OUR older artifact: apply restores its backup
|
|
24
30
|
// and re-patches rather than refusing.
|
|
25
|
-
export const PATCH_REV = "
|
|
31
|
+
export const PATCH_REV = "3";
|
|
26
32
|
export const REV_COMMENT = `${MARKER} rev ${PATCH_REV}`;
|
|
27
33
|
|
|
28
34
|
// The seam strings upstream would declare if the patch were merged. Present in
|
|
@@ -37,59 +43,20 @@ export const PATCHES = [
|
|
|
37
43
|
// --- 1. Session-row seam helper, inserted with the row component's props ---
|
|
38
44
|
[
|
|
39
45
|
"SessionNodeItem header + seam helpers",
|
|
40
|
-
"\t\tfunction SessionNodeItem({ node, currentId, now, onOpen,
|
|
41
|
-
"\t\t/* dsh-git-badge:seam-patch rev
|
|
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 }) {"
|
|
42
48
|
],
|
|
43
|
-
// --- 2. The
|
|
49
|
+
// --- 2. The token sits with the title, before the time cluster ---
|
|
44
50
|
[
|
|
45
51
|
"title span + badge render",
|
|
46
|
-
|
|
47
|
-
|
|
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,",
|
|
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),"
|
|
87
54
|
],
|
|
88
|
-
// ---
|
|
55
|
+
// --- 3. Declare the one seam child on the sidebar.workspaces registration ---
|
|
89
56
|
[
|
|
90
57
|
"seam children registration",
|
|
91
|
-
|
|
92
|
-
|
|
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},"
|
|
93
60
|
],
|
|
94
61
|
];
|
|
95
62
|
|
package/seam/apply.js
CHANGED
|
@@ -47,9 +47,9 @@
|
|
|
47
47
|
* Usage: npx dsh-git-badge apply | revert | status | checkout <path|--clear>
|
|
48
48
|
* (equivalently apply.js <verb>; checkout delegates to checkout.js)
|
|
49
49
|
*/
|
|
50
|
-
import { copyFileSync, existsSync
|
|
50
|
+
import { copyFileSync, existsSync } from "node:fs";
|
|
51
51
|
import { MARKER } from "./anchors.js";
|
|
52
|
-
import { backupState, doPatch, doRestore, installPaths, inspect, KNOWN_GOOD_HASH,
|
|
52
|
+
import { backupState, doPatch, doRestore, installPaths, inspect, KNOWN_GOOD_HASH, revertPlan } from "./patch.js";
|
|
53
53
|
import { installSkill } from "./skill.js";
|
|
54
54
|
import { writeRestartMarker } from "./store.js";
|
|
55
55
|
|
|
@@ -115,6 +115,17 @@ if (verb === "status") {
|
|
|
115
115
|
default:
|
|
116
116
|
console.log(`verdict: ${r.state.toUpperCase()} — see error above.`);
|
|
117
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}`);
|
|
118
129
|
process.exit(VERDICT_EXIT_1.has(r.state) ? 1 : 0);
|
|
119
130
|
}
|
|
120
131
|
|
|
@@ -182,40 +193,48 @@ if (verb === "apply") {
|
|
|
182
193
|
}
|
|
183
194
|
console.log(`now install the plugin: dsh plugin --profile web add dsh-git-badge`);
|
|
184
195
|
} else {
|
|
185
|
-
// revert
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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);
|
|
207
237
|
} else {
|
|
208
|
-
const r = inspect(paths);
|
|
209
|
-
if (r.state === "ours-current" || r.state === "ours-stale" || r.state === "ours-corrupt") {
|
|
210
|
-
console.error("REFUSING: the installed file is patched but no backup exists in");
|
|
211
|
-
console.error("the seam data dir, so the original bytes are unknown here. Options:");
|
|
212
|
-
console.error(" 1. If you applied from a clone of the plugin's repository, run its");
|
|
213
|
-
console.error(" wrapper instead — it keeps its own backup:");
|
|
214
|
-
console.error(" <clone>/seam/apply.sh revert");
|
|
215
|
-
console.error(" 2. Otherwise reinstall the package — pristine files, guaranteed:");
|
|
216
|
-
console.error(" npm i -g @deepseek-ai/dsh@latest");
|
|
217
|
-
process.exit(1);
|
|
218
|
-
}
|
|
219
238
|
console.log("no backup present and nothing of ours installed; client.js left untouched.");
|
|
220
239
|
}
|
|
221
240
|
// restore the no-op host half if no backup of the original exists
|
package/seam/checkout.js
CHANGED
|
@@ -77,8 +77,19 @@ if (!paths.includes(path)) {
|
|
|
77
77
|
process.exit(1);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
const written = writeSessionCheckout(sessionId, path);
|
|
81
|
+
if (written.ok !== true) {
|
|
82
|
+
// The failure the badge cannot show. Say the CAUSE and the three ways out, so
|
|
83
|
+
// this never reads as "the worktree has no changes" — which is exactly what a
|
|
84
|
+
// failed registration looks like from the UI.
|
|
85
|
+
console.error(`[dsh-git-badge] could not write the registration to ${written.file}`);
|
|
86
|
+
console.error(` reason: ${written.error}${written.code === "" ? "" : ` (${written.code})`}`);
|
|
87
|
+
console.error(" The badge therefore keeps describing THIS session's own checkout, so the");
|
|
88
|
+
console.error(" worktree's state will not appear. Pick one:");
|
|
89
|
+
console.error(" 1. give this process write access to that directory, then re-run this command;");
|
|
90
|
+
console.error(" 2. run this command from a shell that already has it;");
|
|
91
|
+
console.error(" 3. add the worktree as its own dsh workspace — a session opened there needs no");
|
|
92
|
+
console.error(" registration at all, because its working directory IS the worktree.");
|
|
82
93
|
process.exit(1);
|
|
83
94
|
}
|
|
84
95
|
console.log(`[dsh-git-badge] this session's checkout is now ${basename(path)} (${branchOf(path)}).`);
|
package/seam/patch.js
CHANGED
|
@@ -22,7 +22,7 @@ const HERE = dirname(fileURLToPath(import.meta.url));
|
|
|
22
22
|
|
|
23
23
|
// sha256 of the upstream build the anchors were last verified against. ADVISORY:
|
|
24
24
|
// status prints it for orientation; apply does NOT require it.
|
|
25
|
-
export const KNOWN_GOOD_HASH = "
|
|
25
|
+
export const KNOWN_GOOD_HASH = "e784543780fb8ac7c38c853afd26b9b1713c6907eec2b07eff37461e07bd3be9";
|
|
26
26
|
|
|
27
27
|
/** Locate the installed DSH package root; null when it cannot be found. */
|
|
28
28
|
export function dshRoot() {
|
|
@@ -120,6 +120,45 @@ export function backupState(paths) {
|
|
|
120
120
|
: "drift";
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/**
|
|
124
|
+
* What `revert` WOULD do, without doing it — the ONE decision the `status`
|
|
125
|
+
* report and the revert branch both read, so they cannot drift apart. They did:
|
|
126
|
+
* `revert: would REFUSE` was documented in SEAM.md and never implemented, which
|
|
127
|
+
* is the same shape of bug as the hole this closes.
|
|
128
|
+
*
|
|
129
|
+
* A legitimate backup is the bytes as found before patching, so the CURRENT
|
|
130
|
+
* anchors must still resolve against it (`backupState` is "patchable"). A store
|
|
131
|
+
* whose backup predates a DSH update satisfies neither condition: it holds an
|
|
132
|
+
* OLDER upstream, so restoring it would downgrade the package AND leave a build
|
|
133
|
+
* this patch cannot re-apply.
|
|
134
|
+
*
|
|
135
|
+
* That store is easy to acquire by accident, because `apply` patches whichever
|
|
136
|
+
* store it is pointed at: the shipped patcher defaults to
|
|
137
|
+
* `$DSH_HOME/git-badge-seam` while this repo's wrapper keeps its own under
|
|
138
|
+
* `seam/`. Apply through one and the other's backup stays behind.
|
|
139
|
+
*
|
|
140
|
+
* @returns {{ action: "restore" | "noop" | "refuse", reason: string }}
|
|
141
|
+
*/
|
|
142
|
+
export function revertPlan(paths) {
|
|
143
|
+
if (!existsSync(paths.backupClient))
|
|
144
|
+
return { action: "refuse", reason: "no backup in this store, so the original bytes are unknown" };
|
|
145
|
+
const r = inspect(paths);
|
|
146
|
+
if (r.hash === sha256(readText(paths.backupClient)))
|
|
147
|
+
return { action: "noop", reason: "installed client.js already matches the backup" };
|
|
148
|
+
if (r.state !== "ours-current" && r.state !== "ours-stale" && r.state !== "ours-corrupt")
|
|
149
|
+
return {
|
|
150
|
+
action: "refuse",
|
|
151
|
+
reason: `the installed client.js is not this repo's artifact (state: ${r.state})`
|
|
152
|
+
};
|
|
153
|
+
const bstate = backupState(paths);
|
|
154
|
+
if (bstate !== "patchable")
|
|
155
|
+
return {
|
|
156
|
+
action: "refuse",
|
|
157
|
+
reason: `the backup is '${bstate}' — a different upstream generation, not a build this patch targets`
|
|
158
|
+
};
|
|
159
|
+
return { action: "restore", reason: "the backup is a build this patch targets" };
|
|
160
|
+
}
|
|
161
|
+
|
|
123
162
|
/**
|
|
124
163
|
* Backup the bytes AS FOUND, patch client.js in place, install the no-op host
|
|
125
164
|
* half, and record the restart marker (`reason` names the actor: apply |
|
package/seam/store.js
CHANGED
|
@@ -105,20 +105,24 @@ export function readSessionCheckouts(file = sessionCheckoutsPath()) {
|
|
|
105
105
|
/**
|
|
106
106
|
* Register the checkout a session is working in. Written by the agent (the
|
|
107
107
|
* git-worktree and /gh checkout paths), read by the node half.
|
|
108
|
+
*
|
|
109
|
+
* Returns the OUTCOME rather than a boolean, because the reason matters here more
|
|
110
|
+
* than anywhere else in this file. A registration that cannot be written is the
|
|
111
|
+
* one failure the badge CANNOT show: the session quietly keeps its inferred
|
|
112
|
+
* checkout, which looks identical to a session with nothing to follow. The caller
|
|
113
|
+
* gets `error`/`code` so it can say what went wrong and where, instead of
|
|
114
|
+
* reporting a bare failure that the next reader has to rediscover the hard way.
|
|
108
115
|
*/
|
|
109
116
|
export function writeSessionCheckout(sessionId, path) {
|
|
117
|
+
const file = sessionCheckoutsPath();
|
|
110
118
|
try {
|
|
111
119
|
mkdirSync(dataDir(), { recursive: true });
|
|
112
120
|
const sessions = readSessionCheckouts();
|
|
113
121
|
sessions[sessionId] = { path, at: new Date().toISOString() };
|
|
114
|
-
writeFileSync(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
);
|
|
119
|
-
return true;
|
|
120
|
-
} catch {
|
|
121
|
-
return false;
|
|
122
|
+
writeFileSync(file, JSON.stringify({ schema: "dsh-git-badge/session-checkout/v1", sessions }), "utf8");
|
|
123
|
+
return { ok: true, file };
|
|
124
|
+
} catch (error) {
|
|
125
|
+
return { ok: false, file, error: String(error?.message ?? error), code: String(error?.code ?? "") };
|
|
122
126
|
}
|
|
123
127
|
}
|
|
124
128
|
|