dsh-git-badge 0.5.1 → 0.5.2
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/lib/client.js +7 -5
- package/lib/index.js +77 -4
- package/package.json +7 -2
package/lib/client.js
CHANGED
|
@@ -111,7 +111,8 @@ window.__ModuleLoader__.load({
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
/**
|
|
114
|
-
*
|
|
114
|
+
* Sync/dirty suffix — INPUT CHIP ONLY. The sidebar row badge shows
|
|
115
|
+
* status + branch alone; the chip is the detailed surface. Rule set:
|
|
115
116
|
* - dirty files render as ✎n (pencil = worktree files, distinct from
|
|
116
117
|
* the ↑/↓ commit-sync axis)
|
|
117
118
|
* - while dirty, BOTH sync counts render including zeros (↑0 ↓2 ✎3),
|
|
@@ -140,10 +141,11 @@ window.__ModuleLoader__.load({
|
|
|
140
141
|
};
|
|
141
142
|
|
|
142
143
|
/**
|
|
143
|
-
* Row badge
|
|
144
|
-
*
|
|
144
|
+
* Row badge — status + branch only (sync/dirty detail lives on the
|
|
145
|
+
* input chip):
|
|
146
|
+
* the_paragliding_app | 🟢 main
|
|
145
147
|
* Always renders the workspace name (so the row keeps its identity);
|
|
146
|
-
* appends the muted `| emoji branch
|
|
148
|
+
* appends the muted `| emoji branch` part only for git workspaces.
|
|
147
149
|
*/
|
|
148
150
|
function WorkspaceGitBadge({ label, cwd }) {
|
|
149
151
|
const info = useGitStatus(cwd, false);
|
|
@@ -152,7 +154,7 @@ window.__ModuleLoader__.load({
|
|
|
152
154
|
children.push(
|
|
153
155
|
react_jsx_runtime.jsx("span", { style: { ...META_STYLE, margin: "0 7px" }, children: "|" }),
|
|
154
156
|
react_jsx_runtime.jsx("span", { style: { ...META_STYLE, marginRight: "4px" }, children: info.dirty ? "🟡" : "🟢" }),
|
|
155
|
-
react_jsx_runtime.jsx("span", { style: META_STYLE, children: info.branch
|
|
157
|
+
react_jsx_runtime.jsx("span", { style: META_STYLE, children: info.branch })
|
|
156
158
|
);
|
|
157
159
|
}
|
|
158
160
|
return react_jsx_runtime.jsx("span", { style: { display: "flex", alignItems: "center", minWidth: 0 }, children });
|
package/lib/index.js
CHANGED
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
* changed / untracked counts in a single call (`--no-optional-locks`
|
|
13
13
|
* guarantees the read never contends with the user's own git
|
|
14
14
|
* operations on the index).
|
|
15
|
+
* - ahead/behind compare against the LOCAL remote-tracking ref, which
|
|
16
|
+
* only moves on fetch — so a TTL-bounded background fetch (60s per
|
|
17
|
+
* toplevel) runs before status when the ref can be stale. `GIT_TERMINAL_PROMPT=0`
|
|
18
|
+
* and the shared timeout keep an offline/slow remote from stalling the
|
|
19
|
+
* route; on fetch failure the stale-ref answer is served as-is.
|
|
15
20
|
* - `detail=1` adds one `log -1` for the hover card; row refresh never
|
|
16
21
|
* pays for it.
|
|
17
22
|
*
|
|
@@ -41,9 +46,13 @@ const name = "dsh-git-badge";
|
|
|
41
46
|
* The exitCode/timeout distinction separates a definitive answer (non-repo)
|
|
42
47
|
* from a transient one (degraded).
|
|
43
48
|
*/
|
|
44
|
-
function runGit(dir, args) {
|
|
49
|
+
function runGit(dir, args, opts) {
|
|
50
|
+
const { env, timeout = 3000 } = opts ?? {};
|
|
51
|
+
// env is a full replacement for execFile; overlay onto process.env so
|
|
52
|
+
// PATH/HOME (credential helpers, global config) survive
|
|
53
|
+
const fullEnv = env === void 0 ? void 0 : { ...process.env, ...env };
|
|
45
54
|
return new Promise((resolve) => {
|
|
46
|
-
execFile("git", args, { cwd: dir, timeout:
|
|
55
|
+
execFile("git", args, { cwd: dir, timeout, env: fullEnv }, (error, stdout) => {
|
|
47
56
|
if (error === void 0 || error === null) resolve({ stdout: String(stdout) });
|
|
48
57
|
else if (error.killed) resolve({ stdout: null, timeout: true });
|
|
49
58
|
else if (error.code === "ENOENT") resolve({ stdout: null, missing: true });
|
|
@@ -55,6 +64,57 @@ function runGit(dir, args) {
|
|
|
55
64
|
/** Marker response for a git invocation that failed or timed out (transient). */
|
|
56
65
|
const GIT_DEGRADED = { git: false, error: "git unavailable (timeout or failure)" };
|
|
57
66
|
|
|
67
|
+
//#region TTL-bounded background fetch
|
|
68
|
+
/**
|
|
69
|
+
* ahead/behind come from the local remote-tracking ref, which only moves on
|
|
70
|
+
* `git fetch` — without one, a remote update (push from elsewhere, GitHub
|
|
71
|
+
* edit) is invisible to the badge until the user happens to fetch. A
|
|
72
|
+
* TTL-bounded fetch closes that gap: at most one network round trip per
|
|
73
|
+
* FETCH_TTL_MS per repository, regardless of how often the route is hit
|
|
74
|
+
* (watcher bursts included). Mirrors what IDEs do (VS Code's throttled
|
|
75
|
+
* auto-fetch), with a TTL rather than a timer: fetch only happens when
|
|
76
|
+
* someone is actually looking at the badge.
|
|
77
|
+
*/
|
|
78
|
+
const FETCH_TTL_MS = 60000;
|
|
79
|
+
/** lastFetchAt per toplevel; in-flight promise per toplevel collapses races. */
|
|
80
|
+
const fetchState = new Map();
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Refresh the remote-tracking refs for toplevel, bounded by TTL. Resolves
|
|
84
|
+
* true only when a fetch actually ran and succeeded; every failure mode
|
|
85
|
+
* (TTL fresh, no upstream configured, git failure, timeout) resolves false
|
|
86
|
+
* and the caller just serves the status it already has. The in-flight map
|
|
87
|
+
* means concurrent requests share one fetch instead of stampeding.
|
|
88
|
+
*/
|
|
89
|
+
async function maybeFetch(toplevel) {
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
const state = fetchState.get(toplevel);
|
|
92
|
+
if (state !== void 0) {
|
|
93
|
+
if (state.inFlight !== null) return state.inFlight;
|
|
94
|
+
if (now - state.lastAttemptAt < FETCH_TTL_MS) return false;
|
|
95
|
+
}
|
|
96
|
+
const inFlight = (async () => {
|
|
97
|
+
// --no-tags --prune: refs-only refresh, cheapest correct form.
|
|
98
|
+
// No remote name: `git fetch` uses the branch's configured upstream,
|
|
99
|
+
// and a bare `fetch --all` would probe every remote for every badge.
|
|
100
|
+
// GIT_TERMINAL_PROMPT=0: a credential prompt must never hang the route.
|
|
101
|
+
const out = await runGit(toplevel, ["fetch", "--quiet", "--no-tags", "--prune"], {
|
|
102
|
+
env: { GIT_TERMINAL_PROMPT: "0" },
|
|
103
|
+
// measured real-world fetch is ~3s on SSH; the status-call 3s budget
|
|
104
|
+
// would kill healthy fetches on a slow link. 8s is the worst-case
|
|
105
|
+
// route latency, paid at most once per FETCH_TTL_MS per workspace.
|
|
106
|
+
timeout: 8000
|
|
107
|
+
});
|
|
108
|
+
const ok = out.stdout !== null;
|
|
109
|
+
fetchState.set(toplevel, { lastAttemptAt: Date.now(), inFlight: null });
|
|
110
|
+
return ok;
|
|
111
|
+
})();
|
|
112
|
+
if (state === void 0) fetchState.set(toplevel, { lastAttemptAt: now, inFlight });
|
|
113
|
+
else state.inFlight = inFlight;
|
|
114
|
+
return inFlight;
|
|
115
|
+
}
|
|
116
|
+
//#endregion
|
|
117
|
+
|
|
58
118
|
/**
|
|
59
119
|
* Parse `git status --porcelain=v2 --branch` output.
|
|
60
120
|
* Headers: `# branch.head <name>` (or `(detached)`), `# branch.upstream
|
|
@@ -108,9 +168,22 @@ async function gitStatus(dir, wantDetail) {
|
|
|
108
168
|
}
|
|
109
169
|
const toplevel = top.stdout.trim();
|
|
110
170
|
if (toplevel === "") return { git: false };
|
|
111
|
-
const
|
|
171
|
+
const sample = () => runGit(toplevel, ["--no-optional-locks", "status", "--porcelain=v2", "--branch"]);
|
|
172
|
+
let statusOut = await sample();
|
|
112
173
|
if (statusOut.stdout === null) return GIT_DEGRADED;
|
|
113
|
-
|
|
174
|
+
let parsed = parseStatusV2(statusOut.stdout);
|
|
175
|
+
// The upstream header is the cheap gate: no upstream configured → the
|
|
176
|
+
// fetch would be a no-op probe, skip it entirely. When it fires and the
|
|
177
|
+
// fetch succeeds, re-sample so this very response already carries the
|
|
178
|
+
// fresh ahead/behind instead of lagging one cycle behind.
|
|
179
|
+
if (parsed.upstream !== void 0) {
|
|
180
|
+
const fetched = await maybeFetch(toplevel);
|
|
181
|
+
if (fetched) {
|
|
182
|
+
statusOut = await sample();
|
|
183
|
+
if (statusOut.stdout === null) return GIT_DEGRADED;
|
|
184
|
+
parsed = parseStatusV2(statusOut.stdout);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
114
187
|
const dirtyFiles = parsed.staged + parsed.unstaged + parsed.unmerged + parsed.untracked;
|
|
115
188
|
const info = {
|
|
116
189
|
git: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-git-badge",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Git status badges for DeepSeek Harness: sidebar workspace rows (via the sidebar.workspaces.row seam) and an input-row chip showing the current conversation's workspace git state. Event-driven freshness over SSE.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -46,5 +46,10 @@
|
|
|
46
46
|
"package.json",
|
|
47
47
|
"README.md",
|
|
48
48
|
"LICENSE"
|
|
49
|
-
]
|
|
49
|
+
],
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@deepseek-ai/cordis": ">=4",
|
|
52
|
+
"@deepseek-ai/dsh-client-runtime": ">=0.1",
|
|
53
|
+
"react": ">=18"
|
|
54
|
+
}
|
|
50
55
|
}
|