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.
Files changed (157) hide show
  1. package/.env.example +4 -9
  2. package/.next/BUILD_ID +1 -1
  3. package/.next/app-build-manifest.json +24 -24
  4. package/.next/app-path-routes-manifest.json +1 -1
  5. package/.next/build-manifest.json +7 -7
  6. package/.next/cache/.tsbuildinfo +1 -1
  7. package/.next/cache/config.json +3 -3
  8. package/.next/cache/webpack/client-production/0.pack +0 -0
  9. package/.next/cache/webpack/client-production/1.pack +0 -0
  10. package/.next/cache/webpack/client-production/2.pack +0 -0
  11. package/.next/cache/webpack/client-production/index.pack +0 -0
  12. package/.next/cache/webpack/client-production/index.pack.old +0 -0
  13. package/.next/cache/webpack/edge-server-production/0.pack +0 -0
  14. package/.next/cache/webpack/edge-server-production/index.pack +0 -0
  15. package/.next/cache/webpack/server-production/0.pack +0 -0
  16. package/.next/cache/webpack/server-production/index.pack +0 -0
  17. package/.next/next-server.js.nft.json +1 -1
  18. package/.next/prerender-manifest.json +1 -1
  19. package/.next/react-loadable-manifest.json +7 -7
  20. package/.next/required-server-files.json +1 -1
  21. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  22. package/.next/server/app/_not-found.html +1 -1
  23. package/.next/server/app/_not-found.rsc +2 -2
  24. package/.next/server/app/api/hooks/claude-done/route.js +1 -19
  25. package/.next/server/app/api/hooks/claude-done/route.js.nft.json +1 -1
  26. package/.next/server/app/api/repositories/clone/[jobId]/route.js +1 -1
  27. package/.next/server/app/api/repositories/clone/[jobId]/route.js.nft.json +1 -1
  28. package/.next/server/app/api/repositories/clone/route.js +1 -1
  29. package/.next/server/app/api/repositories/clone/route.js.nft.json +1 -1
  30. package/.next/server/app/api/repositories/excluded/route.js +36 -0
  31. package/.next/server/app/api/repositories/excluded/route.js.nft.json +1 -0
  32. package/.next/server/app/api/repositories/excluded.body +1 -0
  33. package/.next/server/app/api/repositories/excluded.meta +1 -0
  34. package/.next/server/app/api/repositories/restore/route.js +36 -0
  35. package/.next/server/app/api/repositories/restore/route.js.nft.json +1 -0
  36. package/.next/server/app/api/repositories/route.js +36 -1
  37. package/.next/server/app/api/repositories/route.js.nft.json +1 -1
  38. package/.next/server/app/api/repositories/scan/route.js +1 -1
  39. package/.next/server/app/api/repositories/sync/route.js +36 -1
  40. package/.next/server/app/api/slash-commands/route.js +1 -1
  41. package/.next/server/app/api/slash-commands.body +1 -1
  42. package/.next/server/app/api/worktrees/[id]/auto-yes/route.js +1 -1
  43. package/.next/server/app/api/worktrees/[id]/auto-yes/route.js.nft.json +1 -1
  44. package/.next/server/app/api/worktrees/[id]/current-output/route.js +1 -1
  45. package/.next/server/app/api/worktrees/[id]/current-output/route.js.nft.json +1 -1
  46. package/.next/server/app/api/worktrees/[id]/interrupt/route.js +1 -1
  47. package/.next/server/app/api/worktrees/[id]/interrupt/route.js.nft.json +1 -1
  48. package/.next/server/app/api/worktrees/[id]/kill-session/route.js +1 -1
  49. package/.next/server/app/api/worktrees/[id]/kill-session/route.js.nft.json +1 -1
  50. package/.next/server/app/api/worktrees/[id]/logs/[filename]/route.js +1 -1
  51. package/.next/server/app/api/worktrees/[id]/logs/route.js +1 -1
  52. package/.next/server/app/api/worktrees/[id]/prompt-response/route.js +1 -1
  53. package/.next/server/app/api/worktrees/[id]/prompt-response/route.js.nft.json +1 -1
  54. package/.next/server/app/api/worktrees/[id]/respond/route.js +1 -1
  55. package/.next/server/app/api/worktrees/[id]/respond/route.js.nft.json +1 -1
  56. package/.next/server/app/api/worktrees/[id]/route.js +1 -1
  57. package/.next/server/app/api/worktrees/[id]/route.js.nft.json +1 -1
  58. package/.next/server/app/api/worktrees/[id]/search/route.js +1 -1
  59. package/.next/server/app/api/worktrees/[id]/send/route.js +1 -1
  60. package/.next/server/app/api/worktrees/[id]/send/route.js.nft.json +1 -1
  61. package/.next/server/app/api/worktrees/[id]/slash-commands/route.js +1 -1
  62. package/.next/server/app/api/worktrees/[id]/start-polling/route.js +1 -1
  63. package/.next/server/app/api/worktrees/[id]/start-polling/route.js.nft.json +1 -1
  64. package/.next/server/app/api/worktrees/route.js +1 -1
  65. package/.next/server/app/api/worktrees/route.js.nft.json +1 -1
  66. package/.next/server/app/index.html +2 -2
  67. package/.next/server/app/index.rsc +3 -3
  68. package/.next/server/app/page.js +7 -7
  69. package/.next/server/app/page.js.nft.json +1 -1
  70. package/.next/server/app/page_client-reference-manifest.js +1 -1
  71. package/.next/server/app/proxy/[...path]/route.js +2 -2
  72. package/.next/server/app/worktrees/[id]/files/[...path]/page_client-reference-manifest.js +1 -1
  73. package/.next/server/app/worktrees/[id]/page.js +4 -4
  74. package/.next/server/app/worktrees/[id]/page.js.nft.json +1 -1
  75. package/.next/server/app/worktrees/[id]/page_client-reference-manifest.js +1 -1
  76. package/.next/server/app/worktrees/[id]/simple-terminal/page_client-reference-manifest.js +1 -1
  77. package/.next/server/app/worktrees/[id]/terminal/page_client-reference-manifest.js +1 -1
  78. package/.next/server/app-paths-manifest.json +10 -8
  79. package/.next/server/chunks/5488.js +36 -0
  80. package/.next/server/chunks/6550.js +1 -1
  81. package/.next/server/chunks/7425.js +53 -50
  82. package/.next/server/chunks/7536.js +1 -0
  83. package/.next/server/chunks/8174.js +23 -0
  84. package/.next/server/chunks/9367.js +19 -0
  85. package/.next/server/functions-config-manifest.json +1 -1
  86. package/.next/server/middleware-build-manifest.js +1 -1
  87. package/.next/server/middleware-manifest.json +2 -28
  88. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  89. package/.next/server/pages/404.html +1 -1
  90. package/.next/server/pages/500.html +1 -1
  91. package/.next/server/server-reference-manifest.json +1 -1
  92. package/.next/static/chunks/4327.740cc7fe2d0b5049.js +60 -0
  93. package/.next/static/chunks/4343-ebe884a2a80eb033.js +1 -0
  94. package/.next/static/chunks/6568-38a33aa67d82e12b.js +1 -0
  95. package/.next/static/chunks/816-c254f4e2406e696a.js +1 -0
  96. package/.next/static/chunks/app/layout-4804cfba519283cf.js +1 -0
  97. package/.next/static/chunks/app/page-3926224c4cdf315b.js +1 -0
  98. package/.next/static/chunks/app/worktrees/[id]/page-d64624eb67af57c0.js +1 -0
  99. package/.next/static/chunks/main-b6d727aa9248d4f2.js +1 -0
  100. package/.next/static/chunks/{webpack-3fc79fab9bb738d7.js → webpack-4f85dcef6279c6ee.js} +1 -1
  101. package/.next/static/css/28be35e4727ae7ef.css +3 -0
  102. package/.next/trace +5 -5
  103. package/.next/types/app/api/repositories/excluded/route.ts +343 -0
  104. package/.next/types/app/api/repositories/restore/route.ts +343 -0
  105. package/README.md +2 -2
  106. package/dist/cli/commands/init.d.ts.map +1 -1
  107. package/dist/cli/commands/init.js +2 -13
  108. package/dist/cli/commands/start.d.ts.map +1 -1
  109. package/dist/cli/commands/start.js +3 -7
  110. package/dist/cli/config/security-messages.d.ts +11 -0
  111. package/dist/cli/config/security-messages.d.ts.map +1 -0
  112. package/dist/cli/config/security-messages.js +29 -0
  113. package/dist/cli/types/index.d.ts +0 -1
  114. package/dist/cli/types/index.d.ts.map +1 -1
  115. package/dist/cli/utils/daemon.d.ts.map +1 -1
  116. package/dist/cli/utils/daemon.js +3 -7
  117. package/dist/cli/utils/env-setup.d.ts +0 -4
  118. package/dist/cli/utils/env-setup.d.ts.map +1 -1
  119. package/dist/cli/utils/env-setup.js +0 -14
  120. package/dist/cli/utils/security-logger.d.ts.map +1 -1
  121. package/dist/cli/utils/security-logger.js +1 -2
  122. package/dist/server/src/lib/auto-yes-manager.js +13 -5
  123. package/dist/server/src/lib/claude-poller.js +337 -0
  124. package/dist/server/src/lib/cli-patterns.js +9 -2
  125. package/dist/server/src/lib/cli-tools/base.js +7 -1
  126. package/dist/server/src/lib/cli-tools/codex.js +14 -2
  127. package/dist/server/src/lib/cli-tools/manager.js +27 -0
  128. package/dist/server/src/lib/cli-tools/types.js +7 -0
  129. package/dist/server/src/lib/cli-tools/validation.js +41 -0
  130. package/dist/server/src/lib/db.js +23 -0
  131. package/dist/server/src/lib/env.js +0 -17
  132. package/dist/server/src/lib/logger.js +0 -4
  133. package/dist/server/src/lib/prompt-detector.js +129 -31
  134. package/dist/server/src/lib/ws-server.js +12 -1
  135. package/dist/server/src/types/sidebar.js +16 -31
  136. package/dist/server/src/types/slash-commands.js +2 -0
  137. package/package.json +1 -1
  138. package/.next/server/chunks/1318.js +0 -29
  139. package/.next/server/chunks/2597.js +0 -1
  140. package/.next/server/chunks/2648.js +0 -1
  141. package/.next/server/chunks/9703.js +0 -31
  142. package/.next/server/chunks/9723.js +0 -19
  143. package/.next/server/edge-runtime-webpack.js +0 -2
  144. package/.next/server/edge-runtime-webpack.js.map +0 -1
  145. package/.next/server/src/middleware.js +0 -14
  146. package/.next/server/src/middleware.js.map +0 -1
  147. package/.next/static/chunks/2853-d11a80b03c9a1640.js +0 -1
  148. package/.next/static/chunks/4327.3b84aa049900fdeb.js +0 -60
  149. package/.next/static/chunks/816-7e340dad784be28c.js +0 -1
  150. package/.next/static/chunks/9365-733d8c05712d2888.js +0 -1
  151. package/.next/static/chunks/app/layout-37e55f11dcc8b1bf.js +0 -1
  152. package/.next/static/chunks/app/page-fe35d61f14b90a51.js +0 -1
  153. package/.next/static/chunks/app/worktrees/[id]/page-58fcf2e63c056743.js +0 -1
  154. package/.next/static/chunks/main-a960f4a5e1a2f598.js +0 -1
  155. package/.next/static/css/376b339640084689.css +0 -3
  156. /package/.next/static/{564GHwluX5xIv9qpqLJV2 → bdUePCj-b9Gv5okYGp49O}/_buildManifest.js +0 -0
  157. /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+.+/m;
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
- return `mcbd-${this.id}-${worktreeId}`;
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, 3000));
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, 500));
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
- * This function scans the output from bottom to top looking for numbered options
154
- * with a selection indicator (❯). It requires at least 2 options and a default
155
- * indicator to be considered a valid prompt.
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
- // Look for lines that match the pattern: [optional leading spaces] [❯ or spaces] [number]. [text]
169
- // Note: ANSI codes sometimes cause spaces to be lost after stripping, so we use \s* instead of \s+
170
- const optionPattern = /^\s*([❯ ]\s*)?(\d+)\.\s*(.+)$/;
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 firstOptionIndex = -1;
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
- const rawLine = lines[i]; // Keep original indentation for checking
179
- const match = line.match(optionPattern);
180
- if (match) {
181
- const hasDefault = Boolean(match[1] && match[1].includes('❯'));
182
- const number = parseInt(match[2], 10);
183
- const label = match[3].trim();
184
- // Insert at beginning since we're scanning backwards
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
- else if (options.length > 0 && line && !line.match(/^[-─]+$/)) {
191
- // Check if this is a continuation line (indented line between options)
192
- // Continuation lines typically start with spaces (like " work/github...")
193
- // Also treat very short lines (< 5 chars) as potential word-wrap fragments
194
- const hasLeadingSpaces = rawLine.match(/^\s{2,}[^\d]/) && !rawLine.match(/^\s*\d+\./);
195
- const isShortFragment = line.length < 5 && !line.endsWith('?');
196
- const isContinuationLine = hasLeadingSpaces || isShortFragment;
197
- if (isContinuationLine) {
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
- // Must have at least 2 options AND at least one with ❯ indicator to be considered a prompt
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({ server });
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
- * Determine branch status from Worktree data
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 determineBranchStatus(worktree) {
20
- // Check CLI-specific status first
21
- const claudeStatus = worktree.sessionStatusByCli?.claude;
22
- if (claudeStatus) {
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
- const status = determineBranchStatus(worktree);
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,6 +1,6 @@
1
1
  {
2
2
  "name": "commandmate",
3
- "version": "0.1.12",
3
+ "version": "0.2.0",
4
4
  "description": "Git worktree management with Claude CLI and tmux sessions",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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)}}}};