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,149 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Mstro, Inc. All rights reserved.
|
|
2
|
+
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
5
|
+
import { join, resolve } from 'node:path';
|
|
6
|
+
import { executeGitCommand, sendGitError, stripCoauthorLines } from './git-utils.js';
|
|
7
|
+
import type { HandlerContext } from './handler-context.js';
|
|
8
|
+
import type { GitDirectorySetResponse, GitLogEntry, GitRepoInfo, GitReposDiscoveredResponse, WebSocketMessage, WSContext } from './types.js';
|
|
9
|
+
|
|
10
|
+
export async function handleGitLog(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string): Promise<void> {
|
|
11
|
+
const limit = msg.data?.limit ?? 10;
|
|
12
|
+
const skip = msg.data?.skip ?? 0;
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
// Request one extra to detect if there are more commits
|
|
16
|
+
const fetchCount = limit + 1;
|
|
17
|
+
const args = [
|
|
18
|
+
'log',
|
|
19
|
+
`-${fetchCount}`,
|
|
20
|
+
`--skip=${skip}`,
|
|
21
|
+
'--format=%H%x00%h%x00%s%x00%an%x00%aI'
|
|
22
|
+
];
|
|
23
|
+
const result = await executeGitCommand(args, workingDir);
|
|
24
|
+
|
|
25
|
+
if (result.exitCode !== 0) {
|
|
26
|
+
ctx.send(ws, { type: 'gitError', tabId, data: { error: result.stderr || result.stdout || 'Failed to get log' } });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const allEntries: GitLogEntry[] = result.stdout.trim().split('\n').filter(Boolean).map(line => {
|
|
31
|
+
const parts = line.split('\x00');
|
|
32
|
+
const cleanSubject = stripCoauthorLines(parts[2] || '') || parts[2] || '';
|
|
33
|
+
return { hash: parts[0], shortHash: parts[1], subject: cleanSubject, author: parts[3], date: parts[4] };
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const hasMore = allEntries.length > limit;
|
|
37
|
+
const entries = hasMore ? allEntries.slice(0, limit) : allEntries;
|
|
38
|
+
|
|
39
|
+
ctx.send(ws, { type: 'gitLog', tabId, data: { entries, hasMore, skip } });
|
|
40
|
+
} catch (error: unknown) {
|
|
41
|
+
sendGitError(ctx, ws, tabId, error);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const SKIP_DIRS = ['node_modules', 'vendor', '.git'];
|
|
46
|
+
|
|
47
|
+
function shouldSkipDir(name: string): boolean {
|
|
48
|
+
return name.startsWith('.') || SKIP_DIRS.includes(name);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function getRepoBranch(repoPath: string): Promise<string | undefined> {
|
|
52
|
+
const result = await executeGitCommand(['rev-parse', '--abbrev-ref', 'HEAD'], repoPath);
|
|
53
|
+
return result.exitCode === 0 ? result.stdout.trim() : undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function scanForGitRepos(dir: string, depth: number, maxDepth: number, repos: GitRepoInfo[]): Promise<void> {
|
|
57
|
+
if (depth > maxDepth) return;
|
|
58
|
+
|
|
59
|
+
let entries: string[];
|
|
60
|
+
try {
|
|
61
|
+
entries = readdirSync(dir);
|
|
62
|
+
} catch {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const name of entries) {
|
|
67
|
+
if (shouldSkipDir(name)) continue;
|
|
68
|
+
|
|
69
|
+
const fullPath = join(dir, name);
|
|
70
|
+
const gitPath = join(fullPath, '.git');
|
|
71
|
+
|
|
72
|
+
if (existsSync(gitPath)) {
|
|
73
|
+
repos.push({ path: fullPath, name, branch: await getRepoBranch(fullPath) });
|
|
74
|
+
} else {
|
|
75
|
+
await scanForGitRepos(fullPath, depth + 1, maxDepth, repos);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function handleGitDiscoverRepos(ctx: HandlerContext, ws: WSContext, tabId: string, workingDir: string): Promise<void> {
|
|
81
|
+
try {
|
|
82
|
+
const repos: GitRepoInfo[] = [];
|
|
83
|
+
const rootIsGitRepo = existsSync(join(workingDir, '.git'));
|
|
84
|
+
|
|
85
|
+
if (rootIsGitRepo) {
|
|
86
|
+
repos.push({
|
|
87
|
+
path: workingDir,
|
|
88
|
+
name: workingDir.split('/').pop() || workingDir,
|
|
89
|
+
branch: await getRepoBranch(workingDir),
|
|
90
|
+
});
|
|
91
|
+
} else {
|
|
92
|
+
await scanForGitRepos(workingDir, 1, 3, repos);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const response: GitReposDiscoveredResponse = {
|
|
96
|
+
repos,
|
|
97
|
+
rootIsGitRepo,
|
|
98
|
+
selectedDirectory: ctx.gitDirectories.get(tabId) || null,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
ctx.send(ws, { type: 'gitReposDiscovered', tabId, data: response });
|
|
102
|
+
} catch (error: unknown) {
|
|
103
|
+
sendGitError(ctx, ws, tabId, error);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function handleGitSetDirectory(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string): Promise<void> {
|
|
108
|
+
const directory = msg.data?.directory as string | undefined;
|
|
109
|
+
|
|
110
|
+
if (!directory) {
|
|
111
|
+
ctx.gitDirectories.delete(tabId);
|
|
112
|
+
const response: GitDirectorySetResponse = {
|
|
113
|
+
directory: workingDir,
|
|
114
|
+
isValid: existsSync(join(workingDir, '.git')),
|
|
115
|
+
};
|
|
116
|
+
ctx.send(ws, { type: 'gitDirectorySet', tabId, data: response });
|
|
117
|
+
const { handleGitStatus } = await import('./git-handlers.js');
|
|
118
|
+
handleGitStatus(ctx, ws, tabId, workingDir);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Security: validate path is within working directory to prevent traversal
|
|
123
|
+
const resolvedDir = resolve(directory);
|
|
124
|
+
const resolvedWorkingDir = resolve(workingDir);
|
|
125
|
+
if (!resolvedDir.startsWith(`${resolvedWorkingDir}/`) && resolvedDir !== resolvedWorkingDir) {
|
|
126
|
+
ctx.send(ws, { type: 'gitError', tabId, data: { error: 'Access denied: path outside project directory' } });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const gitPath = join(directory, '.git');
|
|
131
|
+
const isValid = existsSync(gitPath);
|
|
132
|
+
|
|
133
|
+
if (isValid) {
|
|
134
|
+
ctx.gitDirectories.set(tabId, directory);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const response: GitDirectorySetResponse = {
|
|
138
|
+
directory,
|
|
139
|
+
isValid,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
ctx.send(ws, { type: 'gitDirectorySet', tabId, data: response });
|
|
143
|
+
|
|
144
|
+
if (isValid) {
|
|
145
|
+
const { handleGitStatus } = await import('./git-handlers.js');
|
|
146
|
+
handleGitStatus(ctx, ws, tabId, directory);
|
|
147
|
+
handleGitLog(ctx, ws, { type: 'gitLog', data: { limit: 5 } }, tabId, directory);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
// Copyright (c) 2025-present Mstro, Inc. All rights reserved.
|
|
2
2
|
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
3
|
|
|
4
|
-
import { spawn } from 'node:child_process';
|
|
5
|
-
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
6
|
-
import { join } from 'node:path';
|
|
7
4
|
import { getPrBaseBranch, setPrBaseBranch } from '../settings.js';
|
|
8
|
-
import { detectGitProvider, executeGitCommand, spawnCheck, spawnWithOutput, stripCoauthorLines } from './git-handlers.js';
|
|
5
|
+
import { detectGitProvider, executeGitCommand, spawnCheck, spawnHaikuWithPrompt, spawnWithOutput, stripCoauthorLines, truncateDiff } from './git-handlers.js';
|
|
9
6
|
import type { HandlerContext } from './handler-context.js';
|
|
10
7
|
import type { WebSocketMessage, WSContext } from './types.js';
|
|
11
8
|
|
|
@@ -273,20 +270,7 @@ async function handleGitGeneratePRDescription(ctx: HandlerContext, ws: WSContext
|
|
|
273
270
|
}
|
|
274
271
|
|
|
275
272
|
const diffResult = await executeGitCommand(['diff', `${compareRef}...HEAD`], workingDir);
|
|
276
|
-
const diff = diffResult.exitCode === 0 ? diffResult.stdout : '';
|
|
277
|
-
|
|
278
273
|
const statResult = await executeGitCommand(['diff', `${compareRef}...HEAD`, '--stat'], workingDir);
|
|
279
|
-
const stat = statResult.exitCode === 0 ? statResult.stdout.trim() : '';
|
|
280
|
-
|
|
281
|
-
let truncatedDiff = diff;
|
|
282
|
-
if (diff.length > 8000) {
|
|
283
|
-
truncatedDiff = `${diff.slice(0, 4000)}\n\n... [diff truncated] ...\n\n${diff.slice(-3500)}`;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const tempDir = join(workingDir, '.mstro', 'tmp');
|
|
287
|
-
if (!existsSync(tempDir)) {
|
|
288
|
-
mkdirSync(tempDir, { recursive: true });
|
|
289
|
-
}
|
|
290
274
|
|
|
291
275
|
const prompt = `You are generating a pull request title and description for the following changes.
|
|
292
276
|
|
|
@@ -294,10 +278,10 @@ COMMITS (${baseBranch}..HEAD):
|
|
|
294
278
|
${commits}
|
|
295
279
|
|
|
296
280
|
FILES CHANGED:
|
|
297
|
-
${
|
|
281
|
+
${statResult.exitCode === 0 ? statResult.stdout.trim() : ''}
|
|
298
282
|
|
|
299
283
|
DIFF:
|
|
300
|
-
${
|
|
284
|
+
${truncateDiff(diffResult.exitCode === 0 ? diffResult.stdout : '')}
|
|
301
285
|
|
|
302
286
|
Generate a pull request title and description following these rules:
|
|
303
287
|
1. TITLE: First line must be the PR title — imperative mood, under 70 characters
|
|
@@ -310,53 +294,24 @@ Generate a pull request title and description following these rules:
|
|
|
310
294
|
|
|
311
295
|
Respond with ONLY the title and description, nothing else.`;
|
|
312
296
|
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const args = [
|
|
319
|
-
'--print',
|
|
320
|
-
'--model', 'haiku',
|
|
321
|
-
'--system-prompt', systemPrompt,
|
|
322
|
-
promptFile
|
|
323
|
-
];
|
|
324
|
-
|
|
325
|
-
const claude = spawn('claude', args, {
|
|
326
|
-
cwd: workingDir,
|
|
327
|
-
stdio: ['ignore', 'pipe', 'pipe']
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
let stdout = '';
|
|
331
|
-
let stderr = '';
|
|
297
|
+
const result = await spawnHaikuWithPrompt(
|
|
298
|
+
prompt,
|
|
299
|
+
'You are a pull request description assistant. Respond with only the PR title and description, no preamble or explanation.',
|
|
300
|
+
workingDir,
|
|
301
|
+
);
|
|
332
302
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
claude.on('close', (code: number | null) => {
|
|
337
|
-
try { unlinkSync(promptFile); } catch { /* ignore */ }
|
|
338
|
-
|
|
339
|
-
if (code !== 0 || !stdout.trim()) {
|
|
340
|
-
console.error('[WebSocketImproviseHandler] Claude PR description error:', stderr || 'No output');
|
|
341
|
-
ctx.send(ws, { type: 'gitError', tabId, data: { error: 'Failed to generate PR description' } });
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const output = stripCoauthorLines(stdout.trim());
|
|
346
|
-
const lines = output.split('\n');
|
|
347
|
-
const title = lines[0].trim();
|
|
348
|
-
const body = lines.slice(1).join('\n').trim();
|
|
349
|
-
|
|
350
|
-
ctx.send(ws, { type: 'gitPRDescription', tabId, data: { title, body } });
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
claude.on('error', (err: Error) => {
|
|
354
|
-
console.error('[WebSocketImproviseHandler] Failed to spawn Claude for PR description:', err);
|
|
303
|
+
if (result.exitCode !== 0 || !result.stdout.trim()) {
|
|
304
|
+
console.error('[WebSocketImproviseHandler] Claude PR description error:', result.stderr || 'No output');
|
|
355
305
|
ctx.send(ws, { type: 'gitError', tabId, data: { error: 'Failed to generate PR description' } });
|
|
356
|
-
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
357
308
|
|
|
358
|
-
|
|
309
|
+
const output = stripCoauthorLines(result.stdout.trim());
|
|
310
|
+
const lines = output.split('\n');
|
|
311
|
+
const title = lines[0].trim();
|
|
312
|
+
const body = lines.slice(1).join('\n').trim();
|
|
359
313
|
|
|
314
|
+
ctx.send(ws, { type: 'gitPRDescription', tabId, data: { title, body } });
|
|
360
315
|
} catch (error: unknown) {
|
|
361
316
|
ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
|
|
362
317
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
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 { GitTagEntry, WebSocketMessage, WSContext } from './types.js';
|
|
7
|
+
|
|
8
|
+
export async function handleGitListTags(ctx: HandlerContext, ws: WSContext, tabId: string, workingDir: string): Promise<void> {
|
|
9
|
+
try {
|
|
10
|
+
const result = await executeGitCommand(
|
|
11
|
+
['tag', '-l', '--sort=-creatordate', '--format=%(refname:short)|%(objectname:short)|%(creatordate:iso-strict)|%(subject)'],
|
|
12
|
+
workingDir
|
|
13
|
+
);
|
|
14
|
+
if (result.exitCode !== 0) {
|
|
15
|
+
ctx.send(ws, { type: 'gitError', tabId, data: { error: result.stderr || 'Failed to list tags' } });
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const tags: GitTagEntry[] = result.stdout.trim().split('\n')
|
|
20
|
+
.filter(line => line.trim())
|
|
21
|
+
.slice(0, 50)
|
|
22
|
+
.map(line => {
|
|
23
|
+
const parts = line.split('|');
|
|
24
|
+
return {
|
|
25
|
+
name: parts[0]?.trim() || '',
|
|
26
|
+
shortHash: parts[1]?.trim() || '',
|
|
27
|
+
date: parts[2]?.trim() || '',
|
|
28
|
+
message: parts[3]?.trim() || '',
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
ctx.send(ws, { type: 'gitTagList', tabId, data: { tags } });
|
|
33
|
+
} catch (error: unknown) {
|
|
34
|
+
sendGitError(ctx, ws, tabId, error);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function handleGitCreateTag(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string): Promise<void> {
|
|
39
|
+
try {
|
|
40
|
+
const { name, message, commit } = msg.data || {};
|
|
41
|
+
if (!name) {
|
|
42
|
+
ctx.send(ws, { type: 'gitError', tabId, data: { error: 'Tag name is required' } });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (/\s/.test(name) || name.includes('..')) {
|
|
47
|
+
ctx.send(ws, { type: 'gitError', tabId, data: { error: 'Invalid tag name: no spaces or ".." allowed' } });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const args = message
|
|
52
|
+
? ['tag', '-a', name, '-m', message, ...(commit ? [commit] : [])]
|
|
53
|
+
: ['tag', name, ...(commit ? [commit] : [])];
|
|
54
|
+
|
|
55
|
+
const result = await executeGitCommand(args, workingDir);
|
|
56
|
+
if (result.exitCode !== 0) {
|
|
57
|
+
ctx.send(ws, { type: 'gitError', tabId, data: { error: result.stderr || 'Failed to create tag' } });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const hashResult = await executeGitCommand(['rev-parse', '--short', name], workingDir);
|
|
62
|
+
ctx.send(ws, { type: 'gitTagCreated', tabId, data: { name, hash: hashResult.stdout.trim() } });
|
|
63
|
+
} catch (error: unknown) {
|
|
64
|
+
sendGitError(ctx, ws, tabId, error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function handleGitPushTag(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string): Promise<void> {
|
|
69
|
+
try {
|
|
70
|
+
const { name, all } = msg.data || {};
|
|
71
|
+
|
|
72
|
+
const args = all
|
|
73
|
+
? ['push', 'origin', '--tags']
|
|
74
|
+
: ['push', 'origin', name];
|
|
75
|
+
|
|
76
|
+
if (!all && !name) {
|
|
77
|
+
ctx.send(ws, { type: 'gitError', tabId, data: { error: 'Tag name is required' } });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const result = await executeGitCommand(args, workingDir);
|
|
82
|
+
if (result.exitCode !== 0) {
|
|
83
|
+
ctx.send(ws, { type: 'gitError', tabId, data: { error: result.stderr || 'Failed to push tag' } });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
ctx.send(ws, { type: 'gitTagPushed', tabId, data: { name: name || 'all', output: result.stderr || result.stdout } });
|
|
88
|
+
} catch (error: unknown) {
|
|
89
|
+
sendGitError(ctx, ws, tabId, error);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
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 { existsSync, mkdirSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import type { HandlerContext } from './handler-context.js';
|
|
8
|
+
import type { GitFileStatus, WSContext } from './types.js';
|
|
9
|
+
|
|
10
|
+
/** Send a gitError response to the client. */
|
|
11
|
+
export function sendGitError(ctx: HandlerContext, ws: WSContext, tabId: string, error: unknown): void {
|
|
12
|
+
ctx.send(ws, { type: 'gitError', tabId, data: { error: error instanceof Error ? error.message : String(error) } });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Detect git provider from remote URL */
|
|
16
|
+
export function detectGitProvider(remoteUrl: string): 'github' | 'gitlab' | 'unknown' {
|
|
17
|
+
if (remoteUrl.includes('github.com')) return 'github';
|
|
18
|
+
if (remoteUrl.includes('gitlab.com') || remoteUrl.includes('gitlab')) return 'gitlab';
|
|
19
|
+
return 'unknown';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Execute a git command and return stdout */
|
|
23
|
+
export function executeGitCommand(args: string[], workingDir: string): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
const git = spawn('git', args, {
|
|
26
|
+
cwd: workingDir,
|
|
27
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
let stdout = '';
|
|
31
|
+
let stderr = '';
|
|
32
|
+
|
|
33
|
+
git.stdout?.on('data', (data: Buffer) => {
|
|
34
|
+
stdout += data.toString();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
git.stderr?.on('data', (data: Buffer) => {
|
|
38
|
+
stderr += data.toString();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
git.on('close', (code: number | null) => {
|
|
42
|
+
resolve({ stdout, stderr, exitCode: code ?? 1 });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
git.on('error', (err: Error) => {
|
|
46
|
+
resolve({ stdout: '', stderr: err.message, exitCode: 1 });
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Map of simple escape sequences to their character values */
|
|
52
|
+
const ESCAPE_CHARS: Record<string, string> = {
|
|
53
|
+
'\\': '\\',
|
|
54
|
+
'"': '"',
|
|
55
|
+
'n': '\n',
|
|
56
|
+
't': '\t',
|
|
57
|
+
'r': '\r',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** Check if position i starts an octal escape sequence (\nnn) */
|
|
61
|
+
function isOctalEscape(str: string, i: number): boolean {
|
|
62
|
+
return i + 3 < str.length &&
|
|
63
|
+
/[0-7]/.test(str[i + 1]) &&
|
|
64
|
+
/[0-7]{2}/.test(str.slice(i + 2, i + 4));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Unquote a git-quoted path (C-style quoting)
|
|
69
|
+
*/
|
|
70
|
+
export function unquoteGitPath(path: string): string {
|
|
71
|
+
if (!path.startsWith('"') || !path.endsWith('"')) {
|
|
72
|
+
return path;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const inner = path.slice(1, -1);
|
|
76
|
+
let result = '';
|
|
77
|
+
let i = 0;
|
|
78
|
+
|
|
79
|
+
while (i < inner.length) {
|
|
80
|
+
if (inner[i] !== '\\' || i + 1 >= inner.length) {
|
|
81
|
+
result += inner[i];
|
|
82
|
+
i++;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const next = inner[i + 1];
|
|
87
|
+
const escaped = ESCAPE_CHARS[next];
|
|
88
|
+
|
|
89
|
+
if (escaped !== undefined) {
|
|
90
|
+
result += escaped;
|
|
91
|
+
i += 2;
|
|
92
|
+
} else if (isOctalEscape(inner, i)) {
|
|
93
|
+
result += String.fromCharCode(parseInt(inner.slice(i + 1, i + 4), 8));
|
|
94
|
+
i += 4;
|
|
95
|
+
} else {
|
|
96
|
+
result += inner[i];
|
|
97
|
+
i++;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Parse git status --porcelain output into structured format */
|
|
105
|
+
export function parseGitStatus(porcelainOutput: string): { staged: GitFileStatus[]; unstaged: GitFileStatus[]; untracked: GitFileStatus[] } {
|
|
106
|
+
const staged: GitFileStatus[] = [];
|
|
107
|
+
const unstaged: GitFileStatus[] = [];
|
|
108
|
+
const untracked: GitFileStatus[] = [];
|
|
109
|
+
|
|
110
|
+
const lines = porcelainOutput.split('\n').filter(line => line.length >= 4);
|
|
111
|
+
|
|
112
|
+
for (const line of lines) {
|
|
113
|
+
const indexStatus = line[0];
|
|
114
|
+
const workTreeStatus = line[1];
|
|
115
|
+
const rawPath = line.slice(3);
|
|
116
|
+
|
|
117
|
+
const path = unquoteGitPath(rawPath);
|
|
118
|
+
|
|
119
|
+
let filePath = path;
|
|
120
|
+
let originalPath: string | undefined;
|
|
121
|
+
if (rawPath.includes(' -> ')) {
|
|
122
|
+
const parts = rawPath.split(' -> ');
|
|
123
|
+
originalPath = unquoteGitPath(parts[0]);
|
|
124
|
+
filePath = unquoteGitPath(parts[1]);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (indexStatus === '?' && workTreeStatus === '?') {
|
|
128
|
+
untracked.push({ path: filePath, status: '?', staged: false });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (indexStatus !== ' ' && indexStatus !== '?') {
|
|
133
|
+
staged.push({ path: filePath, status: indexStatus as GitFileStatus['status'], staged: true, originalPath });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (workTreeStatus !== ' ' && workTreeStatus !== '?') {
|
|
137
|
+
unstaged.push({ path: filePath, status: workTreeStatus as GitFileStatus['status'], staged: false, originalPath });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { staged, unstaged, untracked };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Check if a binary runs successfully (exit code 0) */
|
|
145
|
+
export function spawnCheck(bin: string, args: string[]): Promise<boolean> {
|
|
146
|
+
return new Promise((resolve) => {
|
|
147
|
+
const proc = spawn(bin, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
148
|
+
proc.on('close', (code) => resolve(code === 0));
|
|
149
|
+
proc.on('error', () => resolve(false));
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Spawn a process and capture stdout/stderr */
|
|
154
|
+
export function spawnWithOutput(bin: string, args: string[], cwd: string): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
155
|
+
return new Promise((resolve) => {
|
|
156
|
+
const proc = spawn(bin, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
157
|
+
let stdout = '';
|
|
158
|
+
let stderr = '';
|
|
159
|
+
proc.stdout?.on('data', (d: Buffer) => { stdout += d.toString(); });
|
|
160
|
+
proc.stderr?.on('data', (d: Buffer) => { stderr += d.toString(); });
|
|
161
|
+
proc.on('close', (code) => resolve({ stdout, stderr, exitCode: code ?? 1 }));
|
|
162
|
+
proc.on('error', (err: Error) => resolve({ stdout: '', stderr: err.message, exitCode: 1 }));
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Truncate a diff to stay within token limits. */
|
|
167
|
+
export function truncateDiff(diff: string, maxLength = 8000): string {
|
|
168
|
+
if (diff.length <= maxLength) return diff;
|
|
169
|
+
const headSize = Math.floor(maxLength / 2);
|
|
170
|
+
const tailSize = Math.floor(maxLength * 0.44);
|
|
171
|
+
return `${diff.slice(0, headSize)}\n\n... [diff truncated] ...\n\n${diff.slice(-tailSize)}`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Spawn Claude Haiku with a prompt file and return captured output. */
|
|
175
|
+
export function spawnHaikuWithPrompt(
|
|
176
|
+
prompt: string,
|
|
177
|
+
systemPrompt: string,
|
|
178
|
+
workingDir: string,
|
|
179
|
+
timeoutMs = 30000,
|
|
180
|
+
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
181
|
+
return new Promise((resolve) => {
|
|
182
|
+
const tempDir = join(workingDir, '.mstro', 'tmp');
|
|
183
|
+
if (!existsSync(tempDir)) mkdirSync(tempDir, { recursive: true });
|
|
184
|
+
const promptFile = join(tempDir, `haiku-${Date.now()}.txt`);
|
|
185
|
+
writeFileSync(promptFile, prompt);
|
|
186
|
+
|
|
187
|
+
const args = ['--print', '--model', 'haiku', '--system-prompt', systemPrompt, promptFile];
|
|
188
|
+
const proc = spawn('claude', args, { cwd: workingDir, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
189
|
+
|
|
190
|
+
let stdout = '';
|
|
191
|
+
let stderr = '';
|
|
192
|
+
proc.stdout?.on('data', (d: Buffer) => { stdout += d.toString(); });
|
|
193
|
+
proc.stderr?.on('data', (d: Buffer) => { stderr += d.toString(); });
|
|
194
|
+
|
|
195
|
+
const timer = setTimeout(() => proc.kill(), timeoutMs);
|
|
196
|
+
|
|
197
|
+
proc.on('close', (code) => {
|
|
198
|
+
clearTimeout(timer);
|
|
199
|
+
try { unlinkSync(promptFile); } catch { /* ignore cleanup errors */ }
|
|
200
|
+
resolve({ stdout, stderr, exitCode: code ?? 1 });
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
proc.on('error', (err: Error) => {
|
|
204
|
+
clearTimeout(timer);
|
|
205
|
+
try { unlinkSync(promptFile); } catch { /* ignore cleanup errors */ }
|
|
206
|
+
resolve({ stdout: '', stderr: err.message, exitCode: 1 });
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Strip injected coauthor/attribution lines from a commit message.
|
|
213
|
+
*/
|
|
214
|
+
export function stripCoauthorLines(message: string): string {
|
|
215
|
+
const lines = message.split('\n');
|
|
216
|
+
const markers = ['co-authored', 'authored-by', 'haiku', 'noreply@anthropic.com'];
|
|
217
|
+
const result: string[] = [];
|
|
218
|
+
for (let i = 0; i < lines.length; i++) {
|
|
219
|
+
const lower = lines[i].toLowerCase();
|
|
220
|
+
if (markers.some(m => lower.includes(m))) {
|
|
221
|
+
if (result.length > 0 && result[result.length - 1].trim() === '') {
|
|
222
|
+
result.pop();
|
|
223
|
+
}
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
result.push(lines[i]);
|
|
227
|
+
}
|
|
228
|
+
if (result.length === 0) return '';
|
|
229
|
+
return result.join('\n').trimEnd();
|
|
230
|
+
}
|