@openchamber/web 1.0.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.
Files changed (114) hide show
  1. package/README.md +34 -0
  2. package/bin/cli.js +561 -0
  3. package/dist/apple-touch-icon-120x120.png +0 -0
  4. package/dist/apple-touch-icon-152x152.png +0 -0
  5. package/dist/apple-touch-icon-167x167.png +0 -0
  6. package/dist/apple-touch-icon-180x180.png +0 -0
  7. package/dist/apple-touch-icon.png +0 -0
  8. package/dist/apple-touch-icon.svg +18 -0
  9. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  10. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  11. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  12. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  13. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  14. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  15. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  16. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  17. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  18. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  19. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  20. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  21. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  22. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  23. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  24. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  25. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  26. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  27. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  28. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  29. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  30. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  31. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  32. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  33. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  34. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  35. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  36. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  37. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  38. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  39. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  40. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  41. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  42. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  43. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  44. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  45. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  46. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  47. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  48. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  49. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  50. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  51. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  52. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  53. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  54. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  55. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  56. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  57. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  58. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  59. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  60. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  61. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  62. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  63. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  64. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  65. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  66. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  67. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  68. package/dist/assets/MonacoDiffViewer-J2AIDXvs.js +1 -0
  69. package/dist/assets/ToolOutputDialog-B0y5ge-3.js +5 -0
  70. package/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  71. package/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  72. package/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  73. package/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  74. package/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
  75. package/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
  76. package/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  77. package/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  78. package/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
  79. package/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
  80. package/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  81. package/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  82. package/dist/assets/index-iDfKTtMQ.css +1 -0
  83. package/dist/assets/index-kNntYPVa.js +2 -0
  84. package/dist/assets/main-BEJ2XliY.css +1 -0
  85. package/dist/assets/main-Ba339xde.js +59 -0
  86. package/dist/assets/vendor--B3aGWKBE.css +32 -0
  87. package/dist/assets/vendor-.pnpm-B1ce5n1Z.js +3192 -0
  88. package/dist/favicon-16.png +0 -0
  89. package/dist/favicon-32.png +0 -0
  90. package/dist/index.html +197 -0
  91. package/dist/logo-dark.svg +4 -0
  92. package/dist/logo-light.svg +4 -0
  93. package/dist/site.webmanifest +36 -0
  94. package/dist/vite.svg +1 -0
  95. package/package.json +92 -0
  96. package/public/apple-touch-icon-120x120.png +0 -0
  97. package/public/apple-touch-icon-152x152.png +0 -0
  98. package/public/apple-touch-icon-167x167.png +0 -0
  99. package/public/apple-touch-icon-180x180.png +0 -0
  100. package/public/apple-touch-icon.png +0 -0
  101. package/public/apple-touch-icon.svg +18 -0
  102. package/public/favicon-16.png +0 -0
  103. package/public/favicon-32.png +0 -0
  104. package/public/logo-dark.svg +4 -0
  105. package/public/logo-light.svg +4 -0
  106. package/public/site.webmanifest +36 -0
  107. package/public/vite.svg +1 -0
  108. package/server/index.d.ts +28 -0
  109. package/server/index.js +3038 -0
  110. package/server/lib/git-identity-storage.js +108 -0
  111. package/server/lib/git-service.js +899 -0
  112. package/server/lib/opencode-config.js +471 -0
  113. package/server/lib/opencode-config.js.d.ts +12 -0
  114. package/server/lib/ui-auth.js +266 -0
