jettypod 4.2.11 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SECURITY-AUDIT-CATASTROPHIC-DELETE.md +196 -0
- package/TEST_HOOK.md +1 -0
- package/hooks/post-checkout +78 -3
- package/hooks/pre-push +33 -0
- package/jettypod.js +215 -25
- package/lib/claudemd.js +12 -4
- package/lib/current-work.js +97 -84
- package/lib/jettypod-backup.js +124 -8
- package/lib/merge-lock.js +30 -0
- package/lib/safe-delete.js +794 -0
- package/lib/session-writer.js +20 -0
- package/lib/worktree-facade.js +43 -0
- package/lib/worktree-manager.js +54 -41
- package/package.json +1 -1
- package/lib/migrations/016-worktree-sessions-table.js +0 -84
- package/lib/worktree-sessions.js +0 -186
package/lib/session-writer.js
CHANGED
|
@@ -19,6 +19,23 @@ function getSessionFilePath() {
|
|
|
19
19
|
return path.join(process.cwd(), '.claude', 'session.md');
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Validate that we're in a worktree, not the root directory
|
|
24
|
+
* @throws {Error} If in root directory
|
|
25
|
+
*/
|
|
26
|
+
function validateWorktreeLocation() {
|
|
27
|
+
const cwd = process.cwd();
|
|
28
|
+
|
|
29
|
+
// Check if current directory is inside .jettypod-work/
|
|
30
|
+
if (!cwd.includes('.jettypod-work')) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
'Cannot create session.md in root directory.\n' +
|
|
33
|
+
'Session files should only be created in worktrees (.jettypod-work/).\n' +
|
|
34
|
+
'Use "jettypod work start <id>" to create a worktree for this work item.'
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
22
39
|
/**
|
|
23
40
|
* Ensure .claude directory exists
|
|
24
41
|
*/
|
|
@@ -69,6 +86,9 @@ function writeSessionFile(workItem, mode, options = {}) {
|
|
|
69
86
|
throw new Error('Work item must have a string status');
|
|
70
87
|
}
|
|
71
88
|
|
|
89
|
+
// Validate we're in a worktree, not root directory
|
|
90
|
+
validateWorktreeLocation();
|
|
91
|
+
|
|
72
92
|
ensureClaudeDir();
|
|
73
93
|
|
|
74
94
|
const skillName = modeToSkillName(mode);
|
package/lib/worktree-facade.js
CHANGED
|
@@ -15,6 +15,29 @@
|
|
|
15
15
|
const worktreeManager = require('./worktree-manager');
|
|
16
16
|
const fs = require('fs');
|
|
17
17
|
const path = require('path');
|
|
18
|
+
const { getDb } = require('./database');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if a worktree already exists for a work item
|
|
22
|
+
*
|
|
23
|
+
* @param {Object} db - Database connection
|
|
24
|
+
* @param {number} workItemId - Work item ID
|
|
25
|
+
* @returns {Promise<Object|null>} Existing worktree or null
|
|
26
|
+
*/
|
|
27
|
+
function checkExistingWorktree(db, workItemId) {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
db.get(
|
|
30
|
+
`SELECT * FROM worktrees
|
|
31
|
+
WHERE work_item_id = ?
|
|
32
|
+
AND status = 'active'`,
|
|
33
|
+
[workItemId],
|
|
34
|
+
(err, row) => {
|
|
35
|
+
if (err) return reject(err);
|
|
36
|
+
resolve(row || null);
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
18
41
|
|
|
19
42
|
/**
|
|
20
43
|
* Start work on a work item with graceful degradation
|
|
@@ -33,6 +56,7 @@ async function startWork(workItem, options = {}) {
|
|
|
33
56
|
}
|
|
34
57
|
|
|
35
58
|
const repoPath = options.repoPath || process.cwd();
|
|
59
|
+
const db = options.db || getDb();
|
|
36
60
|
const result = {
|
|
37
61
|
mode: null, // 'worktree' or 'main'
|
|
38
62
|
path: null, // Working directory path
|
|
@@ -41,6 +65,25 @@ async function startWork(workItem, options = {}) {
|
|
|
41
65
|
warnings: [] // Non-fatal warnings
|
|
42
66
|
};
|
|
43
67
|
|
|
68
|
+
// Explicit check for existing worktree (defensive programming)
|
|
69
|
+
try {
|
|
70
|
+
const existingWorktree = await checkExistingWorktree(db, workItem.id);
|
|
71
|
+
if (existingWorktree) {
|
|
72
|
+
// Another instance is already working on this - fall back to main
|
|
73
|
+
result.mode = 'main';
|
|
74
|
+
result.path = repoPath;
|
|
75
|
+
result.error = {
|
|
76
|
+
message: `Work item #${workItem.id} already has active worktree at ${existingWorktree.worktree_path}`,
|
|
77
|
+
reason: 'duplicate_worktree_prevented'
|
|
78
|
+
};
|
|
79
|
+
result.warnings.push('Another instance may be working on this chore');
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
} catch (checkErr) {
|
|
83
|
+
// If check fails, continue anyway - database constraint will catch duplicates
|
|
84
|
+
result.warnings.push(`Warning: Could not check for existing worktree: ${checkErr.message}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
44
87
|
// Attempt to create worktree
|
|
45
88
|
try {
|
|
46
89
|
const worktree = await worktreeManager.createWorktree(workItem, options);
|
package/lib/worktree-manager.js
CHANGED
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
// Database will be required lazily when implementing actual functions
|
|
15
15
|
// const getDb = require('./database');
|
|
16
16
|
|
|
17
|
+
// Safe delete module - ALWAYS use this for deletions
|
|
18
|
+
const safeDelete = require('./safe-delete');
|
|
19
|
+
|
|
17
20
|
/**
|
|
18
21
|
* Valid worktree statuses
|
|
19
22
|
*/
|
|
@@ -44,9 +47,16 @@ async function createWorktree(workItem, options = {}) {
|
|
|
44
47
|
return Promise.reject(new Error('SAFETY: repoPath must be explicitly provided to createWorktree'));
|
|
45
48
|
}
|
|
46
49
|
|
|
50
|
+
// SAFETY CHECK 0: Verify we're not running from within a worktree
|
|
51
|
+
try {
|
|
52
|
+
safeDelete.ensureNotInWorktree();
|
|
53
|
+
} catch (err) {
|
|
54
|
+
return Promise.reject(err);
|
|
55
|
+
}
|
|
56
|
+
|
|
47
57
|
const gitRoot = options.repoPath;
|
|
48
58
|
|
|
49
|
-
// SAFETY CHECK: Verify gitRoot is actually the main repository
|
|
59
|
+
// SAFETY CHECK 1: Verify gitRoot is actually the main repository
|
|
50
60
|
try {
|
|
51
61
|
const gitRootCheck = execSync('git rev-parse --show-toplevel', {
|
|
52
62
|
cwd: gitRoot,
|
|
@@ -205,7 +215,11 @@ async function createWorktree(workItem, options = {}) {
|
|
|
205
215
|
}
|
|
206
216
|
} else {
|
|
207
217
|
// It's a directory - remove it and create symlink
|
|
208
|
-
|
|
218
|
+
// SAFETY: Use safe delete with gitRoot validation
|
|
219
|
+
const deleteResult = safeDelete.safeRmRecursive(jettypodLink, { gitRoot, force: true });
|
|
220
|
+
if (!deleteResult.success) {
|
|
221
|
+
throw new Error(`Failed to remove .jettypod directory: ${deleteResult.error}`);
|
|
222
|
+
}
|
|
209
223
|
fs.symlinkSync(jettypodTarget, jettypodLink, 'dir');
|
|
210
224
|
}
|
|
211
225
|
} else {
|
|
@@ -324,10 +338,10 @@ async function createWorktree(workItem, options = {}) {
|
|
|
324
338
|
|
|
325
339
|
// Cleanup directory
|
|
326
340
|
if (fs.existsSync(worktreePath)) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
console.error(`Failed to cleanup worktree directory: ${
|
|
341
|
+
// SAFETY: Use safe delete with gitRoot validation
|
|
342
|
+
const deleteResult = safeDelete.safeRmRecursive(worktreePath, { gitRoot, force: true });
|
|
343
|
+
if (!deleteResult.success) {
|
|
344
|
+
console.error(`Failed to cleanup worktree directory: ${deleteResult.error}`);
|
|
331
345
|
}
|
|
332
346
|
}
|
|
333
347
|
|
|
@@ -478,12 +492,27 @@ async function removeDirectoryResilient(dirPath, gitRoot) {
|
|
|
478
492
|
const fs = require('fs');
|
|
479
493
|
const { execSync } = require('child_process');
|
|
480
494
|
|
|
495
|
+
// CRITICAL SAFETY: Validate path before ANY deletion attempt
|
|
496
|
+
const validation = safeDelete.validatePath(dirPath, gitRoot);
|
|
497
|
+
if (!validation.safe) {
|
|
498
|
+
throw new Error(`SAFETY: Cannot remove directory - ${validation.reason}`);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Additional safety: Ensure dirPath is within .jettypod-work
|
|
502
|
+
const path = require('path');
|
|
503
|
+
const resolvedDir = path.resolve(dirPath);
|
|
504
|
+
const worktreeBase = path.resolve(gitRoot, '.jettypod-work');
|
|
505
|
+
if (!resolvedDir.startsWith(worktreeBase)) {
|
|
506
|
+
throw new Error(`SAFETY: Can only remove directories within .jettypod-work/. Got: ${dirPath}`);
|
|
507
|
+
}
|
|
508
|
+
|
|
481
509
|
// Stage 1: Try standard git worktree remove (silent)
|
|
482
510
|
try {
|
|
483
511
|
execSync(`git worktree remove "${dirPath}"`, {
|
|
484
512
|
cwd: gitRoot,
|
|
485
513
|
stdio: 'pipe'
|
|
486
514
|
});
|
|
515
|
+
safeDelete.logDeletion(dirPath, 'git-worktree-remove', true);
|
|
487
516
|
return; // Success - exit silently
|
|
488
517
|
} catch (stage1Err) {
|
|
489
518
|
// Stage 1 failed - escalate with logging
|
|
@@ -497,16 +526,18 @@ async function removeDirectoryResilient(dirPath, gitRoot) {
|
|
|
497
526
|
stdio: 'pipe'
|
|
498
527
|
});
|
|
499
528
|
console.log(`✓ Forced git removal succeeded`);
|
|
529
|
+
safeDelete.logDeletion(dirPath, 'git-worktree-remove-force', true);
|
|
500
530
|
return;
|
|
501
531
|
} catch (stage2Err) {
|
|
502
|
-
console.log(`⚠️ Forced git removal failed, escalating to filesystem removal...`);
|
|
532
|
+
console.log(`⚠️ Forced git removal failed, escalating to safe filesystem removal...`);
|
|
503
533
|
}
|
|
504
534
|
|
|
505
|
-
// Stage 3: Try filesystem removal
|
|
535
|
+
// Stage 3: Try SAFE filesystem removal (validates path again)
|
|
506
536
|
if (fs.existsSync(dirPath)) {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
537
|
+
const deleteResult = safeDelete.safeRmRecursive(dirPath, { gitRoot, force: true });
|
|
538
|
+
|
|
539
|
+
if (deleteResult.success) {
|
|
540
|
+
console.log(`✓ Safe filesystem removal succeeded`);
|
|
510
541
|
|
|
511
542
|
// Clean up git metadata
|
|
512
543
|
try {
|
|
@@ -515,25 +546,14 @@ async function removeDirectoryResilient(dirPath, gitRoot) {
|
|
|
515
546
|
// Non-fatal
|
|
516
547
|
}
|
|
517
548
|
return;
|
|
518
|
-
}
|
|
519
|
-
console.log(`⚠️
|
|
549
|
+
} else {
|
|
550
|
+
console.log(`⚠️ Safe filesystem removal failed: ${deleteResult.error}`);
|
|
551
|
+
throw new Error(`Directory removal failed: ${deleteResult.error}`);
|
|
520
552
|
}
|
|
521
553
|
}
|
|
522
554
|
|
|
523
|
-
//
|
|
524
|
-
|
|
525
|
-
await manualRecursiveDelete(dirPath);
|
|
526
|
-
console.log(`✓ Manual recursive deletion succeeded`);
|
|
527
|
-
|
|
528
|
-
// Clean up git metadata
|
|
529
|
-
try {
|
|
530
|
-
execSync('git worktree prune', { cwd: gitRoot, stdio: 'pipe' });
|
|
531
|
-
} catch (pruneErr) {
|
|
532
|
-
// Non-fatal
|
|
533
|
-
}
|
|
534
|
-
} catch (stage4Err) {
|
|
535
|
-
throw new Error(`All removal strategies failed: ${stage4Err.message}`);
|
|
536
|
-
}
|
|
555
|
+
// If we get here, directory doesn't exist - that's fine
|
|
556
|
+
console.log(`✓ Directory already removed or doesn't exist`);
|
|
537
557
|
}
|
|
538
558
|
|
|
539
559
|
/**
|
|
@@ -566,21 +586,14 @@ async function cleanupWorktree(worktreeId, options = {}) {
|
|
|
566
586
|
const db = options.db || getDb();
|
|
567
587
|
const deleteBranch = options.deleteBranch || false;
|
|
568
588
|
|
|
569
|
-
//
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
if (currentGitDir.includes('.jettypod-work') || currentGitDir !== '.git') {
|
|
577
|
-
return Promise.reject(new Error('SAFETY: Cannot run cleanup from within a worktree'));
|
|
578
|
-
}
|
|
579
|
-
} catch (err) {
|
|
580
|
-
// Ignore - might not be in a git repo
|
|
581
|
-
}
|
|
589
|
+
// NOTE: We intentionally do NOT check ensureNotInWorktree() here.
|
|
590
|
+
// The merge workflow (jettypod work merge) is designed to be run from
|
|
591
|
+
// within the worktree being merged. Safety comes from:
|
|
592
|
+
// 1. Requiring explicit repoPath (not relying on process.cwd())
|
|
593
|
+
// 2. Verifying repoPath is the main repo (check below)
|
|
594
|
+
// 3. Path validation in removeDirectoryResilient()
|
|
582
595
|
|
|
583
|
-
// SAFETY CHECK
|
|
596
|
+
// SAFETY CHECK: Verify gitRoot is actually the main repository
|
|
584
597
|
try {
|
|
585
598
|
const gitRootCheck = execSync('git rev-parse --show-toplevel', {
|
|
586
599
|
cwd: gitRoot,
|
package/package.json
CHANGED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Migration: Create worktree_sessions table
|
|
3
|
-
*
|
|
4
|
-
* Purpose: Track which work item is active in each worktree to support multiple
|
|
5
|
-
* concurrent Claude Code instances working on different features simultaneously.
|
|
6
|
-
*
|
|
7
|
-
* Why this is critical:
|
|
8
|
-
* - Enable true concurrent development with multiple worktrees
|
|
9
|
-
* - Prevent confusion when Claude switches between worktrees
|
|
10
|
-
* - Provide centralized visibility of what's active where
|
|
11
|
-
* - Support automatic cleanup when work items are completed
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
module.exports = {
|
|
15
|
-
id: '016-worktree-sessions-table',
|
|
16
|
-
description: 'Create worktree_sessions table for multi-worktree current work tracking',
|
|
17
|
-
|
|
18
|
-
async up(db) {
|
|
19
|
-
return new Promise((resolve, reject) => {
|
|
20
|
-
db.run(`
|
|
21
|
-
CREATE TABLE IF NOT EXISTS worktree_sessions (
|
|
22
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
23
|
-
worktree_path TEXT UNIQUE NOT NULL,
|
|
24
|
-
work_item_id INTEGER NOT NULL,
|
|
25
|
-
branch_name TEXT NOT NULL,
|
|
26
|
-
last_activity DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
27
|
-
FOREIGN KEY (work_item_id) REFERENCES work_items(id)
|
|
28
|
-
)
|
|
29
|
-
`, (err) => {
|
|
30
|
-
if (err) {
|
|
31
|
-
return reject(err);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Create index on worktree_path for fast lookups
|
|
35
|
-
db.run(`
|
|
36
|
-
CREATE INDEX IF NOT EXISTS idx_worktree_sessions_path
|
|
37
|
-
ON worktree_sessions(worktree_path)
|
|
38
|
-
`, (err) => {
|
|
39
|
-
if (err) {
|
|
40
|
-
return reject(err);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Create index on work_item_id for reverse lookups
|
|
44
|
-
db.run(`
|
|
45
|
-
CREATE INDEX IF NOT EXISTS idx_worktree_sessions_work_item
|
|
46
|
-
ON worktree_sessions(work_item_id)
|
|
47
|
-
`, (err) => {
|
|
48
|
-
if (err) {
|
|
49
|
-
return reject(err);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
resolve();
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
},
|
|
58
|
-
|
|
59
|
-
async down(db) {
|
|
60
|
-
return new Promise((resolve, reject) => {
|
|
61
|
-
// Drop indexes first
|
|
62
|
-
db.run('DROP INDEX IF EXISTS idx_worktree_sessions_work_item', (err) => {
|
|
63
|
-
if (err) {
|
|
64
|
-
return reject(err);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
db.run('DROP INDEX IF EXISTS idx_worktree_sessions_path', (err) => {
|
|
68
|
-
if (err) {
|
|
69
|
-
return reject(err);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Drop table
|
|
73
|
-
db.run('DROP TABLE IF EXISTS worktree_sessions', (err) => {
|
|
74
|
-
if (err) {
|
|
75
|
-
return reject(err);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
resolve();
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
};
|
package/lib/worktree-sessions.js
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Worktree Session Management
|
|
3
|
-
*
|
|
4
|
-
* CRUD operations for tracking which work item is active in each worktree.
|
|
5
|
-
* Enables multiple concurrent Claude Code instances working in different worktrees.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const { getDb } = require('./database');
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Create or update a worktree session
|
|
12
|
-
*
|
|
13
|
-
* @param {string} worktreePath - Absolute path to the worktree
|
|
14
|
-
* @param {number} workItemId - ID of the work item
|
|
15
|
-
* @param {string} branchName - Git branch name
|
|
16
|
-
* @returns {Promise<void>}
|
|
17
|
-
*/
|
|
18
|
-
function createOrUpdateSession(worktreePath, workItemId, branchName) {
|
|
19
|
-
const db = getDb();
|
|
20
|
-
|
|
21
|
-
return new Promise((resolve, reject) => {
|
|
22
|
-
db.run(
|
|
23
|
-
`INSERT OR REPLACE INTO worktree_sessions
|
|
24
|
-
(worktree_path, work_item_id, branch_name, last_activity)
|
|
25
|
-
VALUES (?, ?, ?, datetime('now'))`,
|
|
26
|
-
[worktreePath, workItemId, branchName],
|
|
27
|
-
(err) => {
|
|
28
|
-
if (err) {
|
|
29
|
-
// Provide user-friendly error messages based on error type
|
|
30
|
-
if (err.code === 'SQLITE_BUSY') {
|
|
31
|
-
return reject(new Error('Database is locked by another process. Please try again in a moment.'));
|
|
32
|
-
} else if (err.code === 'SQLITE_CORRUPT') {
|
|
33
|
-
return reject(new Error('Database is corrupted. Please run: jettypod init --reset'));
|
|
34
|
-
} else if (err.code === 'SQLITE_CANTOPEN') {
|
|
35
|
-
return reject(new Error('Cannot open database. Check file permissions or run: jettypod init'));
|
|
36
|
-
} else if (err.code === 'SQLITE_READONLY') {
|
|
37
|
-
return reject(new Error('Database is read-only. Check file permissions.'));
|
|
38
|
-
}
|
|
39
|
-
// Generic database error
|
|
40
|
-
console.error('Database error in createOrUpdateSession:', err);
|
|
41
|
-
return reject(new Error(`Failed to create/update worktree session: ${err.message}`));
|
|
42
|
-
}
|
|
43
|
-
resolve();
|
|
44
|
-
}
|
|
45
|
-
);
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Get session by worktree path
|
|
51
|
-
*
|
|
52
|
-
* @param {string} worktreePath - Absolute path to the worktree
|
|
53
|
-
* @returns {Promise<object|null>} Session object or null if not found
|
|
54
|
-
*/
|
|
55
|
-
function getSessionByWorktreePath(worktreePath) {
|
|
56
|
-
const db = getDb();
|
|
57
|
-
|
|
58
|
-
return new Promise((resolve, reject) => {
|
|
59
|
-
db.get(
|
|
60
|
-
`SELECT * FROM worktree_sessions WHERE worktree_path = ?`,
|
|
61
|
-
[worktreePath],
|
|
62
|
-
(err, row) => {
|
|
63
|
-
if (err) {
|
|
64
|
-
// Provide user-friendly error messages
|
|
65
|
-
if (err.code === 'SQLITE_BUSY') {
|
|
66
|
-
return reject(new Error('Database is locked. Please try again in a moment.'));
|
|
67
|
-
} else if (err.code === 'SQLITE_CORRUPT') {
|
|
68
|
-
return reject(new Error('Database is corrupted. Please run: jettypod init --reset'));
|
|
69
|
-
} else if (err.code === 'SQLITE_CANTOPEN') {
|
|
70
|
-
return reject(new Error('Cannot open database. Run: jettypod init'));
|
|
71
|
-
}
|
|
72
|
-
console.error('Database error in getSessionByWorktreePath:', err);
|
|
73
|
-
return reject(new Error(`Failed to get worktree session: ${err.message}`));
|
|
74
|
-
}
|
|
75
|
-
resolve(row || null);
|
|
76
|
-
}
|
|
77
|
-
);
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Get all active worktree sessions
|
|
83
|
-
*
|
|
84
|
-
* @returns {Promise<Array>} Array of session objects
|
|
85
|
-
*/
|
|
86
|
-
function getAllActiveSessions() {
|
|
87
|
-
const db = getDb();
|
|
88
|
-
|
|
89
|
-
return new Promise((resolve, reject) => {
|
|
90
|
-
db.all(
|
|
91
|
-
`SELECT * FROM worktree_sessions ORDER BY last_activity DESC`,
|
|
92
|
-
[],
|
|
93
|
-
(err, rows) => {
|
|
94
|
-
if (err) {
|
|
95
|
-
// Provide user-friendly error messages
|
|
96
|
-
if (err.code === 'SQLITE_BUSY') {
|
|
97
|
-
return reject(new Error('Database is locked. Please try again in a moment.'));
|
|
98
|
-
} else if (err.code === 'SQLITE_CORRUPT') {
|
|
99
|
-
return reject(new Error('Database is corrupted. Please run: jettypod init --reset'));
|
|
100
|
-
} else if (err.code === 'SQLITE_CANTOPEN') {
|
|
101
|
-
return reject(new Error('Cannot open database. Run: jettypod init'));
|
|
102
|
-
}
|
|
103
|
-
console.error('Database error in getAllActiveSessions:', err);
|
|
104
|
-
return reject(new Error(`Failed to get active sessions: ${err.message}`));
|
|
105
|
-
}
|
|
106
|
-
resolve(rows || []);
|
|
107
|
-
}
|
|
108
|
-
);
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Delete session by worktree path
|
|
114
|
-
*
|
|
115
|
-
* @param {string} worktreePath - Absolute path to the worktree
|
|
116
|
-
* @returns {Promise<void>}
|
|
117
|
-
*/
|
|
118
|
-
function deleteSession(worktreePath) {
|
|
119
|
-
const db = getDb();
|
|
120
|
-
|
|
121
|
-
return new Promise((resolve, reject) => {
|
|
122
|
-
db.run(
|
|
123
|
-
`DELETE FROM worktree_sessions WHERE worktree_path = ?`,
|
|
124
|
-
[worktreePath],
|
|
125
|
-
(err) => {
|
|
126
|
-
if (err) {
|
|
127
|
-
// Provide user-friendly error messages
|
|
128
|
-
if (err.code === 'SQLITE_BUSY') {
|
|
129
|
-
return reject(new Error('Database is locked. Please try again in a moment.'));
|
|
130
|
-
} else if (err.code === 'SQLITE_CORRUPT') {
|
|
131
|
-
return reject(new Error('Database is corrupted. Please run: jettypod init --reset'));
|
|
132
|
-
} else if (err.code === 'SQLITE_CANTOPEN') {
|
|
133
|
-
return reject(new Error('Cannot open database. Run: jettypod init'));
|
|
134
|
-
} else if (err.code === 'SQLITE_READONLY') {
|
|
135
|
-
return reject(new Error('Database is read-only. Check file permissions.'));
|
|
136
|
-
}
|
|
137
|
-
console.error('Database error in deleteSession:', err);
|
|
138
|
-
return reject(new Error(`Failed to delete worktree session: ${err.message}`));
|
|
139
|
-
}
|
|
140
|
-
resolve();
|
|
141
|
-
}
|
|
142
|
-
);
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Delete session by work item ID
|
|
148
|
-
*
|
|
149
|
-
* @param {number} workItemId - ID of the work item
|
|
150
|
-
* @returns {Promise<void>}
|
|
151
|
-
*/
|
|
152
|
-
function deleteSessionByWorkItem(workItemId) {
|
|
153
|
-
const db = getDb();
|
|
154
|
-
|
|
155
|
-
return new Promise((resolve, reject) => {
|
|
156
|
-
db.run(
|
|
157
|
-
`DELETE FROM worktree_sessions WHERE work_item_id = ?`,
|
|
158
|
-
[workItemId],
|
|
159
|
-
(err) => {
|
|
160
|
-
if (err) {
|
|
161
|
-
// Provide user-friendly error messages
|
|
162
|
-
if (err.code === 'SQLITE_BUSY') {
|
|
163
|
-
return reject(new Error('Database is locked. Please try again in a moment.'));
|
|
164
|
-
} else if (err.code === 'SQLITE_CORRUPT') {
|
|
165
|
-
return reject(new Error('Database is corrupted. Please run: jettypod init --reset'));
|
|
166
|
-
} else if (err.code === 'SQLITE_CANTOPEN') {
|
|
167
|
-
return reject(new Error('Cannot open database. Run: jettypod init'));
|
|
168
|
-
} else if (err.code === 'SQLITE_READONLY') {
|
|
169
|
-
return reject(new Error('Database is read-only. Check file permissions.'));
|
|
170
|
-
}
|
|
171
|
-
console.error('Database error in deleteSessionByWorkItem:', err);
|
|
172
|
-
return reject(new Error(`Failed to delete session by work item: ${err.message}`));
|
|
173
|
-
}
|
|
174
|
-
resolve();
|
|
175
|
-
}
|
|
176
|
-
);
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
module.exports = {
|
|
181
|
-
createOrUpdateSession,
|
|
182
|
-
getSessionByWorktreePath,
|
|
183
|
-
getAllActiveSessions,
|
|
184
|
-
deleteSession,
|
|
185
|
-
deleteSessionByWorkItem
|
|
186
|
-
};
|