pi-cc-extensions 0.1.1 → 0.1.3

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.
@@ -5,6 +5,8 @@ interface CachedGitStatus {
5
5
  staged: number;
6
6
  unstaged: number;
7
7
  untracked: number;
8
+ added: number;
9
+ deleted: number;
8
10
  timestamp: number;
9
11
  }
10
12
 
@@ -19,19 +21,25 @@ const CACHE_TTL_MS = 1000; // 1 second for file status
19
21
  const BRANCH_TTL_MS = 500; // Shorter TTL so branch updates quickly after invalidation
20
22
  let cachedStatus: CachedGitStatus | null = null;
21
23
  let cachedBranch: CachedBranch | null = null;
24
+ let statusCwd: string | null = null;
25
+ let branchCwd: string | null = null;
22
26
  let pendingFetch: Promise<void> | null = null;
23
27
  let pendingBranchFetch: Promise<void> | null = null;
28
+ let fetchCwd: string | null = null;
29
+ let branchFetchCwd: string | null = null;
24
30
  let invalidationCounter = 0; // Track invalidations to prevent stale updates
25
31
  let branchInvalidationCounter = 0;
26
32
 
27
33
  /**
28
- * Parse git status --porcelain output
29
- *
34
+ * Parse git status --porcelain output.
35
+ *
30
36
  * Format: XY filename
31
37
  * X = index status, Y = working tree status
32
38
  * ?? = untracked
33
- * Other X values = staged
34
- * Other Y values = unstaged
39
+ *
40
+ * Each file is counted exactly once:
41
+ * - Y ≠ ' ' → unstaged (working tree has changes; if X also shows staged, Y wins)
42
+ * - X ≠ ' ' && X ≠ '?' → staged only (index has changes, working tree is clean)
35
43
  */
36
44
  function parseGitStatusOutput(output: string): { staged: number; unstaged: number; untracked: number } {
37
45
  let staged = 0;
@@ -48,24 +56,24 @@ function parseGitStatusOutput(output: string): { staged: number; unstaged: numbe
48
56
  continue;
49
57
  }
50
58
 
51
- // X position (index/staged)
52
- if (x && x !== " " && x !== "?") {
53
- staged++;
54
- }
55
-
56
- // Y position (working tree/unstaged)
59
+ // Y position (working tree) takes priority — if the file has working-tree
60
+ // changes at all, count it as unstaged.
57
61
  if (y && y !== " ") {
58
62
  unstaged++;
63
+ } else if (x && x !== " " && x !== "?") {
64
+ // X position (index) only counted when working tree is clean.
65
+ staged++;
59
66
  }
60
67
  }
61
68
 
62
69
  return { staged, unstaged, untracked };
63
70
  }
64
71
 
