commandmate 0.3.1 → 0.3.3
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/.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 +4 -4
- 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/required-server-files.json +1 -1
- package/.next/routes-manifest.json +1 -1
- package/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/api/app/update-check/route.js +1 -1
- package/.next/server/app/api/external-apps/[id]/health/route.js +1 -1
- package/.next/server/app/api/external-apps/[id]/route.js +1 -1
- package/.next/server/app/api/external-apps/route.js +1 -1
- package/.next/server/app/api/hooks/claude-done/route.js +1 -1
- package/.next/server/app/api/ollama/models/route.js +1 -0
- package/.next/server/app/api/ollama/models/route.js.nft.json +1 -0
- package/.next/server/app/api/ollama/models.body +1 -0
- package/.next/server/app/api/ollama/models.meta +1 -0
- 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/excluded/route.js +7 -7
- package/.next/server/app/api/repositories/restore/route.js +3 -3
- package/.next/server/app/api/repositories/route.js +13 -11
- 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 +3 -3
- 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]/current-output/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/execution-logs/[logId]/route.js +1 -0
- package/.next/server/app/api/worktrees/[id]/execution-logs/[logId]/route.js.nft.json +1 -0
- package/.next/server/app/api/worktrees/[id]/execution-logs/route.js +9 -0
- package/.next/server/app/api/worktrees/[id]/execution-logs/route.js.nft.json +1 -0
- 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]/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 +2 -2
- 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]/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]/schedules/[scheduleId]/route.js +1 -0
- package/.next/server/app/api/worktrees/[id]/schedules/[scheduleId]/route.js.nft.json +1 -0
- package/.next/server/app/api/worktrees/[id]/schedules/route.js +4 -0
- package/.next/server/app/api/worktrees/[id]/schedules/route.js.nft.json +1 -0
- 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/[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/api/worktrees/route.js.nft.json +1 -1
- package/.next/server/app/login/page.js.nft.json +1 -1
- package/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/server/app/page.js +1 -1
- 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 +1 -1
- package/.next/server/app/worktrees/[id]/files/[...path]/page.js.nft.json +1 -1
- package/.next/server/app/worktrees/[id]/files/[...path]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/page.js +8 -3
- 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]/terminal/page.js.nft.json +1 -1
- package/.next/server/app/worktrees/[id]/terminal/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +10 -5
- package/.next/server/chunks/2314.js +1 -0
- package/.next/server/chunks/3860.js +1 -1
- package/.next/server/chunks/4559.js +1 -1
- package/.next/server/chunks/539.js +10 -10
- package/.next/server/chunks/5853.js +1 -1
- package/.next/server/chunks/6228.js +1 -0
- package/.next/server/chunks/7425.js +112 -37
- package/.next/server/chunks/7566.js +1 -1
- package/.next/server/chunks/8693.js +1 -1
- package/.next/server/chunks/9446.js +1 -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 +5 -5
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/8091-274bc0716106e7fc.js +1 -0
- package/.next/static/chunks/app/page-060057e02b841125.js +1 -0
- package/.next/static/chunks/app/worktrees/[id]/page-78580947c201d698.js +1 -0
- package/.next/static/chunks/{main-db79434ee4a6c931.js → main-2feda12a4d321111.js} +1 -1
- package/.next/static/css/e85de230ef5ddc40.css +3 -0
- package/.next/trace +5 -5
- package/.next/types/app/api/ollama/models/route.ts +343 -0
- package/.next/types/app/api/worktrees/[id]/execution-logs/[logId]/route.ts +343 -0
- package/.next/types/app/api/worktrees/[id]/execution-logs/route.ts +343 -0
- package/.next/types/app/api/worktrees/[id]/schedules/[scheduleId]/route.ts +343 -0
- package/.next/types/app/api/worktrees/[id]/schedules/route.ts +343 -0
- package/README.md +74 -76
- package/dist/cli/utils/docs-reader.d.ts.map +1 -1
- package/dist/cli/utils/docs-reader.js +1 -0
- package/dist/server/server.js +5 -0
- package/dist/server/src/config/cmate-constants.js +79 -0
- package/dist/server/src/config/schedule-config.js +60 -0
- package/dist/server/src/lib/auto-yes-manager.js +2 -2
- package/dist/server/src/lib/claude-executor.js +158 -0
- package/dist/server/src/lib/cli-patterns.js +73 -9
- package/dist/server/src/lib/cli-tools/gemini.js +81 -22
- package/dist/server/src/lib/cli-tools/manager.js +4 -2
- package/dist/server/src/lib/cli-tools/types.js +64 -2
- package/dist/server/src/lib/cli-tools/vibe-local.js +163 -0
- package/dist/server/src/lib/cmate-parser.js +262 -0
- package/dist/server/src/lib/db-instance.js +3 -0
- package/dist/server/src/lib/db-migrations.js +145 -2
- package/dist/server/src/lib/db.js +51 -1
- package/dist/server/src/lib/env-sanitizer.js +57 -0
- package/dist/server/src/lib/prompt-detector.js +4 -3
- package/dist/server/src/lib/response-poller.js +22 -11
- package/dist/server/src/lib/schedule-manager.js +401 -0
- package/dist/server/src/lib/selected-agents-validator.js +99 -0
- package/dist/server/src/types/cmate.js +6 -0
- package/dist/server/src/types/sidebar.js +9 -4
- package/package.json +2 -1
- package/.next/server/chunks/7536.js +0 -1
- package/.next/static/chunks/8091-925542bdfc843dce.js +0 -1
- package/.next/static/chunks/app/page-238b5a70d8c101e9.js +0 -1
- package/.next/static/chunks/app/worktrees/[id]/page-a556551ce5c69dec.js +0 -1
- package/.next/static/css/b9ea6a4fad17dc32.css +0 -3
- /package/.next/static/{hmAjbCPjxX_C0Os7rphI1 → O7EDFfAYQNe_HRbORxQAC}/_buildManifest.js +0 -0
- /package/.next/static/{hmAjbCPjxX_C0Os7rphI1 → O7EDFfAYQNe_HRbORxQAC}/_ssgManifest.js +0 -0
|
@@ -32,6 +32,8 @@ exports.markPendingPromptsAsAnswered = markPendingPromptsAsAnswered;
|
|
|
32
32
|
exports.updateFavorite = updateFavorite;
|
|
33
33
|
exports.updateStatus = updateStatus;
|
|
34
34
|
exports.updateCliToolId = updateCliToolId;
|
|
35
|
+
exports.updateSelectedAgents = updateSelectedAgents;
|
|
36
|
+
exports.updateVibeLocalModel = updateVibeLocalModel;
|
|
35
37
|
exports.getMemosByWorktreeId = getMemosByWorktreeId;
|
|
36
38
|
exports.getMemoById = getMemoById;
|
|
37
39
|
exports.createMemo = createMemo;
|
|
@@ -44,6 +46,7 @@ exports.getWorktreeIdsByRepository = getWorktreeIdsByRepository;
|
|
|
44
46
|
exports.deleteRepositoryWorktrees = deleteRepositoryWorktrees;
|
|
45
47
|
exports.deleteWorktreesByIds = deleteWorktreesByIds;
|
|
46
48
|
const crypto_1 = require("crypto");
|
|
49
|
+
const selected_agents_validator_1 = require("../lib/selected-agents-validator");
|
|
47
50
|
function mapChatMessage(row) {
|
|
48
51
|
return {
|
|
49
52
|
id: row.id,
|
|
@@ -129,6 +132,16 @@ function initDatabase(db) {
|
|
|
129
132
|
* Get latest user message per CLI tool for multiple worktrees (batch query)
|
|
130
133
|
* Optimized to avoid N+1 query problem
|
|
131
134
|
*/
|
|
135
|
+
/**
|
|
136
|
+
* Get latest user message per CLI tool for multiple worktrees (batch query)
|
|
137
|
+
* Optimized to avoid N+1 query problem
|
|
138
|
+
*
|
|
139
|
+
* R4-001: SQL IN clause for cli_tool_id removed to eliminate SQLインジェクション risk.
|
|
140
|
+
* All cli_tool_id values are fetched; filtering happens at application layer.
|
|
141
|
+
* Tool count is at most 4-5, so the performance impact is negligible.
|
|
142
|
+
*
|
|
143
|
+
* R2-002: Return type changed to Partial<Record<CLIToolType, string>>
|
|
144
|
+
*/
|
|
132
145
|
function getLastMessagesByCliBatch(db, worktreeIds) {
|
|
133
146
|
if (worktreeIds.length === 0) {
|
|
134
147
|
return new Map();
|
|
@@ -149,7 +162,6 @@ function getLastMessagesByCliBatch(db, worktreeIds) {
|
|
|
149
162
|
FROM chat_messages
|
|
150
163
|
WHERE worktree_id IN (${placeholders})
|
|
151
164
|
AND role = 'user'
|
|
152
|
-
AND cli_tool_id IN ('claude', 'codex', 'gemini')
|
|
153
165
|
)
|
|
154
166
|
SELECT worktree_id, cli_tool_id, content
|
|
155
167
|
FROM ranked_messages
|
|
@@ -181,6 +193,7 @@ function getWorktrees(db, repositoryPath) {
|
|
|
181
193
|
w.id, w.name, w.path, w.repository_path, w.repository_name, w.description,
|
|
182
194
|
w.last_user_message, w.last_user_message_at, w.last_message_summary,
|
|
183
195
|
w.updated_at, w.favorite, w.status, w.link, w.cli_tool_id, w.last_viewed_at,
|
|
196
|
+
w.selected_agents, w.vibe_local_model,
|
|
184
197
|
(SELECT MAX(timestamp) FROM chat_messages
|
|
185
198
|
WHERE worktree_id = w.id AND role = 'assistant') as last_assistant_message_at
|
|
186
199
|
FROM worktrees w
|
|
@@ -216,6 +229,8 @@ function getWorktrees(db, repositoryPath) {
|
|
|
216
229
|
status: row.status || null,
|
|
217
230
|
link: row.link || undefined,
|
|
218
231
|
cliToolId: row.cli_tool_id ?? 'claude',
|
|
232
|
+
selectedAgents: (0, selected_agents_validator_1.parseSelectedAgents)(row.selected_agents),
|
|
233
|
+
vibeLocalModel: row.vibe_local_model ?? null,
|
|
219
234
|
};
|
|
220
235
|
});
|
|
221
236
|
}
|
|
@@ -250,6 +265,7 @@ function getWorktreeById(db, id) {
|
|
|
250
265
|
w.id, w.name, w.path, w.repository_path, w.repository_name, w.description,
|
|
251
266
|
w.last_user_message, w.last_user_message_at, w.last_message_summary,
|
|
252
267
|
w.updated_at, w.favorite, w.status, w.link, w.cli_tool_id, w.last_viewed_at,
|
|
268
|
+
w.selected_agents, w.vibe_local_model,
|
|
253
269
|
(SELECT MAX(timestamp) FROM chat_messages
|
|
254
270
|
WHERE worktree_id = w.id AND role = 'assistant') as last_assistant_message_at
|
|
255
271
|
FROM worktrees w
|
|
@@ -276,6 +292,8 @@ function getWorktreeById(db, id) {
|
|
|
276
292
|
status: row.status || null,
|
|
277
293
|
link: row.link || undefined,
|
|
278
294
|
cliToolId: row.cli_tool_id ?? 'claude',
|
|
295
|
+
selectedAgents: (0, selected_agents_validator_1.parseSelectedAgents)(row.selected_agents),
|
|
296
|
+
vibeLocalModel: row.vibe_local_model ?? null,
|
|
279
297
|
};
|
|
280
298
|
}
|
|
281
299
|
/**
|
|
@@ -678,6 +696,38 @@ function updateCliToolId(db, id, cliToolId) {
|
|
|
678
696
|
`);
|
|
679
697
|
stmt.run(cliToolId, id);
|
|
680
698
|
}
|
|
699
|
+
/**
|
|
700
|
+
* Update selected_agents for a worktree
|
|
701
|
+
* Issue #368: Persists the user's choice of 2 display agents
|
|
702
|
+
*
|
|
703
|
+
* @param db - Database instance
|
|
704
|
+
* @param id - Worktree ID
|
|
705
|
+
* @param selectedAgents - Tuple of 2 CLIToolType values
|
|
706
|
+
*/
|
|
707
|
+
function updateSelectedAgents(db, id, selectedAgents) {
|
|
708
|
+
const stmt = db.prepare(`
|
|
709
|
+
UPDATE worktrees
|
|
710
|
+
SET selected_agents = ?
|
|
711
|
+
WHERE id = ?
|
|
712
|
+
`);
|
|
713
|
+
stmt.run(JSON.stringify(selectedAgents), id);
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Update vibe_local_model for a worktree
|
|
717
|
+
* Issue #368: Persists the user's Ollama model selection for vibe-local
|
|
718
|
+
*
|
|
719
|
+
* @param db - Database instance
|
|
720
|
+
* @param id - Worktree ID
|
|
721
|
+
* @param model - Model name or null for default
|
|
722
|
+
*/
|
|
723
|
+
function updateVibeLocalModel(db, id, model) {
|
|
724
|
+
const stmt = db.prepare(`
|
|
725
|
+
UPDATE worktrees
|
|
726
|
+
SET vibe_local_model = ?
|
|
727
|
+
WHERE id = ?
|
|
728
|
+
`);
|
|
729
|
+
stmt.run(model, id);
|
|
730
|
+
}
|
|
681
731
|
/**
|
|
682
732
|
* Map database row to WorktreeMemo model
|
|
683
733
|
*/
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Environment Variable Sanitizer
|
|
4
|
+
* Issue #294: Sanitizes environment variables for child processes
|
|
5
|
+
*
|
|
6
|
+
* Removes sensitive environment variables (auth tokens, certificates, database paths)
|
|
7
|
+
* before spawning child processes like `claude -p`.
|
|
8
|
+
*
|
|
9
|
+
* [S1-001/S4-001] Centralized sensitive key management
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.SENSITIVE_ENV_KEYS = void 0;
|
|
13
|
+
exports.sanitizeEnvForChildProcess = sanitizeEnvForChildProcess;
|
|
14
|
+
/**
|
|
15
|
+
* List of environment variable keys that must be removed before
|
|
16
|
+
* passing environment to child processes.
|
|
17
|
+
*
|
|
18
|
+
* These include authentication tokens, TLS certificates, IP restriction
|
|
19
|
+
* settings, and database paths that should not be inherited by spawned
|
|
20
|
+
* CLI tool processes.
|
|
21
|
+
*/
|
|
22
|
+
exports.SENSITIVE_ENV_KEYS = [
|
|
23
|
+
'CLAUDECODE',
|
|
24
|
+
'CM_AUTH_TOKEN_HASH',
|
|
25
|
+
'CM_AUTH_EXPIRE',
|
|
26
|
+
'CM_HTTPS_KEY',
|
|
27
|
+
'CM_HTTPS_CERT',
|
|
28
|
+
'CM_ALLOWED_IPS',
|
|
29
|
+
'CM_TRUST_PROXY',
|
|
30
|
+
'CM_DB_PATH',
|
|
31
|
+
];
|
|
32
|
+
/**
|
|
33
|
+
* Create a sanitized copy of process.env suitable for child processes.
|
|
34
|
+
*
|
|
35
|
+
* Removes all keys listed in SENSITIVE_ENV_KEYS from the environment.
|
|
36
|
+
* Non-sensitive variables (PATH, HOME, NODE_ENV, etc.) are preserved.
|
|
37
|
+
*
|
|
38
|
+
* @returns A shallow copy of process.env with sensitive keys removed
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* import { execFile } from 'child_process';
|
|
43
|
+
* import { sanitizeEnvForChildProcess } from './env-sanitizer';
|
|
44
|
+
*
|
|
45
|
+
* execFile('claude', ['-p', message], {
|
|
46
|
+
* env: sanitizeEnvForChildProcess(),
|
|
47
|
+
* cwd: worktreePath,
|
|
48
|
+
* });
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
function sanitizeEnvForChildProcess() {
|
|
52
|
+
const env = { ...process.env };
|
|
53
|
+
for (const key of exports.SENSITIVE_ENV_KEYS) {
|
|
54
|
+
delete env[key];
|
|
55
|
+
}
|
|
56
|
+
return env;
|
|
57
|
+
}
|
|
@@ -166,11 +166,12 @@ const TEXT_INPUT_PATTERNS = [
|
|
|
166
166
|
/differently/i,
|
|
167
167
|
];
|
|
168
168
|
/**
|
|
169
|
-
* Pattern for ❯ (U+276F) indicator lines used by
|
|
169
|
+
* Pattern for ❯ (U+276F) / ● (U+25CF) indicator lines used by CLI tools to mark the default selection.
|
|
170
|
+
* Claude CLI uses ❯, Gemini CLI uses ●.
|
|
170
171
|
* Used in Pass 1 (existence check) and Pass 2 (option collection) of the 2-pass detection.
|
|
171
172
|
* Anchored at both ends -- ReDoS safe (S4-001).
|
|
172
173
|
*/
|
|
173
|
-
const DEFAULT_OPTION_PATTERN = /^\s
|
|
174
|
+
const DEFAULT_OPTION_PATTERN = /^\s*[\u276F\u25CF]\s*(\d+)\.\s*(.+)$/;
|
|
174
175
|
/**
|
|
175
176
|
* Pattern for normal option lines (no ❯ indicator, just leading whitespace + number).
|
|
176
177
|
* Only applied in Pass 2 when ❯ indicator existence is confirmed by Pass 1.
|
|
@@ -567,7 +568,7 @@ function detectMultipleChoicePrompt(output, options) {
|
|
|
567
568
|
// user input prompt (e.g., "❯ 1", "❯ /command") or idle prompt ("❯").
|
|
568
569
|
// Anything above this line in the scrollback is historical conversation text,
|
|
569
570
|
// not an active prompt. Stop scanning to prevent false positives.
|
|
570
|
-
if (collectedOptions.length === 0 && line.startsWith('\u276F')) {
|
|
571
|
+
if (collectedOptions.length === 0 && (line.startsWith('\u276F') || line.startsWith('\u25CF'))) {
|
|
571
572
|
return noPromptResult(output);
|
|
572
573
|
}
|
|
573
574
|
// Non-option line handling
|
|
@@ -131,7 +131,7 @@ function getPollerKey(worktreeId, cliToolId) {
|
|
|
131
131
|
*/
|
|
132
132
|
function detectPromptWithOptions(output, cliToolId) {
|
|
133
133
|
const promptOptions = (0, cli_patterns_1.buildDetectPromptOptions)(cliToolId);
|
|
134
|
-
return (0, prompt_detector_1.detectPrompt)((0, cli_patterns_1.stripAnsi)(output), promptOptions);
|
|
134
|
+
return (0, prompt_detector_1.detectPrompt)((0, cli_patterns_1.stripBoxDrawing)((0, cli_patterns_1.stripAnsi)(output)), promptOptions);
|
|
135
135
|
}
|
|
136
136
|
/**
|
|
137
137
|
* Clean up Claude response by removing shell setup commands, environment exports, ANSI codes, and banner
|
|
@@ -372,11 +372,12 @@ function extractResponse(output, lastCapturedLine, cliToolId) {
|
|
|
372
372
|
const hasPrompt = promptPattern.test(cleanOutputToCheck);
|
|
373
373
|
const hasSeparator = separatorPattern.test(cleanOutputToCheck);
|
|
374
374
|
const isThinking = thinkingPattern.test(cleanOutputToCheck);
|
|
375
|
-
// Codex/Gemini completion logic: prompt detected and not thinking (separator optional)
|
|
375
|
+
// Codex/Gemini/Vibe-Local completion logic: prompt detected and not thinking (separator optional)
|
|
376
376
|
// - Codex: Interactive TUI, detects › prompt
|
|
377
|
-
// - Gemini:
|
|
377
|
+
// - Gemini: Interactive REPL, detects > / ❯ prompt
|
|
378
|
+
// - Vibe-Local: Interactive REPL, detects > prompt
|
|
378
379
|
// Claude: require both prompt and separator
|
|
379
|
-
const isCodexOrGeminiComplete = (cliToolId === 'codex' || cliToolId === 'gemini') && hasPrompt && !isThinking;
|
|
380
|
+
const isCodexOrGeminiComplete = (cliToolId === 'codex' || cliToolId === 'gemini' || cliToolId === 'vibe-local') && hasPrompt && !isThinking;
|
|
380
381
|
const isClaudeComplete = cliToolId === 'claude' && hasPrompt && hasSeparator && !isThinking;
|
|
381
382
|
if (isCodexOrGeminiComplete || isClaudeComplete) {
|
|
382
383
|
// CLI tool has completed response
|
|
@@ -667,10 +668,15 @@ function startPolling(worktreeId, cliToolId) {
|
|
|
667
668
|
stopPolling(worktreeId, cliToolId);
|
|
668
669
|
// Record start time
|
|
669
670
|
pollingStartTimes.set(pollerKey, Date.now());
|
|
670
|
-
// Start polling
|
|
671
|
-
|
|
672
|
-
|
|
671
|
+
// Start polling with setTimeout chain to prevent race conditions
|
|
672
|
+
scheduleNextResponsePoll(worktreeId, cliToolId);
|
|
673
|
+
}
|
|
674
|
+
/** Schedule next checkForResponse() after current one completes (setTimeout chain) */
|
|
675
|
+
function scheduleNextResponsePoll(worktreeId, cliToolId) {
|
|
676
|
+
const pollerKey = getPollerKey(worktreeId, cliToolId);
|
|
677
|
+
const timerId = setTimeout(async () => {
|
|
673
678
|
// Check if max duration exceeded
|
|
679
|
+
const startTime = pollingStartTimes.get(pollerKey);
|
|
674
680
|
if (startTime && Date.now() - startTime > MAX_POLLING_DURATION) {
|
|
675
681
|
stopPolling(worktreeId, cliToolId);
|
|
676
682
|
return;
|
|
@@ -682,8 +688,13 @@ function startPolling(worktreeId, cliToolId) {
|
|
|
682
688
|
catch (error) {
|
|
683
689
|
console.error(`[Poller] Error:`, error);
|
|
684
690
|
}
|
|
691
|
+
// Schedule next poll ONLY after current one completes
|
|
692
|
+
// Guard: only if poller is still active (not stopped during checkForResponse)
|
|
693
|
+
if (activePollers.has(pollerKey)) {
|
|
694
|
+
scheduleNextResponsePoll(worktreeId, cliToolId);
|
|
695
|
+
}
|
|
685
696
|
}, POLLING_INTERVAL);
|
|
686
|
-
activePollers.set(pollerKey,
|
|
697
|
+
activePollers.set(pollerKey, timerId);
|
|
687
698
|
}
|
|
688
699
|
/**
|
|
689
700
|
* Stop polling for a worktree and CLI tool combination
|
|
@@ -698,9 +709,9 @@ function startPolling(worktreeId, cliToolId) {
|
|
|
698
709
|
*/
|
|
699
710
|
function stopPolling(worktreeId, cliToolId) {
|
|
700
711
|
const pollerKey = getPollerKey(worktreeId, cliToolId);
|
|
701
|
-
const
|
|
702
|
-
if (
|
|
703
|
-
|
|
712
|
+
const timerId = activePollers.get(pollerKey);
|
|
713
|
+
if (timerId) {
|
|
714
|
+
clearTimeout(timerId);
|
|
704
715
|
activePollers.delete(pollerKey);
|
|
705
716
|
pollingStartTimes.delete(pollerKey);
|
|
706
717
|
}
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Schedule Manager
|
|
4
|
+
* Issue #294: Manages scheduled execution of claude -p commands
|
|
5
|
+
*
|
|
6
|
+
* Uses a single timer to periodically scan all worktrees for CMATE.md changes
|
|
7
|
+
* and execute scheduled tasks via croner cron expressions.
|
|
8
|
+
*
|
|
9
|
+
* Patterns:
|
|
10
|
+
* - globalThis for hot reload persistence (same as auto-yes-manager.ts)
|
|
11
|
+
* - Single timer for all worktrees (60 second polling interval)
|
|
12
|
+
* - SIGKILL fire-and-forget for stopAllSchedules (< 1ms, within 3s graceful shutdown)
|
|
13
|
+
*
|
|
14
|
+
* [S3-001] stopAllSchedules() uses synchronous process.kill for immediate cleanup
|
|
15
|
+
* [S3-010] initScheduleManager() is called after initializeWorktrees()
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.MAX_CONCURRENT_SCHEDULES = exports.POLL_INTERVAL_MS = void 0;
|
|
19
|
+
exports.initScheduleManager = initScheduleManager;
|
|
20
|
+
exports.stopAllSchedules = stopAllSchedules;
|
|
21
|
+
exports.getActiveScheduleCount = getActiveScheduleCount;
|
|
22
|
+
exports.isScheduleManagerInitialized = isScheduleManagerInitialized;
|
|
23
|
+
const crypto_1 = require("crypto");
|
|
24
|
+
const croner_1 = require("croner");
|
|
25
|
+
const cmate_parser_1 = require("./cmate-parser");
|
|
26
|
+
const claude_executor_1 = require("./claude-executor");
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// Constants
|
|
29
|
+
// =============================================================================
|
|
30
|
+
/** Polling interval for CMATE.md changes (60 seconds) */
|
|
31
|
+
exports.POLL_INTERVAL_MS = 60 * 1000;
|
|
32
|
+
/** Maximum number of concurrent schedules across all worktrees */
|
|
33
|
+
exports.MAX_CONCURRENT_SCHEDULES = 100;
|
|
34
|
+
/**
|
|
35
|
+
* Get or initialize the global manager state.
|
|
36
|
+
*/
|
|
37
|
+
function getManagerState() {
|
|
38
|
+
if (!globalThis.__scheduleManagerStates) {
|
|
39
|
+
globalThis.__scheduleManagerStates = {
|
|
40
|
+
timerId: null,
|
|
41
|
+
schedules: new Map(),
|
|
42
|
+
initialized: false,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return globalThis.__scheduleManagerStates;
|
|
46
|
+
}
|
|
47
|
+
// =============================================================================
|
|
48
|
+
// Lazy DB Accessor
|
|
49
|
+
// =============================================================================
|
|
50
|
+
/**
|
|
51
|
+
* Lazy-load the DB instance to avoid circular import issues.
|
|
52
|
+
* The db-instance module is loaded at runtime via require() because
|
|
53
|
+
* schedule-manager.ts is imported early in the server lifecycle.
|
|
54
|
+
*
|
|
55
|
+
* @returns The SQLite database instance
|
|
56
|
+
*/
|
|
57
|
+
function getLazyDbInstance() {
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
59
|
+
const { getDbInstance } = require('./db-instance');
|
|
60
|
+
return getDbInstance();
|
|
61
|
+
}
|
|
62
|
+
// =============================================================================
|
|
63
|
+
// DB Operations
|
|
64
|
+
// =============================================================================
|
|
65
|
+
/**
|
|
66
|
+
* Get all worktrees from the database.
|
|
67
|
+
*
|
|
68
|
+
* @returns Array of worktree rows with id and path
|
|
69
|
+
*/
|
|
70
|
+
function getAllWorktrees() {
|
|
71
|
+
try {
|
|
72
|
+
const db = getLazyDbInstance();
|
|
73
|
+
return db.prepare('SELECT id, path FROM worktrees').all();
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
console.error('[schedule-manager] Failed to get worktrees:', error);
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Upsert a schedule entry into the database.
|
|
82
|
+
* If a schedule with the same worktree_id and name exists, it is updated.
|
|
83
|
+
* Otherwise, a new schedule is created.
|
|
84
|
+
*
|
|
85
|
+
* @param worktreeId - The worktree ID to associate the schedule with
|
|
86
|
+
* @param entry - The schedule entry from CMATE.md
|
|
87
|
+
* @returns The schedule ID (existing or newly created)
|
|
88
|
+
*/
|
|
89
|
+
function upsertSchedule(worktreeId, entry) {
|
|
90
|
+
const db = getLazyDbInstance();
|
|
91
|
+
const now = Date.now();
|
|
92
|
+
// Check if schedule already exists
|
|
93
|
+
const existing = db.prepare('SELECT id FROM scheduled_executions WHERE worktree_id = ? AND name = ?').get(worktreeId, entry.name);
|
|
94
|
+
if (existing) {
|
|
95
|
+
db.prepare(`
|
|
96
|
+
UPDATE scheduled_executions
|
|
97
|
+
SET message = ?, cron_expression = ?, cli_tool_id = ?, enabled = ?, updated_at = ?
|
|
98
|
+
WHERE id = ?
|
|
99
|
+
`).run(entry.message, entry.cronExpression, entry.cliToolId, entry.enabled ? 1 : 0, now, existing.id);
|
|
100
|
+
return existing.id;
|
|
101
|
+
}
|
|
102
|
+
const id = (0, crypto_1.randomUUID)();
|
|
103
|
+
db.prepare(`
|
|
104
|
+
INSERT INTO scheduled_executions (id, worktree_id, name, message, cron_expression, cli_tool_id, enabled, created_at, updated_at)
|
|
105
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
106
|
+
`).run(id, worktreeId, entry.name, entry.message, entry.cronExpression, entry.cliToolId, entry.enabled ? 1 : 0, now, now);
|
|
107
|
+
return id;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Create an execution log entry in 'running' status.
|
|
111
|
+
*
|
|
112
|
+
* @param scheduleId - The parent schedule ID
|
|
113
|
+
* @param worktreeId - The worktree ID
|
|
114
|
+
* @param message - The execution message/prompt
|
|
115
|
+
* @returns The new execution log ID
|
|
116
|
+
*/
|
|
117
|
+
function createExecutionLog(scheduleId, worktreeId, message) {
|
|
118
|
+
const db = getLazyDbInstance();
|
|
119
|
+
const now = Date.now();
|
|
120
|
+
const id = (0, crypto_1.randomUUID)();
|
|
121
|
+
db.prepare(`
|
|
122
|
+
INSERT INTO execution_logs (id, schedule_id, worktree_id, message, status, started_at, created_at)
|
|
123
|
+
VALUES (?, ?, ?, ?, 'running', ?, ?)
|
|
124
|
+
`).run(id, scheduleId, worktreeId, message, now, now);
|
|
125
|
+
return id;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Update an execution log entry with results.
|
|
129
|
+
*
|
|
130
|
+
* @param logId - The execution log ID to update
|
|
131
|
+
* @param status - The final execution status
|
|
132
|
+
* @param result - The execution output or error message
|
|
133
|
+
* @param exitCode - The process exit code, or null if unknown
|
|
134
|
+
*/
|
|
135
|
+
function updateExecutionLog(logId, status, result, exitCode) {
|
|
136
|
+
const db = getLazyDbInstance();
|
|
137
|
+
const now = Date.now();
|
|
138
|
+
db.prepare(`
|
|
139
|
+
UPDATE execution_logs SET status = ?, result = ?, exit_code = ?, completed_at = ? WHERE id = ?
|
|
140
|
+
`).run(status, result, exitCode, now, logId);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Update the last_executed_at timestamp for a schedule.
|
|
144
|
+
*
|
|
145
|
+
* @param scheduleId - The schedule ID to update
|
|
146
|
+
*/
|
|
147
|
+
function updateScheduleLastExecuted(scheduleId) {
|
|
148
|
+
const db = getLazyDbInstance();
|
|
149
|
+
const now = Date.now();
|
|
150
|
+
db.prepare('UPDATE scheduled_executions SET last_executed_at = ?, updated_at = ? WHERE id = ?')
|
|
151
|
+
.run(now, now, scheduleId);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Recovery: mark all 'running' execution logs as 'failed' on startup.
|
|
155
|
+
* This handles the case where the server was killed while executions
|
|
156
|
+
* were still in progress.
|
|
157
|
+
*/
|
|
158
|
+
function recoverRunningLogs() {
|
|
159
|
+
try {
|
|
160
|
+
const db = getLazyDbInstance();
|
|
161
|
+
const now = Date.now();
|
|
162
|
+
const result = db.prepare("UPDATE execution_logs SET status = 'failed', completed_at = ? WHERE status = 'running'").run(now);
|
|
163
|
+
if (result.changes > 0) {
|
|
164
|
+
console.warn(`[schedule-manager] Recovered ${result.changes} stale running execution(s) to failed status`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
console.error('[schedule-manager] Failed to recover running logs:', error);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Disable DB schedules that are no longer present in CMATE.md.
|
|
173
|
+
* Sets enabled = 0 for schedules belonging to the given worktrees
|
|
174
|
+
* that are not in the activeScheduleIds set.
|
|
175
|
+
* Skips records already disabled to avoid unnecessary DB writes.
|
|
176
|
+
*
|
|
177
|
+
* @param activeScheduleIds - Set of schedule IDs currently active from CMATE.md
|
|
178
|
+
* @param worktreeIds - Array of worktree IDs that were scanned
|
|
179
|
+
*/
|
|
180
|
+
function disableStaleSchedules(activeScheduleIds, worktreeIds) {
|
|
181
|
+
if (worktreeIds.length === 0)
|
|
182
|
+
return;
|
|
183
|
+
try {
|
|
184
|
+
const db = getLazyDbInstance();
|
|
185
|
+
const now = Date.now();
|
|
186
|
+
const placeholders = worktreeIds.map(() => '?').join(',');
|
|
187
|
+
// Get enabled schedules for the scanned worktrees
|
|
188
|
+
const rows = db.prepare(`SELECT id FROM scheduled_executions WHERE worktree_id IN (${placeholders}) AND enabled = 1`).all(...worktreeIds);
|
|
189
|
+
let disabledCount = 0;
|
|
190
|
+
const updateStmt = db.prepare('UPDATE scheduled_executions SET enabled = 0, updated_at = ? WHERE id = ?');
|
|
191
|
+
for (const row of rows) {
|
|
192
|
+
if (!activeScheduleIds.has(row.id)) {
|
|
193
|
+
updateStmt.run(now, row.id);
|
|
194
|
+
disabledCount++;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (disabledCount > 0) {
|
|
198
|
+
console.log(`[schedule-manager] Disabled ${disabledCount} stale DB schedule(s)`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
console.error('[schedule-manager] Failed to disable stale schedules:', error);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// =============================================================================
|
|
206
|
+
// Schedule Execution
|
|
207
|
+
// =============================================================================
|
|
208
|
+
/**
|
|
209
|
+
* Execute a scheduled task.
|
|
210
|
+
* Guards against concurrent execution of the same schedule.
|
|
211
|
+
*
|
|
212
|
+
* @param state - The schedule state to execute
|
|
213
|
+
*/
|
|
214
|
+
async function executeSchedule(state) {
|
|
215
|
+
if (state.isExecuting) {
|
|
216
|
+
console.warn(`[schedule-manager] Skipping concurrent execution for schedule ${state.entry.name}`);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
state.isExecuting = true;
|
|
220
|
+
const logId = createExecutionLog(state.scheduleId, state.worktreeId, state.entry.message);
|
|
221
|
+
try {
|
|
222
|
+
const db = getLazyDbInstance();
|
|
223
|
+
const worktree = db.prepare('SELECT path, vibe_local_model FROM worktrees WHERE id = ?').get(state.worktreeId);
|
|
224
|
+
if (!worktree) {
|
|
225
|
+
updateExecutionLog(logId, 'failed', 'Worktree not found', null);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
// Build options for vibe-local model
|
|
229
|
+
const options = state.entry.cliToolId === 'vibe-local' && worktree.vibe_local_model
|
|
230
|
+
? { model: worktree.vibe_local_model }
|
|
231
|
+
: undefined;
|
|
232
|
+
const result = await (0, claude_executor_1.executeClaudeCommand)(state.entry.message, worktree.path, state.entry.cliToolId, state.entry.permission, options);
|
|
233
|
+
updateExecutionLog(logId, result.status, result.output, result.exitCode);
|
|
234
|
+
updateScheduleLastExecuted(state.scheduleId);
|
|
235
|
+
console.log(`[schedule-manager] Executed ${state.entry.name}: ${result.status}`);
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
239
|
+
updateExecutionLog(logId, 'failed', errorMessage, null);
|
|
240
|
+
console.error(`[schedule-manager] Execution error for ${state.entry.name}:`, errorMessage);
|
|
241
|
+
}
|
|
242
|
+
finally {
|
|
243
|
+
state.isExecuting = false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// =============================================================================
|
|
247
|
+
// CMATE.md Sync
|
|
248
|
+
// =============================================================================
|
|
249
|
+
/**
|
|
250
|
+
* Sync schedules from CMATE.md files for all worktrees.
|
|
251
|
+
* Reads CMATE.md from each worktree, upserts schedules to DB,
|
|
252
|
+
* creates/updates cron jobs, and removes stale schedules.
|
|
253
|
+
*/
|
|
254
|
+
function syncSchedules() {
|
|
255
|
+
const manager = getManagerState();
|
|
256
|
+
const worktrees = getAllWorktrees();
|
|
257
|
+
// Track which scheduleIds are still valid
|
|
258
|
+
const activeScheduleIds = new Set();
|
|
259
|
+
for (const worktree of worktrees) {
|
|
260
|
+
try {
|
|
261
|
+
const config = (0, cmate_parser_1.readCmateFile)(worktree.path);
|
|
262
|
+
if (!config)
|
|
263
|
+
continue;
|
|
264
|
+
const scheduleRows = config.get('Schedules');
|
|
265
|
+
if (!scheduleRows)
|
|
266
|
+
continue;
|
|
267
|
+
const entries = (0, cmate_parser_1.parseSchedulesSection)(scheduleRows);
|
|
268
|
+
for (const entry of entries) {
|
|
269
|
+
if (manager.schedules.size >= exports.MAX_CONCURRENT_SCHEDULES) {
|
|
270
|
+
console.warn(`[schedule-manager] MAX_CONCURRENT_SCHEDULES (${exports.MAX_CONCURRENT_SCHEDULES}) reached`);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const scheduleId = upsertSchedule(worktree.id, entry);
|
|
274
|
+
activeScheduleIds.add(scheduleId);
|
|
275
|
+
// Check if this schedule already has a running cron job
|
|
276
|
+
const existingState = manager.schedules.get(scheduleId);
|
|
277
|
+
if (existingState) {
|
|
278
|
+
// Update entry if changed
|
|
279
|
+
existingState.entry = entry;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (!entry.enabled || !entry.cronExpression)
|
|
283
|
+
continue;
|
|
284
|
+
// Create new cron job
|
|
285
|
+
try {
|
|
286
|
+
const cronJob = new croner_1.Cron(entry.cronExpression, {
|
|
287
|
+
paused: false,
|
|
288
|
+
protect: true, // Prevent overlapping
|
|
289
|
+
});
|
|
290
|
+
const state = {
|
|
291
|
+
scheduleId,
|
|
292
|
+
worktreeId: worktree.id,
|
|
293
|
+
cronJob,
|
|
294
|
+
isExecuting: false,
|
|
295
|
+
entry,
|
|
296
|
+
};
|
|
297
|
+
// Schedule execution
|
|
298
|
+
cronJob.schedule(() => {
|
|
299
|
+
void executeSchedule(state);
|
|
300
|
+
});
|
|
301
|
+
manager.schedules.set(scheduleId, state);
|
|
302
|
+
console.log(`[schedule-manager] Scheduled ${entry.name} (${entry.cronExpression})`);
|
|
303
|
+
}
|
|
304
|
+
catch (cronError) {
|
|
305
|
+
console.warn(`[schedule-manager] Invalid cron for ${entry.name}:`, cronError);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
console.error(`[schedule-manager] Error syncing schedules for worktree ${worktree.id}:`, error);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// Clean up schedules that no longer exist in CMATE.md
|
|
314
|
+
for (const [scheduleId, state] of manager.schedules) {
|
|
315
|
+
if (!activeScheduleIds.has(scheduleId)) {
|
|
316
|
+
state.cronJob.stop();
|
|
317
|
+
manager.schedules.delete(scheduleId);
|
|
318
|
+
console.log(`[schedule-manager] Removed stale schedule ${state.entry.name}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// Disable DB records for schedules no longer in CMATE.md
|
|
322
|
+
const worktreeIds = worktrees.map(w => w.id);
|
|
323
|
+
disableStaleSchedules(activeScheduleIds, worktreeIds);
|
|
324
|
+
}
|
|
325
|
+
// =============================================================================
|
|
326
|
+
// Manager Lifecycle
|
|
327
|
+
// =============================================================================
|
|
328
|
+
/**
|
|
329
|
+
* Initialize the schedule manager.
|
|
330
|
+
* Must be called after initializeWorktrees() completes.
|
|
331
|
+
*
|
|
332
|
+
* [S3-010] Called after await initializeWorktrees() in server.ts
|
|
333
|
+
*/
|
|
334
|
+
function initScheduleManager() {
|
|
335
|
+
const manager = getManagerState();
|
|
336
|
+
if (manager.initialized) {
|
|
337
|
+
console.log('[schedule-manager] Already initialized, skipping');
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
console.log('[schedule-manager] Initializing...');
|
|
341
|
+
// Recovery: mark stale running logs as failed
|
|
342
|
+
recoverRunningLogs();
|
|
343
|
+
// Initial sync
|
|
344
|
+
syncSchedules();
|
|
345
|
+
// Start periodic sync timer
|
|
346
|
+
manager.timerId = setInterval(() => {
|
|
347
|
+
syncSchedules();
|
|
348
|
+
}, exports.POLL_INTERVAL_MS);
|
|
349
|
+
manager.initialized = true;
|
|
350
|
+
console.log(`[schedule-manager] Initialized with ${manager.schedules.size} schedule(s)`);
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Stop all schedules and clean up resources.
|
|
354
|
+
* Uses synchronous SIGKILL fire-and-forget for immediate cleanup.
|
|
355
|
+
*
|
|
356
|
+
* [S3-001] Designed to complete within gracefulShutdown's 3-second timeout
|
|
357
|
+
*/
|
|
358
|
+
function stopAllSchedules() {
|
|
359
|
+
const manager = getManagerState();
|
|
360
|
+
// Stop the polling timer
|
|
361
|
+
if (manager.timerId !== null) {
|
|
362
|
+
clearInterval(manager.timerId);
|
|
363
|
+
manager.timerId = null;
|
|
364
|
+
}
|
|
365
|
+
// Stop all cron jobs
|
|
366
|
+
for (const [, state] of manager.schedules) {
|
|
367
|
+
try {
|
|
368
|
+
state.cronJob.stop();
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
// Ignore errors during cleanup
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
manager.schedules.clear();
|
|
375
|
+
// Kill all active child processes (fire-and-forget SIGKILL)
|
|
376
|
+
const activeProcesses = (0, claude_executor_1.getActiveProcesses)();
|
|
377
|
+
for (const [pid] of activeProcesses) {
|
|
378
|
+
try {
|
|
379
|
+
process.kill(pid, 'SIGKILL');
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
// Process may have already exited - ignore
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
activeProcesses.clear();
|
|
386
|
+
manager.initialized = false;
|
|
387
|
+
console.log('[schedule-manager] All schedules stopped');
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Get the current number of active schedules.
|
|
391
|
+
* Useful for monitoring and testing.
|
|
392
|
+
*/
|
|
393
|
+
function getActiveScheduleCount() {
|
|
394
|
+
return getManagerState().schedules.size;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Check if the schedule manager is initialized.
|
|
398
|
+
*/
|
|
399
|
+
function isScheduleManagerInitialized() {
|
|
400
|
+
return getManagerState().initialized;
|
|
401
|
+
}
|