commandmate 0.3.0 → 0.3.2
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 +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/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/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/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]/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]/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]/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 +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]/respond/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/route.js +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]/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/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.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 +13 -9
- package/.next/server/chunks/2314.js +1 -0
- package/.next/server/chunks/3860.js +1 -1
- package/.next/server/chunks/6228.js +1 -0
- package/.next/server/chunks/7425.js +85 -30
- package/.next/server/chunks/7536.js +1 -1
- package/.next/server/chunks/7566.js +2 -2
- package/.next/server/functions-config-manifest.json +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/app/worktrees/[id]/page-0c889ab3f30d5af7.js +1 -0
- package/.next/static/css/bd6065b03ddb3efd.css +3 -0
- package/.next/trace +5 -5
- 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/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 +54 -0
- package/dist/server/src/lib/claude-executor.js +147 -0
- package/dist/server/src/lib/claude-session.js +31 -6
- package/dist/server/src/lib/cli-patterns.js +1 -1
- package/dist/server/src/lib/cmate-parser.js +240 -0
- package/dist/server/src/lib/db-instance.js +3 -0
- package/dist/server/src/lib/db-migrations.js +96 -2
- package/dist/server/src/lib/env-sanitizer.js +57 -0
- package/dist/server/src/lib/response-poller.js +3 -2
- package/dist/server/src/lib/schedule-manager.js +397 -0
- package/dist/server/src/types/cmate.js +6 -0
- package/package.json +2 -1
- package/.next/static/chunks/app/worktrees/[id]/page-9418e49bdc1de02c.js +0 -1
- package/.next/static/css/b9ea6a4fad17dc32.css +0 -3
- /package/.next/static/{clTo9tuAoPMLcGRuVENfO → j8HFvzDZj7tHjAnhpXUno}/_buildManifest.js +0 -0
- /package/.next/static/{clTo9tuAoPMLcGRuVENfO → j8HFvzDZj7tHjAnhpXUno}/_ssgManifest.js +0 -0
package/dist/server/server.js
CHANGED
|
@@ -39,6 +39,7 @@ const worktrees_1 = require("./src/lib/worktrees");
|
|
|
39
39
|
const db_instance_1 = require("./src/lib/db-instance");
|
|
40
40
|
const response_poller_1 = require("./src/lib/response-poller");
|
|
41
41
|
const auto_yes_manager_1 = require("./src/lib/auto-yes-manager");
|
|
42
|
+
const schedule_manager_1 = require("./src/lib/schedule-manager");
|
|
42
43
|
const db_migrations_1 = require("./src/lib/db-migrations");
|
|
43
44
|
const env_1 = require("./src/lib/env");
|
|
44
45
|
const db_repository_1 = require("./src/lib/db-repository");
|
|
@@ -227,6 +228,8 @@ app.prepare().then(() => {
|
|
|
227
228
|
console.log(`> WebSocket server ready`);
|
|
228
229
|
// Initialize worktrees after server starts
|
|
229
230
|
await initializeWorktrees();
|
|
231
|
+
// [S3-010] Initialize schedule manager AFTER worktrees are ready
|
|
232
|
+
(0, schedule_manager_1.initScheduleManager)();
|
|
230
233
|
});
|
|
231
234
|
// Graceful shutdown with timeout
|
|
232
235
|
let isShuttingDown = false;
|
|
@@ -241,6 +244,8 @@ app.prepare().then(() => {
|
|
|
241
244
|
(0, response_poller_1.stopAllPolling)();
|
|
242
245
|
// Issue #138: Stop all auto-yes pollers
|
|
243
246
|
(0, auto_yes_manager_1.stopAllAutoYesPolling)();
|
|
247
|
+
// Issue #294: Stop all scheduled executions (SIGKILL fire-and-forget)
|
|
248
|
+
(0, schedule_manager_1.stopAllSchedules)();
|
|
244
249
|
// Close WebSocket connections immediately (don't wait)
|
|
245
250
|
(0, ws_server_1.closeWebSocket)();
|
|
246
251
|
// Force exit after 3 seconds if graceful shutdown fails
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CMATE.md Shared Constants
|
|
4
|
+
* Issue #294: Constants shared between server-side parser and client-side validator
|
|
5
|
+
*
|
|
6
|
+
* This module has NO Node.js dependencies (no 'fs'), so it can be safely
|
|
7
|
+
* imported from both server and client code.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.MAX_SCHEDULE_ENTRIES = exports.MAX_CRON_EXPRESSION_LENGTH = exports.NAME_PATTERN = exports.CONTROL_CHAR_PATTERN = exports.CMATE_FILENAME = void 0;
|
|
11
|
+
exports.sanitizeContent = sanitizeContent;
|
|
12
|
+
exports.isValidCronExpression = isValidCronExpression;
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// File
|
|
15
|
+
// =============================================================================
|
|
16
|
+
/** CMATE.md filename */
|
|
17
|
+
exports.CMATE_FILENAME = 'CMATE.md';
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// Sanitization
|
|
20
|
+
// =============================================================================
|
|
21
|
+
/**
|
|
22
|
+
* Unicode control character regex for sanitization.
|
|
23
|
+
* Matches: C0 control chars (except \t \n \r), C1 control chars,
|
|
24
|
+
* zero-width characters, directional control characters.
|
|
25
|
+
*
|
|
26
|
+
* NOTE: No /g flag on the export — callers must use String.replace(pattern, '')
|
|
27
|
+
* with /g or String.replaceAll() to avoid lastIndex state issues.
|
|
28
|
+
*
|
|
29
|
+
* [S4-002] Strips potentially dangerous Unicode control characters
|
|
30
|
+
*/
|
|
31
|
+
exports.CONTROL_CHAR_PATTERN =
|
|
32
|
+
// eslint-disable-next-line no-control-regex
|
|
33
|
+
/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\x80-\x9F\u200B-\u200F\u2028-\u202F\uFEFF]/;
|
|
34
|
+
/**
|
|
35
|
+
* Remove Unicode control characters from a string.
|
|
36
|
+
* Preserves tabs (\t), newlines (\n), and carriage returns (\r).
|
|
37
|
+
*
|
|
38
|
+
* @param content - Raw string to sanitize
|
|
39
|
+
* @returns Sanitized string with control characters removed
|
|
40
|
+
*/
|
|
41
|
+
function sanitizeContent(content) {
|
|
42
|
+
// Use RegExp constructor with /g to avoid lastIndex state on the shared pattern
|
|
43
|
+
return content.replace(new RegExp(exports.CONTROL_CHAR_PATTERN.source, 'g'), '');
|
|
44
|
+
}
|
|
45
|
+
// =============================================================================
|
|
46
|
+
// Name Validation
|
|
47
|
+
// =============================================================================
|
|
48
|
+
/**
|
|
49
|
+
* Name validation pattern.
|
|
50
|
+
* Allows: ASCII word chars, Japanese chars (CJK, Hiragana, Katakana, Symbols),
|
|
51
|
+
* spaces, and hyphens. Length: 1-100 characters.
|
|
52
|
+
*
|
|
53
|
+
* [S4-011] Prevents injection through name field
|
|
54
|
+
*/
|
|
55
|
+
exports.NAME_PATTERN = /^[\w\u3000-\u303F\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\uF900-\uFAFF\s-]{1,100}$/;
|
|
56
|
+
// =============================================================================
|
|
57
|
+
// Limits
|
|
58
|
+
// =============================================================================
|
|
59
|
+
/** Maximum cron expression length */
|
|
60
|
+
exports.MAX_CRON_EXPRESSION_LENGTH = 100;
|
|
61
|
+
/** Maximum number of schedule entries per worktree */
|
|
62
|
+
exports.MAX_SCHEDULE_ENTRIES = 100;
|
|
63
|
+
// =============================================================================
|
|
64
|
+
// Cron Validation
|
|
65
|
+
// =============================================================================
|
|
66
|
+
/**
|
|
67
|
+
* Validate a cron expression.
|
|
68
|
+
* Checks length and basic format (5-6 fields separated by spaces).
|
|
69
|
+
*
|
|
70
|
+
* @param expression - Cron expression to validate
|
|
71
|
+
* @returns true if the expression appears valid
|
|
72
|
+
*/
|
|
73
|
+
function isValidCronExpression(expression) {
|
|
74
|
+
if (expression.length > exports.MAX_CRON_EXPRESSION_LENGTH) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
const parts = expression.trim().split(/\s+/);
|
|
78
|
+
return parts.length >= 5 && parts.length <= 6;
|
|
79
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Schedule Execution Configuration Constants
|
|
4
|
+
* Issue #294: Centralized constants for schedule-related API routes
|
|
5
|
+
*
|
|
6
|
+
* Eliminates duplication of validation constants and UUID validation
|
|
7
|
+
* across schedules/route.ts, schedules/[scheduleId]/route.ts,
|
|
8
|
+
* and execution-logs/[logId]/route.ts.
|
|
9
|
+
*
|
|
10
|
+
* [S4-014] UUID v4 format validation
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.UUID_V4_PATTERN = exports.DEFAULT_PERMISSIONS = exports.CODEX_SANDBOXES = exports.CLAUDE_PERMISSIONS = exports.MAX_SCHEDULE_CRON_LENGTH = exports.MAX_SCHEDULE_MESSAGE_LENGTH = exports.MAX_SCHEDULE_NAME_LENGTH = void 0;
|
|
14
|
+
exports.isValidUuidV4 = isValidUuidV4;
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// Validation Constants
|
|
17
|
+
// =============================================================================
|
|
18
|
+
/** Maximum schedule name length */
|
|
19
|
+
exports.MAX_SCHEDULE_NAME_LENGTH = 100;
|
|
20
|
+
/** Maximum message length for schedule execution */
|
|
21
|
+
exports.MAX_SCHEDULE_MESSAGE_LENGTH = 10000;
|
|
22
|
+
/** Maximum cron expression length */
|
|
23
|
+
exports.MAX_SCHEDULE_CRON_LENGTH = 100;
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// Permission Constants
|
|
26
|
+
// =============================================================================
|
|
27
|
+
/** Allowed permission values for claude CLI (--permission-mode) */
|
|
28
|
+
exports.CLAUDE_PERMISSIONS = ['default', 'acceptEdits', 'plan', 'dontAsk', 'bypassPermissions'];
|
|
29
|
+
/** Allowed sandbox values for codex CLI (--sandbox) */
|
|
30
|
+
exports.CODEX_SANDBOXES = ['read-only', 'workspace-write', 'danger-full-access'];
|
|
31
|
+
/** Default permission per CLI tool */
|
|
32
|
+
exports.DEFAULT_PERMISSIONS = {
|
|
33
|
+
claude: 'acceptEdits',
|
|
34
|
+
codex: 'workspace-write',
|
|
35
|
+
};
|
|
36
|
+
// =============================================================================
|
|
37
|
+
// UUID Validation
|
|
38
|
+
// =============================================================================
|
|
39
|
+
/**
|
|
40
|
+
* UUID v4 validation pattern.
|
|
41
|
+
* Matches standard UUID v4 format: xxxxxxxx-xxxx-4xxx-[89ab]xxx-xxxxxxxxxxxx
|
|
42
|
+
*
|
|
43
|
+
* [S4-014] Used to validate schedule IDs and execution log IDs
|
|
44
|
+
*/
|
|
45
|
+
exports.UUID_V4_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
46
|
+
/**
|
|
47
|
+
* Validate that a string is a valid UUID v4 format.
|
|
48
|
+
*
|
|
49
|
+
* @param id - String to validate
|
|
50
|
+
* @returns true if the string matches UUID v4 format
|
|
51
|
+
*/
|
|
52
|
+
function isValidUuidV4(id) {
|
|
53
|
+
return exports.UUID_V4_PATTERN.test(id);
|
|
54
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Claude CLI Executor
|
|
4
|
+
* Issue #294: Executes claude -p commands for scheduled executions
|
|
5
|
+
*
|
|
6
|
+
* Security:
|
|
7
|
+
* - Uses execFile (not exec) to prevent shell injection
|
|
8
|
+
* - Sanitizes environment variables via env-sanitizer.ts
|
|
9
|
+
* - Limits output size to prevent memory exhaustion
|
|
10
|
+
* - Enforces execution timeout
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.ALLOWED_CLI_TOOLS = exports.MAX_MESSAGE_LENGTH = exports.EXECUTION_TIMEOUT_MS = exports.MAX_STORED_OUTPUT_SIZE = exports.MAX_OUTPUT_SIZE = void 0;
|
|
14
|
+
exports.truncateOutput = truncateOutput;
|
|
15
|
+
exports.buildCliArgs = buildCliArgs;
|
|
16
|
+
exports.executeClaudeCommand = executeClaudeCommand;
|
|
17
|
+
exports.getActiveProcesses = getActiveProcesses;
|
|
18
|
+
const child_process_1 = require("child_process");
|
|
19
|
+
const env_sanitizer_1 = require("./env-sanitizer");
|
|
20
|
+
const cli_patterns_1 = require("./cli-patterns");
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// Constants
|
|
23
|
+
// =============================================================================
|
|
24
|
+
/** Maximum output buffer size for execFile (1MB) */
|
|
25
|
+
exports.MAX_OUTPUT_SIZE = 1 * 1024 * 1024;
|
|
26
|
+
/** Maximum output size stored in DB (100KB) */
|
|
27
|
+
exports.MAX_STORED_OUTPUT_SIZE = 100 * 1024;
|
|
28
|
+
/** Execution timeout in milliseconds (5 minutes) */
|
|
29
|
+
exports.EXECUTION_TIMEOUT_MS = 5 * 60 * 1000;
|
|
30
|
+
/** Maximum message length sent to claude -p */
|
|
31
|
+
exports.MAX_MESSAGE_LENGTH = 10000;
|
|
32
|
+
/** Allowed CLI tool identifiers for scheduled execution */
|
|
33
|
+
exports.ALLOWED_CLI_TOOLS = new Set(['claude', 'codex']);
|
|
34
|
+
// =============================================================================
|
|
35
|
+
// Executor
|
|
36
|
+
// =============================================================================
|
|
37
|
+
/**
|
|
38
|
+
* Truncate output to MAX_STORED_OUTPUT_SIZE bytes.
|
|
39
|
+
* Appends a truncation notice if truncated.
|
|
40
|
+
*
|
|
41
|
+
* @param output - Raw output string
|
|
42
|
+
* @returns Truncated output string
|
|
43
|
+
*/
|
|
44
|
+
function truncateOutput(output) {
|
|
45
|
+
if (Buffer.byteLength(output, 'utf-8') <= exports.MAX_STORED_OUTPUT_SIZE) {
|
|
46
|
+
return output;
|
|
47
|
+
}
|
|
48
|
+
// Truncate to MAX_STORED_OUTPUT_SIZE bytes
|
|
49
|
+
const buffer = Buffer.from(output, 'utf-8');
|
|
50
|
+
const truncated = buffer.subarray(0, exports.MAX_STORED_OUTPUT_SIZE).toString('utf-8');
|
|
51
|
+
return truncated + '\n\n--- Output truncated (exceeded 100KB limit) ---';
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Build CLI arguments for non-interactive execution based on CLI tool type.
|
|
55
|
+
*
|
|
56
|
+
* - claude: -p <message> --output-format text --permission-mode <permission>
|
|
57
|
+
* - codex: exec <message> --sandbox <permission>
|
|
58
|
+
* - others: -p <message> (fallback)
|
|
59
|
+
*
|
|
60
|
+
* @param message - Prompt message
|
|
61
|
+
* @param cliToolId - CLI tool identifier
|
|
62
|
+
* @param permission - Permission mode (claude: --permission-mode, codex: --sandbox)
|
|
63
|
+
* @returns Array of CLI arguments
|
|
64
|
+
*/
|
|
65
|
+
function buildCliArgs(message, cliToolId, permission) {
|
|
66
|
+
switch (cliToolId) {
|
|
67
|
+
case 'codex':
|
|
68
|
+
return ['exec', message, '--sandbox', permission ?? 'workspace-write'];
|
|
69
|
+
case 'claude':
|
|
70
|
+
default:
|
|
71
|
+
return ['-p', message, '--output-format', 'text', '--permission-mode', permission ?? 'acceptEdits'];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Execute a CLI command in a worktree directory.
|
|
76
|
+
*
|
|
77
|
+
* @param message - Prompt message to send
|
|
78
|
+
* @param cwd - Working directory (worktree path from DB)
|
|
79
|
+
* @param cliToolId - CLI tool to use (default: 'claude')
|
|
80
|
+
* @param permission - Permission mode (claude: --permission-mode, codex: --sandbox)
|
|
81
|
+
* @returns Execution result with output and status
|
|
82
|
+
*/
|
|
83
|
+
async function executeClaudeCommand(message, cwd, cliToolId = 'claude', permission) {
|
|
84
|
+
// Validate cliToolId against whitelist [SEC-001]
|
|
85
|
+
if (!exports.ALLOWED_CLI_TOOLS.has(cliToolId)) {
|
|
86
|
+
return {
|
|
87
|
+
output: '',
|
|
88
|
+
exitCode: null,
|
|
89
|
+
status: 'failed',
|
|
90
|
+
error: `Invalid CLI tool: ${cliToolId}`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// Validate message length
|
|
94
|
+
const truncatedMessage = message.length > exports.MAX_MESSAGE_LENGTH
|
|
95
|
+
? message.substring(0, exports.MAX_MESSAGE_LENGTH)
|
|
96
|
+
: message;
|
|
97
|
+
const args = buildCliArgs(truncatedMessage, cliToolId, permission);
|
|
98
|
+
return new Promise((resolve) => {
|
|
99
|
+
const child = (0, child_process_1.execFile)(cliToolId, args, {
|
|
100
|
+
cwd,
|
|
101
|
+
env: (0, env_sanitizer_1.sanitizeEnvForChildProcess)(),
|
|
102
|
+
maxBuffer: exports.MAX_OUTPUT_SIZE,
|
|
103
|
+
timeout: exports.EXECUTION_TIMEOUT_MS,
|
|
104
|
+
}, (error, stdout, stderr) => {
|
|
105
|
+
if (error) {
|
|
106
|
+
const isTimeout = error.killed || error.code === 'ETIMEDOUT';
|
|
107
|
+
const rawOutput = (0, cli_patterns_1.stripAnsi)(stdout || stderr || error.message);
|
|
108
|
+
const output = truncateOutput(rawOutput);
|
|
109
|
+
resolve({
|
|
110
|
+
output,
|
|
111
|
+
exitCode: error.code ? parseInt(String(error.code), 10) || null : null,
|
|
112
|
+
status: isTimeout ? 'timeout' : 'failed',
|
|
113
|
+
error: error.message,
|
|
114
|
+
});
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const rawOutput = (0, cli_patterns_1.stripAnsi)(stdout || '');
|
|
118
|
+
const output = truncateOutput(rawOutput);
|
|
119
|
+
resolve({
|
|
120
|
+
output,
|
|
121
|
+
exitCode: 0,
|
|
122
|
+
status: 'completed',
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
// Close stdin immediately to prevent hanging on yes/no prompts
|
|
126
|
+
child.stdin?.end();
|
|
127
|
+
// Return the child process PID for tracking
|
|
128
|
+
if (child.pid) {
|
|
129
|
+
// Store PID in global active processes for cleanup on shutdown
|
|
130
|
+
const activeProcesses = getActiveProcesses();
|
|
131
|
+
activeProcesses.set(child.pid, child);
|
|
132
|
+
child.on('exit', () => {
|
|
133
|
+
activeProcesses.delete(child.pid);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Get the global active processes map.
|
|
140
|
+
* Uses globalThis for hot reload persistence.
|
|
141
|
+
*/
|
|
142
|
+
function getActiveProcesses() {
|
|
143
|
+
if (!globalThis.__scheduleActiveProcesses) {
|
|
144
|
+
globalThis.__scheduleActiveProcesses = new Map();
|
|
145
|
+
}
|
|
146
|
+
return globalThis.__scheduleActiveProcesses;
|
|
147
|
+
}
|
|
@@ -136,6 +136,17 @@ exports.CLAUDE_PROMPT_POLL_INTERVAL = 200;
|
|
|
136
136
|
* 40 is an empirical threshold with safety margin.
|
|
137
137
|
*/
|
|
138
138
|
const MAX_SHELL_PROMPT_LENGTH = 40;
|
|
139
|
+
/**
|
|
140
|
+
* Number of tail lines used for error pattern detection in isSessionHealthy()
|
|
141
|
+
*
|
|
142
|
+
* Error patterns are only searched within the last N lines of pane output,
|
|
143
|
+
* not the entire buffer. This prevents false negatives where historical
|
|
144
|
+
* (already recovered) errors in the scrollback trigger unhealthy detection.
|
|
145
|
+
*
|
|
146
|
+
* 10 lines provides sufficient window to catch recent errors while ignoring
|
|
147
|
+
* historical ones that have scrolled up.
|
|
148
|
+
*/
|
|
149
|
+
const HEALTH_CHECK_ERROR_TAIL_LINES = 10;
|
|
139
150
|
/**
|
|
140
151
|
* Cached Claude CLI path
|
|
141
152
|
*/
|
|
@@ -265,21 +276,31 @@ async function isSessionHealthy(sessionName) {
|
|
|
265
276
|
if (trimmed === '') {
|
|
266
277
|
return { healthy: false, reason: 'empty output' };
|
|
267
278
|
}
|
|
268
|
-
//
|
|
279
|
+
// Active state detection: check for Claude prompt BEFORE error patterns.
|
|
280
|
+
// This prevents false negatives where historical (recovered) errors in
|
|
281
|
+
// the pane scrollback cause a currently-active session to be marked unhealthy.
|
|
282
|
+
if (cli_patterns_1.CLAUDE_PROMPT_PATTERN.test(trimmed)) {
|
|
283
|
+
return { healthy: true };
|
|
284
|
+
}
|
|
285
|
+
// S2-F010: Error pattern detection - limited to tail lines only.
|
|
286
|
+
// Only the last HEALTH_CHECK_ERROR_TAIL_LINES lines are searched, so
|
|
287
|
+
// historical errors that have scrolled up do not trigger false negatives.
|
|
288
|
+
const allLines = trimmed.split('\n').filter(line => line.trim() !== '');
|
|
289
|
+
const tailLines = allLines.slice(-HEALTH_CHECK_ERROR_TAIL_LINES);
|
|
290
|
+
const tailText = tailLines.join('\n');
|
|
269
291
|
// MF-001: Check error patterns from cli-patterns.ts (SRP - pattern management centralized)
|
|
270
292
|
for (const pattern of cli_patterns_1.CLAUDE_SESSION_ERROR_PATTERNS) {
|
|
271
|
-
if (
|
|
293
|
+
if (tailText.includes(pattern)) {
|
|
272
294
|
return { healthy: false, reason: `error pattern: ${pattern}` };
|
|
273
295
|
}
|
|
274
296
|
}
|
|
275
297
|
for (const regex of cli_patterns_1.CLAUDE_SESSION_ERROR_REGEX_PATTERNS) {
|
|
276
|
-
if (regex.test(
|
|
298
|
+
if (regex.test(tailText)) {
|
|
277
299
|
return { healthy: false, reason: `error pattern: ${regex.source}` };
|
|
278
300
|
}
|
|
279
301
|
}
|
|
280
302
|
// S2-F002: Extract last line after empty line filtering
|
|
281
|
-
const
|
|
282
|
-
const lastLine = lines[lines.length - 1]?.trim() ?? '';
|
|
303
|
+
const lastLine = allLines[allLines.length - 1]?.trim() ?? '';
|
|
283
304
|
// F006: Line length check BEFORE SHELL_PROMPT_ENDINGS check (early return)
|
|
284
305
|
if (lastLine.length >= MAX_SHELL_PROMPT_LENGTH) {
|
|
285
306
|
// Long lines are not shell prompts -> treat as healthy (early return)
|
|
@@ -415,7 +436,11 @@ async function isClaudeRunning(worktreeId) {
|
|
|
415
436
|
// MF-S3-001: Verify session health to avoid reporting broken sessions as running
|
|
416
437
|
// S2-F001: await + extract .healthy to maintain boolean return type
|
|
417
438
|
const result = await isSessionHealthy(sessionName);
|
|
418
|
-
|
|
439
|
+
if (!result.healthy) {
|
|
440
|
+
console.warn(`[isClaudeRunning] Session ${sessionName} unhealthy: ${result.reason}`);
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
return true;
|
|
419
444
|
}
|
|
420
445
|
/**
|
|
421
446
|
* Get Claude session state
|
|
@@ -268,7 +268,7 @@ exports.CLAUDE_SESSION_ERROR_PATTERNS = [
|
|
|
268
268
|
* SEC-SF-004: See CLAUDE_SESSION_ERROR_PATTERNS JSDoc for pattern maintenance process.
|
|
269
269
|
*/
|
|
270
270
|
exports.CLAUDE_SESSION_ERROR_REGEX_PATTERNS = [
|
|
271
|
-
|
|
271
|
+
/^Error:.*Claude Code/,
|
|
272
272
|
];
|
|
273
273
|
function buildDetectPromptOptions(cliToolId) {
|
|
274
274
|
if (cliToolId === 'claude') {
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CMATE.md Parser
|
|
4
|
+
* Issue #294: Schedule execution feature
|
|
5
|
+
*
|
|
6
|
+
* Parses CMATE.md files (Markdown table format) from worktree root directories.
|
|
7
|
+
* Provides a generic table parser and a specialized schedule section parser.
|
|
8
|
+
*
|
|
9
|
+
* Security:
|
|
10
|
+
* - Path traversal prevention (realpath + worktree directory validation)
|
|
11
|
+
* - Unicode control character sanitization
|
|
12
|
+
* - Name validation with strict pattern matching
|
|
13
|
+
* - Cron expression validation
|
|
14
|
+
*/
|
|
15
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
16
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
17
|
+
};
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.MIN_CRON_INTERVAL = exports.CONTROL_CHAR_REGEX = exports.isValidCronExpression = exports.MAX_SCHEDULE_ENTRIES = exports.MAX_CRON_EXPRESSION_LENGTH = exports.NAME_PATTERN = exports.CMATE_FILENAME = void 0;
|
|
20
|
+
exports.sanitizeMessageContent = sanitizeMessageContent;
|
|
21
|
+
exports.validateCmatePath = validateCmatePath;
|
|
22
|
+
exports.parseCmateFile = parseCmateFile;
|
|
23
|
+
exports.parseSchedulesSection = parseSchedulesSection;
|
|
24
|
+
exports.readCmateFile = readCmateFile;
|
|
25
|
+
const fs_1 = require("fs");
|
|
26
|
+
const path_1 = __importDefault(require("path"));
|
|
27
|
+
const schedule_config_1 = require("../config/schedule-config");
|
|
28
|
+
const cmate_constants_1 = require("../config/cmate-constants");
|
|
29
|
+
Object.defineProperty(exports, "CMATE_FILENAME", { enumerable: true, get: function () { return cmate_constants_1.CMATE_FILENAME; } });
|
|
30
|
+
Object.defineProperty(exports, "NAME_PATTERN", { enumerable: true, get: function () { return cmate_constants_1.NAME_PATTERN; } });
|
|
31
|
+
Object.defineProperty(exports, "MAX_CRON_EXPRESSION_LENGTH", { enumerable: true, get: function () { return cmate_constants_1.MAX_CRON_EXPRESSION_LENGTH; } });
|
|
32
|
+
Object.defineProperty(exports, "MAX_SCHEDULE_ENTRIES", { enumerable: true, get: function () { return cmate_constants_1.MAX_SCHEDULE_ENTRIES; } });
|
|
33
|
+
Object.defineProperty(exports, "isValidCronExpression", { enumerable: true, get: function () { return cmate_constants_1.isValidCronExpression; } });
|
|
34
|
+
/**
|
|
35
|
+
* @deprecated Use CONTROL_CHAR_PATTERN from '../config/cmate-constants' instead.
|
|
36
|
+
* Kept for backward compatibility with existing tests.
|
|
37
|
+
*/
|
|
38
|
+
exports.CONTROL_CHAR_REGEX = new RegExp(cmate_constants_1.CONTROL_CHAR_PATTERN.source, 'g');
|
|
39
|
+
/** Minimum cron interval pattern (every minute) */
|
|
40
|
+
exports.MIN_CRON_INTERVAL = '* * * * *';
|
|
41
|
+
// =============================================================================
|
|
42
|
+
// Sanitization
|
|
43
|
+
// =============================================================================
|
|
44
|
+
/**
|
|
45
|
+
* Remove Unicode control characters from a string.
|
|
46
|
+
* Preserves tabs (\t), newlines (\n), and carriage returns (\r).
|
|
47
|
+
*
|
|
48
|
+
* @param content - Raw string to sanitize
|
|
49
|
+
* @returns Sanitized string with control characters removed
|
|
50
|
+
*/
|
|
51
|
+
function sanitizeMessageContent(content) {
|
|
52
|
+
return (0, cmate_constants_1.sanitizeContent)(content);
|
|
53
|
+
}
|
|
54
|
+
// =============================================================================
|
|
55
|
+
// Path Validation
|
|
56
|
+
// =============================================================================
|
|
57
|
+
/**
|
|
58
|
+
* Validate that a CMATE.md file path is within the expected worktree directory.
|
|
59
|
+
* Prevents path traversal attacks by resolving symlinks and verifying containment.
|
|
60
|
+
*
|
|
61
|
+
* @param filePath - Path to CMATE.md file
|
|
62
|
+
* @param worktreeDir - Expected worktree directory
|
|
63
|
+
* @returns true if path is valid and within worktree directory
|
|
64
|
+
* @throws Error if path traversal is detected
|
|
65
|
+
*/
|
|
66
|
+
function validateCmatePath(filePath, worktreeDir) {
|
|
67
|
+
const realFilePath = (0, fs_1.realpathSync)(filePath);
|
|
68
|
+
const realWorktreeDir = (0, fs_1.realpathSync)(worktreeDir);
|
|
69
|
+
// Ensure the file is within the worktree directory
|
|
70
|
+
if (!realFilePath.startsWith(realWorktreeDir + path_1.default.sep) &&
|
|
71
|
+
realFilePath !== path_1.default.join(realWorktreeDir, cmate_constants_1.CMATE_FILENAME)) {
|
|
72
|
+
throw new Error(`Path traversal detected: ${filePath} is not within ${worktreeDir}`);
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
// =============================================================================
|
|
77
|
+
// Generic Markdown Table Parser
|
|
78
|
+
// =============================================================================
|
|
79
|
+
/**
|
|
80
|
+
* Parse a CMATE.md file into a generic structure.
|
|
81
|
+
* Returns a Map where keys are section names (from ## headers)
|
|
82
|
+
* and values are arrays of row data (each row is an array of cell values).
|
|
83
|
+
*
|
|
84
|
+
* [S1-010] Generic design: returns Map<string, string[][]>
|
|
85
|
+
*
|
|
86
|
+
* @param content - Raw CMATE.md file content
|
|
87
|
+
* @returns Map of section name to table rows
|
|
88
|
+
*/
|
|
89
|
+
function parseCmateFile(content) {
|
|
90
|
+
const result = new Map();
|
|
91
|
+
const lines = content.split('\n');
|
|
92
|
+
let currentSection = null;
|
|
93
|
+
let headerParsed = false;
|
|
94
|
+
let separatorParsed = false;
|
|
95
|
+
for (const line of lines) {
|
|
96
|
+
const trimmed = line.trim();
|
|
97
|
+
// Detect section headers (## SectionName)
|
|
98
|
+
const headerMatch = trimmed.match(/^##\s+(.+)$/);
|
|
99
|
+
if (headerMatch) {
|
|
100
|
+
currentSection = headerMatch[1].trim();
|
|
101
|
+
headerParsed = false;
|
|
102
|
+
separatorParsed = false;
|
|
103
|
+
if (!result.has(currentSection)) {
|
|
104
|
+
result.set(currentSection, []);
|
|
105
|
+
}
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
// Skip empty lines
|
|
109
|
+
if (!trimmed) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
// Skip non-table lines
|
|
113
|
+
if (!trimmed.startsWith('|')) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (!currentSection) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
// Parse table row
|
|
120
|
+
if (!headerParsed) {
|
|
121
|
+
// First row is header - skip it
|
|
122
|
+
headerParsed = true;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (!separatorParsed) {
|
|
126
|
+
// Second row is separator (|---|---|) - skip it
|
|
127
|
+
if (trimmed.match(/^\|[\s-:|]+\|$/)) {
|
|
128
|
+
separatorParsed = true;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
// If it's not a separator, treat it as data
|
|
132
|
+
separatorParsed = true;
|
|
133
|
+
}
|
|
134
|
+
// Parse data row
|
|
135
|
+
const cells = trimmed
|
|
136
|
+
.split('|')
|
|
137
|
+
.slice(1, -1) // Remove leading and trailing empty strings from split
|
|
138
|
+
.map((cell) => cell.trim());
|
|
139
|
+
if (cells.length > 0) {
|
|
140
|
+
result.get(currentSection).push(cells);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
// =============================================================================
|
|
146
|
+
// Schedule Section Parser
|
|
147
|
+
// =============================================================================
|
|
148
|
+
/**
|
|
149
|
+
* Parse the Schedules section of a CMATE.md file into typed ScheduleEntry objects.
|
|
150
|
+
*
|
|
151
|
+
* Expected table format:
|
|
152
|
+
* | Name | Cron | Message | CLI Tool | Enabled |
|
|
153
|
+
* |------|------|---------|----------|---------|
|
|
154
|
+
* | daily-review | 0 9 * * * | Review code changes | claude | true |
|
|
155
|
+
*
|
|
156
|
+
* Entries with invalid names, cron expressions, or missing required fields
|
|
157
|
+
* are silently skipped with a console.warn.
|
|
158
|
+
*
|
|
159
|
+
* @param rows - Raw table rows from parseCmateFile() for the Schedules section
|
|
160
|
+
* @returns Array of validated ScheduleEntry objects
|
|
161
|
+
*/
|
|
162
|
+
function parseSchedulesSection(rows) {
|
|
163
|
+
const entries = [];
|
|
164
|
+
for (const row of rows) {
|
|
165
|
+
if (entries.length >= cmate_constants_1.MAX_SCHEDULE_ENTRIES) {
|
|
166
|
+
console.warn(`[cmate-parser] Maximum schedule entries (${cmate_constants_1.MAX_SCHEDULE_ENTRIES}) reached, skipping remaining`);
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
// Minimum required columns: Name, Cron, Message
|
|
170
|
+
if (row.length < 3) {
|
|
171
|
+
console.warn('[cmate-parser] Skipping row with insufficient columns:', row);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const [name, cronExpression, message, cliToolId, enabledStr, permissionStr] = row;
|
|
175
|
+
// Validate name
|
|
176
|
+
const sanitizedName = sanitizeMessageContent(name);
|
|
177
|
+
if (!cmate_constants_1.NAME_PATTERN.test(sanitizedName)) {
|
|
178
|
+
console.warn(`[cmate-parser] Skipping entry with invalid name: "${sanitizedName}"`);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
// Validate cron expression
|
|
182
|
+
if (!(0, cmate_constants_1.isValidCronExpression)(cronExpression)) {
|
|
183
|
+
console.warn(`[cmate-parser] Skipping entry "${sanitizedName}" with invalid cron: "${cronExpression}"`);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
// Sanitize message
|
|
187
|
+
const sanitizedMessage = sanitizeMessageContent(message);
|
|
188
|
+
if (!sanitizedMessage) {
|
|
189
|
+
console.warn(`[cmate-parser] Skipping entry "${sanitizedName}" with empty message`);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
// Parse enabled (default: true)
|
|
193
|
+
const enabled = enabledStr === undefined ||
|
|
194
|
+
enabledStr === '' ||
|
|
195
|
+
enabledStr.toLowerCase() === 'true';
|
|
196
|
+
// Parse permission with validation
|
|
197
|
+
const resolvedCliToolId = cliToolId?.trim() || 'claude';
|
|
198
|
+
const defaultPermission = schedule_config_1.DEFAULT_PERMISSIONS[resolvedCliToolId] ?? '';
|
|
199
|
+
let permission = permissionStr?.trim() || defaultPermission;
|
|
200
|
+
// Validate permission against allowed values
|
|
201
|
+
const allowedValues = resolvedCliToolId === 'codex' ? schedule_config_1.CODEX_SANDBOXES : schedule_config_1.CLAUDE_PERMISSIONS;
|
|
202
|
+
if (permission && !allowedValues.includes(permission)) {
|
|
203
|
+
console.warn(`[cmate-parser] Invalid permission "${permission}" for ${resolvedCliToolId} in entry "${sanitizedName}", using default "${defaultPermission}"`);
|
|
204
|
+
permission = defaultPermission;
|
|
205
|
+
}
|
|
206
|
+
entries.push({
|
|
207
|
+
name: sanitizedName,
|
|
208
|
+
cronExpression: cronExpression.trim(),
|
|
209
|
+
message: sanitizedMessage,
|
|
210
|
+
cliToolId: resolvedCliToolId,
|
|
211
|
+
enabled,
|
|
212
|
+
permission,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
return entries;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Read and parse a CMATE.md file from a worktree directory.
|
|
219
|
+
*
|
|
220
|
+
* @param worktreeDir - Path to the worktree directory
|
|
221
|
+
* @returns Parsed CmateConfig, or null if the file doesn't exist
|
|
222
|
+
* @throws Error if path traversal is detected
|
|
223
|
+
*/
|
|
224
|
+
function readCmateFile(worktreeDir) {
|
|
225
|
+
const filePath = path_1.default.join(worktreeDir, cmate_constants_1.CMATE_FILENAME);
|
|
226
|
+
try {
|
|
227
|
+
// Validate path before reading
|
|
228
|
+
validateCmatePath(filePath, worktreeDir);
|
|
229
|
+
const content = (0, fs_1.readFileSync)(filePath, 'utf-8');
|
|
230
|
+
return parseCmateFile(content);
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
if (error instanceof Error &&
|
|
234
|
+
'code' in error &&
|
|
235
|
+
error.code === 'ENOENT') {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -45,6 +45,9 @@ function getDbInstance() {
|
|
|
45
45
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
46
46
|
}
|
|
47
47
|
dbInstance = new better_sqlite3_1.default(dbPath);
|
|
48
|
+
// Issue #294: Enable foreign key enforcement BEFORE migrations
|
|
49
|
+
// This ensures ON DELETE CASCADE works correctly for all tables
|
|
50
|
+
dbInstance.pragma('foreign_keys = ON');
|
|
48
51
|
(0, db_migrations_1.runMigrations)(dbInstance);
|
|
49
52
|
}
|
|
50
53
|
return dbInstance;
|