mstro-app 0.4.3 → 0.4.4
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/dist/server/cli/headless/claude-invoker-process.d.ts +11 -0
- package/dist/server/cli/headless/claude-invoker-process.d.ts.map +1 -0
- package/dist/server/cli/headless/claude-invoker-process.js +140 -0
- package/dist/server/cli/headless/claude-invoker-process.js.map +1 -0
- package/dist/server/cli/headless/claude-invoker-stall.d.ts +40 -0
- package/dist/server/cli/headless/claude-invoker-stall.d.ts.map +1 -0
- package/dist/server/cli/headless/claude-invoker-stall.js +98 -0
- package/dist/server/cli/headless/claude-invoker-stall.js.map +1 -0
- package/dist/server/cli/headless/claude-invoker-stream.d.ts +44 -0
- package/dist/server/cli/headless/claude-invoker-stream.d.ts.map +1 -0
- package/dist/server/cli/headless/claude-invoker-stream.js +276 -0
- package/dist/server/cli/headless/claude-invoker-stream.js.map +1 -0
- package/dist/server/cli/headless/claude-invoker-tools.d.ts +21 -0
- package/dist/server/cli/headless/claude-invoker-tools.d.ts.map +1 -0
- package/dist/server/cli/headless/claude-invoker-tools.js +137 -0
- package/dist/server/cli/headless/claude-invoker-tools.js.map +1 -0
- package/dist/server/cli/headless/claude-invoker.d.ts +6 -4
- package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker.js +10 -807
- package/dist/server/cli/headless/claude-invoker.js.map +1 -1
- package/dist/server/cli/headless/haiku-assessments.d.ts +62 -0
- package/dist/server/cli/headless/haiku-assessments.d.ts.map +1 -0
- package/dist/server/cli/headless/haiku-assessments.js +281 -0
- package/dist/server/cli/headless/haiku-assessments.js.map +1 -0
- package/dist/server/cli/headless/headless-logger.d.ts +3 -2
- package/dist/server/cli/headless/headless-logger.d.ts.map +1 -1
- package/dist/server/cli/headless/headless-logger.js +28 -5
- package/dist/server/cli/headless/headless-logger.js.map +1 -1
- package/dist/server/cli/headless/native-timeout-detector.d.ts +44 -0
- package/dist/server/cli/headless/native-timeout-detector.d.ts.map +1 -0
- package/dist/server/cli/headless/native-timeout-detector.js +99 -0
- package/dist/server/cli/headless/native-timeout-detector.js.map +1 -0
- package/dist/server/cli/headless/stall-assessor.d.ts +2 -110
- package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
- package/dist/server/cli/headless/stall-assessor.js +65 -457
- package/dist/server/cli/headless/stall-assessor.js.map +1 -1
- package/dist/server/cli/improvisation-attachments.d.ts +21 -0
- package/dist/server/cli/improvisation-attachments.d.ts.map +1 -0
- package/dist/server/cli/improvisation-attachments.js +116 -0
- package/dist/server/cli/improvisation-attachments.js.map +1 -0
- package/dist/server/cli/improvisation-retry.d.ts +52 -0
- package/dist/server/cli/improvisation-retry.d.ts.map +1 -0
- package/dist/server/cli/improvisation-retry.js +434 -0
- package/dist/server/cli/improvisation-retry.js.map +1 -0
- package/dist/server/cli/improvisation-session-manager.d.ts +10 -266
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +117 -1079
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/cli/improvisation-types.d.ts +86 -0
- package/dist/server/cli/improvisation-types.d.ts.map +1 -0
- package/dist/server/cli/improvisation-types.js +10 -0
- package/dist/server/cli/improvisation-types.js.map +1 -0
- package/dist/server/cli/prompt-builders.d.ts +68 -0
- package/dist/server/cli/prompt-builders.d.ts.map +1 -0
- package/dist/server/cli/prompt-builders.js +312 -0
- package/dist/server/cli/prompt-builders.js.map +1 -0
- package/dist/server/index.js +33 -212
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/bouncer-haiku.d.ts +10 -0
- package/dist/server/mcp/bouncer-haiku.d.ts.map +1 -0
- package/dist/server/mcp/bouncer-haiku.js +152 -0
- package/dist/server/mcp/bouncer-haiku.js.map +1 -0
- package/dist/server/mcp/bouncer-integration.d.ts +3 -4
- package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
- package/dist/server/mcp/bouncer-integration.js +50 -196
- package/dist/server/mcp/bouncer-integration.js.map +1 -1
- package/dist/server/mcp/security-analysis.d.ts +38 -0
- package/dist/server/mcp/security-analysis.d.ts.map +1 -0
- package/dist/server/mcp/security-analysis.js +183 -0
- package/dist/server/mcp/security-analysis.js.map +1 -0
- package/dist/server/mcp/security-audit.d.ts +1 -1
- package/dist/server/mcp/security-audit.d.ts.map +1 -1
- package/dist/server/mcp/security-patterns.d.ts +1 -25
- package/dist/server/mcp/security-patterns.d.ts.map +1 -1
- package/dist/server/mcp/security-patterns.js +55 -260
- package/dist/server/mcp/security-patterns.js.map +1 -1
- package/dist/server/server-setup.d.ts +22 -0
- package/dist/server/server-setup.d.ts.map +1 -0
- package/dist/server/server-setup.js +101 -0
- package/dist/server/server-setup.js.map +1 -0
- package/dist/server/services/file-explorer-ops.d.ts +24 -0
- package/dist/server/services/file-explorer-ops.d.ts.map +1 -0
- package/dist/server/services/file-explorer-ops.js +211 -0
- package/dist/server/services/file-explorer-ops.js.map +1 -0
- package/dist/server/services/files.d.ts +2 -85
- package/dist/server/services/files.d.ts.map +1 -1
- package/dist/server/services/files.js +7 -427
- package/dist/server/services/files.js.map +1 -1
- package/dist/server/services/plan/composer.d.ts.map +1 -1
- package/dist/server/services/plan/composer.js +2 -1
- package/dist/server/services/plan/composer.js.map +1 -1
- package/dist/server/services/plan/executor.d.ts.map +1 -1
- package/dist/server/services/plan/executor.js +3 -1
- package/dist/server/services/plan/executor.js.map +1 -1
- package/dist/server/services/plan/parser-core.d.ts +20 -0
- package/dist/server/services/plan/parser-core.d.ts.map +1 -0
- package/dist/server/services/plan/parser-core.js +350 -0
- package/dist/server/services/plan/parser-core.js.map +1 -0
- package/dist/server/services/plan/parser-migration.d.ts +5 -0
- package/dist/server/services/plan/parser-migration.d.ts.map +1 -0
- package/dist/server/services/plan/parser-migration.js +124 -0
- package/dist/server/services/plan/parser-migration.js.map +1 -0
- package/dist/server/services/plan/parser.d.ts +0 -8
- package/dist/server/services/plan/parser.d.ts.map +1 -1
- package/dist/server/services/plan/parser.js +50 -569
- package/dist/server/services/plan/parser.js.map +1 -1
- package/dist/server/services/plan/review-gate.d.ts +2 -0
- package/dist/server/services/plan/review-gate.d.ts.map +1 -1
- package/dist/server/services/plan/review-gate.js +2 -2
- package/dist/server/services/plan/review-gate.js.map +1 -1
- package/dist/server/services/plan/types.d.ts +2 -0
- package/dist/server/services/plan/types.d.ts.map +1 -1
- package/dist/server/services/platform-credentials.d.ts +24 -0
- package/dist/server/services/platform-credentials.d.ts.map +1 -0
- package/dist/server/services/platform-credentials.js +68 -0
- package/dist/server/services/platform-credentials.js.map +1 -0
- package/dist/server/services/platform.d.ts +1 -31
- package/dist/server/services/platform.d.ts.map +1 -1
- package/dist/server/services/platform.js +10 -119
- package/dist/server/services/platform.js.map +1 -1
- package/dist/server/services/terminal/pty-manager.d.ts +7 -97
- package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
- package/dist/server/services/terminal/pty-manager.js +53 -266
- package/dist/server/services/terminal/pty-manager.js.map +1 -1
- package/dist/server/services/terminal/pty-utils.d.ts +57 -0
- package/dist/server/services/terminal/pty-utils.d.ts.map +1 -0
- package/dist/server/services/terminal/pty-utils.js +141 -0
- package/dist/server/services/terminal/pty-utils.js.map +1 -0
- package/dist/server/services/websocket/file-definition-handlers.d.ts +4 -0
- package/dist/server/services/websocket/file-definition-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/file-definition-handlers.js +153 -0
- package/dist/server/services/websocket/file-definition-handlers.js.map +1 -0
- package/dist/server/services/websocket/file-explorer-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/file-explorer-handlers.js +52 -391
- package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -1
- package/dist/server/services/websocket/file-search-handlers.d.ts +5 -0
- package/dist/server/services/websocket/file-search-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/file-search-handlers.js +238 -0
- package/dist/server/services/websocket/file-search-handlers.js.map +1 -0
- package/dist/server/services/websocket/file-utils.js +3 -3
- package/dist/server/services/websocket/file-utils.js.map +1 -1
- package/dist/server/services/websocket/git-branch-handlers.d.ts +7 -0
- package/dist/server/services/websocket/git-branch-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/git-branch-handlers.js +110 -0
- package/dist/server/services/websocket/git-branch-handlers.js.map +1 -0
- package/dist/server/services/websocket/git-diff-handlers.d.ts +6 -0
- package/dist/server/services/websocket/git-diff-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/git-diff-handlers.js +123 -0
- package/dist/server/services/websocket/git-diff-handlers.js.map +1 -0
- package/dist/server/services/websocket/git-handlers.d.ts +2 -31
- package/dist/server/services/websocket/git-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/git-handlers.js +35 -541
- package/dist/server/services/websocket/git-handlers.js.map +1 -1
- package/dist/server/services/websocket/git-log-handlers.d.ts +6 -0
- package/dist/server/services/websocket/git-log-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/git-log-handlers.js +128 -0
- package/dist/server/services/websocket/git-log-handlers.js.map +1 -0
- package/dist/server/services/websocket/git-pr-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/git-pr-handlers.js +13 -53
- package/dist/server/services/websocket/git-pr-handlers.js.map +1 -1
- package/dist/server/services/websocket/git-tag-handlers.d.ts +6 -0
- package/dist/server/services/websocket/git-tag-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/git-tag-handlers.js +76 -0
- package/dist/server/services/websocket/git-tag-handlers.js.map +1 -0
- package/dist/server/services/websocket/git-utils.d.ts +43 -0
- package/dist/server/services/websocket/git-utils.d.ts.map +1 -0
- package/dist/server/services/websocket/git-utils.js +201 -0
- package/dist/server/services/websocket/git-utils.js.map +1 -0
- package/dist/server/services/websocket/handler.d.ts +2 -0
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +37 -126
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/plan-board-handlers.d.ts +11 -0
- package/dist/server/services/websocket/plan-board-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/plan-board-handlers.js +218 -0
- package/dist/server/services/websocket/plan-board-handlers.js.map +1 -0
- package/dist/server/services/websocket/plan-execution-handlers.d.ts +9 -0
- package/dist/server/services/websocket/plan-execution-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/plan-execution-handlers.js +142 -0
- package/dist/server/services/websocket/plan-execution-handlers.js.map +1 -0
- package/dist/server/services/websocket/plan-handlers.d.ts +7 -2
- package/dist/server/services/websocket/plan-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-handlers.js +6 -925
- package/dist/server/services/websocket/plan-handlers.js.map +1 -1
- package/dist/server/services/websocket/plan-helpers.d.ts +19 -0
- package/dist/server/services/websocket/plan-helpers.d.ts.map +1 -0
- package/dist/server/services/websocket/plan-helpers.js +199 -0
- package/dist/server/services/websocket/plan-helpers.js.map +1 -0
- package/dist/server/services/websocket/plan-issue-handlers.d.ts +12 -0
- package/dist/server/services/websocket/plan-issue-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/plan-issue-handlers.js +162 -0
- package/dist/server/services/websocket/plan-issue-handlers.js.map +1 -0
- package/dist/server/services/websocket/plan-sprint-handlers.d.ts +7 -0
- package/dist/server/services/websocket/plan-sprint-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/plan-sprint-handlers.js +206 -0
- package/dist/server/services/websocket/plan-sprint-handlers.js.map +1 -0
- package/dist/server/services/websocket/quality-complexity.d.ts +14 -0
- package/dist/server/services/websocket/quality-complexity.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-complexity.js +262 -0
- package/dist/server/services/websocket/quality-complexity.js.map +1 -0
- package/dist/server/services/websocket/quality-fix-agent.d.ts +16 -0
- package/dist/server/services/websocket/quality-fix-agent.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-fix-agent.js +140 -0
- package/dist/server/services/websocket/quality-fix-agent.js.map +1 -0
- package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-handlers.js +34 -346
- package/dist/server/services/websocket/quality-handlers.js.map +1 -1
- package/dist/server/services/websocket/quality-linting.d.ts +9 -0
- package/dist/server/services/websocket/quality-linting.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-linting.js +178 -0
- package/dist/server/services/websocket/quality-linting.js.map +1 -0
- package/dist/server/services/websocket/quality-review-agent.d.ts +19 -0
- package/dist/server/services/websocket/quality-review-agent.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-review-agent.js +206 -0
- package/dist/server/services/websocket/quality-review-agent.js.map +1 -0
- package/dist/server/services/websocket/quality-service.d.ts +3 -51
- package/dist/server/services/websocket/quality-service.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-service.js +9 -651
- package/dist/server/services/websocket/quality-service.js.map +1 -1
- package/dist/server/services/websocket/quality-tools.d.ts +23 -0
- package/dist/server/services/websocket/quality-tools.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-tools.js +208 -0
- package/dist/server/services/websocket/quality-tools.js.map +1 -0
- package/dist/server/services/websocket/quality-types.d.ts +59 -0
- package/dist/server/services/websocket/quality-types.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-types.js +101 -0
- package/dist/server/services/websocket/quality-types.js.map +1 -0
- package/dist/server/services/websocket/session-handlers.d.ts +3 -4
- package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/session-handlers.js +3 -378
- package/dist/server/services/websocket/session-handlers.js.map +1 -1
- package/dist/server/services/websocket/session-history.d.ts +4 -0
- package/dist/server/services/websocket/session-history.d.ts.map +1 -0
- package/dist/server/services/websocket/session-history.js +208 -0
- package/dist/server/services/websocket/session-history.js.map +1 -0
- package/dist/server/services/websocket/session-initialization.d.ts +5 -0
- package/dist/server/services/websocket/session-initialization.d.ts.map +1 -0
- package/dist/server/services/websocket/session-initialization.js +163 -0
- package/dist/server/services/websocket/session-initialization.js.map +1 -0
- package/dist/server/services/websocket/types.d.ts +12 -2
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/server/cli/headless/claude-invoker-process.ts +204 -0
- package/server/cli/headless/claude-invoker-stall.ts +164 -0
- package/server/cli/headless/claude-invoker-stream.ts +353 -0
- package/server/cli/headless/claude-invoker-tools.ts +187 -0
- package/server/cli/headless/claude-invoker.ts +15 -1096
- package/server/cli/headless/haiku-assessments.ts +365 -0
- package/server/cli/headless/headless-logger.ts +26 -5
- package/server/cli/headless/native-timeout-detector.ts +117 -0
- package/server/cli/headless/stall-assessor.ts +65 -618
- package/server/cli/improvisation-attachments.ts +148 -0
- package/server/cli/improvisation-retry.ts +602 -0
- package/server/cli/improvisation-session-manager.ts +140 -1349
- package/server/cli/improvisation-types.ts +98 -0
- package/server/cli/prompt-builders.ts +370 -0
- package/server/index.ts +35 -246
- package/server/mcp/bouncer-haiku.ts +182 -0
- package/server/mcp/bouncer-integration.ts +87 -248
- package/server/mcp/security-analysis.ts +217 -0
- package/server/mcp/security-audit.ts +1 -1
- package/server/mcp/security-patterns.ts +60 -283
- package/server/server-setup.ts +114 -0
- package/server/services/file-explorer-ops.ts +293 -0
- package/server/services/files.ts +20 -532
- package/server/services/plan/composer.ts +2 -1
- package/server/services/plan/executor.ts +3 -1
- package/server/services/plan/parser-core.ts +406 -0
- package/server/services/plan/parser-migration.ts +128 -0
- package/server/services/plan/parser.ts +52 -620
- package/server/services/plan/review-gate.ts +4 -2
- package/server/services/plan/types.ts +2 -0
- package/server/services/platform-credentials.ts +83 -0
- package/server/services/platform.ts +15 -141
- package/server/services/terminal/pty-manager.ts +66 -313
- package/server/services/terminal/pty-utils.ts +176 -0
- package/server/services/websocket/file-definition-handlers.ts +165 -0
- package/server/services/websocket/file-explorer-handlers.ts +37 -452
- package/server/services/websocket/file-search-handlers.ts +291 -0
- package/server/services/websocket/file-utils.ts +3 -3
- package/server/services/websocket/git-branch-handlers.ts +130 -0
- package/server/services/websocket/git-diff-handlers.ts +140 -0
- package/server/services/websocket/git-handlers.ts +40 -625
- package/server/services/websocket/git-log-handlers.ts +149 -0
- package/server/services/websocket/git-pr-handlers.ts +17 -62
- package/server/services/websocket/git-tag-handlers.ts +91 -0
- package/server/services/websocket/git-utils.ts +230 -0
- package/server/services/websocket/handler.ts +39 -126
- package/server/services/websocket/plan-board-handlers.ts +277 -0
- package/server/services/websocket/plan-execution-handlers.ts +184 -0
- package/server/services/websocket/plan-handlers.ts +8 -1114
- package/server/services/websocket/plan-helpers.ts +215 -0
- package/server/services/websocket/plan-issue-handlers.ts +204 -0
- package/server/services/websocket/plan-sprint-handlers.ts +252 -0
- package/server/services/websocket/quality-complexity.ts +294 -0
- package/server/services/websocket/quality-fix-agent.ts +181 -0
- package/server/services/websocket/quality-handlers.ts +36 -404
- package/server/services/websocket/quality-linting.ts +187 -0
- package/server/services/websocket/quality-review-agent.ts +246 -0
- package/server/services/websocket/quality-service.ts +11 -762
- package/server/services/websocket/quality-tools.ts +209 -0
- package/server/services/websocket/quality-types.ts +169 -0
- package/server/services/websocket/session-handlers.ts +5 -437
- package/server/services/websocket/session-history.ts +222 -0
- package/server/services/websocket/session-initialization.ts +209 -0
- package/server/services/websocket/types.ts +17 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Mstro, Inc. All rights reserved.
|
|
2
|
+
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { relative } from 'node:path';
|
|
6
|
+
import type { HandlerContext } from './handler-context.js';
|
|
7
|
+
import type { WebSocketMessage, WSContext } from './types.js';
|
|
8
|
+
|
|
9
|
+
type SearchMatch = { filePath: string; line: number; column: number; lineContent: string; contextBefore: string[]; contextAfter: string[] };
|
|
10
|
+
|
|
11
|
+
function appendGlobArgs(args: string[], globStr: string, prefix: string): void {
|
|
12
|
+
for (const glob of globStr.split(',')) {
|
|
13
|
+
const trimmed = glob.trim();
|
|
14
|
+
if (trimmed) args.push('--glob', `${prefix}${trimmed}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function buildRgArgs(query: string, options: Record<string, unknown>): string[] {
|
|
19
|
+
const args: string[] = ['--json', '--no-heading'];
|
|
20
|
+
if (!options.caseSensitive) args.push('-i');
|
|
21
|
+
if (options.wholeWord) args.push('-w');
|
|
22
|
+
if (!options.regex) args.push('-F');
|
|
23
|
+
args.push('-C', options.contextLines !== undefined ? String(options.contextLines) : '1');
|
|
24
|
+
if (options.includeGlob) appendGlobArgs(args, options.includeGlob as string, '');
|
|
25
|
+
if (options.excludeGlob) appendGlobArgs(args, options.excludeGlob as string, '!');
|
|
26
|
+
args.push('--', query, '.');
|
|
27
|
+
return args;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Process a single JSON line from rg output. Returns true if search should stop (maxResults reached). */
|
|
31
|
+
function processRgSearchLine(
|
|
32
|
+
line: string,
|
|
33
|
+
workingDir: string,
|
|
34
|
+
batch: SearchMatch[],
|
|
35
|
+
seenFiles: Set<string>,
|
|
36
|
+
contextMap: Map<string, { before: string[]; after: string[] }>,
|
|
37
|
+
counters: { totalMatches: number; fileCount: number },
|
|
38
|
+
maxResults: number,
|
|
39
|
+
flushBatch: () => void,
|
|
40
|
+
): boolean {
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(line);
|
|
43
|
+
if (parsed.type === 'match') {
|
|
44
|
+
return processRgMatch(parsed, workingDir, batch, seenFiles, contextMap, counters, maxResults, flushBatch);
|
|
45
|
+
}
|
|
46
|
+
if (parsed.type === 'context') {
|
|
47
|
+
appendRgContext(parsed, workingDir, batch);
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
// Skip malformed JSON lines
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function processRgMatch(
|
|
56
|
+
parsed: { data: { path: { text: string }; line_number: number; lines: { text: string }; submatches?: Array<{ start: number }> } },
|
|
57
|
+
workingDir: string,
|
|
58
|
+
batch: SearchMatch[],
|
|
59
|
+
seenFiles: Set<string>,
|
|
60
|
+
contextMap: Map<string, { before: string[]; after: string[] }>,
|
|
61
|
+
counters: { totalMatches: number; fileCount: number },
|
|
62
|
+
maxResults: number,
|
|
63
|
+
flushBatch: () => void,
|
|
64
|
+
): boolean {
|
|
65
|
+
const filePath = relative(workingDir, parsed.data.path.text);
|
|
66
|
+
const lineNumber = parsed.data.line_number;
|
|
67
|
+
const lineContent = parsed.data.lines.text.replace(/\n$/, '');
|
|
68
|
+
const column = parsed.data.submatches?.[0]?.start ?? 0;
|
|
69
|
+
|
|
70
|
+
if (!seenFiles.has(filePath)) {
|
|
71
|
+
seenFiles.add(filePath);
|
|
72
|
+
counters.fileCount++;
|
|
73
|
+
}
|
|
74
|
+
counters.totalMatches++;
|
|
75
|
+
|
|
76
|
+
const key = `${filePath}:${lineNumber}`;
|
|
77
|
+
const ctxLines = contextMap.get(key) || { before: [], after: [] };
|
|
78
|
+
batch.push({ filePath, line: lineNumber, column: column + 1, lineContent, contextBefore: ctxLines.before, contextAfter: [] });
|
|
79
|
+
|
|
80
|
+
if (counters.totalMatches >= maxResults) {
|
|
81
|
+
flushBatch();
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
if (batch.length >= 50) flushBatch();
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function appendRgContext(
|
|
89
|
+
parsed: { data: { path: { text: string }; line_number: number; lines: { text: string } } },
|
|
90
|
+
workingDir: string,
|
|
91
|
+
batch: SearchMatch[],
|
|
92
|
+
): void {
|
|
93
|
+
const filePath = relative(workingDir, parsed.data.path.text);
|
|
94
|
+
const lineNumber = parsed.data.line_number;
|
|
95
|
+
const lineContent = parsed.data.lines.text.replace(/\n$/, '');
|
|
96
|
+
|
|
97
|
+
const lastMatch = batch[batch.length - 1];
|
|
98
|
+
if (!lastMatch || lastMatch.filePath !== filePath) return;
|
|
99
|
+
if (lineNumber < lastMatch.line) {
|
|
100
|
+
lastMatch.contextBefore.push(lineContent);
|
|
101
|
+
} else {
|
|
102
|
+
lastMatch.contextAfter.push(lineContent);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function handleSearchFileContents(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string): void {
|
|
107
|
+
const query = msg.data?.query;
|
|
108
|
+
if (!query) {
|
|
109
|
+
ctx.send(ws, { type: 'contentSearchError', tabId, data: { error: 'Search query is required' } });
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
handleCancelSearch(ctx, tabId);
|
|
114
|
+
|
|
115
|
+
const options = msg.data.options || {};
|
|
116
|
+
const startTime = Date.now();
|
|
117
|
+
let totalMatches = 0;
|
|
118
|
+
let fileCount = 0;
|
|
119
|
+
const seenFiles = new Set<string>();
|
|
120
|
+
const maxResults = options.maxResults || 5000;
|
|
121
|
+
let batch: SearchMatch[] = [];
|
|
122
|
+
|
|
123
|
+
const args = buildRgArgs(query, options);
|
|
124
|
+
|
|
125
|
+
const rgProcess = spawn('rg', args, { cwd: workingDir, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
126
|
+
ctx.activeSearches.set(tabId, rgProcess);
|
|
127
|
+
|
|
128
|
+
let buffer = '';
|
|
129
|
+
const contextMap = new Map<string, { before: string[]; after: string[] }>();
|
|
130
|
+
|
|
131
|
+
const flushBatch = () => {
|
|
132
|
+
if (batch.length > 0) {
|
|
133
|
+
ctx.send(ws, { type: 'contentSearchResults', tabId, data: { matches: batch, partial: true } });
|
|
134
|
+
batch = [];
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const searchState = { totalMatches, fileCount };
|
|
139
|
+
|
|
140
|
+
rgProcess.stdout?.on('data', (chunk: Buffer) => {
|
|
141
|
+
buffer += chunk.toString();
|
|
142
|
+
const lines = buffer.split('\n');
|
|
143
|
+
buffer = lines.pop() || '';
|
|
144
|
+
|
|
145
|
+
for (const line of lines) {
|
|
146
|
+
if (!line.trim()) continue;
|
|
147
|
+
if (processRgSearchLine(line, workingDir, batch, seenFiles, contextMap, searchState, maxResults, flushBatch)) {
|
|
148
|
+
rgProcess.kill();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
totalMatches = searchState.totalMatches;
|
|
153
|
+
fileCount = searchState.fileCount;
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
rgProcess.stderr?.on('data', (chunk: Buffer) => {
|
|
157
|
+
const errText = chunk.toString().trim();
|
|
158
|
+
if (errText && !errText.includes('No files were searched')) {
|
|
159
|
+
console.error(`[Search] rg stderr: ${errText}`);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
rgProcess.on('close', (_code) => {
|
|
164
|
+
ctx.activeSearches.delete(tabId);
|
|
165
|
+
flushBatch();
|
|
166
|
+
|
|
167
|
+
ctx.send(ws, {
|
|
168
|
+
type: 'contentSearchComplete',
|
|
169
|
+
tabId,
|
|
170
|
+
data: {
|
|
171
|
+
totalMatches,
|
|
172
|
+
fileCount,
|
|
173
|
+
truncated: totalMatches >= maxResults,
|
|
174
|
+
durationMs: Date.now() - startTime,
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
rgProcess.on('error', (err) => {
|
|
180
|
+
ctx.activeSearches.delete(tabId);
|
|
181
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
182
|
+
handleSearchFallback(ctx, ws, query, options, tabId, workingDir);
|
|
183
|
+
} else {
|
|
184
|
+
ctx.send(ws, { type: 'contentSearchError', tabId, data: { error: err.message } });
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Process a single grep output line. Returns true if search should stop. */
|
|
190
|
+
function processGrepLine(
|
|
191
|
+
line: string,
|
|
192
|
+
batch: SearchMatch[],
|
|
193
|
+
seenFiles: Set<string>,
|
|
194
|
+
counters: { totalMatches: number; fileCount: number },
|
|
195
|
+
maxResults: number,
|
|
196
|
+
flushBatch: () => void,
|
|
197
|
+
): boolean {
|
|
198
|
+
const match = line.match(/^\.\/(.+?):(\d+):(.*)$/);
|
|
199
|
+
if (!match) return false;
|
|
200
|
+
|
|
201
|
+
const filePath = match[1];
|
|
202
|
+
const lineNumber = parseInt(match[2], 10);
|
|
203
|
+
const lineContent = match[3];
|
|
204
|
+
|
|
205
|
+
if (!seenFiles.has(filePath)) {
|
|
206
|
+
seenFiles.add(filePath);
|
|
207
|
+
counters.fileCount++;
|
|
208
|
+
}
|
|
209
|
+
counters.totalMatches++;
|
|
210
|
+
|
|
211
|
+
batch.push({ filePath, line: lineNumber, column: 1, lineContent, contextBefore: [], contextAfter: [] });
|
|
212
|
+
|
|
213
|
+
if (counters.totalMatches >= maxResults) {
|
|
214
|
+
flushBatch();
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
if (batch.length >= 50) flushBatch();
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function handleSearchFallback(ctx: HandlerContext, ws: WSContext, query: string, options: Record<string, unknown>, tabId: string, workingDir: string): void {
|
|
222
|
+
const startTime = Date.now();
|
|
223
|
+
const args: string[] = ['-rn'];
|
|
224
|
+
if (!options.caseSensitive) args.push('-i');
|
|
225
|
+
if (options.includeGlob) {
|
|
226
|
+
for (const glob of String(options.includeGlob).split(',')) {
|
|
227
|
+
const trimmed = glob.trim();
|
|
228
|
+
if (trimmed) args.push(`--include=${trimmed}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
args.push('--', query, '.');
|
|
232
|
+
|
|
233
|
+
const grepProcess = spawn('grep', args, { cwd: workingDir, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
234
|
+
ctx.activeSearches.set(tabId, grepProcess);
|
|
235
|
+
|
|
236
|
+
let buffer = '';
|
|
237
|
+
let totalMatches = 0;
|
|
238
|
+
let fileCount = 0;
|
|
239
|
+
const seenFiles = new Set<string>();
|
|
240
|
+
const maxResults = (options.maxResults as number) || 5000;
|
|
241
|
+
let batch: SearchMatch[] = [];
|
|
242
|
+
const grepState = { totalMatches, fileCount };
|
|
243
|
+
|
|
244
|
+
const flushGrepBatch = () => {
|
|
245
|
+
if (batch.length > 0) {
|
|
246
|
+
ctx.send(ws, { type: 'contentSearchResults', tabId, data: { matches: batch, partial: true } });
|
|
247
|
+
batch = [];
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
grepProcess.stdout?.on('data', (chunk: Buffer) => {
|
|
252
|
+
buffer += chunk.toString();
|
|
253
|
+
const lines = buffer.split('\n');
|
|
254
|
+
buffer = lines.pop() || '';
|
|
255
|
+
|
|
256
|
+
for (const line of lines) {
|
|
257
|
+
if (!line.trim()) continue;
|
|
258
|
+
if (processGrepLine(line, batch, seenFiles, grepState, maxResults, flushGrepBatch)) {
|
|
259
|
+
grepProcess.kill();
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
totalMatches = grepState.totalMatches;
|
|
264
|
+
fileCount = grepState.fileCount;
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
grepProcess.on('close', () => {
|
|
268
|
+
ctx.activeSearches.delete(tabId);
|
|
269
|
+
if (batch.length > 0) {
|
|
270
|
+
ctx.send(ws, { type: 'contentSearchResults', tabId, data: { matches: batch, partial: true } });
|
|
271
|
+
}
|
|
272
|
+
ctx.send(ws, {
|
|
273
|
+
type: 'contentSearchComplete',
|
|
274
|
+
tabId,
|
|
275
|
+
data: { totalMatches, fileCount, truncated: totalMatches >= maxResults, durationMs: Date.now() - startTime },
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
grepProcess.on('error', (err) => {
|
|
280
|
+
ctx.activeSearches.delete(tabId);
|
|
281
|
+
ctx.send(ws, { type: 'contentSearchError', tabId, data: { error: `Search unavailable: ${err.message}` } });
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function handleCancelSearch(ctx: HandlerContext, tabId: string): void {
|
|
286
|
+
const process = ctx.activeSearches.get(tabId);
|
|
287
|
+
if (process) {
|
|
288
|
+
process.kill();
|
|
289
|
+
ctx.activeSearches.delete(tabId);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { existsSync, readdirSync, readFileSync, type Stats, statSync } from 'node:fs';
|
|
11
|
-
import { extname, join, relative, sep } from 'node:path';
|
|
11
|
+
import { extname, join, relative, resolve, sep } from 'node:path';
|
|
12
12
|
import type { CacheEntry, } from './types.js';
|
|
13
13
|
|
|
14
14
|
// Directories always excluded from autocomplete scanning
|
|
@@ -286,8 +286,8 @@ function readTextContent(fullPath: string, filePath: string, fileName: string, s
|
|
|
286
286
|
}
|
|
287
287
|
|
|
288
288
|
function validateFileAccess(fullPath: string, filePath: string, fileName: string, workingDir: string): FileContentResult | null {
|
|
289
|
-
const normalizedPath =
|
|
290
|
-
if (!normalizedPath.startsWith(
|
|
289
|
+
const normalizedPath = resolve(fullPath);
|
|
290
|
+
if (!normalizedPath.startsWith(resolve(workingDir)) && !isPathInSafeLocation(normalizedPath)) {
|
|
291
291
|
return { path: filePath, fileName, content: '', error: 'Access denied: path outside allowed locations' };
|
|
292
292
|
}
|
|
293
293
|
if (!existsSync(fullPath)) {
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Mstro, Inc. All rights reserved.
|
|
2
|
+
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
import { executeGitCommand, sendGitError } from './git-utils.js';
|
|
5
|
+
import type { HandlerContext } from './handler-context.js';
|
|
6
|
+
import type { GitBranchEntry, WebSocketMessage, WSContext } from './types.js';
|
|
7
|
+
|
|
8
|
+
export async function handleGitListBranches(ctx: HandlerContext, ws: WSContext, tabId: string, workingDir: string): Promise<void> {
|
|
9
|
+
try {
|
|
10
|
+
const result = await executeGitCommand(
|
|
11
|
+
['branch', '-a', '--format=%(refname:short)|%(objectname:short)|%(upstream:short)|%(HEAD)'],
|
|
12
|
+
workingDir
|
|
13
|
+
);
|
|
14
|
+
if (result.exitCode !== 0) {
|
|
15
|
+
ctx.send(ws, { type: 'gitError', tabId, data: { error: result.stderr || 'Failed to list branches' } });
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const currentBranchResult = await executeGitCommand(['rev-parse', '--abbrev-ref', 'HEAD'], workingDir);
|
|
20
|
+
const currentBranch = currentBranchResult.stdout.trim() || 'HEAD';
|
|
21
|
+
|
|
22
|
+
const branches: GitBranchEntry[] = result.stdout.trim().split('\n')
|
|
23
|
+
.filter(line => line.trim())
|
|
24
|
+
.map(line => {
|
|
25
|
+
const [name, shortHash, upstream, head] = line.split('|');
|
|
26
|
+
const isRemote = name.includes('/') && (name.startsWith('origin/') || name.includes('remotes/'));
|
|
27
|
+
return {
|
|
28
|
+
name: name.trim(),
|
|
29
|
+
shortHash: shortHash?.trim() || '',
|
|
30
|
+
isRemote,
|
|
31
|
+
isCurrent: head?.trim() === '*',
|
|
32
|
+
upstream: upstream?.trim() || undefined,
|
|
33
|
+
};
|
|
34
|
+
})
|
|
35
|
+
.filter(b => b.name !== 'origin/HEAD');
|
|
36
|
+
|
|
37
|
+
ctx.send(ws, { type: 'gitBranchList', tabId, data: { branches, current: currentBranch } });
|
|
38
|
+
} catch (error: unknown) {
|
|
39
|
+
sendGitError(ctx, ws, tabId, error);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function handleGitCheckout(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string): Promise<void> {
|
|
44
|
+
try {
|
|
45
|
+
const { branch, create, startPoint } = msg.data || {};
|
|
46
|
+
if (!branch) {
|
|
47
|
+
ctx.send(ws, { type: 'gitError', tabId, data: { error: 'Branch name is required' } });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const statusResult = await executeGitCommand(['status', '--porcelain'], workingDir);
|
|
52
|
+
if (statusResult.stdout.trim()) {
|
|
53
|
+
ctx.send(ws, { type: 'gitError', tabId, data: { error: 'Commit or stash changes before switching branches' } });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const prevResult = await executeGitCommand(['rev-parse', '--abbrev-ref', 'HEAD'], workingDir);
|
|
58
|
+
const previous = prevResult.stdout.trim();
|
|
59
|
+
|
|
60
|
+
const args = create
|
|
61
|
+
? ['checkout', '-b', branch, ...(startPoint ? [startPoint] : [])]
|
|
62
|
+
: ['checkout', branch];
|
|
63
|
+
|
|
64
|
+
const result = await executeGitCommand(args, workingDir);
|
|
65
|
+
if (result.exitCode !== 0) {
|
|
66
|
+
ctx.send(ws, { type: 'gitError', tabId, data: { error: result.stderr || 'Failed to checkout branch' } });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
ctx.send(ws, { type: 'gitCheckedOut', tabId, data: { branch, previous } });
|
|
71
|
+
// Re-fetch status after checkout - import handleGitStatus at call site
|
|
72
|
+
const { handleGitStatus } = await import('./git-handlers.js');
|
|
73
|
+
handleGitStatus(ctx, ws, tabId, workingDir);
|
|
74
|
+
} catch (error: unknown) {
|
|
75
|
+
sendGitError(ctx, ws, tabId, error);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function handleGitCreateBranch(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string): Promise<void> {
|
|
80
|
+
try {
|
|
81
|
+
const { name, startPoint, checkout } = msg.data || {};
|
|
82
|
+
if (!name) {
|
|
83
|
+
ctx.send(ws, { type: 'gitError', tabId, data: { error: 'Branch name is required' } });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const args = ['branch', name, ...(startPoint ? [startPoint] : [])];
|
|
88
|
+
const result = await executeGitCommand(args, workingDir);
|
|
89
|
+
if (result.exitCode !== 0) {
|
|
90
|
+
ctx.send(ws, { type: 'gitError', tabId, data: { error: result.stderr || 'Failed to create branch' } });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const hashResult = await executeGitCommand(['rev-parse', '--short', name], workingDir);
|
|
95
|
+
|
|
96
|
+
if (checkout) {
|
|
97
|
+
await executeGitCommand(['checkout', name], workingDir);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
ctx.send(ws, { type: 'gitBranchCreated', tabId, data: { name, hash: hashResult.stdout.trim() } });
|
|
101
|
+
} catch (error: unknown) {
|
|
102
|
+
sendGitError(ctx, ws, tabId, error);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function handleGitDeleteBranch(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string): Promise<void> {
|
|
107
|
+
try {
|
|
108
|
+
const { name, force } = msg.data || {};
|
|
109
|
+
if (!name) {
|
|
110
|
+
ctx.send(ws, { type: 'gitError', tabId, data: { error: 'Branch name is required' } });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const currentResult = await executeGitCommand(['rev-parse', '--abbrev-ref', 'HEAD'], workingDir);
|
|
115
|
+
if (currentResult.stdout.trim() === name) {
|
|
116
|
+
ctx.send(ws, { type: 'gitError', tabId, data: { error: 'Cannot delete the currently checked out branch' } });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const result = await executeGitCommand(['branch', force ? '-D' : '-d', name], workingDir);
|
|
121
|
+
if (result.exitCode !== 0) {
|
|
122
|
+
ctx.send(ws, { type: 'gitError', tabId, data: { error: result.stderr || 'Failed to delete branch' } });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
ctx.send(ws, { type: 'gitBranchDeleted', tabId, data: { name } });
|
|
127
|
+
} catch (error: unknown) {
|
|
128
|
+
sendGitError(ctx, ws, tabId, error);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Mstro, Inc. All rights reserved.
|
|
2
|
+
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { executeGitCommand, sendGitError, stripCoauthorLines } from './git-utils.js';
|
|
7
|
+
import type { HandlerContext } from './handler-context.js';
|
|
8
|
+
import type { GitCommitFile, WebSocketMessage, WSContext } from './types.js';
|
|
9
|
+
|
|
10
|
+
export async function handleGitDiff(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string): Promise<void> {
|
|
11
|
+
try {
|
|
12
|
+
const { path, staged } = msg.data || {};
|
|
13
|
+
if (!path) {
|
|
14
|
+
ctx.send(ws, { type: 'gitError', tabId, data: { error: 'File path is required' } });
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const originalResult = await executeGitCommand(['show', `HEAD:${path}`], workingDir);
|
|
19
|
+
const original = originalResult.exitCode === 0 ? originalResult.stdout : '';
|
|
20
|
+
|
|
21
|
+
let modified: string;
|
|
22
|
+
if (staged) {
|
|
23
|
+
const indexResult = await executeGitCommand(['show', `:${path}`], workingDir);
|
|
24
|
+
modified = indexResult.exitCode === 0 ? indexResult.stdout : '';
|
|
25
|
+
} else {
|
|
26
|
+
const fullPath = join(workingDir, path);
|
|
27
|
+
try {
|
|
28
|
+
modified = readFileSync(fullPath, 'utf-8');
|
|
29
|
+
} catch {
|
|
30
|
+
modified = '';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
ctx.send(ws, {
|
|
35
|
+
type: 'gitDiffResult',
|
|
36
|
+
tabId,
|
|
37
|
+
data: { path, original, modified, staged: !!staged },
|
|
38
|
+
});
|
|
39
|
+
} catch (error: unknown) {
|
|
40
|
+
sendGitError(ctx, ws, tabId, error);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Parse a single name-status line into {status, oldPath?} */
|
|
45
|
+
function parseStatusLine(line: string | undefined): { status: string; oldPath?: string } {
|
|
46
|
+
if (!line) return { status: 'M' };
|
|
47
|
+
const parts = line.split('\t');
|
|
48
|
+
const status = (parts[0] || 'M').charAt(0); // R100 -> R, C100 -> C
|
|
49
|
+
const oldPath = (status === 'R' || status === 'C') && parts.length >= 3 ? parts[1] : undefined;
|
|
50
|
+
return { status, oldPath };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Parse numstat + name-status output into GitCommitFile[] */
|
|
54
|
+
function parseCommitFiles(numstatOutput: string, statusOutput: string): GitCommitFile[] {
|
|
55
|
+
const numstatLines = numstatOutput.trim().split('\n').filter(Boolean);
|
|
56
|
+
const statusLines = statusOutput.trim().split('\n').filter(Boolean);
|
|
57
|
+
|
|
58
|
+
return numstatLines.map((line, i) => {
|
|
59
|
+
const numParts = line.split('\t');
|
|
60
|
+
const additions = numParts[0] === '-' ? 0 : parseInt(numParts[0], 10) || 0;
|
|
61
|
+
const deletions = numParts[1] === '-' ? 0 : parseInt(numParts[1], 10) || 0;
|
|
62
|
+
const { status, oldPath } = parseStatusLine(statusLines[i]);
|
|
63
|
+
return { path: numParts[2] || '', status, additions, deletions, ...(oldPath ? { oldPath } : {}) };
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Get the changed files for a commit via diff-tree */
|
|
68
|
+
async function getCommitFiles(hash: string, workingDir: string): Promise<GitCommitFile[]> {
|
|
69
|
+
const isRoot = (await executeGitCommand(['rev-parse', `${hash}^`], workingDir)).exitCode !== 0;
|
|
70
|
+
const extra = isRoot ? ['--root'] : [];
|
|
71
|
+
const numstatResult = await executeGitCommand(
|
|
72
|
+
['diff-tree', '-r', '--numstat', '--no-commit-id', ...extra, hash], workingDir
|
|
73
|
+
);
|
|
74
|
+
const statusResult = await executeGitCommand(
|
|
75
|
+
['diff-tree', '-r', '--name-status', '--no-commit-id', ...extra, hash], workingDir
|
|
76
|
+
);
|
|
77
|
+
return parseCommitFiles(numstatResult.stdout, statusResult.stdout);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function handleGitShowCommit(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string): Promise<void> {
|
|
81
|
+
try {
|
|
82
|
+
const hash = msg.data?.hash as string | undefined;
|
|
83
|
+
if (!hash) {
|
|
84
|
+
ctx.send(ws, { type: 'gitError', tabId, data: { error: 'Commit hash is required' } });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const metaResult = await executeGitCommand([
|
|
89
|
+
'show', '-s', '--format=%H%x00%h%x00%s%x00%b%x00%an%x00%aI', hash
|
|
90
|
+
], workingDir);
|
|
91
|
+
|
|
92
|
+
if (metaResult.exitCode !== 0) {
|
|
93
|
+
ctx.send(ws, { type: 'gitError', tabId, data: { error: metaResult.stderr || 'Failed to get commit details' } });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const parts = metaResult.stdout.trim().split('\x00');
|
|
98
|
+
const subject = stripCoauthorLines(parts[2] || '') || parts[2] || '';
|
|
99
|
+
const files = await getCommitFiles(hash, workingDir);
|
|
100
|
+
|
|
101
|
+
ctx.send(ws, {
|
|
102
|
+
type: 'gitCommitDetail',
|
|
103
|
+
tabId,
|
|
104
|
+
data: {
|
|
105
|
+
hash: parts[0], shortHash: parts[1], subject,
|
|
106
|
+
body: stripCoauthorLines(parts[3] || '').trim(),
|
|
107
|
+
author: parts[4], date: parts[5], files,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
} catch (error: unknown) {
|
|
111
|
+
sendGitError(ctx, ws, tabId, error);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function handleGitCommitDiff(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string): Promise<void> {
|
|
116
|
+
try {
|
|
117
|
+
const hash = msg.data?.hash as string | undefined;
|
|
118
|
+
const path = msg.data?.path as string | undefined;
|
|
119
|
+
if (!hash || !path) {
|
|
120
|
+
ctx.send(ws, { type: 'gitError', tabId, data: { error: 'Commit hash and file path are required' } });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Get the file content at this commit
|
|
125
|
+
const modifiedResult = await executeGitCommand(['show', `${hash}:${path}`], workingDir);
|
|
126
|
+
const modified = modifiedResult.exitCode === 0 ? modifiedResult.stdout : '';
|
|
127
|
+
|
|
128
|
+
// Get the file content at the parent commit
|
|
129
|
+
const originalResult = await executeGitCommand(['show', `${hash}^:${path}`], workingDir);
|
|
130
|
+
const original = originalResult.exitCode === 0 ? originalResult.stdout : '';
|
|
131
|
+
|
|
132
|
+
ctx.send(ws, {
|
|
133
|
+
type: 'gitCommitDiffResult',
|
|
134
|
+
tabId,
|
|
135
|
+
data: { hash, path, original, modified },
|
|
136
|
+
});
|
|
137
|
+
} catch (error: unknown) {
|
|
138
|
+
sendGitError(ctx, ws, tabId, error);
|
|
139
|
+
}
|
|
140
|
+
}
|