65
- function runGit(args: string[], timeoutMs = 200): Promise<string | null> {
72
+ function runGit(args: string[], cwd: string, timeoutMs = 200): Promise<string | null> {
66
73
  return new Promise((resolve) => {
67
74
  const proc = spawn("git", args, {
68
75
  stdio: ["ignore", "pipe", "pipe"],
76
+ cwd,
69
77
  });
70
78
 
71
79
  let stdout = "";
@@ -83,7 +91,7 @@ function runGit(args: string[], timeoutMs = 200): Promise<string | null> {
83
91
  });
84
92
 
85
93
  proc.on("close", (code) => {
86
- finish(code === 0 ? stdout.trim() : null);
94
+ finish(code === 0 ? stdout.replace(/\n+$/, "") : null);
87
95
  });
88
96
 
89
97
  proc.on("error", () => {
@@ -101,20 +109,42 @@ function runGit(args: string[], timeoutMs = 200): Promise<string | null> {
101
109
  * Fetch current git branch asynchronously.
102
110
  * For detached HEAD, returns the short commit SHA (matches provider's "detached" behavior).
103
111
  */
104
- async function fetchGitBranch(): Promise<string | null> {
105
- const branch = await runGit(["branch", "--show-current"]);
112
+ async function fetchGitBranch(cwd: string): Promise<string | null> {
113
+ const branch = await runGit(["branch", "--show-current"], cwd);
106
114
  if (branch === null) return null;
107
115
  if (branch) return branch;
108
116
 
109
- const sha = await runGit(["rev-parse", "--short", "HEAD"]);
117
+ const sha = await runGit(["rev-parse", "--short", "HEAD"], cwd);
110
118
  return sha ? `${sha} (detached)` : "detached";
111
119
  }
112
120
 
113
121
  /**
114
- * Fetch git status asynchronously
122
+ * Parse git diff --shortstat HEAD output.
123
+ * Format: " N files changed, M insertions(+), K deletions(-)"
115
124
  */
116
- async function fetchGitStatus(): Promise<{ staged: number; unstaged: number; untracked: number } | null> {
117
- const output = await runGit(["status", "--porcelain"], 500);
125
+ function parseDiffStat(output: string): { added: number; deleted: number } {
126
+ const added = (output.match(/(\d+) insertion/) ?? [])[1];
127
+ const deleted = (output.match(/(\d+) deletion/) ?? [])[1];
128
+ return {
129
+ added: added ? Number(added) : 0,
130
+ deleted: deleted ? Number(deleted) : 0,
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Fetch line-level diff stats (additions / deletions) from HEAD.
136
+ */
137
+ async function fetchDiffStat(cwd: string): Promise<{ added: number; deleted: number } | null> {
138
+ const output = await runGit(["diff", "--shortstat", "HEAD"], cwd, 1000);
139
+ if (output === null) return null;
140
+ return parseDiffStat(output);
141
+ }
142
+
143
+ /**
144
+ * Fetch git status asynchronously.
145
+ */
146
+ async function fetchGitStatus(cwd: string): Promise<{ staged: number; unstaged: number; untracked: number } | null> {
147
+ const output = await runGit(["status", "--porcelain"], cwd, 500);
118
148
  if (output === null) return null;
119
149
  return parseGitStatusOutput(output);
120
150
  }
@@ -123,9 +153,15 @@ async function fetchGitStatus(): Promise<{ staged: number; unstaged: number; unt
123
153
  * Get the current git branch with caching.
124
154
  * Falls back to provider branch if our cache is empty.
125
155
  */
126
- export function getCurrentBranch(providerBranch: string | null): string | null {
156
+ export function getCurrentBranch(providerBranch: string | null, cwd: string): string | null {
127
157
  const now = Date.now();
128
158
 
159
+ // Invalidate cache if cwd changed (different repo)
160
+ if (branchCwd !== cwd) {
161
+ cachedBranch = null;
162
+ branchCwd = cwd;
163
+ }
164
+
129
165
  // Return cached if fresh
130
166
  if (cachedBranch && now - cachedBranch.timestamp < BRANCH_TTL_MS) {
131
167
  return cachedBranch.branch;
@@ -134,9 +170,10 @@ export function getCurrentBranch(providerBranch: string | null): string | null {
134
170
  // Trigger background fetch if not already pending
135
171
  if (!pendingBranchFetch) {
136
172
  const fetchId = branchInvalidationCounter;
137
- pendingBranchFetch = fetchGitBranch().then((result) => {
138
- // Cache result if no invalidation happened (including null for non-git dirs)
139
- if (fetchId === branchInvalidationCounter) {
173
+ branchFetchCwd = cwd;
174
+ pendingBranchFetch = fetchGitBranch(cwd).then((result) => {
175
+ // Cache result if no invalidation happened AND cwd hasn't changed
176
+ if (fetchId === branchInvalidationCounter && branchFetchCwd === cwd) {
140
177
  cachedBranch = {
141
178
  branch: result,
142
179
  timestamp: Date.now(),
@@ -156,12 +193,18 @@ export function getCurrentBranch(providerBranch: string | null): string | null {
156
193
  * This is designed for synchronous render() calls - returns last known value
157
194
  * while refreshing in background.
158
195
  */
159
- export function getGitStatus(providerBranch: string | null, pollingMode: GitPollingMode = "full"): GitStatus {
196
+ export function getGitStatus(providerBranch: string | null, cwd: string, pollingMode: GitPollingMode = "full"): GitStatus {
160
197
  const now = Date.now();
161
- const branch = pollingMode === "off" ? providerBranch : getCurrentBranch(providerBranch);
198
+ const branch = pollingMode === "off" ? providerBranch : getCurrentBranch(providerBranch, cwd);
162
199
 
163
200
  if (pollingMode !== "full") {
164
- return { branch, staged: 0, unstaged: 0, untracked: 0 };
201
+ return { branch, staged: 0, unstaged: 0, untracked: 0, added: 0, deleted: 0 };
202
+ }
203
+
204
+ // Invalidate cache if cwd changed (different repo)
205
+ if (statusCwd !== cwd) {
206
+ cachedStatus = null;
207
+ statusCwd = cwd;
165
208
  }
166
209
 
167
210
  // Return cached if fresh
@@ -171,18 +214,38 @@ export function getGitStatus(providerBranch: string | null, pollingMode: GitPoll
171
214
  staged: cachedStatus.staged,
172
215
  unstaged: cachedStatus.unstaged,
173
216
  untracked: cachedStatus.untracked,
217
+ added: cachedStatus.added,
218
+ deleted: cachedStatus.deleted,
174
219
  };
175
220
  }
176
221
 
177
222
  // Trigger background fetch if not already pending
178
223
  if (!pendingFetch) {
179
224
  const fetchId = invalidationCounter; // Capture current counter
180
- pendingFetch = fetchGitStatus().then((result) => {
181
- // Cache result if no invalidation happened (including null for non-git dirs)
182
- if (fetchId === invalidationCounter) {
183
- cachedStatus = result
184
- ? { staged: result.staged, unstaged: result.unstaged, untracked: result.untracked, timestamp: Date.now() }
185
- : { staged: 0, unstaged: 0, untracked: 0, timestamp: Date.now() };
225
+ fetchCwd = cwd;
226
+ pendingFetch = Promise.all([
227
+ fetchGitStatus(cwd),
228
+ fetchDiffStat(cwd),
229
+ ]).then(([statusResult, diffResult]) => {
230
+ // Cache result if no invalidation happened AND cwd hasn't changed
231
+ if (fetchId === invalidationCounter && fetchCwd === cwd) {
232
+ cachedStatus = statusResult
233
+ ? {
234
+ staged: statusResult.staged,
235
+ unstaged: statusResult.unstaged,
236
+ untracked: statusResult.untracked,
237
+ added: diffResult?.added ?? 0,
238
+ deleted: diffResult?.deleted ?? 0,
239
+ timestamp: Date.now(),
240
+ }
241
+ : {
242
+ staged: 0,
243
+ unstaged: 0,
244
+ untracked: 0,
245
+ added: 0,
246
+ deleted: 0,
247
+ timestamp: Date.now(),
248
+ };
186
249
  }
187
250
  pendingFetch = null;
188
251
  });
@@ -195,10 +258,12 @@ export function getGitStatus(providerBranch: string | null, pollingMode: GitPoll
195
258
  staged: cachedStatus.staged,
196
259
  unstaged: cachedStatus.unstaged,
197
260
  untracked: cachedStatus.untracked,
261
+ added: cachedStatus.added,
262
+ deleted: cachedStatus.deleted,
198
263
  };
199
264
  }
200
265
 
201
- return { branch, staged: 0, unstaged: 0, untracked: 0 };
266
+ return { branch, staged: 0, unstaged: 0, untracked: 0, added: 0, deleted: 0 };
202
267
  }
203
268
 
204
269
  /**