commandmate 0.1.10 → 0.1.11
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 +8 -3
- package/.next/BUILD_ID +1 -1
- package/.next/app-build-manifest.json +11 -11
- package/.next/app-path-routes-manifest.json +1 -1
- package/.next/build-manifest.json +2 -2
- 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/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 +1 -1
- package/.next/server/app/api/external-apps/[id]/health/route.js +11 -12
- package/.next/server/app/api/external-apps/[id]/route.js +14 -15
- package/.next/server/app/api/external-apps/route.js +12 -13
- package/.next/server/app/api/hooks/claude-done/route.js +1 -1
- package/.next/server/app/api/repositories/clone/[jobId]/route.js +1 -1
- package/.next/server/app/api/repositories/clone/route.js +1 -1
- package/.next/server/app/api/repositories/route.js +1 -1
- package/.next/server/app/api/repositories/scan/route.js +1 -1
- package/.next/server/app/api/repositories/sync/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]/cli-tool/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/current-output/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/files/[...path]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/interrupt/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/kill-session/route.js +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 +7 -7
- package/.next/server/app/api/worktrees/[id]/memos/[memoId]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/memos/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/messages/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/prompt-response/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/respond/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/route.js +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]/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]/tree/[...path]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/tree/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/upload/[...path]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/viewed/route.js +1 -1
- package/.next/server/app/api/worktrees/route.js +1 -1
- package/.next/server/app/index.html +2 -2
- package/.next/server/app/index.rsc +2 -2
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/proxy/[...path]/route.js +12 -13
- package/.next/server/app/worktrees/[id]/files/[...path]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/page.js +3 -3
- 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 -10
- package/.next/server/chunks/1318.js +4 -4
- package/.next/server/chunks/1528.js +1 -1
- package/.next/server/chunks/7425.js +97 -48
- package/.next/server/chunks/9723.js +1 -1
- package/.next/server/functions-config-manifest.json +1 -1
- package/.next/server/middleware-manifest.json +5 -5
- 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/server/src/middleware.js +2 -2
- package/.next/server/src/middleware.js.map +1 -1
- package/.next/static/chunks/app/worktrees/[id]/page-720605c2fb074444.js +1 -0
- package/.next/trace +5 -5
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +6 -4
- package/dist/cli/commands/start.d.ts +2 -0
- package/dist/cli/commands/start.d.ts.map +1 -1
- package/dist/cli/commands/start.js +64 -17
- package/dist/cli/commands/status.d.ts +4 -1
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +95 -6
- package/dist/cli/commands/stop.d.ts +2 -0
- package/dist/cli/commands/stop.d.ts.map +1 -1
- package/dist/cli/commands/stop.js +27 -10
- package/dist/cli/index.js +16 -2
- package/dist/cli/types/index.d.ts +20 -0
- package/dist/cli/types/index.d.ts.map +1 -1
- package/dist/cli/utils/daemon-factory.d.ts +105 -0
- package/dist/cli/utils/daemon-factory.d.ts.map +1 -0
- package/dist/cli/utils/daemon-factory.js +117 -0
- package/dist/cli/utils/daemon.d.ts.map +1 -1
- package/dist/cli/utils/daemon.js +4 -0
- package/dist/cli/utils/env-setup.d.ts +24 -12
- package/dist/cli/utils/env-setup.d.ts.map +1 -1
- package/dist/cli/utils/env-setup.js +64 -43
- package/dist/cli/utils/input-validators.d.ts +103 -0
- package/dist/cli/utils/input-validators.d.ts.map +1 -0
- package/dist/cli/utils/input-validators.js +163 -0
- package/dist/cli/utils/install-context.d.ts +53 -0
- package/dist/cli/utils/install-context.d.ts.map +1 -0
- package/dist/cli/utils/install-context.js +96 -0
- package/dist/cli/utils/pid-manager.d.ts +34 -0
- package/dist/cli/utils/pid-manager.d.ts.map +1 -1
- package/dist/cli/utils/pid-manager.js +43 -0
- package/dist/cli/utils/port-allocator.d.ts +108 -0
- package/dist/cli/utils/port-allocator.d.ts.map +1 -0
- package/dist/cli/utils/port-allocator.js +166 -0
- package/dist/cli/utils/resource-resolvers.d.ts +92 -0
- package/dist/cli/utils/resource-resolvers.d.ts.map +1 -0
- package/dist/cli/utils/resource-resolvers.js +175 -0
- package/dist/cli/utils/worktree-detector.d.ts +82 -0
- package/dist/cli/utils/worktree-detector.d.ts.map +1 -0
- package/dist/cli/utils/worktree-detector.js +221 -0
- package/dist/lib/errors.d.ts +111 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +153 -0
- package/dist/server/server.js +3 -0
- package/dist/server/src/cli/utils/install-context.js +96 -0
- package/dist/server/src/config/system-directories.js +40 -0
- package/dist/server/src/lib/auto-yes-manager.js +325 -0
- package/dist/server/src/lib/auto-yes-resolver.js +34 -0
- package/dist/server/src/lib/cli-patterns.js +6 -1
- package/dist/server/src/lib/db-instance.js +12 -2
- package/dist/server/src/lib/db-migrations.js +68 -1
- package/dist/server/src/lib/db-path-resolver.js +99 -0
- package/dist/server/src/lib/db.js +21 -0
- package/dist/server/src/lib/env.js +52 -3
- package/dist/server/src/lib/worktrees.js +36 -1
- package/dist/server/src/types/external-apps.js +20 -0
- package/package.json +1 -1
- package/.next/static/chunks/app/worktrees/[id]/page-aea2d5e7e28955be.js +0 -1
- /package/.next/static/chunks/app/{page-96a8aa2ec30a44e9.js → page-fe35d61f14b90a51.js} +0 -0
- /package/.next/static/{8o5rUyZun0GklIHWDgkmv → gRNW5YXY43KqCKbCdaJoJ}/_buildManifest.js +0 -0
- /package/.next/static/{8o5rUyZun0GklIHWDgkmv → gRNW5YXY43KqCKbCdaJoJ}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Auto Yes Manager - Server-side in-memory state management
|
|
4
|
+
*
|
|
5
|
+
* Manages auto-yes mode per worktree using an in-memory Map.
|
|
6
|
+
* State is automatically cleared on server restart.
|
|
7
|
+
*
|
|
8
|
+
* Issue #138: Added server-side polling functionality to handle
|
|
9
|
+
* auto-yes responses when browser tabs are in background.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.MAX_CONCURRENT_POLLERS = exports.MAX_CONSECUTIVE_ERRORS = exports.MAX_BACKOFF_MS = exports.POLLING_INTERVAL_MS = void 0;
|
|
13
|
+
exports.isValidWorktreeId = isValidWorktreeId;
|
|
14
|
+
exports.calculateBackoffInterval = calculateBackoffInterval;
|
|
15
|
+
exports.isAutoYesExpired = isAutoYesExpired;
|
|
16
|
+
exports.getAutoYesState = getAutoYesState;
|
|
17
|
+
exports.setAutoYesEnabled = setAutoYesEnabled;
|
|
18
|
+
exports.clearAllAutoYesStates = clearAllAutoYesStates;
|
|
19
|
+
exports.getActivePollerCount = getActivePollerCount;
|
|
20
|
+
exports.clearAllPollerStates = clearAllPollerStates;
|
|
21
|
+
exports.getLastServerResponseTimestamp = getLastServerResponseTimestamp;
|
|
22
|
+
exports.startAutoYesPolling = startAutoYesPolling;
|
|
23
|
+
exports.stopAutoYesPolling = stopAutoYesPolling;
|
|
24
|
+
exports.stopAllAutoYesPolling = stopAllAutoYesPolling;
|
|
25
|
+
const cli_session_1 = require("./cli-session");
|
|
26
|
+
const prompt_detector_1 = require("./prompt-detector");
|
|
27
|
+
const auto_yes_resolver_1 = require("./auto-yes-resolver");
|
|
28
|
+
const tmux_1 = require("./tmux");
|
|
29
|
+
const manager_1 = require("./cli-tools/manager");
|
|
30
|
+
const cli_patterns_1 = require("./cli-patterns");
|
|
31
|
+
// =============================================================================
|
|
32
|
+
// Constants (Issue #138)
|
|
33
|
+
// =============================================================================
|
|
34
|
+
/** Polling interval in milliseconds */
|
|
35
|
+
exports.POLLING_INTERVAL_MS = 2000;
|
|
36
|
+
/** Maximum backoff interval in milliseconds (60 seconds) */
|
|
37
|
+
exports.MAX_BACKOFF_MS = 60000;
|
|
38
|
+
/** Number of consecutive errors before applying backoff */
|
|
39
|
+
exports.MAX_CONSECUTIVE_ERRORS = 5;
|
|
40
|
+
/** Maximum concurrent pollers (DoS protection) */
|
|
41
|
+
exports.MAX_CONCURRENT_POLLERS = 50;
|
|
42
|
+
/** Timeout duration: 1 hour in milliseconds */
|
|
43
|
+
const AUTO_YES_TIMEOUT_MS = 3600000;
|
|
44
|
+
/** Worktree ID validation pattern (security: prevent command injection) */
|
|
45
|
+
const WORKTREE_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
46
|
+
// =============================================================================
|
|
47
|
+
// In-memory State
|
|
48
|
+
// =============================================================================
|
|
49
|
+
/** In-memory storage for auto-yes states */
|
|
50
|
+
const autoYesStates = new Map();
|
|
51
|
+
/** In-memory storage for poller states (Issue #138) */
|
|
52
|
+
const autoYesPollerStates = new Map();
|
|
53
|
+
// =============================================================================
|
|
54
|
+
// Utility Functions
|
|
55
|
+
// =============================================================================
|
|
56
|
+
/**
|
|
57
|
+
* Validate worktree ID format (security measure)
|
|
58
|
+
* Only allows alphanumeric characters, hyphens, and underscores.
|
|
59
|
+
*/
|
|
60
|
+
function isValidWorktreeId(worktreeId) {
|
|
61
|
+
if (!worktreeId || worktreeId.length === 0)
|
|
62
|
+
return false;
|
|
63
|
+
return WORKTREE_ID_PATTERN.test(worktreeId);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Calculate backoff interval based on consecutive errors
|
|
67
|
+
*/
|
|
68
|
+
function calculateBackoffInterval(consecutiveErrors) {
|
|
69
|
+
if (consecutiveErrors < exports.MAX_CONSECUTIVE_ERRORS) {
|
|
70
|
+
return exports.POLLING_INTERVAL_MS;
|
|
71
|
+
}
|
|
72
|
+
// Exponential backoff: 2^(errors - 4) * 2000
|
|
73
|
+
// 5 errors: 2^1 * 2000 = 4000
|
|
74
|
+
// 6 errors: 2^2 * 2000 = 8000
|
|
75
|
+
// etc.
|
|
76
|
+
const backoffMultiplier = Math.pow(2, consecutiveErrors - exports.MAX_CONSECUTIVE_ERRORS + 1);
|
|
77
|
+
const backoffMs = exports.POLLING_INTERVAL_MS * backoffMultiplier;
|
|
78
|
+
return Math.min(backoffMs, exports.MAX_BACKOFF_MS);
|
|
79
|
+
}
|
|
80
|
+
// =============================================================================
|
|
81
|
+
// Auto-Yes State Management (Existing)
|
|
82
|
+
// =============================================================================
|
|
83
|
+
/**
|
|
84
|
+
* Check if an auto-yes state has expired
|
|
85
|
+
*/
|
|
86
|
+
function isAutoYesExpired(state) {
|
|
87
|
+
return Date.now() > state.expiresAt;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Get the auto-yes state for a worktree
|
|
91
|
+
* Returns null if no state exists or if expired (auto-disables on expiry)
|
|
92
|
+
*/
|
|
93
|
+
function getAutoYesState(worktreeId) {
|
|
94
|
+
const state = autoYesStates.get(worktreeId);
|
|
95
|
+
if (!state)
|
|
96
|
+
return null;
|
|
97
|
+
// Auto-disable if expired
|
|
98
|
+
if (isAutoYesExpired(state)) {
|
|
99
|
+
const disabledState = {
|
|
100
|
+
...state,
|
|
101
|
+
enabled: false,
|
|
102
|
+
};
|
|
103
|
+
autoYesStates.set(worktreeId, disabledState);
|
|
104
|
+
return disabledState;
|
|
105
|
+
}
|
|
106
|
+
return state;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Set the auto-yes enabled state for a worktree
|
|
110
|
+
*/
|
|
111
|
+
function setAutoYesEnabled(worktreeId, enabled) {
|
|
112
|
+
if (enabled) {
|
|
113
|
+
const now = Date.now();
|
|
114
|
+
const state = {
|
|
115
|
+
enabled: true,
|
|
116
|
+
enabledAt: now,
|
|
117
|
+
expiresAt: now + AUTO_YES_TIMEOUT_MS,
|
|
118
|
+
};
|
|
119
|
+
autoYesStates.set(worktreeId, state);
|
|
120
|
+
return state;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
const existing = autoYesStates.get(worktreeId);
|
|
124
|
+
const state = {
|
|
125
|
+
enabled: false,
|
|
126
|
+
enabledAt: existing?.enabledAt ?? 0,
|
|
127
|
+
expiresAt: existing?.expiresAt ?? 0,
|
|
128
|
+
};
|
|
129
|
+
autoYesStates.set(worktreeId, state);
|
|
130
|
+
return state;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Clear all auto-yes states (for testing)
|
|
135
|
+
*/
|
|
136
|
+
function clearAllAutoYesStates() {
|
|
137
|
+
autoYesStates.clear();
|
|
138
|
+
}
|
|
139
|
+
// =============================================================================
|
|
140
|
+
// Server-side Polling (Issue #138)
|
|
141
|
+
// =============================================================================
|
|
142
|
+
/**
|
|
143
|
+
* Get the number of active pollers
|
|
144
|
+
*/
|
|
145
|
+
function getActivePollerCount() {
|
|
146
|
+
return autoYesPollerStates.size;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Clear all poller states (for testing)
|
|
150
|
+
*/
|
|
151
|
+
function clearAllPollerStates() {
|
|
152
|
+
stopAllAutoYesPolling();
|
|
153
|
+
autoYesPollerStates.clear();
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Get the last server response timestamp for a worktree
|
|
157
|
+
* Used by clients to prevent duplicate responses
|
|
158
|
+
*/
|
|
159
|
+
function getLastServerResponseTimestamp(worktreeId) {
|
|
160
|
+
const pollerState = autoYesPollerStates.get(worktreeId);
|
|
161
|
+
return pollerState?.lastServerResponseTimestamp ?? null;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Update the last server response timestamp
|
|
165
|
+
*/
|
|
166
|
+
function updateLastServerResponseTimestamp(worktreeId, timestamp) {
|
|
167
|
+
const pollerState = autoYesPollerStates.get(worktreeId);
|
|
168
|
+
if (pollerState) {
|
|
169
|
+
pollerState.lastServerResponseTimestamp = timestamp;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Reset error count for a poller
|
|
174
|
+
*/
|
|
175
|
+
function resetErrorCount(worktreeId) {
|
|
176
|
+
const pollerState = autoYesPollerStates.get(worktreeId);
|
|
177
|
+
if (pollerState) {
|
|
178
|
+
pollerState.consecutiveErrors = 0;
|
|
179
|
+
pollerState.currentInterval = exports.POLLING_INTERVAL_MS;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Increment error count and apply backoff if needed
|
|
184
|
+
*/
|
|
185
|
+
function incrementErrorCount(worktreeId) {
|
|
186
|
+
const pollerState = autoYesPollerStates.get(worktreeId);
|
|
187
|
+
if (pollerState) {
|
|
188
|
+
pollerState.consecutiveErrors++;
|
|
189
|
+
pollerState.currentInterval = calculateBackoffInterval(pollerState.consecutiveErrors);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Internal polling function (setTimeout recursive)
|
|
194
|
+
*/
|
|
195
|
+
async function pollAutoYes(worktreeId, cliToolId) {
|
|
196
|
+
// Check if poller was stopped
|
|
197
|
+
const pollerState = autoYesPollerStates.get(worktreeId);
|
|
198
|
+
if (!pollerState)
|
|
199
|
+
return;
|
|
200
|
+
// Check if auto-yes is still enabled
|
|
201
|
+
const autoYesState = getAutoYesState(worktreeId);
|
|
202
|
+
if (!autoYesState?.enabled || isAutoYesExpired(autoYesState)) {
|
|
203
|
+
stopAutoYesPolling(worktreeId);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
// 1. Capture tmux output
|
|
208
|
+
const output = await (0, cli_session_1.captureSessionOutput)(worktreeId, cliToolId, 5000);
|
|
209
|
+
// 2. Strip ANSI codes and detect prompt
|
|
210
|
+
const cleanOutput = (0, cli_patterns_1.stripAnsi)(output);
|
|
211
|
+
const promptDetection = (0, prompt_detector_1.detectPrompt)(cleanOutput);
|
|
212
|
+
if (!promptDetection.isPrompt || !promptDetection.promptData) {
|
|
213
|
+
// No prompt detected, schedule next poll
|
|
214
|
+
scheduleNextPoll(worktreeId, cliToolId);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
// 3. Resolve auto answer
|
|
218
|
+
const answer = (0, auto_yes_resolver_1.resolveAutoAnswer)(promptDetection.promptData);
|
|
219
|
+
if (answer === null) {
|
|
220
|
+
// Cannot auto-answer this prompt
|
|
221
|
+
scheduleNextPoll(worktreeId, cliToolId);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
// 4. Send answer to tmux
|
|
225
|
+
const manager = manager_1.CLIToolManager.getInstance();
|
|
226
|
+
const cliTool = manager.getTool(cliToolId);
|
|
227
|
+
const sessionName = cliTool.getSessionName(worktreeId);
|
|
228
|
+
// Send answer followed by Enter
|
|
229
|
+
await (0, tmux_1.sendKeys)(sessionName, answer, false);
|
|
230
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
231
|
+
await (0, tmux_1.sendKeys)(sessionName, '', true);
|
|
232
|
+
// 5. Update timestamp
|
|
233
|
+
updateLastServerResponseTimestamp(worktreeId, Date.now());
|
|
234
|
+
// 6. Reset error count on success
|
|
235
|
+
resetErrorCount(worktreeId);
|
|
236
|
+
// Log success (without sensitive content)
|
|
237
|
+
console.info(`[Auto-Yes Poller] Sent response for worktree: ${worktreeId}`);
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
// Increment error count on failure
|
|
241
|
+
incrementErrorCount(worktreeId);
|
|
242
|
+
// Log error (without sensitive details)
|
|
243
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
244
|
+
console.warn(`[Auto-Yes Poller] Error for worktree ${worktreeId}: ${errorMessage}`);
|
|
245
|
+
}
|
|
246
|
+
// Schedule next poll
|
|
247
|
+
scheduleNextPoll(worktreeId, cliToolId);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Schedule the next polling iteration
|
|
251
|
+
*/
|
|
252
|
+
function scheduleNextPoll(worktreeId, cliToolId) {
|
|
253
|
+
const pollerState = autoYesPollerStates.get(worktreeId);
|
|
254
|
+
if (!pollerState)
|
|
255
|
+
return;
|
|
256
|
+
pollerState.timerId = setTimeout(() => {
|
|
257
|
+
pollAutoYes(worktreeId, cliToolId);
|
|
258
|
+
}, pollerState.currentInterval);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Start server-side auto-yes polling for a worktree
|
|
262
|
+
*/
|
|
263
|
+
function startAutoYesPolling(worktreeId, cliToolId) {
|
|
264
|
+
// Validate worktree ID (security)
|
|
265
|
+
if (!isValidWorktreeId(worktreeId)) {
|
|
266
|
+
return { started: false, reason: 'invalid worktree ID' };
|
|
267
|
+
}
|
|
268
|
+
// Check if auto-yes is enabled
|
|
269
|
+
const autoYesState = getAutoYesState(worktreeId);
|
|
270
|
+
if (!autoYesState?.enabled) {
|
|
271
|
+
return { started: false, reason: 'auto-yes not enabled' };
|
|
272
|
+
}
|
|
273
|
+
// Check concurrent poller limit (DoS protection)
|
|
274
|
+
// If this worktree already has a poller, don't count it toward the limit
|
|
275
|
+
const existingPoller = autoYesPollerStates.has(worktreeId);
|
|
276
|
+
if (!existingPoller && autoYesPollerStates.size >= exports.MAX_CONCURRENT_POLLERS) {
|
|
277
|
+
return { started: false, reason: 'max concurrent pollers reached' };
|
|
278
|
+
}
|
|
279
|
+
// Stop existing poller if any
|
|
280
|
+
if (existingPoller) {
|
|
281
|
+
stopAutoYesPolling(worktreeId);
|
|
282
|
+
}
|
|
283
|
+
// Create new poller state
|
|
284
|
+
const pollerState = {
|
|
285
|
+
timerId: null,
|
|
286
|
+
cliToolId,
|
|
287
|
+
consecutiveErrors: 0,
|
|
288
|
+
currentInterval: exports.POLLING_INTERVAL_MS,
|
|
289
|
+
lastServerResponseTimestamp: null,
|
|
290
|
+
};
|
|
291
|
+
autoYesPollerStates.set(worktreeId, pollerState);
|
|
292
|
+
// Start polling immediately
|
|
293
|
+
pollerState.timerId = setTimeout(() => {
|
|
294
|
+
pollAutoYes(worktreeId, cliToolId);
|
|
295
|
+
}, exports.POLLING_INTERVAL_MS);
|
|
296
|
+
console.info(`[Auto-Yes Poller] Started for worktree: ${worktreeId}, cliTool: ${cliToolId}`);
|
|
297
|
+
return { started: true };
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Stop server-side auto-yes polling for a worktree
|
|
301
|
+
*/
|
|
302
|
+
function stopAutoYesPolling(worktreeId) {
|
|
303
|
+
const pollerState = autoYesPollerStates.get(worktreeId);
|
|
304
|
+
if (!pollerState)
|
|
305
|
+
return;
|
|
306
|
+
// Clear timer
|
|
307
|
+
if (pollerState.timerId) {
|
|
308
|
+
clearTimeout(pollerState.timerId);
|
|
309
|
+
}
|
|
310
|
+
// Remove state
|
|
311
|
+
autoYesPollerStates.delete(worktreeId);
|
|
312
|
+
console.info(`[Auto-Yes Poller] Stopped for worktree: ${worktreeId}`);
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Stop all server-side auto-yes polling (graceful shutdown)
|
|
316
|
+
*/
|
|
317
|
+
function stopAllAutoYesPolling() {
|
|
318
|
+
for (const [worktreeId, pollerState] of autoYesPollerStates.entries()) {
|
|
319
|
+
if (pollerState.timerId) {
|
|
320
|
+
clearTimeout(pollerState.timerId);
|
|
321
|
+
}
|
|
322
|
+
console.info(`[Auto-Yes Poller] Stopped for worktree: ${worktreeId} (shutdown)`);
|
|
323
|
+
}
|
|
324
|
+
autoYesPollerStates.clear();
|
|
325
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Auto Yes Resolver - Determines automatic answer for prompt data
|
|
4
|
+
*
|
|
5
|
+
* Rules:
|
|
6
|
+
* - yes/no prompt -> 'y'
|
|
7
|
+
* - multiple_choice with default option -> default option number
|
|
8
|
+
* - multiple_choice without default -> first option number
|
|
9
|
+
* - option requiring text input -> null (skip)
|
|
10
|
+
* - unknown type -> null (skip)
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.resolveAutoAnswer = resolveAutoAnswer;
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the automatic answer for a given prompt
|
|
16
|
+
* @returns The answer string to send, or null if auto-answer is not possible
|
|
17
|
+
*/
|
|
18
|
+
function resolveAutoAnswer(promptData) {
|
|
19
|
+
if (promptData.type === 'yes_no') {
|
|
20
|
+
return 'y';
|
|
21
|
+
}
|
|
22
|
+
if (promptData.type === 'multiple_choice') {
|
|
23
|
+
const defaultOpt = promptData.options.find(o => o.isDefault);
|
|
24
|
+
const target = defaultOpt ?? promptData.options[0];
|
|
25
|
+
if (!target) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
if (target.requiresTextInput) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return target.number.toString();
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
@@ -33,8 +33,13 @@ exports.CODEX_THINKING_PATTERN = /•\s*(Planning|Searching|Exploring|Running|Th
|
|
|
33
33
|
/**
|
|
34
34
|
* Claude prompt pattern (waiting for input)
|
|
35
35
|
* Supports both legacy '>' and new '❯' (U+276F) prompt characters
|
|
36
|
+
* Issue #132: Also matches prompts with recommended commands (e.g., "❯ /work-plan")
|
|
37
|
+
*
|
|
38
|
+
* Matches:
|
|
39
|
+
* - Empty prompt: "❯ " or "> "
|
|
40
|
+
* - Prompt with command: "❯ /work-plan" or "> npm install"
|
|
36
41
|
*/
|
|
37
|
-
exports.CLAUDE_PROMPT_PATTERN = /^[>❯]\s
|
|
42
|
+
exports.CLAUDE_PROMPT_PATTERN = /^[>❯](\s*$|\s+\S)/m;
|
|
38
43
|
/**
|
|
39
44
|
* Claude separator pattern
|
|
40
45
|
*/
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Database instance singleton
|
|
4
4
|
* Provides a shared database connection for API routes
|
|
5
|
+
*
|
|
6
|
+
* Issue #135: DB path resolution fix
|
|
7
|
+
* Uses getEnv().CM_DB_PATH for consistent path handling across all install types
|
|
5
8
|
*/
|
|
6
9
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
10
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
@@ -12,10 +15,14 @@ exports.closeDbInstance = closeDbInstance;
|
|
|
12
15
|
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
13
16
|
const path_1 = __importDefault(require("path"));
|
|
14
17
|
const db_migrations_1 = require("./db-migrations");
|
|
18
|
+
const env_1 = require("./env");
|
|
15
19
|
let dbInstance = null;
|
|
16
20
|
/**
|
|
17
21
|
* Get or create the database instance
|
|
18
22
|
*
|
|
23
|
+
* Issue #135: Now uses getEnv().CM_DB_PATH instead of direct DATABASE_PATH access
|
|
24
|
+
* This ensures consistent DB path resolution for both global and local installs.
|
|
25
|
+
*
|
|
19
26
|
* @returns Singleton database instance
|
|
20
27
|
*
|
|
21
28
|
* @example
|
|
@@ -26,13 +33,16 @@ let dbInstance = null;
|
|
|
26
33
|
*/
|
|
27
34
|
function getDbInstance() {
|
|
28
35
|
if (!dbInstance) {
|
|
29
|
-
|
|
36
|
+
// Issue #135: Use getEnv() for consistent DB path resolution
|
|
37
|
+
const env = (0, env_1.getEnv)();
|
|
38
|
+
const dbPath = env.CM_DB_PATH;
|
|
30
39
|
// Ensure the database directory exists
|
|
31
40
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
32
41
|
const fs = require('fs');
|
|
33
42
|
const dir = path_1.default.dirname(dbPath);
|
|
34
43
|
if (!fs.existsSync(dir)) {
|
|
35
|
-
|
|
44
|
+
// SEC-003: Set directory permissions to 0o700 (owner only)
|
|
45
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
36
46
|
}
|
|
37
47
|
dbInstance = new better_sqlite3_1.default(dbPath);
|
|
38
48
|
(0, db_migrations_1.runMigrations)(dbInstance);
|
|
@@ -19,7 +19,7 @@ const db_1 = require("./db");
|
|
|
19
19
|
* Current schema version
|
|
20
20
|
* Increment this when adding new migrations
|
|
21
21
|
*/
|
|
22
|
-
exports.CURRENT_SCHEMA_VERSION =
|
|
22
|
+
exports.CURRENT_SCHEMA_VERSION = 16;
|
|
23
23
|
/**
|
|
24
24
|
* Migration registry
|
|
25
25
|
* All migrations should be added to this array in order
|
|
@@ -650,6 +650,73 @@ const migrations = [
|
|
|
650
650
|
`);
|
|
651
651
|
console.log('✓ Removed initial_branch column from worktrees table');
|
|
652
652
|
}
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
version: 16,
|
|
656
|
+
name: 'add-issue-no-to-external-apps',
|
|
657
|
+
up: (db) => {
|
|
658
|
+
// Issue #136: Add issue_no column to external_apps table
|
|
659
|
+
// This column identifies which worktree/issue an external app belongs to
|
|
660
|
+
// NULL = main app (non-worktree), integer = issue number
|
|
661
|
+
db.exec(`
|
|
662
|
+
ALTER TABLE external_apps ADD COLUMN issue_no INTEGER;
|
|
663
|
+
`);
|
|
664
|
+
// Create index for efficient querying by issue_no
|
|
665
|
+
db.exec(`
|
|
666
|
+
CREATE INDEX IF NOT EXISTS idx_external_apps_issue_no ON external_apps(issue_no);
|
|
667
|
+
`);
|
|
668
|
+
console.log('✓ Added issue_no column to external_apps table');
|
|
669
|
+
console.log('✓ Created index idx_external_apps_issue_no');
|
|
670
|
+
},
|
|
671
|
+
down: (db) => {
|
|
672
|
+
// SQLite doesn't support DROP COLUMN directly
|
|
673
|
+
// Recreate table without issue_no column
|
|
674
|
+
db.exec(`
|
|
675
|
+
-- 1. Create backup table without issue_no
|
|
676
|
+
CREATE TABLE external_apps_backup AS
|
|
677
|
+
SELECT id, name, display_name, description, path_prefix, target_port,
|
|
678
|
+
target_host, app_type, websocket_enabled, websocket_path_pattern,
|
|
679
|
+
enabled, created_at, updated_at
|
|
680
|
+
FROM external_apps;
|
|
681
|
+
|
|
682
|
+
-- 2. Drop original table (this also drops indexes)
|
|
683
|
+
DROP TABLE external_apps;
|
|
684
|
+
|
|
685
|
+
-- 3. Recreate table without issue_no
|
|
686
|
+
CREATE TABLE external_apps (
|
|
687
|
+
id TEXT PRIMARY KEY,
|
|
688
|
+
name TEXT NOT NULL UNIQUE,
|
|
689
|
+
display_name TEXT NOT NULL,
|
|
690
|
+
description TEXT,
|
|
691
|
+
path_prefix TEXT NOT NULL UNIQUE,
|
|
692
|
+
target_port INTEGER NOT NULL,
|
|
693
|
+
target_host TEXT DEFAULT 'localhost',
|
|
694
|
+
app_type TEXT NOT NULL CHECK(app_type IN ('sveltekit', 'streamlit', 'nextjs', 'other')),
|
|
695
|
+
websocket_enabled INTEGER DEFAULT 0,
|
|
696
|
+
websocket_path_pattern TEXT,
|
|
697
|
+
enabled INTEGER DEFAULT 1,
|
|
698
|
+
created_at INTEGER NOT NULL,
|
|
699
|
+
updated_at INTEGER NOT NULL
|
|
700
|
+
);
|
|
701
|
+
|
|
702
|
+
-- 4. Restore data
|
|
703
|
+
INSERT INTO external_apps (id, name, display_name, description, path_prefix,
|
|
704
|
+
target_port, target_host, app_type, websocket_enabled, websocket_path_pattern,
|
|
705
|
+
enabled, created_at, updated_at)
|
|
706
|
+
SELECT id, name, display_name, description, path_prefix,
|
|
707
|
+
target_port, target_host, app_type, websocket_enabled, websocket_path_pattern,
|
|
708
|
+
enabled, created_at, updated_at
|
|
709
|
+
FROM external_apps_backup;
|
|
710
|
+
|
|
711
|
+
-- 5. Drop backup
|
|
712
|
+
DROP TABLE external_apps_backup;
|
|
713
|
+
|
|
714
|
+
-- 6. Recreate original indexes
|
|
715
|
+
CREATE INDEX idx_external_apps_path_prefix ON external_apps(path_prefix);
|
|
716
|
+
CREATE INDEX idx_external_apps_enabled ON external_apps(enabled);
|
|
717
|
+
`);
|
|
718
|
+
console.log('✓ Removed issue_no column from external_apps table');
|
|
719
|
+
}
|
|
653
720
|
}
|
|
654
721
|
];
|
|
655
722
|
/**
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* DB Path Resolver
|
|
4
|
+
* Issue #135: DB path resolution logic fix
|
|
5
|
+
* Issue #136: Import from install-context.ts to avoid circular imports
|
|
6
|
+
*
|
|
7
|
+
* Provides consistent DB path resolution for both global and local installs.
|
|
8
|
+
* This module centralizes DB path logic to follow SRP (Single Responsibility Principle).
|
|
9
|
+
*
|
|
10
|
+
* @module db-path-resolver
|
|
11
|
+
*/
|
|
12
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
13
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
14
|
+
};
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.getDefaultDbPath = getDefaultDbPath;
|
|
17
|
+
exports.getIssueDbPath = getIssueDbPath;
|
|
18
|
+
exports.validateDbPath = validateDbPath;
|
|
19
|
+
const path_1 = __importDefault(require("path"));
|
|
20
|
+
const os_1 = require("os");
|
|
21
|
+
const install_context_1 = require("../cli/utils/install-context");
|
|
22
|
+
const system_directories_1 = require("../config/system-directories");
|
|
23
|
+
/**
|
|
24
|
+
* Get the default database path based on install type
|
|
25
|
+
*
|
|
26
|
+
* For global installs: ~/.commandmate/data/cm.db
|
|
27
|
+
* For local installs: <cwd>/data/cm.db (as absolute path)
|
|
28
|
+
*
|
|
29
|
+
* @returns Absolute path to the default database file
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* const dbPath = getDefaultDbPath();
|
|
34
|
+
* // Global: /Users/username/.commandmate/data/cm.db
|
|
35
|
+
* // Local: /path/to/project/data/cm.db
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
function getDefaultDbPath() {
|
|
39
|
+
if ((0, install_context_1.isGlobalInstall)()) {
|
|
40
|
+
return path_1.default.join((0, os_1.homedir)(), '.commandmate', 'data', 'cm.db');
|
|
41
|
+
}
|
|
42
|
+
return path_1.default.resolve(process.cwd(), 'data', 'cm.db');
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Get the database path for a specific issue (worktree)
|
|
46
|
+
* Issue #136: Worktree-specific DB path
|
|
47
|
+
*
|
|
48
|
+
* @param issueNo - The issue number
|
|
49
|
+
* @returns Absolute path to the issue-specific database file
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* const dbPath = getIssueDbPath(135);
|
|
54
|
+
* // Returns: /Users/username/.commandmate/data/cm-135.db
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
function getIssueDbPath(issueNo) {
|
|
58
|
+
const configDir = (0, install_context_1.getConfigDir)();
|
|
59
|
+
return path_1.default.join(configDir, 'data', `cm-${issueNo}.db`);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Validate database path for security
|
|
63
|
+
*
|
|
64
|
+
* SEC-001: Protects against writing to system directories
|
|
65
|
+
* - Global install: DB path must be within home directory
|
|
66
|
+
* - Local install: DB path must not be in system directories
|
|
67
|
+
*
|
|
68
|
+
* @param dbPath - The database path to validate
|
|
69
|
+
* @returns The resolved absolute path if valid
|
|
70
|
+
* @throws Error if path is in a forbidden location
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```typescript
|
|
74
|
+
* // Valid paths
|
|
75
|
+
* validateDbPath('~/.commandmate/data/cm.db'); // Global
|
|
76
|
+
* validateDbPath('./data/cm.db'); // Local
|
|
77
|
+
*
|
|
78
|
+
* // Invalid paths (throws)
|
|
79
|
+
* validateDbPath('/etc/cm.db');
|
|
80
|
+
* validateDbPath('/var/lib/cm.db');
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
function validateDbPath(dbPath) {
|
|
84
|
+
const resolvedPath = path_1.default.resolve(dbPath);
|
|
85
|
+
if ((0, install_context_1.isGlobalInstall)()) {
|
|
86
|
+
// Global install: DB path must be within home directory
|
|
87
|
+
const homeDir = (0, os_1.homedir)();
|
|
88
|
+
if (!resolvedPath.startsWith(homeDir)) {
|
|
89
|
+
throw new Error(`Security error: DB path must be within home directory: ${resolvedPath}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
// Local install: DB path must not be in system directories (SEC-001)
|
|
94
|
+
if ((0, system_directories_1.isSystemDirectory)(resolvedPath)) {
|
|
95
|
+
throw new Error(`Security error: DB path cannot be in system directory: ${resolvedPath}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return resolvedPath;
|
|
99
|
+
}
|
|
@@ -41,6 +41,7 @@ exports.saveInitialBranch = saveInitialBranch;
|
|
|
41
41
|
exports.getInitialBranch = getInitialBranch;
|
|
42
42
|
exports.getWorktreeIdsByRepository = getWorktreeIdsByRepository;
|
|
43
43
|
exports.deleteRepositoryWorktrees = deleteRepositoryWorktrees;
|
|
44
|
+
exports.deleteWorktreesByIds = deleteWorktreesByIds;
|
|
44
45
|
const crypto_1 = require("crypto");
|
|
45
46
|
function mapChatMessage(row) {
|
|
46
47
|
return {
|
|
@@ -875,3 +876,23 @@ function deleteRepositoryWorktrees(db, repositoryPath) {
|
|
|
875
876
|
const result = stmt.run(repositoryPath);
|
|
876
877
|
return { deletedCount: result.changes };
|
|
877
878
|
}
|
|
879
|
+
/**
|
|
880
|
+
* Delete worktrees by their IDs
|
|
881
|
+
* Related data (chat_messages, session_states, worktree_memos) will be
|
|
882
|
+
* automatically deleted via CASCADE foreign key constraints.
|
|
883
|
+
*
|
|
884
|
+
* @param db - Database instance
|
|
885
|
+
* @param worktreeIds - Array of worktree IDs to delete
|
|
886
|
+
* @returns Object containing the count of deleted worktrees
|
|
887
|
+
*/
|
|
888
|
+
function deleteWorktreesByIds(db, worktreeIds) {
|
|
889
|
+
if (worktreeIds.length === 0) {
|
|
890
|
+
return { deletedCount: 0 };
|
|
891
|
+
}
|
|
892
|
+
const placeholders = worktreeIds.map(() => '?').join(',');
|
|
893
|
+
const stmt = db.prepare(`
|
|
894
|
+
DELETE FROM worktrees WHERE id IN (${placeholders})
|
|
895
|
+
`);
|
|
896
|
+
const result = stmt.run(...worktreeIds);
|
|
897
|
+
return { deletedCount: result.changes };
|
|
898
|
+
}
|