mstro-app 0.2.0 → 0.3.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 (153) hide show
  1. package/PRIVACY.md +126 -0
  2. package/README.md +24 -23
  3. package/bin/commands/login.js +79 -49
  4. package/bin/mstro.js +305 -39
  5. package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
  6. package/dist/server/cli/headless/claude-invoker.js +137 -30
  7. package/dist/server/cli/headless/claude-invoker.js.map +1 -1
  8. package/dist/server/cli/headless/mcp-config.js +2 -2
  9. package/dist/server/cli/headless/mcp-config.js.map +1 -1
  10. package/dist/server/cli/headless/runner.d.ts +6 -1
  11. package/dist/server/cli/headless/runner.d.ts.map +1 -1
  12. package/dist/server/cli/headless/runner.js +59 -4
  13. package/dist/server/cli/headless/runner.js.map +1 -1
  14. package/dist/server/cli/headless/stall-assessor.d.ts +3 -1
  15. package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
  16. package/dist/server/cli/headless/stall-assessor.js +20 -1
  17. package/dist/server/cli/headless/stall-assessor.js.map +1 -1
  18. package/dist/server/cli/headless/tool-watchdog.d.ts +4 -1
  19. package/dist/server/cli/headless/tool-watchdog.d.ts.map +1 -1
  20. package/dist/server/cli/headless/tool-watchdog.js +30 -24
  21. package/dist/server/cli/headless/tool-watchdog.js.map +1 -1
  22. package/dist/server/cli/headless/types.d.ts +20 -2
  23. package/dist/server/cli/headless/types.d.ts.map +1 -1
  24. package/dist/server/cli/improvisation-session-manager.d.ts +30 -3
  25. package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
  26. package/dist/server/cli/improvisation-session-manager.js +224 -31
  27. package/dist/server/cli/improvisation-session-manager.js.map +1 -1
  28. package/dist/server/index.js +6 -4
  29. package/dist/server/index.js.map +1 -1
  30. package/dist/server/mcp/bouncer-cli.js +53 -14
  31. package/dist/server/mcp/bouncer-cli.js.map +1 -1
  32. package/dist/server/mcp/bouncer-integration.d.ts +1 -1
  33. package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
  34. package/dist/server/mcp/bouncer-integration.js +70 -7
  35. package/dist/server/mcp/bouncer-integration.js.map +1 -1
  36. package/dist/server/mcp/security-audit.d.ts +3 -3
  37. package/dist/server/mcp/security-audit.d.ts.map +1 -1
  38. package/dist/server/mcp/security-audit.js.map +1 -1
  39. package/dist/server/mcp/server.js +3 -2
  40. package/dist/server/mcp/server.js.map +1 -1
  41. package/dist/server/services/analytics.d.ts +2 -2
  42. package/dist/server/services/analytics.d.ts.map +1 -1
  43. package/dist/server/services/analytics.js +13 -1
  44. package/dist/server/services/analytics.js.map +1 -1
  45. package/dist/server/services/files.js +7 -7
  46. package/dist/server/services/files.js.map +1 -1
  47. package/dist/server/services/pathUtils.js +1 -1
  48. package/dist/server/services/pathUtils.js.map +1 -1
  49. package/dist/server/services/platform.d.ts +2 -2
  50. package/dist/server/services/platform.d.ts.map +1 -1
  51. package/dist/server/services/platform.js +13 -1
  52. package/dist/server/services/platform.js.map +1 -1
  53. package/dist/server/services/sentry.d.ts +1 -1
  54. package/dist/server/services/sentry.d.ts.map +1 -1
  55. package/dist/server/services/sentry.js.map +1 -1
  56. package/dist/server/services/terminal/pty-manager.d.ts +12 -0
  57. package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
  58. package/dist/server/services/terminal/pty-manager.js +81 -6
  59. package/dist/server/services/terminal/pty-manager.js.map +1 -1
  60. package/dist/server/services/websocket/file-explorer-handlers.d.ts +5 -0
  61. package/dist/server/services/websocket/file-explorer-handlers.d.ts.map +1 -0
  62. package/dist/server/services/websocket/file-explorer-handlers.js +518 -0
  63. package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -0
  64. package/dist/server/services/websocket/file-utils.d.ts +4 -0
  65. package/dist/server/services/websocket/file-utils.d.ts.map +1 -1
  66. package/dist/server/services/websocket/file-utils.js +27 -8
  67. package/dist/server/services/websocket/file-utils.js.map +1 -1
  68. package/dist/server/services/websocket/git-handlers.d.ts +36 -0
  69. package/dist/server/services/websocket/git-handlers.d.ts.map +1 -0
  70. package/dist/server/services/websocket/git-handlers.js +797 -0
  71. package/dist/server/services/websocket/git-handlers.js.map +1 -0
  72. package/dist/server/services/websocket/git-pr-handlers.d.ts +4 -0
  73. package/dist/server/services/websocket/git-pr-handlers.d.ts.map +1 -0
  74. package/dist/server/services/websocket/git-pr-handlers.js +299 -0
  75. package/dist/server/services/websocket/git-pr-handlers.js.map +1 -0
  76. package/dist/server/services/websocket/git-worktree-handlers.d.ts +4 -0
  77. package/dist/server/services/websocket/git-worktree-handlers.d.ts.map +1 -0
  78. package/dist/server/services/websocket/git-worktree-handlers.js +353 -0
  79. package/dist/server/services/websocket/git-worktree-handlers.js.map +1 -0
  80. package/dist/server/services/websocket/handler-context.d.ts +32 -0
  81. package/dist/server/services/websocket/handler-context.d.ts.map +1 -0
  82. package/dist/server/services/websocket/handler-context.js +4 -0
  83. package/dist/server/services/websocket/handler-context.js.map +1 -0
  84. package/dist/server/services/websocket/handler.d.ts +27 -359
  85. package/dist/server/services/websocket/handler.d.ts.map +1 -1
  86. package/dist/server/services/websocket/handler.js +68 -2329
  87. package/dist/server/services/websocket/handler.js.map +1 -1
  88. package/dist/server/services/websocket/index.d.ts +1 -1
  89. package/dist/server/services/websocket/index.d.ts.map +1 -1
  90. package/dist/server/services/websocket/index.js.map +1 -1
  91. package/dist/server/services/websocket/session-handlers.d.ts +10 -0
  92. package/dist/server/services/websocket/session-handlers.d.ts.map +1 -0
  93. package/dist/server/services/websocket/session-handlers.js +508 -0
  94. package/dist/server/services/websocket/session-handlers.js.map +1 -0
  95. package/dist/server/services/websocket/settings-handlers.d.ts +6 -0
  96. package/dist/server/services/websocket/settings-handlers.d.ts.map +1 -0
  97. package/dist/server/services/websocket/settings-handlers.js +125 -0
  98. package/dist/server/services/websocket/settings-handlers.js.map +1 -0
  99. package/dist/server/services/websocket/tab-handlers.d.ts +10 -0
  100. package/dist/server/services/websocket/tab-handlers.d.ts.map +1 -0
  101. package/dist/server/services/websocket/tab-handlers.js +131 -0
  102. package/dist/server/services/websocket/tab-handlers.js.map +1 -0
  103. package/dist/server/services/websocket/terminal-handlers.d.ts +9 -0
  104. package/dist/server/services/websocket/terminal-handlers.d.ts.map +1 -0
  105. package/dist/server/services/websocket/terminal-handlers.js +220 -0
  106. package/dist/server/services/websocket/terminal-handlers.js.map +1 -0
  107. package/dist/server/services/websocket/types.d.ts +63 -2
  108. package/dist/server/services/websocket/types.d.ts.map +1 -1
  109. package/dist/server/utils/agent-manager.d.ts +22 -2
  110. package/dist/server/utils/agent-manager.d.ts.map +1 -1
  111. package/dist/server/utils/agent-manager.js +2 -2
  112. package/dist/server/utils/agent-manager.js.map +1 -1
  113. package/dist/server/utils/port-manager.js.map +1 -1
  114. package/hooks/bouncer.sh +17 -3
  115. package/package.json +7 -3
  116. package/server/README.md +176 -159
  117. package/server/cli/headless/claude-invoker.ts +172 -43
  118. package/server/cli/headless/mcp-config.ts +8 -8
  119. package/server/cli/headless/runner.ts +57 -4
  120. package/server/cli/headless/stall-assessor.ts +25 -0
  121. package/server/cli/headless/tool-watchdog.ts +33 -25
  122. package/server/cli/headless/types.ts +11 -2
  123. package/server/cli/improvisation-session-manager.ts +285 -37
  124. package/server/index.ts +15 -13
  125. package/server/mcp/README.md +59 -67
  126. package/server/mcp/bouncer-cli.ts +73 -20
  127. package/server/mcp/bouncer-integration.ts +99 -16
  128. package/server/mcp/security-audit.ts +4 -4
  129. package/server/mcp/server.ts +6 -5
  130. package/server/services/analytics.ts +16 -4
  131. package/server/services/files.ts +13 -13
  132. package/server/services/pathUtils.ts +2 -2
  133. package/server/services/platform.ts +17 -6
  134. package/server/services/sentry.ts +1 -1
  135. package/server/services/terminal/pty-manager.ts +88 -11
  136. package/server/services/websocket/file-explorer-handlers.ts +587 -0
  137. package/server/services/websocket/file-utils.ts +28 -9
  138. package/server/services/websocket/git-handlers.ts +924 -0
  139. package/server/services/websocket/git-pr-handlers.ts +363 -0
  140. package/server/services/websocket/git-worktree-handlers.ts +403 -0
  141. package/server/services/websocket/handler-context.ts +44 -0
  142. package/server/services/websocket/handler.ts +85 -2680
  143. package/server/services/websocket/index.ts +1 -1
  144. package/server/services/websocket/session-handlers.ts +575 -0
  145. package/server/services/websocket/settings-handlers.ts +150 -0
  146. package/server/services/websocket/tab-handlers.ts +150 -0
  147. package/server/services/websocket/terminal-handlers.ts +277 -0
  148. package/server/services/websocket/types.ts +137 -0
  149. package/server/utils/agent-manager.ts +6 -6
  150. package/server/utils/port-manager.ts +1 -1
  151. package/bin/release.sh +0 -110
  152. package/server/services/platform.test.ts +0 -1304
  153. package/server/services/websocket/handler.test.ts +0 -20
