dsh-git-badge 0.13.0 → 0.14.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 +29 -0
- package/lib/client.js +182 -18
- package/lib/index.js +90 -0
- package/package.json +10 -8
- package/seam/apply.js +6 -0
- package/seam/postinstall.js +4 -0
- package/seam/skill.js +53 -0
- package/skill/gh/SKILL.md +68 -0
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,35 @@ npm. 0.6.0 is the first release recorded here.
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.14.1] - 2026-09-15
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- **The chip's hover card top line is now an `action:` row** — the gh skill
|
|
18
|
+
invocation the checkout justifies (e.g. `/gh push`), in the card's ordinary
|
|
19
|
+
label/value layout, replacing the next-step line's why-plus-copy-chip. The
|
|
20
|
+
actionable surface remains the (+) menu's `/gh` picker; the card row is its
|
|
21
|
+
read-only pointer.
|
|
22
|
+
- **The branch pull-down is removed from the chip.** The (+) menu's `/gh`
|
|
23
|
+
picker supersedes it as the actionable surface, and the chip returns to
|
|
24
|
+
mark + branch + PR token + notice.
|
|
25
|
+
|
|
26
|
+
## [0.14.0] - 2026-09-15
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **A "next" row at the top of the chip's hover card.** The node half derives
|
|
31
|
+
the single highest-priority next step from fields the status response already
|
|
32
|
+
carries — a paused operation and its resume command, `git pull --ff-only`
|
|
33
|
+
when behind, `git push -u origin <branch>` when no upstream exists on a dirty
|
|
34
|
+
branch, `git push` when ahead, a conservative commit command when dirty
|
|
35
|
+
(`add -p` for unstaged work, `-A` only for untracked-only changes, which
|
|
36
|
+
`add -p` cannot see), and `gh pr checks <n> --watch` when CI fails. Each
|
|
37
|
+
suggestion carries its reason, and the command is a click-to-copy chip — it
|
|
38
|
+
runs in the user's own terminal, never through dsh. Clean, synced, nothing
|
|
39
|
+
failing → no row. Pure derivation: no extra git invocations, and the ranking
|
|
40
|
+
is unit-tested in the node suite.
|
|
41
|
+
|
|
13
42
|
## [0.13.0] - 2026-09-15
|
|
14
43
|
|
|
15
44
|
### Added
|
package/lib/client.js
CHANGED
|
@@ -50,6 +50,11 @@ window.__ModuleLoader__.load({
|
|
|
50
50
|
primitives = null;
|
|
51
51
|
}
|
|
52
52
|
const Tooltip = primitives === null ? void 0 : primitives.Tooltip;
|
|
53
|
+
// The shell's pull-down primitive (what the input-area selectors use) and
|
|
54
|
+
// its candidate ranker. Same guarded require as Tooltip: a shell without
|
|
55
|
+
// them loses the pull-down affordances, never the badges.
|
|
56
|
+
const Menu = primitives === null ? void 0 : primitives.Menu;
|
|
57
|
+
const rankByName = primitives === null ? void 0 : primitives.rankByName;
|
|
53
58
|
|
|
54
59
|
/** Module-level git-status cache. Entries are invalidated by SSE events, never by age. */
|
|
55
60
|
const GIT_CACHE = new Map();
|
|
@@ -225,6 +230,48 @@ window.__ModuleLoader__.load({
|
|
|
225
230
|
return "ok";
|
|
226
231
|
}
|
|
227
232
|
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* The /gh picker's sub-actions — the skill invocations the checkout
|
|
236
|
+
* justifies, most urgent first. Same ranking the server's nextStep uses,
|
|
237
|
+
* but spoken in skill arguments; the generic "next" entry is skipped when
|
|
238
|
+
* a specific rule already names the same situation (its why would be
|
|
239
|
+
* identical, and two rows for one fact is noise).
|
|
240
|
+
*/
|
|
241
|
+
function ghSkillActions(info) {
|
|
242
|
+
if (info === void 0 || info === null || info.git !== true) return [];
|
|
243
|
+
const subs = [];
|
|
244
|
+
const seen = new Set();
|
|
245
|
+
const add = (args, why) => {
|
|
246
|
+
if (seen.has(args)) return;
|
|
247
|
+
seen.add(args);
|
|
248
|
+
subs.push({ args, why });
|
|
249
|
+
};
|
|
250
|
+
const ahead = info.ahead || 0;
|
|
251
|
+
const behind = info.behind || 0;
|
|
252
|
+
const staged = info.stagedFiles || 0;
|
|
253
|
+
const unstaged = info.unstagedFiles || 0;
|
|
254
|
+
const untracked = info.untrackedFiles || 0;
|
|
255
|
+
if (behind > 0) add("pull", behind + " behind " + (info.upstream ?? "upstream"));
|
|
256
|
+
if (ahead > 0) add("push", ahead + " ahead of " + (info.upstream ?? "upstream"));
|
|
257
|
+
if (staged > 0) add("commit", staged + " staged");
|
|
258
|
+
if (unstaged > 0) add("commit", unstaged + " unstaged");
|
|
259
|
+
if (unstaged === 0 && untracked > 0) add("commit", untracked + " untracked");
|
|
260
|
+
const pr = info.pr;
|
|
261
|
+
if (pr !== void 0 && pr !== null && pr.number !== void 0) {
|
|
262
|
+
if (pr.state === "failing") add("checks " + pr.number, "checks failing on #" + pr.number);
|
|
263
|
+
add("pr view " + pr.number, "open pull request #" + pr.number);
|
|
264
|
+
} else if (ahead === 0 && behind === 0 && info.upstream !== void 0 && staged + unstaged + untracked === 0) {
|
|
265
|
+
add("pr", "branch is pushed and has no pull request");
|
|
266
|
+
}
|
|
267
|
+
const next = info.next;
|
|
268
|
+
if (next !== void 0 && next !== null && typeof next.command === "string"
|
|
269
|
+
&& !subs.some((entry) => entry.why === next.why)) {
|
|
270
|
+
add("next", next.why);
|
|
271
|
+
}
|
|
272
|
+
return subs.slice(0, 6);
|
|
273
|
+
}
|
|
274
|
+
|
|
228
275
|
/**
|
|
229
276
|
* Mark fill per status, from the app's own semantic tokens so BOTH surfaces
|
|
230
277
|
* follow light/dark and custom themes — which a hardcoded emoji cannot do.
|
|
@@ -617,6 +664,7 @@ window.__ModuleLoader__.load({
|
|
|
617
664
|
const CARD_LABEL = { color: "var(--dsw-alias-label-tertiary, #9ea7ad)", flex: "none", minWidth: "62px" };
|
|
618
665
|
const CARD_VALUE = { minWidth: 0, overflowWrap: "anywhere" };
|
|
619
666
|
|
|
667
|
+
|
|
620
668
|
/**
|
|
621
669
|
* Hover card body for the input chip — INPUT CHIP ONLY. Pure presentation
|
|
622
670
|
* over fields the status response already carries, plus the `detail=1`
|
|
@@ -640,6 +688,15 @@ window.__ModuleLoader__.load({
|
|
|
640
688
|
}, label)
|
|
641
689
|
);
|
|
642
690
|
};
|
|
691
|
+
// The "action" row — the top line, in the card's ordinary row layout:
|
|
692
|
+
// the gh skill invocation the checkout justifies, derived from the same
|
|
693
|
+
// state the /gh picker uses (first sub-action wins). Plain text, no
|
|
694
|
+
// affordance: the actionable surface is the (+) menu's /gh picker, and
|
|
695
|
+
// this row is the read-only pointer to it.
|
|
696
|
+
const topAction = ghSkillActions(data)[0];
|
|
697
|
+
if (topAction !== void 0) {
|
|
698
|
+
add("action:", "/gh " + topAction.args);
|
|
699
|
+
}
|
|
643
700
|
add("branch", info.branch);
|
|
644
701
|
add("upstream", info.upstream === void 0 ? "none configured" : info.upstream);
|
|
645
702
|
if (info.ahead !== void 0 || info.behind !== void 0) {
|
|
@@ -731,6 +788,28 @@ window.__ModuleLoader__.load({
|
|
|
731
788
|
const seamNotice = seamHint !== null && !seamNoticeDismissed ? seamHint : null;
|
|
732
789
|
const detail = useGitStatus(target, { pr: true, detail: true, enabled: hovered });
|
|
733
790
|
if (info === void 0 || info.git !== true) return null;
|
|
791
|
+
|
|
792
|
+
// The status mark wrapped in the chip's hover card — the card's ONLY
|
|
793
|
+
// anchor. Pointer rest here (and only here) enables the detail=1 fetch,
|
|
794
|
+
// so the extras are paid for exactly when the card that can render them
|
|
795
|
+
// is about to open. Defined inside the component: it closes over the
|
|
796
|
+
// hovered state that gates the detail fetch.
|
|
797
|
+
const hoverableMark = (() => {
|
|
798
|
+
const mark = react_jsx_runtime.jsx("span", {
|
|
799
|
+
onPointerEnter: () => setHovered(true),
|
|
800
|
+
style: { display: "inline-flex", alignItems: "center", flex: "none", cursor: "default" },
|
|
801
|
+
children: react_jsx_runtime.jsx(StatusMark, { info })
|
|
802
|
+
});
|
|
803
|
+
if (Tooltip === void 0) return mark;
|
|
804
|
+
return react_jsx_runtime.jsx(Tooltip, {
|
|
805
|
+
side: "top",
|
|
806
|
+
maxWidth: 360,
|
|
807
|
+
// a function label keeps the card's element tree out of every render
|
|
808
|
+
// until the tooltip actually opens
|
|
809
|
+
label: () => react_jsx_runtime.jsx(HoverCard, { info, detail }),
|
|
810
|
+
children: mark
|
|
811
|
+
});
|
|
812
|
+
})();
|
|
734
813
|
// the mark is an element now rather than a leading glyph in the string, so
|
|
735
814
|
// the SAME StatusMark the sidebar row draws carries the status here too;
|
|
736
815
|
// the container's 4px gap supplies the space the emoji's own did
|
|
@@ -757,12 +836,17 @@ window.__ModuleLoader__.load({
|
|
|
757
836
|
// null until the delayed check in apply() proves the seam never
|
|
758
837
|
// appeared.
|
|
759
838
|
title: seamHint === null ? undefined : seamHint,
|
|
760
|
-
// the card is fetched for a pointer that RESTS here, not one that
|
|
761
|
-
// merely crosses the chip
|
|
762
|
-
onPointerEnter: () => setHovered(true),
|
|
763
839
|
children: [
|
|
764
|
-
|
|
765
|
-
|
|
840
|
+
// The hover card belongs to the STATUS SYMBOL, not the badge: the
|
|
841
|
+
// circle is the "what does this colour mean" element, and a card
|
|
842
|
+
// opening sideways from a hover on the branch text got in the way of
|
|
843
|
+
// the very pull-down this row now carries. The detail fetch
|
|
844
|
+
// (enabled: hovered) follows the same pointer, so nothing pays for
|
|
845
|
+
// extras the card no longer shows. The seam-absent native title
|
|
846
|
+
// stays on the whole chip — it is about the badge, not the mark.
|
|
847
|
+
hoverableMark,
|
|
848
|
+
|
|
849
|
+
react_jsx_runtime.jsx("span", { key: "text", children: text }),
|
|
766
850
|
prToken === ""
|
|
767
851
|
? null
|
|
768
852
|
: prUrl === void 0
|
|
@@ -864,25 +948,18 @@ window.__ModuleLoader__.load({
|
|
|
864
948
|
),
|
|
865
949
|
]
|
|
866
950
|
});
|
|
867
|
-
// No Tooltip primitive (a shell that does not seed it):
|
|
868
|
-
//
|
|
951
|
+
// No Tooltip primitive (a shell that does not seed it): the mark renders
|
|
952
|
+
// bare and the badge still works — the card is an enhancement, the
|
|
869
953
|
// badge is the feature.
|
|
870
|
-
|
|
871
|
-
return react_jsx_runtime.jsx(Tooltip, {
|
|
872
|
-
side: "top",
|
|
873
|
-
maxWidth: 360,
|
|
874
|
-
// a function label keeps the card's element tree out of every render
|
|
875
|
-
// until the tooltip actually opens
|
|
876
|
-
label: () => react_jsx_runtime.jsx(HoverCard, { info, detail }),
|
|
877
|
-
children: chip
|
|
878
|
-
});
|
|
954
|
+
return chip;
|
|
879
955
|
}
|
|
956
|
+
|
|
880
957
|
//#endregion
|
|
881
958
|
|
|
882
959
|
// `workspaces` is no longer required: both surfaces target an id and the
|
|
883
960
|
// node half resolves the workspace, so the client never needs a service
|
|
884
961
|
// lookup. Fewer declared services also means fewer ways to fail to load.
|
|
885
|
-
const inject = ["slots"];
|
|
962
|
+
const inject = ["slots", "inputTriggers"];
|
|
886
963
|
|
|
887
964
|
/**
|
|
888
965
|
* Register the badge into the seams. The seam owner hands each entry the row
|
|
@@ -890,6 +967,93 @@ window.__ModuleLoader__.load({
|
|
|
890
967
|
* deliberately ignores the cwd it may also be given.
|
|
891
968
|
*/
|
|
892
969
|
function apply(ctx) {
|
|
970
|
+
// The /gh command contribution (the (+) commands menu): the state-aware
|
|
971
|
+
// entry point to the gh skill. available() gates on the cached git
|
|
972
|
+
// status; options() lists only the sub-actions the checkout justifies;
|
|
973
|
+
// onSelect SENDS the skill invocation ("/gh push") as a conversation
|
|
974
|
+
// message — the agent executes the skill under its own approval flow,
|
|
975
|
+
// and the shell consumes the /gh token afterwards. This is the one
|
|
976
|
+
// blessed route from a menu pick to a git action; it is a SEND, which
|
|
977
|
+
// is why the destructive-step confirmation lives in the skill text.
|
|
978
|
+
// Guarded: a host without commandUi loses the menu entry, never the
|
|
979
|
+
// badges.
|
|
980
|
+
if (typeof ctx.inject === "function") {
|
|
981
|
+
ctx.inject(["commandUi", "sessions"], (scope) => {
|
|
982
|
+
try {
|
|
983
|
+
scope.effect(() => scope.commandUi.register({
|
|
984
|
+
name: "gh",
|
|
985
|
+
// the contract calls description() as a FUNCTION at menu-build
|
|
986
|
+
// time (candidates: contribution.description()) — a string throws
|
|
987
|
+
// TypeError on every candidates pass and kills the whole menu,
|
|
988
|
+
// (+) button included
|
|
989
|
+
description: () => "git/gh actions for this checkout — push, pull, pr, commit, checks",
|
|
990
|
+
available: (session) => {
|
|
991
|
+
// called during the host's menu build — a throw here kills the
|
|
992
|
+
// whole menu, so it degrades to "hidden" instead
|
|
993
|
+
try {
|
|
994
|
+
const query = targetQuery({ kind: "session", id: session?.sessionId }, { pr: true });
|
|
995
|
+
return query !== void 0 && GIT_CACHE.get(query)?.data?.git === true;
|
|
996
|
+
} catch (error) {
|
|
997
|
+
console.error("[dsh-git-badge] /gh available failed:", error);
|
|
998
|
+
return false;
|
|
999
|
+
}
|
|
1000
|
+
},
|
|
1001
|
+
ui: {
|
|
1002
|
+
options: async (session, signal) => {
|
|
1003
|
+
try {
|
|
1004
|
+
const sessionId = session?.sessionId;
|
|
1005
|
+
if (sessionId === void 0) return [];
|
|
1006
|
+
const query = targetQuery({ kind: "session", id: sessionId }, { pr: true });
|
|
1007
|
+
if (query === void 0) return [];
|
|
1008
|
+
// cache-first: the badge's own knowledge opens the picker
|
|
1009
|
+
// instantly; a cold session pays one status fetch.
|
|
1010
|
+
let info = GIT_CACHE.get(query)?.data;
|
|
1011
|
+
if (info === void 0 || info === null) {
|
|
1012
|
+
info = await fetch("/api/git-badge?" + query).then((r) => r.json());
|
|
1013
|
+
}
|
|
1014
|
+
const actions = ghSkillActions(info);
|
|
1015
|
+
return actions.map((action) => ({ label: "/gh " + action.args, detail: action.why, args: action.args }));
|
|
1016
|
+
} catch (error) {
|
|
1017
|
+
console.error("[dsh-git-badge] /gh options failed:", error);
|
|
1018
|
+
return [];
|
|
1019
|
+
}
|
|
1020
|
+
},
|
|
1021
|
+
onSelect: async (option, session) => {
|
|
1022
|
+
// Send the invocation: the message lands in the transcript
|
|
1023
|
+
// and the agent executes the gh skill — the approval flow
|
|
1024
|
+
// for anything destructive is the agent's, per the skill
|
|
1025
|
+
// text. The prompt path is the conversation's own: the
|
|
1026
|
+
// sessions service binds a sessionId to { session } and
|
|
1027
|
+
// binding.session.prompt(content, "queue") is exactly what
|
|
1028
|
+
// the composer's send() calls.
|
|
1029
|
+
const sessionId = session?.sessionId;
|
|
1030
|
+
const sessions = typeof scope.sessions === "function" ? scope.sessions() : scope.sessions;
|
|
1031
|
+
const binding = sessions?.binding?.(sessionId);
|
|
1032
|
+
const target = binding?.session;
|
|
1033
|
+
if (target?.prompt === void 0) {
|
|
1034
|
+
console.error("[dsh-git-badge] /gh: no prompt path (binding missing or session-less)", {
|
|
1035
|
+
sessionId,
|
|
1036
|
+
hasSessions: sessions !== void 0,
|
|
1037
|
+
hasBinding: binding !== void 0
|
|
1038
|
+
});
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
const result = await target.prompt([{ type: "text", text: "/gh " + option.args }], "queue");
|
|
1042
|
+
if (result !== void 0 && result !== null && result.ok === false) {
|
|
1043
|
+
console.error("[dsh-git-badge] /gh submit refused:", result.error);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
}), "dsh-git-badge: /gh contribution");
|
|
1048
|
+
} catch (error) {
|
|
1049
|
+
// a failed registration must degrade to "no /gh entry", never to
|
|
1050
|
+
// a broken commands menu — the (+) button and the / menu are the
|
|
1051
|
+
// host's, and this callback runs inside their boot
|
|
1052
|
+
console.error("[dsh-git-badge] /gh contribution failed:", error);
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
|
|
893
1057
|
// inject() re-evaluates when a seam's declaration appears, so boot
|
|
894
1058
|
// order relative to the workspace browser does not matter. On an
|
|
895
1059
|
// unpatched install the seam is never declared, so this callback never
|
|
@@ -971,7 +1135,7 @@ window.__ModuleLoader__.load({
|
|
|
971
1135
|
// Additive; the host reads apply/inject and ignores the rest. The suite
|
|
972
1136
|
// drives these to assert the REQUEST contract — which surface asks for the
|
|
973
1137
|
// expensive extras — without a browser, a fetch or a network.
|
|
974
|
-
exports.__internals = { targetQuery, formatPrToken, formatFileBreakdown, formatPrDetail, formatCheckoutDetail, worktreeDetail, actionToken };
|
|
1138
|
+
exports.__internals = { targetQuery, formatPrToken, formatFileBreakdown, formatPrDetail, formatCheckoutDetail, worktreeDetail, actionToken, ghSkillActions };
|
|
975
1139
|
return module.exports;
|
|
976
1140
|
}
|
|
977
1141
|
});
|
package/lib/index.js
CHANGED
|
@@ -662,6 +662,93 @@ function operationMarker(gitDir) {
|
|
|
662
662
|
return null;
|
|
663
663
|
}
|
|
664
664
|
|
|
665
|
+
/**
|
|
666
|
+
* The one command a paused operation resumes with. A squash merge records no
|
|
667
|
+
* MERGE_HEAD (its conclusion is an ordinary commit), and bisect's next step is
|
|
668
|
+
* the user's judgement call (good/bad) — it gets a WHY but no command.
|
|
669
|
+
*/
|
|
670
|
+
const OPERATION_NEXT = {
|
|
671
|
+
merge: "git merge --continue",
|
|
672
|
+
squash: "git commit",
|
|
673
|
+
"cherry-pick": "git cherry-pick --continue",
|
|
674
|
+
revert: "git revert --continue",
|
|
675
|
+
rebase: "git rebase --continue",
|
|
676
|
+
sequencer: "git cherry-pick --continue",
|
|
677
|
+
bisect: void 0
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* The hover card's "next" row: the single highest-priority thing to do next,
|
|
682
|
+
* as a conservative command plus the reason it applies. Pure derivation over
|
|
683
|
+
* fields the status response already carries — no extra git invocations, so it
|
|
684
|
+
* rides the base response and a future surface gets it for free. Ranking (the
|
|
685
|
+
* first match wins):
|
|
686
|
+
*
|
|
687
|
+
* 1. a paused operation (conflicts included) — resuming it subsumes everything
|
|
688
|
+
* else; unmerged files without a marker fall back to `git status`
|
|
689
|
+
* 2. behind — reconciling comes before publishing
|
|
690
|
+
* 3. no upstream on a dirty branch — publish it for the first time
|
|
691
|
+
* 4. ahead — publish
|
|
692
|
+
* 5. dirty — stage and commit (add -p for unstaged work; -A only when the
|
|
693
|
+
* change is untracked-only, which add -p cannot see)
|
|
694
|
+
* 6. PR checks failing — watch them land
|
|
695
|
+
*
|
|
696
|
+
* Clean, synced, nothing failing → null: the row is omitted entirely, because
|
|
697
|
+
* "no suggestion" is also a suggestion. Commands are conservative on purpose —
|
|
698
|
+
* the card COPIES them into the user's terminal, it never runs them, but the
|
|
699
|
+
* register still avoids anything destructive.
|
|
700
|
+
*
|
|
701
|
+
* @returns {{ command: string|undefined, why: string } | null}
|
|
702
|
+
*/
|
|
703
|
+
function nextStep(info) {
|
|
704
|
+
if (info === void 0 || info === null || info.git !== true) return null;
|
|
705
|
+
const unmerged = info.unmergedFiles || 0;
|
|
706
|
+
const plural = (n) => (n === 1 ? "" : "s");
|
|
707
|
+
if (info.operation !== void 0 && info.operation !== null) {
|
|
708
|
+
const op = String(info.operation);
|
|
709
|
+
return {
|
|
710
|
+
command: OPERATION_NEXT[op],
|
|
711
|
+
why: unmerged > 0
|
|
712
|
+
? `${unmerged} unmerged file${plural(unmerged)} blocking the paused ${op}`
|
|
713
|
+
: `a ${op} is paused mid-operation`
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
if (unmerged > 0) {
|
|
717
|
+
return { command: "git status", why: `${unmerged} unmerged file${plural(unmerged)} to resolve` };
|
|
718
|
+
}
|
|
719
|
+
const ahead = info.ahead || 0;
|
|
720
|
+
const behind = info.behind || 0;
|
|
721
|
+
if (behind > 0) {
|
|
722
|
+
return { command: "git pull --ff-only", why: `${behind} behind ${info.upstream ?? "upstream"}` };
|
|
723
|
+
}
|
|
724
|
+
const staged = info.stagedFiles || 0;
|
|
725
|
+
const unstaged = info.unstagedFiles || 0;
|
|
726
|
+
const untracked = info.untrackedFiles || 0;
|
|
727
|
+
if (info.upstream === void 0 && staged + unstaged + untracked > 0) {
|
|
728
|
+
return { command: `git push -u origin ${info.branch}`, why: "no upstream configured" };
|
|
729
|
+
}
|
|
730
|
+
if (ahead > 0) {
|
|
731
|
+
return { command: "git push", why: `${ahead} ahead of ${info.upstream ?? "upstream"}` };
|
|
732
|
+
}
|
|
733
|
+
if (staged + unstaged + untracked > 0) {
|
|
734
|
+
const why = [
|
|
735
|
+
staged > 0 ? `${staged} staged` : null,
|
|
736
|
+
unstaged > 0 ? `${unstaged} unstaged` : null,
|
|
737
|
+
untracked > 0 ? `${untracked} untracked` : null
|
|
738
|
+
].filter(Boolean).join(", ");
|
|
739
|
+
const command = staged > 0
|
|
740
|
+
? "git commit"
|
|
741
|
+
: untracked > 0 && unstaged === 0
|
|
742
|
+
? "git add -A && git commit"
|
|
743
|
+
: "git add -p && git commit";
|
|
744
|
+
return { command, why: `work to commit: ${why}` };
|
|
745
|
+
}
|
|
746
|
+
if (info.pr !== void 0 && info.pr !== null && info.pr.number !== void 0 && info.pr.state === "failing") {
|
|
747
|
+
return { command: `gh pr checks ${info.pr.number} --watch`, why: `checks failing on #${info.pr.number}` };
|
|
748
|
+
}
|
|
749
|
+
return null;
|
|
750
|
+
}
|
|
751
|
+
|
|
665
752
|
/**
|
|
666
753
|
* Git status for dir; { git: false } when dir is not a repository,
|
|
667
754
|
* GIT_DEGRADED on transient git failure.
|
|
@@ -787,6 +874,8 @@ async function gitStatusUncached(dir, wantDetail, wantPr) {
|
|
|
787
874
|
const pr = prStatusFor(toplevel, parsed.branch, notifyChange);
|
|
788
875
|
if (pr !== void 0) info.pr = pr;
|
|
789
876
|
}
|
|
877
|
+
const next = nextStep(info);
|
|
878
|
+
if (next !== null) info.next = next;
|
|
790
879
|
return info;
|
|
791
880
|
}
|
|
792
881
|
|
|
@@ -1355,6 +1444,7 @@ export {
|
|
|
1355
1444
|
config,
|
|
1356
1445
|
OPERATION_MARKERS,
|
|
1357
1446
|
operationMarker,
|
|
1447
|
+
nextStep,
|
|
1358
1448
|
outerGitDir,
|
|
1359
1449
|
parseStatusV2,
|
|
1360
1450
|
parseWorktreeList,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-git-badge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
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",
|
|
@@ -47,17 +47,19 @@
|
|
|
47
47
|
"node": ">=20"
|
|
48
48
|
},
|
|
49
49
|
"files": [
|
|
50
|
-
"
|
|
50
|
+
"CHANGELOG.md",
|
|
51
|
+
"LICENSE",
|
|
52
|
+
"README.md",
|
|
53
|
+
"cordis.patch.yml",
|
|
51
54
|
"lib/client.js",
|
|
52
|
-
"
|
|
55
|
+
"lib/index.js",
|
|
56
|
+
"package.json",
|
|
53
57
|
"seam/anchors.js",
|
|
58
|
+
"seam/apply.js",
|
|
54
59
|
"seam/postinstall.js",
|
|
60
|
+
"seam/skill.js",
|
|
55
61
|
"seam/store.js",
|
|
56
62
|
"seam/stub-index.js",
|
|
57
|
-
"
|
|
58
|
-
"package.json",
|
|
59
|
-
"README.md",
|
|
60
|
-
"CHANGELOG.md",
|
|
61
|
-
"LICENSE"
|
|
63
|
+
"skill/gh/SKILL.md"
|
|
62
64
|
]
|
|
63
65
|
}
|
package/seam/apply.js
CHANGED
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
seamPresent,
|
|
56
56
|
} from "./anchors.js";
|
|
57
57
|
|
|
58
|
+
import { installSkill } from "./skill.js";
|
|
58
59
|
import { dataDir, writeRestartMarker } from "./store.js";
|
|
59
60
|
|
|
60
61
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -165,6 +166,11 @@ function writeText(path, text) {
|
|
|
165
166
|
|
|
166
167
|
const VERDICT_EXIT_1 = new Set(["drift", "unreadable", "missing", "ours-corrupt"]);
|
|
167
168
|
|
|
169
|
+
// keep the /gh skill in step with the package on every patcher run
|
|
170
|
+
if (["apply", "revert", "status"].includes(process.argv[2])) {
|
|
171
|
+
console.log(`[dsh-git-badge] gh skill: ${installSkill()}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
168
174
|
const verb = process.argv[2];
|
|
169
175
|
if (!verb || !["apply", "revert", "status"].includes(verb)) {
|
|
170
176
|
console.error("usage: apply.js apply|revert|status");
|
package/seam/postinstall.js
CHANGED
|
@@ -26,6 +26,7 @@ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from
|
|
|
26
26
|
import { dirname, join } from "node:path";
|
|
27
27
|
import { fileURLToPath } from "node:url";
|
|
28
28
|
import { applyPatch, firstFailure, isOurs, seamPresent } from "./anchors.js";
|
|
29
|
+
import { installSkill } from "./skill.js";
|
|
29
30
|
import { dataDir, writeRestartMarker } from "./store.js";
|
|
30
31
|
|
|
31
32
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -41,6 +42,9 @@ function dshClientPath() {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
function main() {
|
|
45
|
+
// The skill does not depend on the DSH install — install it before any
|
|
46
|
+
// client-shaped early return, so a market install always gets /gh.
|
|
47
|
+
console.log(`[dsh-git-badge] gh skill: ${installSkill()} — /gh in the input's commands menu.`);
|
|
44
48
|
const client = dshClientPath();
|
|
45
49
|
if (client === null || !existsSync(client)) {
|
|
46
50
|
console.log(`[dsh-git-badge] DSH install not found; skipping seam setup. ${HINT}`);
|
package/seam/skill.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gh skill installer — copies the packaged skill into the user's skills
|
|
3
|
+
* catalog so `/gh` exists in the input's commands menu.
|
|
4
|
+
*
|
|
5
|
+
* WHY A SKILL: the commands menu executes git through the AGENT, not through
|
|
6
|
+
* this plugin — the agent owns the approval/sandbox semantics for mutating
|
|
7
|
+
* commands. The skill is the knowledge; the `/gh` command contribution in the
|
|
8
|
+
* client half is only the state-aware menu entry that invokes it.
|
|
9
|
+
*
|
|
10
|
+
* GUARDRAILS — a skill must never break an install: every failure path returns
|
|
11
|
+
* a reason string instead of throwing, and a current target is a no-op (the
|
|
12
|
+
* byte comparison makes reinstalls and updates idempotent).
|
|
13
|
+
*/
|
|
14
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
|
|
18
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
|
|
20
|
+
export const SKILL_NAME = "gh";
|
|
21
|
+
|
|
22
|
+
/** The user-level skills catalog — DSH_HOME honours the harness override. */
|
|
23
|
+
export function skillsDir() {
|
|
24
|
+
return join(process.env.DSH_HOME || join(process.env.HOME || "", ".dsh"), "skills");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function skillSource() {
|
|
28
|
+
return join(HERE, "..", "skill", "gh", "SKILL.md");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function skillTarget() {
|
|
32
|
+
return join(skillsDir(), SKILL_NAME, "SKILL.md");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Install (or update) the gh skill. Never throws.
|
|
37
|
+
* @returns {"installed"|"current"|"no packaged skill"|string} what happened.
|
|
38
|
+
*/
|
|
39
|
+
export function installSkill() {
|
|
40
|
+
try {
|
|
41
|
+
const source = skillSource();
|
|
42
|
+
if (!existsSync(source)) return "no packaged skill";
|
|
43
|
+
const target = skillTarget();
|
|
44
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
45
|
+
if (existsSync(target) && readFileSync(target, "utf8") === readFileSync(source, "utf8")) {
|
|
46
|
+
return "current";
|
|
47
|
+
}
|
|
48
|
+
copyFileSync(source, target);
|
|
49
|
+
return "installed";
|
|
50
|
+
} catch (error) {
|
|
51
|
+
return "skipped (" + String(error?.message ?? error) + ")";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gh
|
|
3
|
+
description: Git and GitHub operations for the current checkout, driven by an invocation argument — /gh push, /gh pr, /gh pull, /gh commit, /gh checks <n>, or bare /gh to do the single highest-priority next action.
|
|
4
|
+
whenToUse: When the user picks a git action from the commands menu (/gh push, /gh pr, …) or asks to sync, publish, or open a pull request for the checkout they are working in.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# gh — git/GitHub actions for this checkout
|
|
8
|
+
|
|
9
|
+
You are invoked as `/gh <sub>` (or bare `/gh`). The sub-command chooses the
|
|
10
|
+
operation. Anything destructive requires explicit user confirmation before you
|
|
11
|
+
run it, every command is reported with its output, and a failure is reported
|
|
12
|
+
verbatim rather than retried blind.
|
|
13
|
+
|
|
14
|
+
## First: establish state
|
|
15
|
+
|
|
16
|
+
Run one status call and read it before acting (except for `checks`, which acts
|
|
17
|
+
on its argument):
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
git --no-optional-locks status --porcelain=v2 --branch
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Note branch, upstream, ahead/behind, and the dirty breakdown (staged /
|
|
24
|
+
unstaged / untracked / unmerged), plus any in-progress operation marker
|
|
25
|
+
(MERGE_HEAD, rebase-merge, …).
|
|
26
|
+
|
|
27
|
+
## Sub-commands
|
|
28
|
+
|
|
29
|
+
- **`/gh next`** (or bare `/gh`) — do the single highest-priority action, first
|
|
30
|
+
match wins: resume a paused operation (`git merge --continue`,
|
|
31
|
+
`git rebase --continue`, …; a squash merge concludes with `git commit`);
|
|
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,
|
|
35
|
+
`git add -p && git commit` for unstaged edits, `git add -A && git commit`
|
|
36
|
+
only for untracked-only changes, which `add -p` cannot see); `gh pr checks
|
|
37
|
+
<n> --watch` when CI fails. Clean and synced → say so and stop.
|
|
38
|
+
|
|
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.
|
|
43
|
+
|
|
44
|
+
- **`/gh pr`** — create a pull request for the current branch: push first if
|
|
45
|
+
needed, then `gh pr create` with a title from the branch's commits and a body
|
|
46
|
+
summarising them. If a PR already exists, report its state (`gh pr view`)
|
|
47
|
+
instead of creating another.
|
|
48
|
+
|
|
49
|
+
- **`/gh pull`** — `git pull --ff-only`. On failure, report the divergence and
|
|
50
|
+
ask; do not merge or rebase without the user choosing.
|
|
51
|
+
|
|
52
|
+
- **`/gh commit`** — commit the current work: `git commit` when work is already
|
|
53
|
+
staged; otherwise `git add -p` interactively is not available to you, so
|
|
54
|
+
stage deliberately (`git add <paths>` named from the status output) and say
|
|
55
|
+
exactly what you staged before committing. Untracked-only changes: `git add
|
|
56
|
+
-A && git commit` after listing what -A will pick up. Write the message from
|
|
57
|
+
the diff, not a template.
|
|
58
|
+
|
|
59
|
+
- **`/gh checks <n>`** — `gh pr checks <n>` and summarise: what fails, what is
|
|
60
|
+
pending, and the single most useful next step.
|
|
61
|
+
|
|
62
|
+
## Hard rules
|
|
63
|
+
|
|
64
|
+
- Never `git reset --hard`, `git clean`, `git checkout -- .`, `git push
|
|
65
|
+
--force` (plain), or any history rewrite without an explicit user yes.
|
|
66
|
+
- `GIT_TERMINAL_PROMPT=0` on network commands so a missing credential fails
|
|
67
|
+
fast instead of hanging.
|
|
68
|
+
- One operation per invocation; report the outcome, then stop.
|