commandmate 0.1.12 → 0.2.0
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/.env.example +4 -9
- package/.next/BUILD_ID +1 -1
- package/.next/app-build-manifest.json +24 -24
- package/.next/app-path-routes-manifest.json +1 -1
- package/.next/build-manifest.json +7 -7
- package/.next/cache/.tsbuildinfo +1 -1
- package/.next/cache/config.json +3 -3
- package/.next/cache/webpack/client-production/0.pack +0 -0
- package/.next/cache/webpack/client-production/1.pack +0 -0
- package/.next/cache/webpack/client-production/2.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack.old +0 -0
- package/.next/cache/webpack/edge-server-production/0.pack +0 -0
- package/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/.next/cache/webpack/server-production/0.pack +0 -0
- package/.next/cache/webpack/server-production/index.pack +0 -0
- package/.next/next-server.js.nft.json +1 -1
- package/.next/prerender-manifest.json +1 -1
- package/.next/react-loadable-manifest.json +7 -7
- package/.next/required-server-files.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +2 -2
- package/.next/server/app/api/hooks/claude-done/route.js +1 -19
- package/.next/server/app/api/hooks/claude-done/route.js.nft.json +1 -1
- package/.next/server/app/api/repositories/clone/[jobId]/route.js +1 -1
- package/.next/server/app/api/repositories/clone/[jobId]/route.js.nft.json +1 -1
- package/.next/server/app/api/repositories/clone/route.js +1 -1
- package/.next/server/app/api/repositories/clone/route.js.nft.json +1 -1
- package/.next/server/app/api/repositories/excluded/route.js +36 -0
- package/.next/server/app/api/repositories/excluded/route.js.nft.json +1 -0
- package/.next/server/app/api/repositories/excluded.body +1 -0
- package/.next/server/app/api/repositories/excluded.meta +1 -0
- package/.next/server/app/api/repositories/restore/route.js +36 -0
- package/.next/server/app/api/repositories/restore/route.js.nft.json +1 -0
- package/.next/server/app/api/repositories/route.js +36 -1
- package/.next/server/app/api/repositories/route.js.nft.json +1 -1
- package/.next/server/app/api/repositories/scan/route.js +1 -1
- package/.next/server/app/api/repositories/sync/route.js +36 -1
- package/.next/server/app/api/slash-commands/route.js +1 -1
- package/.next/server/app/api/slash-commands.body +1 -1
- package/.next/server/app/api/worktrees/[id]/auto-yes/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/auto-yes/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/current-output/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/current-output/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/interrupt/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/interrupt/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/kill-session/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/kill-session/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/logs/[filename]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/logs/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/prompt-response/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/prompt-response/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/respond/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/respond/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/search/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/send/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/send/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/slash-commands/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/start-polling/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/start-polling/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/route.js +1 -1
- package/.next/server/app/api/worktrees/route.js.nft.json +1 -1
- package/.next/server/app/index.html +2 -2
- package/.next/server/app/index.rsc +3 -3
- package/.next/server/app/page.js +7 -7
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/proxy/[...path]/route.js +2 -2
- package/.next/server/app/worktrees/[id]/files/[...path]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/page.js +4 -4
- package/.next/server/app/worktrees/[id]/page.js.nft.json +1 -1
- package/.next/server/app/worktrees/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/simple-terminal/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/terminal/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +10 -8
- package/.next/server/chunks/5488.js +36 -0
- package/.next/server/chunks/6550.js +1 -1
- package/.next/server/chunks/7425.js +53 -50
- package/.next/server/chunks/7536.js +1 -0
- package/.next/server/chunks/8174.js +23 -0
- package/.next/server/chunks/9367.js +19 -0
- package/.next/server/functions-config-manifest.json +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-manifest.json +2 -28
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/4327.740cc7fe2d0b5049.js +60 -0
- package/.next/static/chunks/4343-ebe884a2a80eb033.js +1 -0
- package/.next/static/chunks/6568-38a33aa67d82e12b.js +1 -0
- package/.next/static/chunks/816-c254f4e2406e696a.js +1 -0
- package/.next/static/chunks/app/layout-4804cfba519283cf.js +1 -0
- package/.next/static/chunks/app/page-3926224c4cdf315b.js +1 -0
- package/.next/static/chunks/app/worktrees/[id]/page-d64624eb67af57c0.js +1 -0
- package/.next/static/chunks/main-b6d727aa9248d4f2.js +1 -0
- package/.next/static/chunks/{webpack-3fc79fab9bb738d7.js → webpack-4f85dcef6279c6ee.js} +1 -1
- package/.next/static/css/28be35e4727ae7ef.css +3 -0
- package/.next/trace +5 -5
- package/.next/types/app/api/repositories/excluded/route.ts +343 -0
- package/.next/types/app/api/repositories/restore/route.ts +343 -0
- package/README.md +2 -2
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +2 -13
- package/dist/cli/commands/start.d.ts.map +1 -1
- package/dist/cli/commands/start.js +3 -7
- package/dist/cli/config/security-messages.d.ts +11 -0
- package/dist/cli/config/security-messages.d.ts.map +1 -0
- package/dist/cli/config/security-messages.js +29 -0
- package/dist/cli/types/index.d.ts +0 -1
- package/dist/cli/types/index.d.ts.map +1 -1
- package/dist/cli/utils/daemon.d.ts.map +1 -1
- package/dist/cli/utils/daemon.js +3 -7
- package/dist/cli/utils/env-setup.d.ts +0 -4
- package/dist/cli/utils/env-setup.d.ts.map +1 -1
- package/dist/cli/utils/env-setup.js +0 -14
- package/dist/cli/utils/security-logger.d.ts.map +1 -1
- package/dist/cli/utils/security-logger.js +1 -2
- package/dist/server/src/lib/auto-yes-manager.js +13 -5
- package/dist/server/src/lib/claude-poller.js +337 -0
- package/dist/server/src/lib/cli-patterns.js +9 -2
- package/dist/server/src/lib/cli-tools/base.js +7 -1
- package/dist/server/src/lib/cli-tools/codex.js +14 -2
- package/dist/server/src/lib/cli-tools/manager.js +27 -0
- package/dist/server/src/lib/cli-tools/types.js +7 -0
- package/dist/server/src/lib/cli-tools/validation.js +41 -0
- package/dist/server/src/lib/db.js +23 -0
- package/dist/server/src/lib/env.js +0 -17
- package/dist/server/src/lib/logger.js +0 -4
- package/dist/server/src/lib/prompt-detector.js +129 -31
- package/dist/server/src/lib/ws-server.js +12 -1
- package/dist/server/src/types/sidebar.js +16 -31
- package/dist/server/src/types/slash-commands.js +2 -0
- package/package.json +1 -1
- package/.next/server/chunks/1318.js +0 -29
- package/.next/server/chunks/2597.js +0 -1
- package/.next/server/chunks/2648.js +0 -1
- package/.next/server/chunks/9703.js +0 -31
- package/.next/server/chunks/9723.js +0 -19
- package/.next/server/edge-runtime-webpack.js +0 -2
- package/.next/server/edge-runtime-webpack.js.map +0 -1
- package/.next/server/src/middleware.js +0 -14
- package/.next/server/src/middleware.js.map +0 -1
- package/.next/static/chunks/2853-d11a80b03c9a1640.js +0 -1
- package/.next/static/chunks/4327.3b84aa049900fdeb.js +0 -60
- package/.next/static/chunks/816-7e340dad784be28c.js +0 -1
- package/.next/static/chunks/9365-733d8c05712d2888.js +0 -1
- package/.next/static/chunks/app/layout-37e55f11dcc8b1bf.js +0 -1
- package/.next/static/chunks/app/page-fe35d61f14b90a51.js +0 -1
- package/.next/static/chunks/app/worktrees/[id]/page-58fcf2e63c056743.js +0 -1
- package/.next/static/chunks/main-a960f4a5e1a2f598.js +0 -1
- package/.next/static/css/376b339640084689.css +0 -3
- /package/.next/static/{564GHwluX5xIv9qpqLJV2 → bdUePCj-b9Gv5okYGp49O}/_buildManifest.js +0 -0
- /package/.next/static/{564GHwluX5xIv9qpqLJV2 → bdUePCj-b9Gv5okYGp49O}/_ssgManifest.js +0 -0
|
@@ -28,8 +28,9 @@ exports.CLAUDE_THINKING_PATTERN = new RegExp(`[${exports.CLAUDE_SPINNER_CHARS.jo
|
|
|
28
28
|
/**
|
|
29
29
|
* Codex thinking pattern
|
|
30
30
|
* Matches activity indicators like "• Planning", "• Searching", etc.
|
|
31
|
+
* T1.1: Extended to include "Ran" and "Deciding"
|
|
31
32
|
*/
|
|
32
|
-
exports.CODEX_THINKING_PATTERN = /•\s*(Planning|Searching|Exploring|Running|Thinking|Working|Reading|Writing|Analyzing)/m;
|
|
33
|
+
exports.CODEX_THINKING_PATTERN = /•\s*(Planning|Searching|Exploring|Running|Thinking|Working|Reading|Writing|Analyzing|Ran|Deciding)/m;
|
|
33
34
|
/**
|
|
34
35
|
* Claude prompt pattern (waiting for input)
|
|
35
36
|
* Supports both legacy '>' and new '❯' (U+276F) prompt characters
|
|
@@ -46,8 +47,9 @@ exports.CLAUDE_PROMPT_PATTERN = /^[>❯](\s*$|\s+\S)/m;
|
|
|
46
47
|
exports.CLAUDE_SEPARATOR_PATTERN = /^─{10,}$/m;
|
|
47
48
|
/**
|
|
48
49
|
* Codex prompt pattern
|
|
50
|
+
* T1.2: Improved to detect empty prompts as well
|
|
49
51
|
*/
|
|
50
|
-
exports.CODEX_PROMPT_PATTERN = /^›\s
|
|
52
|
+
exports.CODEX_PROMPT_PATTERN = /^›\s*/m;
|
|
51
53
|
/**
|
|
52
54
|
* Codex separator pattern
|
|
53
55
|
*/
|
|
@@ -114,6 +116,11 @@ function getCliToolPatterns(cliToolId) {
|
|
|
114
116
|
/^\s*for shortcuts$/, // Shortcuts hint
|
|
115
117
|
/╭─+╮/, // Box drawing (top)
|
|
116
118
|
/╰─+╯/, // Box drawing (bottom)
|
|
119
|
+
// T1.3: Additional skip patterns for Codex
|
|
120
|
+
/•\s*Ran\s+/, // Command execution lines
|
|
121
|
+
/^\s*└/, // Tree output (completion indicator)
|
|
122
|
+
/^\s*│/, // Continuation lines
|
|
123
|
+
/\(.*esc to interrupt\)/, // Interrupt hint
|
|
117
124
|
],
|
|
118
125
|
};
|
|
119
126
|
case 'gemini':
|
|
@@ -7,6 +7,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
7
7
|
exports.BaseCLITool = void 0;
|
|
8
8
|
const child_process_1 = require("child_process");
|
|
9
9
|
const util_1 = require("util");
|
|
10
|
+
const validation_1 = require("./validation");
|
|
10
11
|
const tmux_1 = require("../tmux");
|
|
11
12
|
const execAsync = (0, util_1.promisify)(child_process_1.exec);
|
|
12
13
|
/**
|
|
@@ -31,11 +32,16 @@ class BaseCLITool {
|
|
|
31
32
|
* Generate session name for a worktree
|
|
32
33
|
* Format: mcbd-{cli_tool_id}-{worktree_id}
|
|
33
34
|
*
|
|
35
|
+
* T2.3: Added validation to prevent command injection (MF4-001)
|
|
36
|
+
*
|
|
34
37
|
* @param worktreeId - Worktree ID
|
|
35
38
|
* @returns Session name
|
|
39
|
+
* @throws Error if the resulting session name is invalid
|
|
36
40
|
*/
|
|
37
41
|
getSessionName(worktreeId) {
|
|
38
|
-
|
|
42
|
+
const sessionName = `mcbd-${this.id}-${worktreeId}`;
|
|
43
|
+
(0, validation_1.validateSessionName)(sessionName);
|
|
44
|
+
return sessionName;
|
|
39
45
|
}
|
|
40
46
|
/**
|
|
41
47
|
* Interrupt processing by sending Escape key
|
|
@@ -10,6 +10,12 @@ const tmux_1 = require("../tmux");
|
|
|
10
10
|
const child_process_1 = require("child_process");
|
|
11
11
|
const util_1 = require("util");
|
|
12
12
|
const execAsync = (0, util_1.promisify)(child_process_1.exec);
|
|
13
|
+
/**
|
|
14
|
+
* Codex initialization timing constants
|
|
15
|
+
* T2.6: Extracted as constants for maintainability
|
|
16
|
+
*/
|
|
17
|
+
const CODEX_INIT_WAIT_MS = 3000; // Wait for Codex to start
|
|
18
|
+
const CODEX_MODEL_SELECT_WAIT_MS = 200; // Wait between model selection keystrokes
|
|
13
19
|
/**
|
|
14
20
|
* Codex CLI tool implementation
|
|
15
21
|
* Manages Codex sessions using tmux
|
|
@@ -59,11 +65,17 @@ class CodexTool extends base_1.BaseCLITool {
|
|
|
59
65
|
// Start Codex CLI in interactive mode
|
|
60
66
|
await (0, tmux_1.sendKeys)(sessionName, 'codex', true);
|
|
61
67
|
// Wait for Codex to initialize (and potentially show update notification)
|
|
62
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
68
|
+
await new Promise((resolve) => setTimeout(resolve, CODEX_INIT_WAIT_MS));
|
|
63
69
|
// Auto-skip update notification if present (select option 2: Skip)
|
|
64
70
|
await (0, tmux_1.sendKeys)(sessionName, '2', true);
|
|
65
71
|
// Wait a moment for the selection to process
|
|
66
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
72
|
+
await new Promise((resolve) => setTimeout(resolve, CODEX_MODEL_SELECT_WAIT_MS));
|
|
73
|
+
// T2.6: Skip model selection dialog by sending Down arrow + Enter
|
|
74
|
+
// This selects the default model and proceeds to the prompt
|
|
75
|
+
await execAsync(`tmux send-keys -t "${sessionName}" Down`);
|
|
76
|
+
await new Promise((resolve) => setTimeout(resolve, CODEX_MODEL_SELECT_WAIT_MS));
|
|
77
|
+
await execAsync(`tmux send-keys -t "${sessionName}" Enter`);
|
|
78
|
+
await new Promise((resolve) => setTimeout(resolve, CODEX_MODEL_SELECT_WAIT_MS));
|
|
67
79
|
console.log(`✓ Started Codex session: ${sessionName}`);
|
|
68
80
|
}
|
|
69
81
|
catch (error) {
|
|
@@ -8,6 +8,8 @@ exports.CLIToolManager = void 0;
|
|
|
8
8
|
const claude_1 = require("./claude");
|
|
9
9
|
const codex_1 = require("./codex");
|
|
10
10
|
const gemini_1 = require("./gemini");
|
|
11
|
+
const response_poller_1 = require("../response-poller");
|
|
12
|
+
const claude_poller_1 = require("../claude-poller");
|
|
11
13
|
/**
|
|
12
14
|
* CLI Tool Manager (Singleton)
|
|
13
15
|
* Provides centralized access to all CLI tools
|
|
@@ -139,5 +141,30 @@ class CLIToolManager {
|
|
|
139
141
|
const allInfo = await this.getAllToolsInfo();
|
|
140
142
|
return allInfo.filter(info => info.installed);
|
|
141
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Stop pollers for a specific worktree and CLI tool
|
|
146
|
+
* T2.4: Abstraction for poller stopping (MF1-001 DIP compliance)
|
|
147
|
+
*
|
|
148
|
+
* This method abstracts the poller stopping logic so API layer
|
|
149
|
+
* doesn't need to know about specific poller implementations.
|
|
150
|
+
*
|
|
151
|
+
* @param worktreeId - Worktree ID
|
|
152
|
+
* @param cliToolId - CLI tool ID
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* ```typescript
|
|
156
|
+
* const manager = CLIToolManager.getInstance();
|
|
157
|
+
* manager.stopPollers('my-worktree', 'claude');
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
stopPollers(worktreeId, cliToolId) {
|
|
161
|
+
// Stop response-poller for all tools
|
|
162
|
+
(0, response_poller_1.stopPolling)(worktreeId, cliToolId);
|
|
163
|
+
// claude-poller is Claude-specific
|
|
164
|
+
if (cliToolId === 'claude') {
|
|
165
|
+
(0, claude_poller_1.stopPolling)(worktreeId);
|
|
166
|
+
}
|
|
167
|
+
// Future: Add other tool-specific pollers here if needed
|
|
168
|
+
}
|
|
142
169
|
}
|
|
143
170
|
exports.CLIToolManager = CLIToolManager;
|
|
@@ -3,3 +3,10 @@
|
|
|
3
3
|
* Type definitions and interfaces for CLI tools
|
|
4
4
|
*/
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.CLI_TOOL_IDS = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* CLI Tool IDs constant array
|
|
9
|
+
* T2.1: Single source of truth for CLI tool IDs
|
|
10
|
+
* CLIToolType is derived from this constant (DRY principle)
|
|
11
|
+
*/
|
|
12
|
+
exports.CLI_TOOL_IDS = ['claude', 'codex', 'gemini'];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Session name validation module
|
|
4
|
+
* Issue #4: T2.2 - Security validation for session names (MF4-001)
|
|
5
|
+
*
|
|
6
|
+
* This module provides validation functions to prevent command injection
|
|
7
|
+
* attacks through session names used in tmux commands.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.SESSION_NAME_PATTERN = void 0;
|
|
11
|
+
exports.validateSessionName = validateSessionName;
|
|
12
|
+
/**
|
|
13
|
+
* Session name pattern
|
|
14
|
+
* Only allows alphanumeric characters, underscores, and hyphens
|
|
15
|
+
* This prevents command injection through shell special characters
|
|
16
|
+
*
|
|
17
|
+
* Pattern breakdown:
|
|
18
|
+
* - ^ : Start of string
|
|
19
|
+
* - [a-zA-Z0-9_-] : Allowed characters (alphanumeric, underscore, hyphen)
|
|
20
|
+
* - + : One or more characters (empty strings not allowed)
|
|
21
|
+
* - $ : End of string
|
|
22
|
+
*/
|
|
23
|
+
exports.SESSION_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
24
|
+
/**
|
|
25
|
+
* Validate session name format
|
|
26
|
+
* Throws an error if the session name contains invalid characters
|
|
27
|
+
*
|
|
28
|
+
* @param sessionName - Session name to validate
|
|
29
|
+
* @throws Error if session name is invalid
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* validateSessionName('mcbd-claude-test'); // OK
|
|
34
|
+
* validateSessionName('test;rm -rf'); // Throws Error
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
function validateSessionName(sessionName) {
|
|
38
|
+
if (!exports.SESSION_NAME_PATTERN.test(sessionName)) {
|
|
39
|
+
throw new Error(`Invalid session name format: ${sessionName}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -19,6 +19,7 @@ exports.getMessages = getMessages;
|
|
|
19
19
|
exports.getLastUserMessage = getLastUserMessage;
|
|
20
20
|
exports.getLastMessage = getLastMessage;
|
|
21
21
|
exports.deleteAllMessages = deleteAllMessages;
|
|
22
|
+
exports.deleteMessagesByCliTool = deleteMessagesByCliTool;
|
|
22
23
|
exports.getSessionState = getSessionState;
|
|
23
24
|
exports.updateSessionState = updateSessionState;
|
|
24
25
|
exports.setInProgressMessageId = setInProgressMessageId;
|
|
@@ -458,6 +459,28 @@ function deleteAllMessages(db, worktreeId) {
|
|
|
458
459
|
stmt.run(worktreeId);
|
|
459
460
|
console.log(`[deleteAllMessages] Deleted all messages for worktree: ${worktreeId}`);
|
|
460
461
|
}
|
|
462
|
+
/**
|
|
463
|
+
* Delete messages for a specific CLI tool in a worktree
|
|
464
|
+
* Issue #4: T4.2 - Individual CLI tool session termination (MF3-001)
|
|
465
|
+
*
|
|
466
|
+
* Used when killing only a specific CLI tool's session to clear its message history
|
|
467
|
+
* while preserving messages from other CLI tools.
|
|
468
|
+
* Note: Log files are preserved for historical reference
|
|
469
|
+
*
|
|
470
|
+
* @param db - Database instance
|
|
471
|
+
* @param worktreeId - Worktree ID
|
|
472
|
+
* @param cliTool - CLI tool ID to delete messages for
|
|
473
|
+
* @returns Number of deleted messages
|
|
474
|
+
*/
|
|
475
|
+
function deleteMessagesByCliTool(db, worktreeId, cliTool) {
|
|
476
|
+
const stmt = db.prepare(`
|
|
477
|
+
DELETE FROM chat_messages
|
|
478
|
+
WHERE worktree_id = ? AND cli_tool_id = ?
|
|
479
|
+
`);
|
|
480
|
+
const result = stmt.run(worktreeId, cliTool);
|
|
481
|
+
console.log(`[deleteMessagesByCliTool] Deleted ${result.changes} messages for worktree: ${worktreeId}, cliTool: ${cliTool}`);
|
|
482
|
+
return result.changes;
|
|
483
|
+
}
|
|
461
484
|
/**
|
|
462
485
|
* Get session state for a worktree
|
|
463
486
|
*/
|
|
@@ -22,7 +22,6 @@ exports.getDatabasePathWithDeprecationWarning = getDatabasePathWithDeprecationWa
|
|
|
22
22
|
exports.getLogConfig = getLogConfig;
|
|
23
23
|
exports.getEnv = getEnv;
|
|
24
24
|
exports.validateEnv = validateEnv;
|
|
25
|
-
exports.isAuthRequired = isAuthRequired;
|
|
26
25
|
const path_1 = __importDefault(require("path"));
|
|
27
26
|
const db_path_resolver_1 = require("./db-path-resolver");
|
|
28
27
|
// ============================================================
|
|
@@ -37,7 +36,6 @@ exports.ENV_MAPPING = {
|
|
|
37
36
|
CM_ROOT_DIR: 'MCBD_ROOT_DIR',
|
|
38
37
|
CM_PORT: 'MCBD_PORT',
|
|
39
38
|
CM_BIND: 'MCBD_BIND',
|
|
40
|
-
CM_AUTH_TOKEN: 'MCBD_AUTH_TOKEN',
|
|
41
39
|
CM_LOG_LEVEL: 'MCBD_LOG_LEVEL',
|
|
42
40
|
CM_LOG_FORMAT: 'MCBD_LOG_FORMAT',
|
|
43
41
|
CM_LOG_DIR: 'MCBD_LOG_DIR',
|
|
@@ -160,7 +158,6 @@ function getEnv() {
|
|
|
160
158
|
const rootDir = getEnvByKey('CM_ROOT_DIR') || process.cwd();
|
|
161
159
|
const port = parseInt(getEnvByKey('CM_PORT') || '3000', 10);
|
|
162
160
|
const bind = getEnvByKey('CM_BIND') || '127.0.0.1';
|
|
163
|
-
const authToken = getEnvByKey('CM_AUTH_TOKEN');
|
|
164
161
|
// Issue #135: DB path resolution with proper fallback chain
|
|
165
162
|
// Priority: CM_DB_PATH > DATABASE_PATH (deprecated) > getDefaultDbPath()
|
|
166
163
|
const databasePath = getEnvByKey('CM_DB_PATH')
|
|
@@ -176,10 +173,6 @@ function getEnv() {
|
|
|
176
173
|
if (bind !== '127.0.0.1' && bind !== '0.0.0.0' && bind !== 'localhost') {
|
|
177
174
|
throw new Error(`Invalid CM_BIND: ${bind}. Must be '127.0.0.1', '0.0.0.0', or 'localhost'.`);
|
|
178
175
|
}
|
|
179
|
-
// Require auth token for public binding
|
|
180
|
-
if (bind === '0.0.0.0' && !authToken) {
|
|
181
|
-
throw new Error('CM_AUTH_TOKEN (or MCBD_AUTH_TOKEN) is required when CM_BIND=0.0.0.0');
|
|
182
|
-
}
|
|
183
176
|
// Issue #135: Validate DB path for security (SEC-001)
|
|
184
177
|
let validatedDbPath;
|
|
185
178
|
try {
|
|
@@ -195,7 +188,6 @@ function getEnv() {
|
|
|
195
188
|
CM_ROOT_DIR: path_1.default.resolve(rootDir),
|
|
196
189
|
CM_PORT: port,
|
|
197
190
|
CM_BIND: bind,
|
|
198
|
-
CM_AUTH_TOKEN: authToken,
|
|
199
191
|
CM_DB_PATH: validatedDbPath,
|
|
200
192
|
};
|
|
201
193
|
}
|
|
@@ -217,12 +209,3 @@ function validateEnv() {
|
|
|
217
209
|
return { valid: false, errors };
|
|
218
210
|
}
|
|
219
211
|
}
|
|
220
|
-
/**
|
|
221
|
-
* Check if authentication is required based on bind address
|
|
222
|
-
*
|
|
223
|
-
* @returns True if authentication is required
|
|
224
|
-
*/
|
|
225
|
-
function isAuthRequired() {
|
|
226
|
-
const env = getEnv();
|
|
227
|
-
return env.CM_BIND === '0.0.0.0';
|
|
228
|
-
}
|
|
@@ -35,10 +35,6 @@ const SENSITIVE_PATTERNS = [
|
|
|
35
35
|
{ pattern: /(password|passwd|pwd)[=:]\s*\S+/gi, replacement: '$1=[REDACTED]' },
|
|
36
36
|
// Token/secret related
|
|
37
37
|
{ pattern: /(token|secret|api_key|apikey|auth)[=:]\s*\S+/gi, replacement: '$1=[REDACTED]' },
|
|
38
|
-
// CM_AUTH_TOKEN (new name - Issue #76)
|
|
39
|
-
{ pattern: /CM_AUTH_TOKEN=\S+/gi, replacement: 'CM_AUTH_TOKEN=[REDACTED]' },
|
|
40
|
-
// MCBD_AUTH_TOKEN (legacy name)
|
|
41
|
-
{ pattern: /MCBD_AUTH_TOKEN=\S+/gi, replacement: 'MCBD_AUTH_TOKEN=[REDACTED]' },
|
|
42
38
|
// Authorization header
|
|
43
39
|
{ pattern: /Authorization:\s*\S+/gi, replacement: 'Authorization: [REDACTED]' },
|
|
44
40
|
// SSH key
|
|
@@ -147,14 +147,84 @@ const TEXT_INPUT_PATTERNS = [
|
|
|
147
147
|
/custom/i,
|
|
148
148
|
/differently/i,
|
|
149
149
|
];
|
|
150
|
+
/**
|
|
151
|
+
* Pattern for ❯ (U+276F) indicator lines used by Claude CLI to mark the default selection.
|
|
152
|
+
* Used in Pass 1 (existence check) and Pass 2 (option collection) of the 2-pass detection.
|
|
153
|
+
* Anchored at both ends -- ReDoS safe (S4-001).
|
|
154
|
+
*/
|
|
155
|
+
const DEFAULT_OPTION_PATTERN = /^\s*\u276F\s*(\d+)\.\s*(.+)$/;
|
|
156
|
+
/**
|
|
157
|
+
* Pattern for normal option lines (no ❯ indicator, just leading whitespace + number).
|
|
158
|
+
* Only applied in Pass 2 when ❯ indicator existence is confirmed by Pass 1.
|
|
159
|
+
* Anchored at both ends -- ReDoS safe (S4-001).
|
|
160
|
+
*/
|
|
161
|
+
const NORMAL_OPTION_PATTERN = /^\s*(\d+)\.\s*(.+)$/;
|
|
162
|
+
/**
|
|
163
|
+
* Defensive check: protection against future unknown false positive patterns.
|
|
164
|
+
* Note: The actual false positive pattern in Issue #161 ("1. Create file\n2. Run tests")
|
|
165
|
+
* IS consecutive from 1, so this validation alone does not prevent it.
|
|
166
|
+
* The primary defense layers are: Layer 1 (thinking check in caller) + Layer 2 (2-pass
|
|
167
|
+
* cursor detection). This function provides Layer 3 defense against future unknown
|
|
168
|
+
* patterns with scattered/non-consecutive numbering.
|
|
169
|
+
*
|
|
170
|
+
* [S3-010] This validation assumes Claude CLI always uses consecutive numbering
|
|
171
|
+
* starting from 1. If in the future Claude CLI is observed to filter choices and
|
|
172
|
+
* output non-consecutive numbers (e.g., 1, 2, 4), consider relaxing this validation
|
|
173
|
+
* (e.g., only check starts-from-1, remove consecutive requirement).
|
|
174
|
+
*/
|
|
175
|
+
function isConsecutiveFromOne(numbers) {
|
|
176
|
+
if (numbers.length === 0)
|
|
177
|
+
return false;
|
|
178
|
+
if (numbers[0] !== 1)
|
|
179
|
+
return false;
|
|
180
|
+
for (let i = 1; i < numbers.length; i++) {
|
|
181
|
+
if (numbers[i] !== numbers[i - 1] + 1)
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Continuation line detection for multiline option text wrapping.
|
|
188
|
+
* Detects lines that are part of a previous option's text, wrapped due to terminal width.
|
|
189
|
+
*
|
|
190
|
+
* Called within detectMultipleChoicePrompt() Pass 2 reverse scan, only when
|
|
191
|
+
* options.length > 0 (at least one option already detected):
|
|
192
|
+
* const rawLine = lines[i]; // Original line with indentation preserved
|
|
193
|
+
* const line = lines[i].trim(); // Trimmed line
|
|
194
|
+
* if (options.length > 0 && line && !line.match(/^[-─]+$/)) {
|
|
195
|
+
* if (isContinuationLine(rawLine, line)) { continue; }
|
|
196
|
+
* }
|
|
197
|
+
*
|
|
198
|
+
* Each condition's responsibility:
|
|
199
|
+
* - hasLeadingSpaces: Indented non-option line (label text wrapping with indentation)
|
|
200
|
+
* - isShortFragment: Short fragment (< 5 chars, e.g., filename tail)
|
|
201
|
+
* - isPathContinuation: Path string continuation (Issue #181)
|
|
202
|
+
*
|
|
203
|
+
* @param rawLine - Original line with indentation preserved (lines[i])
|
|
204
|
+
* @param line - Trimmed line (lines[i].trim())
|
|
205
|
+
* @returns true if the line should be treated as a continuation of a previous option
|
|
206
|
+
*/
|
|
207
|
+
function isContinuationLine(rawLine, line) {
|
|
208
|
+
// Indented non-option line
|
|
209
|
+
const hasLeadingSpaces = rawLine.match(/^\s{2,}[^\d]/) && !rawLine.match(/^\s*\d+\./);
|
|
210
|
+
// Short fragment (< 5 chars, excluding question-ending lines)
|
|
211
|
+
const isShortFragment = line.length < 5 && !line.endsWith('?');
|
|
212
|
+
// Path string continuation: lines starting with / or ~, or alphanumeric-only fragments (2+ chars)
|
|
213
|
+
const isPathContinuation = /^[\/~]/.test(line) || (line.length >= 2 && /^[a-zA-Z0-9_-]+$/.test(line));
|
|
214
|
+
return !!(hasLeadingSpaces) || isShortFragment || isPathContinuation;
|
|
215
|
+
}
|
|
150
216
|
/**
|
|
151
217
|
* Detect multiple choice prompts (numbered list with ❯ indicator)
|
|
152
218
|
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
219
|
+
* Uses a 2-pass detection approach (Issue #161):
|
|
220
|
+
* - Pass 1: Scan 50-line window for ❯ indicator lines (defaultOptionPattern).
|
|
221
|
+
* If no ❯ lines found, immediately return isPrompt: false.
|
|
222
|
+
* - Pass 2: Only if ❯ was found, re-scan collecting options using both
|
|
223
|
+
* defaultOptionPattern (isDefault=true) and normalOptionPattern (isDefault=false).
|
|
224
|
+
*
|
|
225
|
+
* This prevents normal numbered lists from being accumulated in the options array.
|
|
156
226
|
*
|
|
157
|
-
* Example:
|
|
227
|
+
* Example of valid prompt:
|
|
158
228
|
* Do you want to proceed?
|
|
159
229
|
* ❯ 1. Yes
|
|
160
230
|
* 2. No
|
|
@@ -165,36 +235,56 @@ const TEXT_INPUT_PATTERNS = [
|
|
|
165
235
|
*/
|
|
166
236
|
function detectMultipleChoicePrompt(output) {
|
|
167
237
|
const lines = output.split('\n');
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
238
|
+
// Calculate scan window: last 50 lines
|
|
239
|
+
const scanStart = Math.max(0, lines.length - 50);
|
|
240
|
+
// ==========================================================================
|
|
241
|
+
// Pass 1: Check for ❯ indicator existence in scan window
|
|
242
|
+
// If no ❯ lines found, there is no multiple_choice prompt.
|
|
243
|
+
// ==========================================================================
|
|
244
|
+
let hasDefaultLine = false;
|
|
245
|
+
for (let i = scanStart; i < lines.length; i++) {
|
|
246
|
+
const line = lines[i].trim();
|
|
247
|
+
if (DEFAULT_OPTION_PATTERN.test(line)) {
|
|
248
|
+
hasDefaultLine = true;
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (!hasDefaultLine) {
|
|
253
|
+
return {
|
|
254
|
+
isPrompt: false,
|
|
255
|
+
cleanContent: output.trim(),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
// ==========================================================================
|
|
259
|
+
// Pass 2: Collect options (only executed when ❯ was found in Pass 1)
|
|
260
|
+
// Scan from end to find options, using both patterns.
|
|
261
|
+
// ==========================================================================
|
|
171
262
|
const options = [];
|
|
172
263
|
let questionEndIndex = -1;
|
|
173
|
-
let
|
|
174
|
-
// Scan from the end to find options
|
|
175
|
-
// Increased from 20 to 50 to handle multi-line wrapped options
|
|
176
|
-
for (let i = lines.length - 1; i >= 0 && i >= lines.length - 50; i--) {
|
|
264
|
+
for (let i = lines.length - 1; i >= scanStart; i--) {
|
|
177
265
|
const line = lines[i].trim();
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
if (
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
options.unshift({ number, label, isDefault: hasDefault });
|
|
186
|
-
if (firstOptionIndex === -1) {
|
|
187
|
-
firstOptionIndex = i;
|
|
188
|
-
}
|
|
266
|
+
// Try DEFAULT_OPTION_PATTERN first (❯ indicator)
|
|
267
|
+
const defaultMatch = line.match(DEFAULT_OPTION_PATTERN);
|
|
268
|
+
if (defaultMatch) {
|
|
269
|
+
const number = parseInt(defaultMatch[1], 10);
|
|
270
|
+
const label = defaultMatch[2].trim();
|
|
271
|
+
options.unshift({ number, label, isDefault: true });
|
|
272
|
+
continue;
|
|
189
273
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
274
|
+
// Try NORMAL_OPTION_PATTERN (no ❯ indicator)
|
|
275
|
+
const normalMatch = line.match(NORMAL_OPTION_PATTERN);
|
|
276
|
+
if (normalMatch) {
|
|
277
|
+
const number = parseInt(normalMatch[1], 10);
|
|
278
|
+
const label = normalMatch[2].trim();
|
|
279
|
+
options.unshift({ number, label, isDefault: false });
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
// Non-option line handling
|
|
283
|
+
if (options.length > 0 && line && !line.match(/^[-─]+$/)) {
|
|
284
|
+
// Check if this is a continuation line (indented line between options,
|
|
285
|
+
// or path/filename fragments from terminal width wrapping - Issue #181)
|
|
286
|
+
const rawLine = lines[i]; // Original line with indentation preserved
|
|
287
|
+
if (isContinuationLine(rawLine, line)) {
|
|
198
288
|
// Skip continuation lines and continue scanning for more options
|
|
199
289
|
continue;
|
|
200
290
|
}
|
|
@@ -203,7 +293,15 @@ function detectMultipleChoicePrompt(output) {
|
|
|
203
293
|
break;
|
|
204
294
|
}
|
|
205
295
|
}
|
|
206
|
-
//
|
|
296
|
+
// Layer 3: Consecutive number validation (defensive measure)
|
|
297
|
+
const optionNumbers = options.map(opt => opt.number);
|
|
298
|
+
if (!isConsecutiveFromOne(optionNumbers)) {
|
|
299
|
+
return {
|
|
300
|
+
isPrompt: false,
|
|
301
|
+
cleanContent: output.trim(),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
// Layer 4: Must have at least 2 options AND at least one with ❯ indicator
|
|
207
305
|
const hasDefaultIndicator = options.some(opt => opt.isDefault);
|
|
208
306
|
if (options.length < 2 || !hasDefaultIndicator) {
|
|
209
307
|
return {
|
|
@@ -27,7 +27,18 @@ const rooms = new Map();
|
|
|
27
27
|
* ```
|
|
28
28
|
*/
|
|
29
29
|
function setupWebSocket(server) {
|
|
30
|
-
wss = new ws_1.WebSocketServer({
|
|
30
|
+
wss = new ws_1.WebSocketServer({ noServer: true });
|
|
31
|
+
// Handle upgrade requests - only accept app WebSocket connections, not Next.js HMR
|
|
32
|
+
server.on('upgrade', (request, socket, head) => {
|
|
33
|
+
const pathname = request.url || '/';
|
|
34
|
+
// Let Next.js handle its own HMR WebSocket connections
|
|
35
|
+
if (pathname.startsWith('/_next/')) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
39
|
+
wss.emit('connection', ws, request);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
31
42
|
// Handle WebSocket server errors (e.g., invalid frames from clients)
|
|
32
43
|
wss.on('error', (error) => {
|
|
33
44
|
console.error('[WS Server] Error:', error.message);
|
|
@@ -5,43 +5,22 @@
|
|
|
5
5
|
* Types for sidebar components and branch status display
|
|
6
6
|
*/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.deriveCliStatus = deriveCliStatus;
|
|
8
9
|
exports.calculateHasUnread = calculateHasUnread;
|
|
9
10
|
exports.toBranchItem = toBranchItem;
|
|
10
11
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* Status priority:
|
|
14
|
-
* - waiting: Claude asked a yes/no prompt, waiting for user's answer (green dot)
|
|
15
|
-
* - running: Claude is actively processing user's request (spinner)
|
|
16
|
-
* - ready: Session running, waiting for user's new message (green dot)
|
|
17
|
-
* - idle: Session not running (gray dot)
|
|
12
|
+
* Derive BranchStatus from per-CLI tool session status flags.
|
|
13
|
+
* Shared by sidebar (toBranchItem) and WorktreeDetailRefactored tab dots.
|
|
18
14
|
*/
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (
|
|
23
|
-
if (claudeStatus.isWaitingForResponse) {
|
|
24
|
-
return 'waiting';
|
|
25
|
-
}
|
|
26
|
-
if (claudeStatus.isProcessing) {
|
|
27
|
-
return 'running';
|
|
28
|
-
}
|
|
29
|
-
// Session running but not processing = ready (waiting for user to type new message)
|
|
30
|
-
if (claudeStatus.isRunning) {
|
|
31
|
-
return 'ready';
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
// Fall back to legacy status fields
|
|
35
|
-
if (worktree.isWaitingForResponse) {
|
|
15
|
+
function deriveCliStatus(toolStatus) {
|
|
16
|
+
if (!toolStatus)
|
|
17
|
+
return 'idle';
|
|
18
|
+
if (toolStatus.isWaitingForResponse)
|
|
36
19
|
return 'waiting';
|
|
37
|
-
|
|
38
|
-
if (worktree.isProcessing) {
|
|
20
|
+
if (toolStatus.isProcessing)
|
|
39
21
|
return 'running';
|
|
40
|
-
|
|
41
|
-
// Session running but not processing = ready
|
|
42
|
-
if (worktree.isSessionRunning) {
|
|
22
|
+
if (toolStatus.isRunning)
|
|
43
23
|
return 'ready';
|
|
44
|
-
}
|
|
45
24
|
return 'idle';
|
|
46
25
|
}
|
|
47
26
|
/**
|
|
@@ -74,7 +53,9 @@ function calculateHasUnread(worktree) {
|
|
|
74
53
|
* @returns SidebarBranchItem for sidebar display
|
|
75
54
|
*/
|
|
76
55
|
function toBranchItem(worktree) {
|
|
77
|
-
|
|
56
|
+
// Issue #4: Sidebar no longer shows per-CLI session status.
|
|
57
|
+
// Status is always 'idle' since detailed status is shown in WorktreeDetail.
|
|
58
|
+
const status = 'idle';
|
|
78
59
|
// Use new hasUnread logic based on lastAssistantMessageAt and lastViewedAt
|
|
79
60
|
const hasUnread = calculateHasUnread(worktree);
|
|
80
61
|
return {
|
|
@@ -85,5 +66,9 @@ function toBranchItem(worktree) {
|
|
|
85
66
|
hasUnread,
|
|
86
67
|
lastActivity: worktree.updatedAt,
|
|
87
68
|
description: worktree.description,
|
|
69
|
+
cliStatus: {
|
|
70
|
+
claude: deriveCliStatus(worktree.sessionStatusByCli?.claude),
|
|
71
|
+
codex: deriveCliStatus(worktree.sessionStatusByCli?.codex),
|
|
72
|
+
},
|
|
88
73
|
};
|
|
89
74
|
}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Slash Command Types
|
|
4
4
|
*
|
|
5
5
|
* Type definitions for slash commands loaded from .claude/commands/*.md
|
|
6
|
+
*
|
|
7
|
+
* Issue #4: Added cliTools field for CLI tool-specific command filtering
|
|
6
8
|
*/
|
|
7
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
10
|
exports.COMMAND_CATEGORIES = exports.CATEGORY_LABELS = void 0;
|
package/package.json
CHANGED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
"use strict";exports.id=1318,exports.ids=[1318],exports.modules={61318:(e,t,r)=>{r.d(t,{v:()=>_});var o=r(61282),s=r(92048),a=r(55315),i=r.n(a);class n{constructor(){}static getInstance(){return n.instance||(n.instance=new n),n.instance}normalize(e){let t=e.match(/^ssh:\/\/git@([^:\/]+)(:\d+)?\/(.+?)(\.git)?$/);if(t)return`https://${t[1]}/${t[3]}`.toLowerCase().replace(/\/$/,"");let r=e.match(/^git@([^:]+):(.+?)(\.git)?$/);return r?`https://${r[1]}/${r[2]}`.toLowerCase().replace(/\/$/,""):e.replace(/\.git\/?$/,"").replace(/\/$/,"").toLowerCase()}isSameRepository(e,t){return this.normalize(e)===this.normalize(t)}extractRepoName(e){let t=e.match(/^ssh:\/\/git@[^\/]+\/(.+?)(\.git)?$/);if(t){let e=t[1].split("/");return e[e.length-1]}let r=e.match(/:(.+?)(\.git)?$/);if(r&&e.startsWith("git@")){let e=r[1].split("/");return e[e.length-1]}let o=e.match(/\/([^\/]+?)(\.git)?$/);return o?o[1]:""}getUrlType(e){return e.startsWith("https://")?"https":e.startsWith("git@")||e.startsWith("ssh://")?"ssh":null}validate(e){if(!e||"string"!=typeof e||""===e.trim())return{valid:!1,error:"EMPTY_URL"};let t=e.trim();return t.startsWith("https://")?/^https:\/\/[^\/]+\/[^\/]+\/[^\/]+(\.git)?$/.test(t)?{valid:!0}:{valid:!1,error:"INVALID_URL_FORMAT"}:t.startsWith("git@")?/^git@[^:]+:.+\/.+(\.git)?$/.test(t)?{valid:!0}:{valid:!1,error:"INVALID_URL_FORMAT"}:t.startsWith("ssh://")&&/^ssh:\/\/git@[^\/]+(:\d+)?\/[^\/]+\/.+(\.git)?$/.test(t)?{valid:!0}:{valid:!1,error:"INVALID_URL_FORMAT"}}}var l=r(57440),c=r(84770);function d(e){return{id:e.id,cloneUrl:e.clone_url,normalizedCloneUrl:e.normalized_clone_url,targetPath:e.target_path,repositoryId:e.repository_id||void 0,status:e.status,pid:e.pid||void 0,progress:e.progress,errorCategory:e.error_category||void 0,errorCode:e.error_code||void 0,errorMessage:e.error_message||void 0,startedAt:e.started_at?new Date(e.started_at):void 0,completedAt:e.completed_at?new Date(e.completed_at):void 0,createdAt:new Date(e.created_at)}}function u(e,t){let r=e.prepare(`
|
|
2
|
-
SELECT * FROM clone_jobs
|
|
3
|
-
WHERE id = ?
|
|
4
|
-
`).get(t);return r?d(r):null}function g(e,t,r){let o=[],s=[];void 0!==r.status&&(o.push("status = ?"),s.push(r.status)),void 0!==r.pid&&(o.push("pid = ?"),s.push(r.pid)),void 0!==r.progress&&(o.push("progress = ?"),s.push(r.progress)),void 0!==r.repositoryId&&(o.push("repository_id = ?"),s.push(r.repositoryId)),void 0!==r.errorCategory&&(o.push("error_category = ?"),s.push(r.errorCategory)),void 0!==r.errorCode&&(o.push("error_code = ?"),s.push(r.errorCode)),void 0!==r.errorMessage&&(o.push("error_message = ?"),s.push(r.errorMessage)),void 0!==r.startedAt&&(o.push("started_at = ?"),s.push(r.startedAt.getTime())),void 0!==r.completedAt&&(o.push("completed_at = ?"),s.push(r.completedAt.getTime())),0!==o.length&&(s.push(t),e.prepare(`
|
|
5
|
-
UPDATE clone_jobs
|
|
6
|
-
SET ${o.join(", ")}
|
|
7
|
-
WHERE id = ?
|
|
8
|
-
`).run(...s))}var h=r(67722);class p extends Error{constructor(e){super(e.message),this.name="CloneManagerError",this.category=e.category,this.code=e.code,this.recoverable=e.recoverable,this.suggestedAction=e.suggestedAction}}let m={EMPTY_URL:{category:"validation",code:"EMPTY_URL",message:"Clone URL is required",recoverable:!0,suggestedAction:"Please enter a valid git clone URL"},INVALID_URL_FORMAT:{category:"validation",code:"INVALID_URL_FORMAT",message:"Invalid URL format. Please use HTTPS or SSH URL.",recoverable:!0,suggestedAction:"Enter a valid URL like https://github.com/owner/repo or git@github.com:owner/repo"},DUPLICATE_CLONE_URL:{category:"validation",code:"DUPLICATE_CLONE_URL",message:"This repository is already registered",recoverable:!1,suggestedAction:"Use the existing repository instead"},CLONE_IN_PROGRESS:{category:"validation",code:"CLONE_IN_PROGRESS",message:"A clone operation is already in progress for this URL",recoverable:!1,suggestedAction:"Wait for the current clone to complete"},DIRECTORY_EXISTS:{category:"filesystem",code:"DIRECTORY_EXISTS",message:"Target directory already exists",recoverable:!0,suggestedAction:"Choose a different directory or remove the existing one"},INVALID_TARGET_PATH:{category:"validation",code:"INVALID_TARGET_PATH",message:"Target path is invalid or outside allowed directory",recoverable:!0,suggestedAction:"Use a path within the configured base directory"},AUTH_FAILED:{category:"auth",code:"AUTH_FAILED",message:"Authentication failed",recoverable:!0,suggestedAction:"Check your credentials or SSH keys"},NETWORK_ERROR:{category:"network",code:"NETWORK_ERROR",message:"Network error occurred",recoverable:!0,suggestedAction:"Check your internet connection and try again"},GIT_ERROR:{category:"git",code:"GIT_ERROR",message:"Git command failed",recoverable:!1,suggestedAction:"Check the error message for details"},CLONE_TIMEOUT:{category:"network",code:"CLONE_TIMEOUT",message:"Clone operation timed out",recoverable:!0,suggestedAction:"Try again or clone a smaller repository"}};class _{constructor(e,t={}){this.db=e,this.urlNormalizer=n.getInstance(),this.config={basePath:t.basePath||process.env.WORKTREE_BASE_PATH||"/tmp/repos",timeout:t.timeout||6e5},this.activeProcesses=new Map}validateCloneRequest(e){let t=this.urlNormalizer.validate(e);return t.valid?{valid:!0,normalizedUrl:this.urlNormalizer.normalize(e),repoName:this.urlNormalizer.extractRepoName(e)}:{valid:!1,error:m[t.error||"INVALID_URL_FORMAT"]}}checkDuplicateRepository(e){return function(e,t){let r=e.prepare(`
|
|
9
|
-
SELECT * FROM repositories
|
|
10
|
-
WHERE normalized_clone_url = ?
|
|
11
|
-
`).get(t);return r?{id:r.id,name:r.name,path:r.path,enabled:1===r.enabled,cloneUrl:r.clone_url||void 0,normalizedCloneUrl:r.normalized_clone_url||void 0,cloneSource:r.clone_source,isEnvManaged:1===r.is_env_managed,createdAt:new Date(r.created_at),updatedAt:new Date(r.updated_at)}:null}(this.db,e)}checkActiveCloneJob(e){return function(e,t){let r=e.prepare(`
|
|
12
|
-
SELECT * FROM clone_jobs
|
|
13
|
-
WHERE normalized_clone_url = ?
|
|
14
|
-
AND status IN ('pending', 'running')
|
|
15
|
-
ORDER BY created_at DESC
|
|
16
|
-
LIMIT 1
|
|
17
|
-
`).get(t);return r?d(r):null}(this.db,e)}createCloneJob(e){return function(e,t){let r=(0,c.randomUUID)(),o=Date.now();return e.prepare(`
|
|
18
|
-
INSERT INTO clone_jobs (
|
|
19
|
-
id, clone_url, normalized_clone_url, target_path,
|
|
20
|
-
status, progress, created_at
|
|
21
|
-
)
|
|
22
|
-
VALUES (?, ?, ?, ?, 'pending', 0, ?)
|
|
23
|
-
`).run(r,t.cloneUrl,t.normalizedCloneUrl,t.targetPath,o),{id:r,cloneUrl:t.cloneUrl,normalizedCloneUrl:t.normalizedCloneUrl,targetPath:t.targetPath,status:"pending",progress:0,createdAt:new Date(o)}}(this.db,e)}getTargetPath(e){return i().join(this.config.basePath,e)}async startCloneJob(e,t){let r=this.validateCloneRequest(e);if(!r.valid)return{success:!1,error:r.error};let o=r.normalizedUrl,a=r.repoName,i=this.checkDuplicateRepository(o);if(i)return{success:!1,error:{...m.DUPLICATE_CLONE_URL,message:`This repository is already registered as "${i.name}"`}};let n=this.checkActiveCloneJob(o);if(n)return{success:!1,jobId:n.id,error:m.CLONE_IN_PROGRESS};let c=t||this.getTargetPath(a);if(t&&!(0,l.j)(t,this.config.basePath))return{success:!1,error:{...m.INVALID_TARGET_PATH,message:`Target path must be within ${this.config.basePath}`}};if((0,s.existsSync)(c))return{success:!1,error:{...m.DIRECTORY_EXISTS,message:`Target directory already exists: ${c}`}};let d=this.createCloneJob({cloneUrl:e,normalizedCloneUrl:o,targetPath:c});return this.executeClone(d.id,e,c).catch(e=>{console.error(`[CloneManager] Clone failed for job ${d.id}:`,e)}),{success:!0,jobId:d.id}}async executeClone(e,t,r){g(this.db,e,{status:"running",startedAt:new Date});let a=i().dirname(r);return(0,s.existsSync)(a)||(0,s.mkdirSync)(a,{recursive:!0}),new Promise((s,a)=>{let i=(0,o.spawn)("git",["clone","--progress",t,r],{stdio:["ignore","pipe","pipe"]});this.activeProcesses.set(e,i),i.pid&&g(this.db,e,{pid:i.pid});let n="";i.stderr?.on("data",t=>{n+=t.toString();let r=this.parseGitProgress(t.toString());null!==r&&g(this.db,e,{progress:r})});let l=setTimeout(()=>{i.kill("SIGTERM"),g(this.db,e,{status:"failed",errorCategory:"network",errorCode:"CLONE_TIMEOUT",errorMessage:"Clone operation timed out",completedAt:new Date}),this.activeProcesses.delete(e),a(new p(m.CLONE_TIMEOUT))},this.config.timeout);i.on("close",async o=>{if(clearTimeout(l),this.activeProcesses.delete(e),0===o)await this.onCloneSuccess(e,t,r),s();else{let t=this.parseGitError(n,o);g(this.db,e,{status:"failed",errorCategory:t.category,errorCode:t.code,errorMessage:t.message,completedAt:new Date}),a(new p(t))}}),i.on("error",t=>{clearTimeout(l),this.activeProcesses.delete(e);let r={category:"system",code:"SPAWN_ERROR",message:`Failed to spawn git process: ${t.message}`,recoverable:!1,suggestedAction:"Ensure git is installed and available in PATH"};g(this.db,e,{status:"failed",errorCategory:r.category,errorCode:r.code,errorMessage:r.message,completedAt:new Date}),a(new p(r))})})}async onCloneSuccess(e,t,r){let o=u(this.db,e);if(!o)return;let s=this.urlNormalizer.getUrlType(t),a=function(e,t){let r=(0,c.randomUUID)(),o=Date.now();return e.prepare(`
|
|
24
|
-
INSERT INTO repositories (
|
|
25
|
-
id, name, path, enabled, clone_url, normalized_clone_url,
|
|
26
|
-
clone_source, is_env_managed, created_at, updated_at
|
|
27
|
-
)
|
|
28
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
29
|
-
`).run(r,t.name,t.path,!1!==t.enabled?1:0,t.cloneUrl||null,t.normalizedCloneUrl||null,t.cloneSource,t.isEnvManaged?1:0,o,o),{id:r,name:t.name,path:t.path,enabled:!1!==t.enabled,cloneUrl:t.cloneUrl,normalizedCloneUrl:t.normalizedCloneUrl,cloneSource:t.cloneSource,isEnvManaged:t.isEnvManaged||!1,createdAt:new Date(o),updatedAt:new Date(o)}}(this.db,{name:i().basename(r),path:r,cloneUrl:t,normalizedCloneUrl:o.normalizedCloneUrl,cloneSource:s||"https"});try{let e=await (0,h.e9)(r);e.length>0&&((0,h.h2)(this.db,e),console.log(`[CloneManager] Registered ${e.length} worktree(s) for ${r}`))}catch(e){console.error(`[CloneManager] Failed to scan worktrees for ${r}:`,e)}g(this.db,e,{status:"completed",progress:100,repositoryId:a.id,completedAt:new Date})}parseGitProgress(e){let t=e.match(/(?:Receiving objects|Resolving deltas|Cloning into[^:]*?):\s*(\d+)%/);if(t){let e=parseInt(t[1],10);return isNaN(e)?null:e}return null}parseGitError(e,t){let r=e.toLowerCase(),o=e.substring(0,200);return["authentication failed","permission denied","could not read from remote repository"].some(e=>r.includes(e))?{...m.AUTH_FAILED,message:`Authentication failed: ${o}`}:["could not resolve host","connection refused","network is unreachable"].some(e=>r.includes(e))?{...m.NETWORK_ERROR,message:`Network error: ${o}`}:{...m.GIT_ERROR,message:`Git clone failed (exit code ${t}): ${o}`}}getCloneJobStatus(e){let t=u(this.db,e);if(!t)return null;let r={jobId:t.id,status:t.status,progress:t.progress,repositoryId:t.repositoryId};return"failed"===t.status&&t.errorCode&&(r.error={category:t.errorCategory||"system",code:t.errorCode,message:t.errorMessage||"Unknown error"}),r}cancelCloneJob(e){let t=this.activeProcesses.get(e);if(t)return t.kill("SIGTERM"),this.activeProcesses.delete(e),g(this.db,e,{status:"cancelled",completedAt:new Date}),!0;let r=u(this.db,e);return!!r&&"pending"===r.status&&(g(this.db,e,{status:"cancelled",completedAt:new Date}),!0)}}},57440:(e,t,r)=>{r.d(t,{j:()=>a});var o=r(55315),s=r.n(o);function a(e,t){if(!e||""===e.trim()||e.includes("\0"))return!1;let r=e;try{r=decodeURIComponent(e)}catch{r=e}if(r.includes("\0"))return!1;let o=s().resolve(t),a=s().resolve(t,r),i=s().relative(o,a);return!(i.startsWith("..")||s().isAbsolute(i))}},67722:(e,t,r)=>{r.d(t,{Lj:()=>u,a$:()=>c,e9:()=>d,h2:()=>g});var o=r(61282),s=r(21764),a=r(55315),i=r.n(a),n=r(75748),l=r(93346);function c(){let e=process.env.WORKTREE_REPOS;if(e&&e.trim())return e.split(",").map(e=>e.trim()).filter(e=>e.length>0);let t=(0,l.Hb)("CM_ROOT_DIR");return t&&t.trim()?[t.trim()]:[]}async function d(e){let t=(0,s.promisify)(o.exec);try{let{stdout:r}=await t("git worktree list",{cwd:e}),o=function(e){if(!e||""===e.trim())return[];let t=e.trim().split("\n"),r=[];for(let e of t){let t=e.trim();if(!t)continue;let o=t.match(/^(.+?)\s+([a-z0-9]+)\s+(?:\[(.+?)\]|\(detached HEAD\))/);if(o){let[,e,t,s]=o;r.push({path:e.trim(),branch:s||`detached-${t}`,commit:t})}}return r}(r),s=i().resolve(e),a=i().basename(s);return o.map(e=>({id:function(e,t){if(!e)return"";let r=e=>e.toLowerCase().replace(/[^a-z0-9-]/g,"-").replace(/-+/g,"-").replace(/^-|-$/g,""),o=r(e);if(t){let e=r(t);return`${e}-${o}`}return o}(e.branch,a),name:e.branch,path:i().resolve(e.path),repositoryPath:s,repositoryName:a})).filter(e=>["/etc","/root","/sys","/proc","/dev","/boot","/bin","/sbin","/usr/bin","/usr/sbin"].some(t=>e.path.startsWith(t))?(console.warn(`Skipping potentially unsafe worktree path: ${e.path}`),!1):!(e.path.includes("\0")||e.path.includes(".."))||(console.warn(`Skipping path with potentially malicious characters: ${e.path}`),!1))}catch(r){let e=r instanceof Error?r.message:String(r),t=r.code;if(e?.includes("not a git repository")||128===t)return[];throw r}}async function u(e){let t=[];for(let r of e)try{console.log(`Scanning repository: ${r}`);let e=await d(r);t.push(...e),console.log(` Found ${e.length} worktree(s)`)}catch(e){console.error(`Error scanning repository ${r}:`,e)}return t}function g(e,t){if(0===t.length)return;let r=new Map;for(let e of t){let t=e.repositoryPath||"";r.has(t)||r.set(t,[]),r.get(t).push(e)}for(let[t,o]of r){if(!t)continue;let r=(0,n.Pv)(e,t),s=new Set(o.map(e=>e.id)),a=r.filter(e=>!s.has(e));if(a.length>0){let r=(0,n.pM)(e,a);console.log(`Removed ${r.deletedCount} deleted worktree(s) from ${t}`)}for(let t of o)(0,n.ly)(e,t)}}}};
|