@@ -0,0 +1,797 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+ import { spawn } from 'node:child_process';
4
+ import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { handleGitPRMessage } from './git-pr-handlers.js';
7
+ import { handleGitWorktreeMessage } from './git-worktree-handlers.js';
8
+ /** Detect git provider from remote URL */
9
+ export function detectGitProvider(remoteUrl) {
10
+ if (remoteUrl.includes('github.com'))
11
+ return 'github';
12
+ if (remoteUrl.includes('gitlab.com') || remoteUrl.includes('gitlab'))
13
+ return 'gitlab';
14
+ return 'unknown';
15
+ }
16
+ /** Execute a git command and return stdout */
17
+ export function executeGitCommand(args, workingDir) {
18
+ return new Promise((resolve) => {
19
+ const git = spawn('git', args, {
20
+ cwd: workingDir,
21
+ stdio: ['ignore', 'pipe', 'pipe']
22
+ });
23
+ let stdout = '';
24
+ let stderr = '';
25
+ git.stdout?.on('data', (data) => {
26
+ stdout += data.toString();
27
+ });
28
+ git.stderr?.on('data', (data) => {
29
+ stderr += data.toString();
30
+ });
31
+ git.on('close', (code) => {
32
+ resolve({ stdout, stderr, exitCode: code ?? 1 });
33
+ });
34
+ git.on('error', (err) => {
35
+ resolve({ stdout: '', stderr: err.message, exitCode: 1 });
36
+ });
37
+ });
38
+ }
39
+ /** Map of simple escape sequences to their character values */
40
+ const ESCAPE_CHARS = {
41
+ '\\': '\\',
42
+ '"': '"',
43
+ 'n': '\n',
44
+ 't': '\t',
45
+ 'r': '\r',
46
+ };
47
+ /** Check if position i starts an octal escape sequence (\nnn) */
48
+ function isOctalEscape(str, i) {
49
+ return i + 3 < str.length &&
50
+ /[0-7]/.test(str[i + 1]) &&
51
+ /[0-7]{2}/.test(str.slice(i + 2, i + 4));
52
+ }
53
+ /**
54
+ * Unquote a git-quoted path (C-style quoting)
55
+ */
56
+ export function unquoteGitPath(path) {
57
+ if (!path.startsWith('"') || !path.endsWith('"')) {
58
+ return path;
59
+ }
60
+ const inner = path.slice(1, -1);
61
+ let result = '';
62
+ let i = 0;
63
+ while (i < inner.length) {
64
+ if (inner[i] !== '\\' || i + 1 >= inner.length) {
65
+ result += inner[i];
66
+ i++;
67
+ continue;
68
+ }
69
+ const next = inner[i + 1];
70
+ const escaped = ESCAPE_CHARS[next];
71
+ if (escaped !== undefined) {
72
+ result += escaped;
73
+ i += 2;
74
+ }
75
+ else if (isOctalEscape(inner, i)) {
76
+ result += String.fromCharCode(parseInt(inner.slice(i + 1, i + 4), 8));
77
+ i += 4;
78
+ }
79
+ else {
80
+ result += inner[i];
81
+ i++;
82
+ }
83
+ }
84
+ return result;
85
+ }
86
+ /** Parse git status --porcelain output into structured format */
87
+ export function parseGitStatus(porcelainOutput) {
88
+ const staged = [];
89
+ const unstaged = [];
90
+ const untracked = [];
91
+ const lines = porcelainOutput.split('\n').filter(line => line.length >= 4);
92
+ for (const line of lines) {
93
+ const indexStatus = line[0];
94
+ const workTreeStatus = line[1];
95
+ const rawPath = line.slice(3);
96
+ const path = unquoteGitPath(rawPath);
97
+ let filePath = path;
98
+ let originalPath;
99
+ if (rawPath.includes(' -> ')) {
100
+ const parts = rawPath.split(' -> ');
101
+ originalPath = unquoteGitPath(parts[0]);
102
+ filePath = unquoteGitPath(parts[1]);
103
+ }
104
+ if (indexStatus === '?' && workTreeStatus === '?') {
105
+ untracked.push({ path: filePath, status: '?', staged: false });
106
+ continue;
107
+ }
108
+ if (indexStatus !== ' ' && indexStatus !== '?') {
109
+ staged.push({ path: filePath, status: indexStatus, staged: true, originalPath });
110
+ }
111
+ if (workTreeStatus !== ' ' && workTreeStatus !== '?') {
112
+ unstaged.push({ path: filePath, status: workTreeStatus, staged: false, originalPath });
113
+ }
114
+ }
115
+ return { staged, unstaged, untracked };
116
+ }
117
+ /** Check if a binary runs successfully (exit code 0) */
118
+ export function spawnCheck(bin, args) {
119
+ return new Promise((resolve) => {
120
+ const proc = spawn(bin, args, { stdio: ['ignore', 'pipe', 'pipe'] });
121
+ proc.on('close', (code) => resolve(code === 0));
122
+ proc.on('error', () => resolve(false));
123
+ });
124
+ }
125
+ /** Spawn a process and capture stdout/stderr */
126
+ export function spawnWithOutput(bin, args, cwd) {
127
+ return new Promise((resolve) => {
128
+ const proc = spawn(bin, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
129
+ let stdout = '';
130
+ let stderr = '';
131
+ proc.stdout?.on('data', (d) => { stdout += d.toString(); });
132
+ proc.stderr?.on('data', (d) => { stderr += d.toString(); });
133
+ proc.on('close', (code) => resolve({ stdout, stderr, exitCode: code ?? 1 }));
134
+ proc.on('error', (err) => resolve({ stdout: '', stderr: err.message, exitCode: 1 }));
135
+ });
136
+ }
137
+ /**
138
+ * Strip injected coauthor/attribution lines from a commit message.
139
+ */
140
+ export function stripCoauthorLines(message) {
141
+ const lines = message.split('\n');
142
+ const markers = ['co-authored', 'authored-by', 'haiku', 'noreply@anthropic.com'];
143
+ const result = [];
144
+ for (let i = 0; i < lines.length; i++) {
145
+ const lower = lines[i].toLowerCase();
146
+ if (markers.some(m => lower.includes(m))) {
147
+ if (result.length > 0 && result[result.length - 1].trim() === '') {
148
+ result.pop();
149
+ }
150
+ continue;
151
+ }
152
+ result.push(lines[i]);
153
+ }
154
+ if (result.length === 0)
155
+ return '';
156
+ return result.join('\n').trimEnd();
157
+ }
158
+ // PR message types that route to git-pr-handlers
159
+ const GIT_PR_TYPES = new Set([
160
+ 'gitGetRemoteInfo', 'gitCreatePR', 'gitGeneratePRDescription',
161
+ ]);
162
+ // Worktree/merge message types that route to git-worktree-handlers
163
+ const GIT_WORKTREE_TYPES = new Set([
164
+ 'gitWorktreeList', 'gitWorktreeCreate', 'gitWorktreeRemove',
165
+ 'tabWorktreeSwitch', 'gitWorktreePush', 'gitWorktreeCreatePR',
166
+ 'gitMergePreview', 'gitWorktreeMerge', 'gitMergeAbort', 'gitMergeComplete',
167
+ ]);
168
+ /** Route git messages to appropriate sub-handler */
169
+ export function handleGitMessage(ctx, ws, msg, tabId, workingDir) {
170
+ const gitDir = ctx.gitDirectories.get(tabId) || workingDir;
171
+ if (GIT_PR_TYPES.has(msg.type)) {
172
+ handleGitPRMessage(ctx, ws, msg, tabId, gitDir, workingDir);
173
+ return;
174
+ }
175
+ if (GIT_WORKTREE_TYPES.has(msg.type)) {
176
+ handleGitWorktreeMessage(ctx, ws, msg, tabId, gitDir, workingDir);
177
+ return;
178
+ }
179
+ const handlers = {
180
+ gitStatus: () => handleGitStatus(ctx, ws, tabId, gitDir),
181
+ gitStage: () => handleGitStage(ctx, ws, msg, tabId, gitDir),
182
+ gitUnstage: () => handleGitUnstage(ctx, ws, msg, tabId, gitDir),
183
+ gitCommit: () => handleGitCommit(ctx, ws, msg, tabId, gitDir),
184
+ gitCommitWithAI: () => handleGitCommitWithAI(ctx, ws, msg, tabId, gitDir),
185
+ gitPush: () => handleGitPush(ctx, ws, tabId, gitDir),
186
+ gitPull: () => handleGitPull(ctx, ws, tabId, gitDir),
187
+ gitLog: () => handleGitLog(ctx, ws, msg, tabId, gitDir),
188
+ gitDiscoverRepos: () => handleGitDiscoverRepos(ctx, ws, tabId, workingDir),
189
+ gitSetDirectory: () => handleGitSetDirectory(ctx, ws, msg, tabId, workingDir),
190
+ gitListBranches: () => handleGitListBranches(ctx, ws, tabId, gitDir),
191
+ gitCheckout: () => handleGitCheckout(ctx, ws, msg, tabId, gitDir),
192
+ gitCreateBranch: () => handleGitCreateBranch(ctx, ws, msg, tabId, gitDir),
193
+ gitDeleteBranch: () => handleGitDeleteBranch(ctx, ws, msg, tabId, gitDir),
194
+ gitDiff: () => handleGitDiff(ctx, ws, msg, tabId, gitDir),
195
+ gitListTags: () => handleGitListTags(ctx, ws, tabId, gitDir),
196
+ gitCreateTag: () => handleGitCreateTag(ctx, ws, msg, tabId, gitDir),
197
+ gitPushTag: () => handleGitPushTag(ctx, ws, msg, tabId, gitDir),
198
+ };
199
+ handlers[msg.type]?.();
200
+ }
201
+ export async function handleGitStatus(ctx, ws, tabId, workingDir) {
202
+ try {
203
+ const statusResult = await executeGitCommand(['status', '--porcelain=v1'], workingDir);
204
+ if (statusResult.exitCode !== 0) {
205
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: statusResult.stderr || statusResult.stdout || 'Failed to get git status' } });
206
+ return;
207
+ }
208
+ const branchResult = await executeGitCommand(['rev-parse', '--abbrev-ref', 'HEAD'], workingDir);
209
+ const branch = branchResult.stdout.trim() || 'HEAD';
210
+ let ahead = 0;
211
+ let behind = 0;
212
+ let hasUpstream = false;
213
+ const trackingResult = await executeGitCommand(['rev-list', '--left-right', '--count', `${branch}...@{u}`], workingDir);
214
+ if (trackingResult.exitCode === 0) {
215
+ hasUpstream = true;
216
+ const parts = trackingResult.stdout.trim().split(/\s+/);
217
+ ahead = parseInt(parts[0], 10) || 0;
218
+ behind = parseInt(parts[1], 10) || 0;
219
+ }
220
+ else {
221
+ const localResult = await executeGitCommand(['rev-list', '--count', 'HEAD'], workingDir);
222
+ if (localResult.exitCode === 0) {
223
+ ahead = parseInt(localResult.stdout.trim(), 10) || 0;
224
+ }
225
+ }
226
+ const { staged, unstaged, untracked } = parseGitStatus(statusResult.stdout);
227
+ const response = {
228
+ branch,
229
+ isDirty: staged.length > 0 || unstaged.length > 0 || untracked.length > 0,
230
+ staged,
231
+ unstaged,
232
+ untracked,
233
+ ahead,
234
+ behind,
235
+ hasUpstream,
236
+ };
237
+ ctx.send(ws, { type: 'gitStatus', tabId, data: response });
238
+ }
239
+ catch (error) {
240
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
241
+ }
242
+ }
243
+ async function handleGitStage(ctx, ws, msg, tabId, workingDir) {
244
+ const stageAll = !!msg.data?.stageAll;
245
+ const paths = msg.data?.paths;
246
+ if (!stageAll && (!paths || paths.length === 0)) {
247
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: 'No paths specified for staging' } });
248
+ return;
249
+ }
250
+ try {
251
+ const args = stageAll ? ['add', '-A'] : ['add', '--', ...paths];
252
+ const result = await executeGitCommand(args, workingDir);
253
+ if (result.exitCode !== 0) {
254
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: result.stderr || result.stdout || 'Failed to stage files' } });
255
+ return;
256
+ }
257
+ ctx.send(ws, { type: 'gitStaged', tabId, data: { paths: paths || [] } });
258
+ }
259
+ catch (error) {
260
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
261
+ }
262
+ }
263
+ async function handleGitUnstage(ctx, ws, msg, tabId, workingDir) {
264
+ const paths = msg.data?.paths;
265
+ if (!paths || paths.length === 0) {
266
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: 'No paths specified for unstaging' } });
267
+ return;
268
+ }
269
+ try {
270
+ const result = await executeGitCommand(['reset', 'HEAD', '--', ...paths], workingDir);
271
+ if (result.exitCode !== 0) {
272
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: result.stderr || result.stdout || 'Failed to unstage files' } });
273
+ return;
274
+ }
275
+ ctx.send(ws, { type: 'gitUnstaged', tabId, data: { paths } });
276
+ }
277
+ catch (error) {
278
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
279
+ }
280
+ }
281
+ async function handleGitCommit(ctx, ws, msg, tabId, workingDir) {
282
+ const message = msg.data?.message;
283
+ if (!message) {
284
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: 'Commit message is required' } });
285
+ return;
286
+ }
287
+ try {
288
+ const result = await executeGitCommand(['commit', '-m', message], workingDir);
289
+ if (result.exitCode !== 0) {
290
+ let errorMsg = result.stderr || result.stdout || 'Failed to commit';
291
+ if (errorMsg.includes('nothing to commit') || errorMsg.includes('no changes added')) {
292
+ errorMsg = 'No changes staged for commit. Use "Stage" to add files before committing.';
293
+ handleGitStatus(ctx, ws, tabId, workingDir);
294
+ }
295
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: errorMsg } });
296
+ return;
297
+ }
298
+ const hashResult = await executeGitCommand(['rev-parse', '--short', 'HEAD'], workingDir);
299
+ const hash = hashResult.stdout.trim();
300
+ ctx.send(ws, { type: 'gitCommitted', tabId, data: { hash, message } });
301
+ handleGitStatus(ctx, ws, tabId, workingDir);
302
+ }
303
+ catch (error) {
304
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
305
+ }
306
+ }
307
+ async function handleGitCommitWithAI(ctx, ws, msg, tabId, workingDir) {
308
+ try {
309
+ const statusResult = await executeGitCommand(['status', '--porcelain=v1'], workingDir);
310
+ const { staged } = parseGitStatus(statusResult.stdout);
311
+ if (staged.length === 0) {
312
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: 'No staged changes to commit' } });
313
+ return;
314
+ }
315
+ const diffResult = await executeGitCommand(['diff', '--cached'], workingDir);
316
+ const diff = diffResult.stdout;
317
+ const logResult = await executeGitCommand(['log', '--oneline', '-5'], workingDir);
318
+ const recentCommits = logResult.stdout.trim();
319
+ const tempDir = join(workingDir, '.mstro', 'tmp');
320
+ if (!existsSync(tempDir)) {
321
+ mkdirSync(tempDir, { recursive: true });
322
+ }
323
+ let truncatedDiff = diff;
324
+ if (diff.length > 8000) {
325
+ truncatedDiff = `${diff.slice(0, 4000)}\n\n... [diff truncated] ...\n\n${diff.slice(-3500)}`;
326
+ }
327
+ const prompt = `You are generating a git commit message for the following staged changes.
328
+
329
+ RECENT COMMIT MESSAGES (for style reference):
330
+ ${recentCommits || 'No recent commits'}
331
+
332
+ STAGED FILES:
333
+ ${staged.map(f => `${f.status} ${f.path}`).join('\n')}
334
+
335
+ DIFF OF STAGED CHANGES:
336
+ ${truncatedDiff}
337
+
338
+ Generate a commit message following these rules:
339
+ 1. First line: imperative mood, max 72 characters (e.g., "Add user authentication", "Fix memory leak in parser")
340
+ 2. If the changes are complex, add a blank line then bullet points explaining the key changes
341
+ 3. Focus on the "why" not just the "what"
342
+ 4. Match the style of recent commits if possible
343
+ 5. No emojis unless the repo already uses them
344
+
345
+ Respond with ONLY the commit message, nothing else.`;
346
+ const promptFile = join(tempDir, `commit-msg-${Date.now()}.txt`);
347
+ writeFileSync(promptFile, prompt);
348
+ const systemPrompt = 'You are a commit message assistant. Respond with only the commit message, no preamble or explanation.';
349
+ const args = [
350
+ '--print',
351
+ '--model', 'haiku',
352
+ '--system-prompt', systemPrompt,
353
+ promptFile
354
+ ];
355
+ const claude = spawn('claude', args, {
356
+ cwd: workingDir,
357
+ stdio: ['ignore', 'pipe', 'pipe']
358
+ });
359
+ let stdout = '';
360
+ let stderr = '';
361
+ claude.stdout?.on('data', (data) => {
362
+ stdout += data.toString();
363
+ });
364
+ claude.stderr?.on('data', (data) => {
365
+ stderr += data.toString();
366
+ });
367
+ claude.on('close', async (code) => {
368
+ try {
369
+ unlinkSync(promptFile);
370
+ }
371
+ catch {
372
+ // Ignore cleanup errors
373
+ }
374
+ if (code !== 0 || !stdout.trim()) {
375
+ console.error('[WebSocketImproviseHandler] Claude commit message error:', stderr || 'No output');
376
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: 'Failed to generate commit message' } });
377
+ return;
378
+ }
379
+ const commitMessage = extractCommitMessage(stdout.trim());
380
+ const autoCommit = !!msg.data?.autoCommit;
381
+ ctx.send(ws, { type: 'gitCommitMessage', tabId, data: { message: commitMessage, autoCommit } });
382
+ if (msg.data?.autoCommit) {
383
+ const commitResult = await executeGitCommand(['commit', '-m', commitMessage], workingDir);
384
+ if (commitResult.exitCode !== 0) {
385
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: commitResult.stderr || commitResult.stdout || 'Failed to commit' } });
386
+ return;
387
+ }
388
+ const hashResult = await executeGitCommand(['rev-parse', '--short', 'HEAD'], workingDir);
389
+ const hash = hashResult.stdout.trim();
390
+ ctx.send(ws, { type: 'gitCommitted', tabId, data: { hash, message: commitMessage } });
391
+ handleGitStatus(ctx, ws, tabId, workingDir);
392
+ }
393
+ });
394
+ claude.on('error', (err) => {
395
+ console.error('[WebSocketImproviseHandler] Failed to spawn Claude for commit:', err);
396
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: 'Failed to generate commit message' } });
397
+ });
398
+ setTimeout(() => {
399
+ claude.kill();
400
+ }, 30000);
401
+ }
402
+ catch (error) {
403
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
404
+ }
405
+ }
406
+ function extractCommitMessage(output) {
407
+ const patterns = [
408
+ /(?:here'?s?\s+(?:the\s+)?commit\s+message:?\s*\n+)([\s\S]+)/i,
409
+ /(?:commit\s+message:?\s*\n+)([\s\S]+)/i,
410
+ /(?:suggested\s+commit\s+message:?\s*\n+)([\s\S]+)/i,
411
+ ];
412
+ for (const pattern of patterns) {
413
+ const match = output.match(pattern);
414
+ if (match?.[1]) {
415
+ return stripCoauthorLines(match[1].trim());
416
+ }
417
+ }
418
+ const paragraphs = output.split(/\n\n+/).filter(p => p.trim());
419
+ if (paragraphs.length <= 1) {
420
+ return stripCoauthorLines(output.trim());
421
+ }
422
+ const firstParagraph = paragraphs[0].trim();
423
+ const firstLine = firstParagraph.split('\n')[0].trim();
424
+ const reasoningPatterns = [
425
+ /^(Now|Based|Looking|After|Here|Let me|I\s+(can|will|see|notice|'ll|would))/i,
426
+ /^The\s+\w+\s+(file|changes?|commit|diff)/i,
427
+ /\b(I can|I will|I'll|let me|analyzing|looking at)\b/i,
428
+ ];
429
+ const looksLikeReasoning = reasoningPatterns.some(p => p.test(firstParagraph));
430
+ const firstLineTooLong = firstLine.length > 80;
431
+ const endsWithPeriod = firstLine.endsWith('.');
432
+ if (looksLikeReasoning || (firstLineTooLong && endsWithPeriod)) {
433
+ const commitMessage = paragraphs.slice(1).join('\n\n').trim();
434
+ const extractedFirstLine = commitMessage.split('\n')[0].trim();
435
+ if (extractedFirstLine.length > 0 && extractedFirstLine.length <= 100) {
436
+ return stripCoauthorLines(commitMessage);
437
+ }
438
+ }
439
+ if (paragraphs.length >= 2) {
440
+ const secondParagraph = paragraphs[1].trim();
441
+ const secondFirstLine = secondParagraph.split('\n')[0].trim();
442
+ if (secondFirstLine.length <= 72 &&
443
+ /^[A-Z][a-z]/.test(secondFirstLine) &&
444
+ !secondFirstLine.endsWith('.')) {
445
+ return stripCoauthorLines(paragraphs.slice(1).join('\n\n').trim());
446
+ }
447
+ }
448
+ return stripCoauthorLines(output.trim());
449
+ }
450
+ async function handleGitPush(ctx, ws, tabId, workingDir) {
451
+ try {
452
+ const branchResult = await executeGitCommand(['rev-parse', '--abbrev-ref', 'HEAD'], workingDir);
453
+ const branch = branchResult.stdout.trim();
454
+ const upstreamCheck = await executeGitCommand(['rev-parse', '--abbrev-ref', `${branch}@{u}`], workingDir);
455
+ const hasUpstream = upstreamCheck.exitCode === 0;
456
+ const pushArgs = hasUpstream ? ['push'] : ['push', '-u', 'origin', branch];
457
+ const result = await executeGitCommand(pushArgs, workingDir);
458
+ if (result.exitCode !== 0) {
459
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: result.stderr || result.stdout || 'Failed to push' } });
460
+ return;
461
+ }
462
+ ctx.send(ws, { type: 'gitPushed', tabId, data: { output: result.stdout || result.stderr } });
463
+ }
464
+ catch (error) {
465
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
466
+ }
467
+ }
468
+ async function handleGitPull(ctx, ws, tabId, workingDir) {
469
+ try {
470
+ const result = await executeGitCommand(['pull'], workingDir);
471
+ if (result.exitCode !== 0) {
472
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: result.stderr || result.stdout || 'Failed to pull' } });
473
+ return;
474
+ }
475
+ ctx.send(ws, { type: 'gitPulled', tabId, data: { output: result.stdout || result.stderr } });
476
+ }
477
+ catch (error) {
478
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
479
+ }
480
+ }
481
+ async function handleGitLog(ctx, ws, msg, tabId, workingDir) {
482
+ const limit = msg.data?.limit ?? 10;
483
+ try {
484
+ const result = await executeGitCommand([
485
+ 'log',
486
+ `-${limit}`,
487
+ '--format=%H|%h|%s|%an|%aI'
488
+ ], workingDir);
489
+ if (result.exitCode !== 0) {
490
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: result.stderr || result.stdout || 'Failed to get log' } });
491
+ return;
492
+ }
493
+ const entries = result.stdout.trim().split('\n').filter(Boolean).map(line => {
494
+ const [hash, shortHash, subject, author, date] = line.split('|');
495
+ const cleanSubject = stripCoauthorLines(subject || '') || subject || '';
496
+ return { hash, shortHash, subject: cleanSubject, author, date };
497
+ });
498
+ ctx.send(ws, { type: 'gitLog', tabId, data: { entries } });
499
+ }
500
+ catch (error) {
501
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
502
+ }
503
+ }
504
+ /** Directories to skip when scanning for git repos */
505
+ const SKIP_DIRS = ['node_modules', 'vendor', '.git'];
506
+ function shouldSkipDir(name) {
507
+ return name.startsWith('.') || SKIP_DIRS.includes(name);
508
+ }
509
+ async function getRepoBranch(repoPath) {
510
+ const result = await executeGitCommand(['rev-parse', '--abbrev-ref', 'HEAD'], repoPath);
511
+ return result.exitCode === 0 ? result.stdout.trim() : undefined;
512
+ }
513
+ async function scanForGitRepos(dir, depth, maxDepth, repos) {
514
+ if (depth > maxDepth)
515
+ return;
516
+ let entries;
517
+ try {
518
+ entries = readdirSync(dir);
519
+ }
520
+ catch {
521
+ return;
522
+ }
523
+ for (const name of entries) {
524
+ if (shouldSkipDir(name))
525
+ continue;
526
+ const fullPath = join(dir, name);
527
+ const gitPath = join(fullPath, '.git');
528
+ if (existsSync(gitPath)) {
529
+ repos.push({ path: fullPath, name, branch: await getRepoBranch(fullPath) });
530
+ }
531
+ else {
532
+ await scanForGitRepos(fullPath, depth + 1, maxDepth, repos);
533
+ }
534
+ }
535
+ }
536
+ async function handleGitDiscoverRepos(ctx, ws, tabId, workingDir) {
537
+ try {
538
+ const repos = [];
539
+ const rootIsGitRepo = existsSync(join(workingDir, '.git'));
540
+ if (rootIsGitRepo) {
541
+ repos.push({
542
+ path: workingDir,
543
+ name: workingDir.split('/').pop() || workingDir,
544
+ branch: await getRepoBranch(workingDir),
545
+ });
546
+ }
547
+ else {
548
+ await scanForGitRepos(workingDir, 1, 3, repos);
549
+ }
550
+ const response = {
551
+ repos,
552
+ rootIsGitRepo,
553
+ selectedDirectory: ctx.gitDirectories.get(tabId) || null,
554
+ };
555
+ ctx.send(ws, { type: 'gitReposDiscovered', tabId, data: response });
556
+ }
557
+ catch (error) {
558
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
559
+ }
560
+ }
561
+ async function handleGitSetDirectory(ctx, ws, msg, tabId, workingDir) {
562
+ const directory = msg.data?.directory;
563
+ if (!directory) {
564
+ ctx.gitDirectories.delete(tabId);
565
+ const response = {
566
+ directory: workingDir,
567
+ isValid: existsSync(join(workingDir, '.git')),
568
+ };
569
+ ctx.send(ws, { type: 'gitDirectorySet', tabId, data: response });
570
+ handleGitStatus(ctx, ws, tabId, workingDir);
571
+ return;
572
+ }
573
+ const gitPath = join(directory, '.git');
574
+ const isValid = existsSync(gitPath);
575
+ if (isValid) {
576
+ ctx.gitDirectories.set(tabId, directory);
577
+ }
578
+ const response = {
579
+ directory,
580
+ isValid,
581
+ };
582
+ ctx.send(ws, { type: 'gitDirectorySet', tabId, data: response });
583
+ if (isValid) {
584
+ handleGitStatus(ctx, ws, tabId, directory);
585
+ handleGitLog(ctx, ws, { type: 'gitLog', data: { limit: 5 } }, tabId, directory);
586
+ }
587
+ }
588
+ async function handleGitListBranches(ctx, ws, tabId, workingDir) {
589
+ try {
590
+ const result = await executeGitCommand(['branch', '-a', '--format=%(refname:short)|%(objectname:short)|%(upstream:short)|%(HEAD)'], workingDir);
591
+ if (result.exitCode !== 0) {
592
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: result.stderr || 'Failed to list branches' } });
593
+ return;
594
+ }
595
+ const currentBranchResult = await executeGitCommand(['rev-parse', '--abbrev-ref', 'HEAD'], workingDir);
596
+ const currentBranch = currentBranchResult.stdout.trim() || 'HEAD';
597
+ const branches = result.stdout.trim().split('\n')
598
+ .filter(line => line.trim())
599
+ .map(line => {
600
+ const [name, shortHash, upstream, head] = line.split('|');
601
+ const isRemote = name.includes('/') && (name.startsWith('origin/') || name.includes('remotes/'));
602
+ return {
603
+ name: name.trim(),
604
+ shortHash: shortHash?.trim() || '',
605
+ isRemote,
606
+ isCurrent: head?.trim() === '*',
607
+ upstream: upstream?.trim() || undefined,
608
+ };
609
+ })
610
+ .filter(b => b.name !== 'origin/HEAD');
611
+ ctx.send(ws, { type: 'gitBranchList', tabId, data: { branches, current: currentBranch } });
612
+ }
613
+ catch (error) {
614
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
615
+ }
616
+ }
617
+ async function handleGitCheckout(ctx, ws, msg, tabId, workingDir) {
618
+ try {
619
+ const { branch, create, startPoint } = msg.data || {};
620
+ if (!branch) {
621
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: 'Branch name is required' } });
622
+ return;
623
+ }
624
+ const statusResult = await executeGitCommand(['status', '--porcelain'], workingDir);
625
+ if (statusResult.stdout.trim()) {
626
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: 'Commit or stash changes before switching branches' } });
627
+ return;
628
+ }
629
+ const prevResult = await executeGitCommand(['rev-parse', '--abbrev-ref', 'HEAD'], workingDir);
630
+ const previous = prevResult.stdout.trim();
631
+ const args = create
632
+ ? ['checkout', '-b', branch, ...(startPoint ? [startPoint] : [])]
633
+ : ['checkout', branch];
634
+ const result = await executeGitCommand(args, workingDir);
635
+ if (result.exitCode !== 0) {
636
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: result.stderr || 'Failed to checkout branch' } });
637
+ return;
638
+ }
639
+ ctx.send(ws, { type: 'gitCheckedOut', tabId, data: { branch, previous } });
640
+ handleGitStatus(ctx, ws, tabId, workingDir);
641
+ }
642
+ catch (error) {
643
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
644
+ }
645
+ }
646
+ async function handleGitCreateBranch(ctx, ws, msg, tabId, workingDir) {
647
+ try {
648
+ const { name, startPoint, checkout } = msg.data || {};
649
+ if (!name) {
650
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: 'Branch name is required' } });
651
+ return;
652
+ }
653
+ const args = ['branch', name, ...(startPoint ? [startPoint] : [])];
654
+ const result = await executeGitCommand(args, workingDir);
655
+ if (result.exitCode !== 0) {
656
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: result.stderr || 'Failed to create branch' } });
657
+ return;
658
+ }
659
+ const hashResult = await executeGitCommand(['rev-parse', '--short', name], workingDir);
660
+ if (checkout) {
661
+ await executeGitCommand(['checkout', name], workingDir);
662
+ }
663
+ ctx.send(ws, { type: 'gitBranchCreated', tabId, data: { name, hash: hashResult.stdout.trim() } });
664
+ }
665
+ catch (error) {
666
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
667
+ }
668
+ }
669
+ async function handleGitDeleteBranch(ctx, ws, msg, tabId, workingDir) {
670
+ try {
671
+ const { name, force } = msg.data || {};
672
+ if (!name) {
673
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: 'Branch name is required' } });
674
+ return;
675
+ }
676
+ const currentResult = await executeGitCommand(['rev-parse', '--abbrev-ref', 'HEAD'], workingDir);
677
+ if (currentResult.stdout.trim() === name) {
678
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: 'Cannot delete the currently checked out branch' } });
679
+ return;
680
+ }
681
+ const result = await executeGitCommand(['branch', force ? '-D' : '-d', name], workingDir);
682
+ if (result.exitCode !== 0) {
683
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: result.stderr || 'Failed to delete branch' } });
684
+ return;
685
+ }
686
+ ctx.send(ws, { type: 'gitBranchDeleted', tabId, data: { name } });
687
+ }
688
+ catch (error) {
689
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
690
+ }
691
+ }
692
+ async function handleGitDiff(ctx, ws, msg, tabId, workingDir) {
693
+ try {
694
+ const { path, staged } = msg.data || {};
695
+ if (!path) {
696
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: 'File path is required' } });
697
+ return;
698
+ }
699
+ const originalResult = await executeGitCommand(['show', `HEAD:${path}`], workingDir);
700
+ const original = originalResult.exitCode === 0 ? originalResult.stdout : '';
701
+ let modified;
702
+ if (staged) {
703
+ const indexResult = await executeGitCommand(['show', `:${path}`], workingDir);
704
+ modified = indexResult.exitCode === 0 ? indexResult.stdout : '';
705
+ }
706
+ else {
707
+ const fullPath = join(workingDir, path);
708
+ try {
709
+ modified = readFileSync(fullPath, 'utf-8');
710
+ }
711
+ catch {
712
+ modified = '';
713
+ }
714
+ }
715
+ ctx.send(ws, {
716
+ type: 'gitDiffResult',
717
+ tabId,
718
+ data: { path, original, modified, staged: !!staged },
719
+ });
720
+ }
721
+ catch (error) {
722
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
723
+ }
724
+ }
725
+ async function handleGitListTags(ctx, ws, tabId, workingDir) {
726
+ try {
727
+ const result = await executeGitCommand(['tag', '-l', '--sort=-creatordate', '--format=%(refname:short)|%(objectname:short)|%(creatordate:iso-strict)|%(subject)'], workingDir);
728
+ if (result.exitCode !== 0) {
729
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: result.stderr || 'Failed to list tags' } });
730
+ return;
731
+ }
732
+ const tags = result.stdout.trim().split('\n')
733
+ .filter(line => line.trim())
734
+ .slice(0, 50)
735
+ .map(line => {
736
+ const parts = line.split('|');
737
+ return {
738
+ name: parts[0]?.trim() || '',
739
+ shortHash: parts[1]?.trim() || '',
740
+ date: parts[2]?.trim() || '',
741
+ message: parts[3]?.trim() || '',
742
+ };
743
+ });
744
+ ctx.send(ws, { type: 'gitTagList', tabId, data: { tags } });
745
+ }
746
+ catch (error) {
747
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
748
+ }
749
+ }
750
+ async function handleGitCreateTag(ctx, ws, msg, tabId, workingDir) {
751
+ try {
752
+ const { name, message, commit } = msg.data || {};
753
+ if (!name) {
754
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: 'Tag name is required' } });
755
+ return;
756
+ }
757
+ if (/\s/.test(name) || name.includes('..')) {
758
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: 'Invalid tag name: no spaces or ".." allowed' } });
759
+ return;
760
+ }
761
+ const args = message
762
+ ? ['tag', '-a', name, '-m', message, ...(commit ? [commit] : [])]
763
+ : ['tag', name, ...(commit ? [commit] : [])];
764
+ const result = await executeGitCommand(args, workingDir);
765
+ if (result.exitCode !== 0) {
766
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: result.stderr || 'Failed to create tag' } });
767
+ return;
768
+ }
769
+ const hashResult = await executeGitCommand(['rev-parse', '--short', name], workingDir);
770
+ ctx.send(ws, { type: 'gitTagCreated', tabId, data: { name, hash: hashResult.stdout.trim() } });
771
+ }
772
+ catch (error) {
773
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
774
+ }
775
+ }
776
+ async function handleGitPushTag(ctx, ws, msg, tabId, workingDir) {
777
+ try {
778
+ const { name, all } = msg.data || {};
779
+ const args = all
780
+ ? ['push', 'origin', '--tags']
781
+ : ['push', 'origin', name];
782
+ if (!all && !name) {
783
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: 'Tag name is required' } });
784
+ return;
785
+ }
786
+ const result = await executeGitCommand(args, workingDir);
787
+ if (result.exitCode !== 0) {
788
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: result.stderr || 'Failed to push tag' } });
789
+ return;
790
+ }
791
+ ctx.send(ws, { type: 'gitTagPushed', tabId, data: { name: name || 'all', output: result.stderr || result.stdout } });
792
+ }
793
+ catch (error) {
794
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
795
+ }
796
+ }
797
+ //# sourceMappingURL=git-handlers.js.map