@@ -0,0 +1,899 @@
1
+ import simpleGit from 'simple-git';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ const fsp = fs.promises;
5
+
6
+ export async function isGitRepository(directory) {
7
+ if (!directory || !fs.existsSync(directory)) {
8
+ return false;
9
+ }
10
+
11
+ const gitDir = path.join(directory, '.git');
12
+ return fs.existsSync(gitDir);
13
+ }
14
+
15
+ export async function ensureOpenChamberIgnored(directory) {
16
+ if (!directory || !fs.existsSync(directory)) {
17
+ return false;
18
+ }
19
+
20
+ const gitDir = path.join(directory, '.git');
21
+ if (!fs.existsSync(gitDir)) {
22
+ return false;
23
+ }
24
+
25
+ const infoDir = path.join(gitDir, 'info');
26
+ const excludePath = path.join(infoDir, 'exclude');
27
+ const entry = '/.openchamber/';
28
+
29
+ try {
30
+ await fsp.mkdir(infoDir, { recursive: true });
31
+ let contents = '';
32
+ try {
33
+ contents = await fsp.readFile(excludePath, 'utf8');
34
+ } catch (readError) {
35
+ if (readError && readError.code !== 'ENOENT') {
36
+ throw readError;
37
+ }
38
+ }
39
+
40
+ const lines = contents.split(/\r?\n/).map((line) => line.trim());
41
+ if (!lines.includes(entry)) {
42
+ const prefix = contents.length > 0 && !contents.endsWith('\n') ? '\n' : '';
43
+ await fsp.appendFile(excludePath, `${prefix}${entry}\n`, 'utf8');
44
+ }
45
+
46
+ return true;
47
+ } catch (error) {
48
+ console.error('Failed to ensure .openchamber ignore:', error);
49
+ throw error;
50
+ }
51
+ }
52
+
53
+ export async function getGlobalIdentity() {
54
+ const git = simpleGit();
55
+
56
+ try {
57
+ const userName = await git.getConfig('user.name', 'global').catch(() => null);
58
+ const userEmail = await git.getConfig('user.email', 'global').catch(() => null);
59
+ const sshCommand = await git.getConfig('core.sshCommand', 'global').catch(() => null);
60
+
61
+ return {
62
+ userName: userName?.value || null,
63
+ userEmail: userEmail?.value || null,
64
+ sshCommand: sshCommand?.value || null
65
+ };
66
+ } catch (error) {
67
+ console.error('Failed to get global Git identity:', error);
68
+ return {
69
+ userName: null,
70
+ userEmail: null,
71
+ sshCommand: null
72
+ };
73
+ }
74
+ }
75
+
76
+ export async function getCurrentIdentity(directory) {
77
+ const git = simpleGit(directory);
78
+
79
+ try {
80
+
81
+ const userName = await git.getConfig('user.name', 'local').catch(() =>
82
+ git.getConfig('user.name', 'global')
83
+ );
84
+
85
+ const userEmail = await git.getConfig('user.email', 'local').catch(() =>
86
+ git.getConfig('user.email', 'global')
87
+ );
88
+
89
+ const sshCommand = await git.getConfig('core.sshCommand', 'local').catch(() =>
90
+ git.getConfig('core.sshCommand', 'global')
91
+ );
92
+
93
+ return {
94
+ userName: userName?.value || null,
95
+ userEmail: userEmail?.value || null,
96
+ sshCommand: sshCommand?.value || null
97
+ };
98
+ } catch (error) {
99
+ console.error('Failed to get current Git identity:', error);
100
+ return {
101
+ userName: null,
102
+ userEmail: null,
103
+ sshCommand: null
104
+ };
105
+ }
106
+ }
107
+
108
+ export async function setLocalIdentity(directory, profile) {
109
+ const git = simpleGit(directory);
110
+
111
+ try {
112
+
113
+ await git.addConfig('user.name', profile.userName, false, 'local');
114
+ await git.addConfig('user.email', profile.userEmail, false, 'local');
115
+
116
+ if (profile.sshKey) {
117
+ await git.addConfig(
118
+ 'core.sshCommand',
119
+ `ssh -i ${profile.sshKey}`,
120
+ false,
121
+ 'local'
122
+ );
123
+ }
124
+
125
+ return true;
126
+ } catch (error) {
127
+ console.error('Failed to set Git identity:', error);
128
+ throw error;
129
+ }
130
+ }
131
+
132
+ export async function getStatus(directory) {
133
+ const git = simpleGit(directory);
134
+
135
+ try {
136
+ const status = await git.status();
137
+
138
+ const [stagedStatsRaw, workingStatsRaw] = await Promise.all([
139
+ git.raw(['diff', '--cached', '--numstat']).catch(() => ''),
140
+ git.raw(['diff', '--numstat']).catch(() => ''),
141
+ ]);
142
+
143
+ const diffStatsMap = new Map();
144
+
145
+ const accumulateStats = (raw) => {
146
+ if (!raw) return;
147
+ raw
148
+ .split('\n')
149
+ .map((line) => line.trim())
150
+ .filter(Boolean)
151
+ .forEach((line) => {
152
+ const parts = line.split('\t');
153
+ if (parts.length < 3) {
154
+ return;
155
+ }
156
+ const [insertionsRaw, deletionsRaw, ...pathParts] = parts;
157
+ const path = pathParts.join('\t');
158
+ if (!path) {
159
+ return;
160
+ }
161
+ const insertions = insertionsRaw === '-' ? 0 : parseInt(insertionsRaw, 10) || 0;
162
+ const deletions = deletionsRaw === '-' ? 0 : parseInt(deletionsRaw, 10) || 0;
163
+
164
+ const existing = diffStatsMap.get(path) || { insertions: 0, deletions: 0 };
165
+ diffStatsMap.set(path, {
166
+ insertions: existing.insertions + insertions,
167
+ deletions: existing.deletions + deletions,
168
+ });
169
+ });
170
+ };
171
+
172
+ accumulateStats(stagedStatsRaw);
173
+ accumulateStats(workingStatsRaw);
174
+
175
+ const diffStats = Object.fromEntries(diffStatsMap.entries());
176
+
177
+ const newFileStats = await Promise.all(
178
+ status.files.map(async (file) => {
179
+ const working = (file.working_dir || '').trim();
180
+ const indexStatus = (file.index || '').trim();
181
+ const statusCode = working || indexStatus;
182
+
183
+ if (statusCode !== '?' && statusCode !== 'A') {
184
+ return null;
185
+ }
186
+
187
+ const existing = diffStats[file.path];
188
+ if (existing && existing.insertions > 0) {
189
+ return null;
190
+ }
191
+
192
+ const absolutePath = path.join(directory, file.path);
193
+
194
+ try {
195
+ const stat = await fsp.stat(absolutePath);
196
+ if (!stat.isFile()) {
197
+ return null;
198
+ }
199
+
200
+ const buffer = await fsp.readFile(absolutePath);
201
+ if (buffer.indexOf(0) !== -1) {
202
+ return {
203
+ path: file.path,
204
+ insertions: existing?.insertions ?? 0,
205
+ deletions: existing?.deletions ?? 0,
206
+ };
207
+ }
208
+
209
+ const normalized = buffer.toString('utf8').replace(/\r\n/g, '\n');
210
+ if (!normalized.length) {
211
+ return {
212
+ path: file.path,
213
+ insertions: 0,
214
+ deletions: 0,
215
+ };
216
+ }
217
+
218
+ const segments = normalized.split('\n');
219
+ if (normalized.endsWith('\n')) {
220
+ segments.pop();
221
+ }
222
+
223
+ const lineCount = segments.length;
224
+ return {
225
+ path: file.path,
226
+ insertions: lineCount,
227
+ deletions: 0,
228
+ };
229
+ } catch (error) {
230
+ console.warn('Failed to estimate diff stats for new file', file.path, error);
231
+ return null;
232
+ }
233
+ })
234
+ );
235
+
236
+ for (const entry of newFileStats) {
237
+ if (!entry) continue;
238
+ diffStats[entry.path] = {
239
+ insertions: entry.insertions,
240
+ deletions: entry.deletions,
241
+ };
242
+ }
243
+
244
+ return {
245
+ current: status.current,
246
+ tracking: status.tracking,
247
+ ahead: status.ahead,
248
+ behind: status.behind,
249
+ files: status.files.map(f => ({
250
+ path: f.path,
251
+ index: f.index,
252
+ working_dir: f.working_dir
253
+ })),
254
+ isClean: status.isClean(),
255
+ diffStats,
256
+ };
257
+ } catch (error) {
258
+ console.error('Failed to get Git status:', error);
259
+ throw error;
260
+ }
261
+ }
262
+
263
+ export async function getDiff(directory, { path, staged = false, contextLines = 3 } = {}) {
264
+ const git = simpleGit(directory);
265
+
266
+ try {
267
+ const args = ['diff', '--no-color'];
268
+
269
+ if (typeof contextLines === 'number' && !Number.isNaN(contextLines)) {
270
+ args.push(`-U${Math.max(0, contextLines)}`);
271
+ }
272
+
273
+ if (staged) {
274
+ args.push('--cached');
275
+ }
276
+
277
+ if (path) {
278
+ args.push('--', path);
279
+ }
280
+
281
+ const diff = await git.raw(args);
282
+ if (diff && diff.trim().length > 0) {
283
+ return diff;
284
+ }
285
+
286
+ if (staged) {
287
+ return diff;
288
+ }
289
+
290
+ try {
291
+ await git.raw(['ls-files', '--error-unmatch', path]);
292
+ return diff;
293
+ } catch {
294
+ const noIndexArgs = ['diff', '--no-color'];
295
+ if (typeof contextLines === 'number' && !Number.isNaN(contextLines)) {
296
+ noIndexArgs.push(`-U${Math.max(0, contextLines)}`);
297
+ }
298
+ noIndexArgs.push('--no-index', '--', '/dev/null', path);
299
+ const noIndexDiff = await git.raw(noIndexArgs);
300
+ return noIndexDiff;
301
+ }
302
+ } catch (error) {
303
+ console.error('Failed to get Git diff:', error);
304
+ throw error;
305
+ }
306
+ }
307
+
308
+ export async function getFileDiff(directory, { path: filePath, staged = false } = {}) {
309
+ if (!directory || !filePath) {
310
+ throw new Error('directory and path are required for getFileDiff');
311
+ }
312
+
313
+ const git = simpleGit(directory);
314
+
315
+ let original = '';
316
+ try {
317
+ original = await git.show([`HEAD:${filePath}`]);
318
+ } catch {
319
+
320
+ original = '';
321
+ }
322
+
323
+ const fullPath = path.join(directory, filePath);
324
+ let modified = '';
325
+ try {
326
+ const stat = await fsp.stat(fullPath);
327
+ if (stat.isFile()) {
328
+ modified = await fsp.readFile(fullPath, 'utf8');
329
+ }
330
+ } catch (error) {
331
+ if (error && typeof error === 'object' && error.code === 'ENOENT') {
332
+
333
+ modified = '';
334
+ } else {
335
+ console.error('Failed to read modified file contents for diff:', error);
336
+ throw error;
337
+ }
338
+ }
339
+
340
+ return {
341
+ original,
342
+ modified,
343
+ path: filePath,
344
+ };
345
+ }
346
+
347
+ export async function revertFile(directory, filePath) {
348
+ const git = simpleGit(directory);
349
+ const repoRoot = path.resolve(directory);
350
+ const absoluteTarget = path.resolve(repoRoot, filePath);
351
+
352
+ if (!absoluteTarget.startsWith(repoRoot + path.sep) && absoluteTarget !== repoRoot) {
353
+ throw new Error('Invalid file path');
354
+ }
355
+
356
+ const isTracked = await git
357
+ .raw(['ls-files', '--error-unmatch', filePath])
358
+ .then(() => true)
359
+ .catch(() => false);
360
+
361
+ if (!isTracked) {
362
+ try {
363
+ await git.raw(['clean', '-f', '-d', '--', filePath]);
364
+ return;
365
+ } catch (cleanError) {
366
+ try {
367
+ await fsp.rm(absoluteTarget, { recursive: true, force: true });
368
+ return;
369
+ } catch (fsError) {
370
+ if (fsError && typeof fsError === 'object' && fsError.code === 'ENOENT') {
371
+ return;
372
+ }
373
+ console.error('Failed to remove untracked file during revert:', fsError);
374
+ throw fsError;
375
+ }
376
+ }
377
+ }
378
+
379
+ try {
380
+ await git.raw(['restore', '--staged', filePath]);
381
+ } catch (error) {
382
+ await git.raw(['reset', 'HEAD', '--', filePath]).catch(() => {});
383
+ }
384
+
385
+ try {
386
+ await git.raw(['restore', filePath]);
387
+ } catch (error) {
388
+ try {
389
+ await git.raw(['checkout', '--', filePath]);
390
+ } catch (fallbackError) {
391
+ console.error('Failed to revert git file:', fallbackError);
392
+ throw fallbackError;
393
+ }
394
+ }
395
+ }
396
+
397
+ export async function collectDiffs(directory, files = []) {
398
+ const results = [];
399
+ for (const filePath of files) {
400
+ try {
401
+ const diff = await getDiff(directory, { path: filePath });
402
+ if (diff && diff.trim().length > 0) {
403
+ results.push({ path: filePath, diff });
404
+ }
405
+ } catch (error) {
406
+ console.error(`Failed to diff ${filePath}:`, error);
407
+ }
408
+ }
409
+ return results;
410
+ }
411
+
412
+ export async function pull(directory, options = {}) {
413
+ const git = simpleGit(directory);
414
+
415
+ try {
416
+ const result = await git.pull(
417
+ options.remote || 'origin',
418
+ options.branch,
419
+ options.options || {}
420
+ );
421
+
422
+ return {
423
+ success: true,
424
+ summary: result.summary,
425
+ files: result.files,
426
+ insertions: result.insertions,
427
+ deletions: result.deletions
428
+ };
429
+ } catch (error) {
430
+ console.error('Failed to pull:', error);
431
+ throw error;
432
+ }
433
+ }
434
+
435
+ export async function push(directory, options = {}) {
436
+ const git = simpleGit(directory);
437
+
438
+ try {
439
+ const result = await git.push(
440
+ options.remote || 'origin',
441
+ options.branch,
442
+ options.options || {}
443
+ );
444
+
445
+ return {
446
+ success: true,
447
+ pushed: result.pushed,
448
+ repo: result.repo,
449
+ ref: result.ref
450
+ };
451
+ } catch (error) {
452
+ console.error('Failed to push:', error);
453
+ throw error;
454
+ }
455
+ }
456
+
457
+ export async function deleteRemoteBranch(directory, options = {}) {
458
+ const { branch, remote } = options;
459
+ if (!branch) {
460
+ throw new Error('branch is required to delete remote branch');
461
+ }
462
+
463
+ const git = simpleGit(directory);
464
+ const targetBranch = branch.startsWith('refs/heads/')
465
+ ? branch.substring('refs/heads/'.length)
466
+ : branch;
467
+ const remoteName = remote || 'origin';
468
+
469
+ try {
470
+ await git.push(remoteName, `:${targetBranch}`);
471
+ return { success: true };
472
+ } catch (error) {
473
+ console.error('Failed to delete remote branch:', error);
474
+ throw error;
475
+ }
476
+ }
477
+
478
+ export async function fetch(directory, options = {}) {
479
+ const git = simpleGit(directory);
480
+
481
+ try {
482
+ await git.fetch(
483
+ options.remote || 'origin',
484
+ options.branch,
485
+ options.options || {}
486
+ );
487
+
488
+ return { success: true };
489
+ } catch (error) {
490
+ console.error('Failed to fetch:', error);
491
+ throw error;
492
+ }
493
+ }
494
+
495
+ export async function commit(directory, message, options = {}) {
496
+ const git = simpleGit(directory);
497
+
498
+ try {
499
+
500
+ if (options.addAll) {
501
+ await git.add('.');
502
+ } else if (Array.isArray(options.files) && options.files.length > 0) {
503
+ await git.add(options.files);
504
+ }
505
+
506
+ const commitArgs =
507
+ !options.addAll && Array.isArray(options.files) && options.files.length > 0
508
+ ? options.files
509
+ : undefined;
510
+
511
+ const result = await git.commit(message, commitArgs);
512
+
513
+ return {
514
+ success: true,
515
+ commit: result.commit,
516
+ branch: result.branch,
517
+ summary: result.summary
518
+ };
519
+ } catch (error) {
520
+ console.error('Failed to commit:', error);
521
+ throw error;
522
+ }
523
+ }
524
+
525
+ export async function getBranches(directory) {
526
+ const git = simpleGit(directory);
527
+
528
+ try {
529
+ const result = await git.branch();
530
+
531
+ const allBranches = result.all;
532
+ const remoteBranches = allBranches.filter(branch => branch.startsWith('remotes/'));
533
+ const activeRemoteBranches = await filterActiveRemoteBranches(git, remoteBranches);
534
+
535
+ const filteredAll = [
536
+ ...allBranches.filter(branch => !branch.startsWith('remotes/')),
537
+ ...activeRemoteBranches
538
+ ];
539
+
540
+ return {
541
+ all: filteredAll,
542
+ current: result.current,
543
+ branches: result.branches
544
+ };
545
+ } catch (error) {
546
+ console.error('Failed to get branches:', error);
547
+ throw error;
548
+ }
549
+ }
550
+
551
+ async function filterActiveRemoteBranches(git, remoteBranches) {
552
+ try {
553
+
554
+ const lsRemoteResult = await git.raw(['ls-remote', '--heads', 'origin']);
555
+ const actualRemoteBranches = new Set();
556
+
557
+ const lines = lsRemoteResult.trim().split('\n');
558
+ for (const line of lines) {
559
+ if (line.includes('\trefs/heads/')) {
560
+ const branchName = line.split('\t')[1].replace('refs/heads/', '');
561
+ actualRemoteBranches.add(branchName);
562
+ }
563
+ }
564
+
565
+ return remoteBranches.filter(remoteBranch => {
566
+
567
+ const match = remoteBranch.match(/^remotes\/[^\/]+\/(.+)$/);
568
+ if (!match) return false;
569
+
570
+ const branchName = match[1];
571
+ return actualRemoteBranches.has(branchName);
572
+ });
573
+ } catch (error) {
574
+ console.warn('Failed to filter active remote branches, returning all:', error.message);
575
+ return remoteBranches;
576
+ }
577
+ }
578
+
579
+ export async function createBranch(directory, branchName, options = {}) {
580
+ const git = simpleGit(directory);
581
+
582
+ try {
583
+ await git.checkoutBranch(branchName, options.startPoint || 'HEAD');
584
+ return { success: true, branch: branchName };
585
+ } catch (error) {
586
+ console.error('Failed to create branch:', error);
587
+ throw error;
588
+ }
589
+ }
590
+
591
+ export async function checkoutBranch(directory, branchName) {
592
+ const git = simpleGit(directory);
593
+
594
+ try {
595
+ await git.checkout(branchName);
596
+ return { success: true, branch: branchName };
597
+ } catch (error) {
598
+ console.error('Failed to checkout branch:', error);
599
+ throw error;
600
+ }
601
+ }
602
+
603
+ export async function getWorktrees(directory) {
604
+ const git = simpleGit(directory);
605
+
606
+ try {
607
+ const result = await git.raw(['worktree', 'list', '--porcelain']);
608
+
609
+ const worktrees = [];
610
+ const lines = result.split('\n');
611
+ let current = {};
612
+
613
+ for (const line of lines) {
614
+ if (line.startsWith('worktree ')) {
615
+ if (current.worktree) {
616
+ worktrees.push(current);
617
+ }
618
+ current = { worktree: line.substring(9) };
619
+ } else if (line.startsWith('HEAD ')) {
620
+ current.head = line.substring(5);
621
+ } else if (line.startsWith('branch ')) {
622
+ current.branch = line.substring(7);
623
+ } else if (line === '') {
624
+ if (current.worktree) {
625
+ worktrees.push(current);
626
+ current = {};
627
+ }
628
+ }
629
+ }
630
+
631
+ if (current.worktree) {
632
+ worktrees.push(current);
633
+ }
634
+
635
+ return worktrees;
636
+ } catch (error) {
637
+ console.error('Failed to list worktrees:', error);
638
+ throw error;
639
+ }
640
+ }
641
+
642
+ export async function addWorktree(directory, worktreePath, branch, options = {}) {
643
+ const git = simpleGit(directory);
644
+
645
+ try {
646
+ const args = ['worktree', 'add'];
647
+
648
+ if (options.createBranch) {
649
+ args.push('-b', branch);
650
+ }
651
+
652
+ args.push(worktreePath);
653
+
654
+ if (!options.createBranch) {
655
+ args.push(branch);
656
+ }
657
+
658
+ await git.raw(args);
659
+
660
+ return {
661
+ success: true,
662
+ path: worktreePath,
663
+ branch
664
+ };
665
+ } catch (error) {
666
+ console.error('Failed to add worktree:', error);
667
+ throw error;
668
+ }
669
+ }
670
+
671
+ export async function removeWorktree(directory, worktreePath, options = {}) {
672
+ const git = simpleGit(directory);
673
+
674
+ try {
675
+ const args = ['worktree', 'remove', worktreePath];
676
+
677
+ if (options.force) {
678
+ args.push('--force');
679
+ }
680
+
681
+ await git.raw(args);
682
+
683
+ return { success: true };
684
+ } catch (error) {
685
+ console.error('Failed to remove worktree:', error);
686
+ throw error;
687
+ }
688
+ }
689
+
690
+ export async function deleteBranch(directory, branch, options = {}) {
691
+ const git = simpleGit(directory);
692
+
693
+ try {
694
+ const branchName = branch.startsWith('refs/heads/')
695
+ ? branch.substring('refs/heads/'.length)
696
+ : branch;
697
+ const args = ['branch', options.force ? '-D' : '-d', branchName];
698
+ await git.raw(args);
699
+ return { success: true };
700
+ } catch (error) {
701
+ console.error('Failed to delete branch:', error);
702
+ throw error;
703
+ }
704
+ }
705
+
706
+ export async function getLog(directory, options = {}) {
707
+ const git = simpleGit(directory);
708
+
709
+ try {
710
+ const maxCount = options.maxCount || 50;
711
+ const baseLog = await git.log({
712
+ maxCount,
713
+ from: options.from,
714
+ to: options.to,
715
+ file: options.file
716
+ });
717
+
718
+ const logArgs = [
719
+ 'log',
720
+ `--max-count=${maxCount}`,
721
+ '--date=iso',
722
+ '--pretty=format:%H%x1f%an%x1f%ae%x1f%ad%x1f%s%x1e',
723
+ '--shortstat'
724
+ ];
725
+
726
+ if (options.from && options.to) {
727
+ logArgs.push(`${options.from}..${options.to}`);
728
+ } else if (options.from) {
729
+ logArgs.push(`${options.from}..HEAD`);
730
+ } else if (options.to) {
731
+ logArgs.push(options.to);
732
+ }
733
+
734
+ if (options.file) {
735
+ logArgs.push('--', options.file);
736
+ }
737
+
738
+ const rawLog = await git.raw(logArgs);
739
+ const records = rawLog
740
+ .split('\x1e')
741
+ .map((entry) => entry.trim())
742
+ .filter(Boolean);
743
+
744
+ const statsMap = new Map();
745
+
746
+ records.forEach((record) => {
747
+ const lines = record.split('\n').filter((line) => line.trim().length > 0);
748
+ const header = lines.shift() || '';
749
+ const [hash] = header.split('\x1f');
750
+ if (!hash) {
751
+ return;
752
+ }
753
+
754
+ let filesChanged = 0;
755
+ let insertions = 0;
756
+ let deletions = 0;
757
+
758
+ lines.forEach((line) => {
759
+ const filesMatch = line.match(/(\d+)\s+files?\s+changed/);
760
+ const insertMatch = line.match(/(\d+)\s+insertions?\(\+\)/);
761
+ const deleteMatch = line.match(/(\d+)\s+deletions?\(-\)/);
762
+
763
+ if (filesMatch) {
764
+ filesChanged = parseInt(filesMatch[1], 10);
765
+ }
766
+ if (insertMatch) {
767
+ insertions = parseInt(insertMatch[1], 10);
768
+ }
769
+ if (deleteMatch) {
770
+ deletions = parseInt(deleteMatch[1], 10);
771
+ }
772
+ });
773
+
774
+ statsMap.set(hash, { filesChanged, insertions, deletions });
775
+ });
776
+
777
+ const merged = baseLog.all.map((entry) => {
778
+ const stats = statsMap.get(entry.hash) || { filesChanged: 0, insertions: 0, deletions: 0 };
779
+ return {
780
+ hash: entry.hash,
781
+ date: entry.date,
782
+ message: entry.message,
783
+ refs: entry.refs || '',
784
+ body: entry.body || '',
785
+ author_name: entry.author_name,
786
+ author_email: entry.author_email,
787
+ filesChanged: stats.filesChanged,
788
+ insertions: stats.insertions,
789
+ deletions: stats.deletions
790
+ };
791
+ });
792
+
793
+ return {
794
+ all: merged,
795
+ latest: merged[0] || null,
796
+ total: baseLog.total
797
+ };
798
+ } catch (error) {
799
+ console.error('Failed to get log:', error);
800
+ throw error;
801
+ }
802
+ }
803
+
804
+ export async function isLinkedWorktree(directory) {
805
+ const git = simpleGit(directory);
806
+ try {
807
+ const [gitDir, gitCommonDir] = await Promise.all([
808
+ git.raw(['rev-parse', '--git-dir']).then((output) => output.trim()),
809
+ git.raw(['rev-parse', '--git-common-dir']).then((output) => output.trim())
810
+ ]);
811
+ return gitDir !== gitCommonDir;
812
+ } catch (error) {
813
+ console.error('Failed to determine worktree type:', error);
814
+ return false;
815
+ }
816
+ }
817
+
818
+ export async function getCommitFiles(directory, commitHash) {
819
+ const git = simpleGit(directory);
820
+
821
+ try {
822
+
823
+ const numstatRaw = await git.raw([
824
+ 'show',
825
+ '--numstat',
826
+ '--format=',
827
+ commitHash
828
+ ]);
829
+
830
+ const files = [];
831
+ const lines = numstatRaw.trim().split('\n').filter(Boolean);
832
+
833
+ for (const line of lines) {
834
+ const parts = line.split('\t');
835
+ if (parts.length < 3) continue;
836
+
837
+ const [insertionsRaw, deletionsRaw, ...pathParts] = parts;
838
+ const filePath = pathParts.join('\t');
839
+ if (!filePath) continue;
840
+
841
+ const insertions = insertionsRaw === '-' ? 0 : parseInt(insertionsRaw, 10) || 0;
842
+ const deletions = deletionsRaw === '-' ? 0 : parseInt(deletionsRaw, 10) || 0;
843
+ const isBinary = insertionsRaw === '-' && deletionsRaw === '-';
844
+
845
+ let changeType = 'M';
846
+ let displayPath = filePath;
847
+
848
+ if (filePath.includes(' => ')) {
849
+ changeType = 'R';
850
+
851
+ const match = filePath.match(/(?:\{[^}]*\s=>\s[^}]*\}|.*\s=>\s.*)/);
852
+ if (match) {
853
+ displayPath = filePath;
854
+ }
855
+ }
856
+
857
+ files.push({
858
+ path: displayPath,
859
+ insertions,
860
+ deletions,
861
+ isBinary,
862
+ changeType
863
+ });
864
+ }
865
+
866
+ const nameStatusRaw = await git.raw([
867
+ 'show',
868
+ '--name-status',
869
+ '--format=',
870
+ commitHash
871
+ ]).catch(() => '');
872
+
873
+ const statusMap = new Map();
874
+ const statusLines = nameStatusRaw.trim().split('\n').filter(Boolean);
875
+ for (const line of statusLines) {
876
+ const match = line.match(/^([AMDRC])\d*\t(.+)$/);
877
+ if (match) {
878
+ const [, status, path] = match;
879
+ statusMap.set(path, status);
880
+ }
881
+ }
882
+
883
+ for (const file of files) {
884
+ const basePath = file.path.includes(' => ')
885
+ ? file.path.split(' => ').pop()?.replace(/[{}]/g, '') || file.path
886
+ : file.path;
887
+
888
+ const status = statusMap.get(basePath) || statusMap.get(file.path);
889
+ if (status) {
890
+ file.changeType = status;
891
+ }
892
+ }
893
+
894
+ return { files };
895
+ } catch (error) {
896
+ console.error('Failed to get commit files:', error);
897
+ throw error;
898
+ }
899
+ }