jettypod 4.1.4 → 4.1.6

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/cucumber.js CHANGED
@@ -1,6 +1,5 @@
1
1
  module.exports = {
2
2
  default: {
3
- paths: ['features/**/*.feature'],
4
3
  require: ['features/**/steps.js', 'features/**/simple-steps.js', 'features/**/*.steps.js'],
5
4
  format: ['progress'],
6
5
  publishQuiet: true
@@ -1,5 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
+ const { getDb } = require('./database');
4
+ const { getSessionByWorktreePath, getAllActiveSessions } = require('./worktree-sessions');
3
5
 
4
6
  /**
5
7
  * Get path to current-work.json file
@@ -10,37 +12,100 @@ function getCurrentWorkPath() {
10
12
  }
11
13
 
12
14
  /**
13
- * Get current work item from file
15
+ * Get current work item from worktree session database
14
16
  * @returns {Object|null} Current work item or null if not set
15
- * @throws {Error} If file exists but cannot be read due to permissions
16
17
  */
17
- function getCurrentWork() {
18
- const currentWorkPath = getCurrentWorkPath();
18
+ async function getCurrentWork() {
19
+ const worktreePath = process.cwd();
20
+ const db = getDb();
19
21
 
20
- if (!fs.existsSync(currentWorkPath)) {
22
+ // Query worktree session and join with work_items
23
+ let row;
24
+ try {
25
+ row = await new Promise((resolve, reject) => {
26
+ db.get(
27
+ `SELECT
28
+ wi.id, wi.title, wi.type, wi.status, wi.parent_id, wi.epic_id,
29
+ parent.title as parent_title,
30
+ epic.title as epic_title
31
+ FROM worktree_sessions ws
32
+ JOIN work_items wi ON ws.work_item_id = wi.id
33
+ LEFT JOIN work_items parent ON wi.parent_id = parent.id
34
+ LEFT JOIN work_items epic ON wi.epic_id = epic.id
35
+ WHERE ws.worktree_path = ?`,
36
+ [worktreePath],
37
+ (err, row) => {
38
+ if (err) {
39
+ return reject(err);
40
+ }
41
+ resolve(row);
42
+ }
43
+ );
44
+ });
45
+ } catch (dbErr) {
46
+ // Handle database errors gracefully - log but don't crash
47
+ console.error('Database error querying current work:', dbErr.message);
48
+ // Return null instead of throwing - graceful degradation
21
49
  return null;
22
50
  }
23
51
 
24
- try {
25
- const content = fs.readFileSync(currentWorkPath, 'utf-8');
26
- const workItem = JSON.parse(content);
27
-
52
+ if (row) {
28
53
  // Validate required fields
29
- if (!workItem.id || !workItem.title || !workItem.type) {
30
- console.warn('Warning: Current work file is missing required fields, ignoring');
54
+ if (!row.id || !row.title || !row.type) {
55
+ console.warn('Warning: Work item is missing required fields, ignoring');
31
56
  return null;
32
57
  }
58
+ return row;
59
+ }
33
60
 
34
- return workItem;
35
- } catch (err) {
36
- if (err.code === 'EACCES') {
37
- throw new Error(`No read permission for current work file: ${currentWorkPath}`);
61
+ // No session found - check if we should warn about main branch
62
+ try {
63
+ const allSessions = await getAllActiveSessions();
64
+ if (allSessions.length > 0) {
65
+ // Get all work items in ONE query using IN clause
66
+ const ids = allSessions.map(s => s.work_item_id);
67
+ const placeholders = ids.map(() => '?').join(',');
68
+
69
+ let workItems;
70
+ try {
71
+ workItems = await new Promise((resolve, reject) => {
72
+ db.all(
73
+ `SELECT id, title FROM work_items WHERE id IN (${placeholders})`,
74
+ ids,
75
+ (err, rows) => {
76
+ if (err) reject(err);
77
+ else resolve(rows || []);
78
+ }
79
+ );
80
+ });
81
+ } catch (workItemErr) {
82
+ // Database error getting work items - log but continue
83
+ console.error('Database error getting work items:', workItemErr.message);
84
+ workItems = [];
85
+ }
86
+
87
+ // Create map of id -> title
88
+ const titleMap = {};
89
+ for (const wi of workItems) {
90
+ titleMap[wi.id] = wi.title;
91
+ }
92
+
93
+ // Display warnings
94
+ console.warn('No active work in main branch');
95
+ console.warn('\nActive work in other worktrees:');
96
+ for (const session of allSessions) {
97
+ const title = titleMap[session.work_item_id];
98
+ if (title) {
99
+ console.warn(` • ${title} (${session.worktree_path})`);
100
+ }
101
+ }
38
102
  }
39
-
40
- // Corrupted JSON - warn but don't throw
41
- console.warn(`Warning: Corrupted current work file: ${err.message}`);
42
- return null;
103
+ } catch (listErr) {
104
+ // Ignore listing errors - gracefully fail
105
+ console.error('Error listing active sessions:', listErr.message);
43
106
  }
107
+
108
+ return null;
44
109
  }
45
110
 
46
111
  /**
@@ -71,54 +136,41 @@ function validateWorkItem(workItem) {
71
136
  }
72
137
 
73
138
  /**
74
- * Set current work item to file
139
+ * Set current work item in worktree session database
75
140
  * @param {Object} workItem - Work item to set as current
76
- * @throws {Error} If workItem is invalid or file cannot be written
141
+ * @throws {Error} If workItem is invalid
77
142
  */
78
- function setCurrentWork(workItem) {
143
+ async function setCurrentWork(workItem) {
79
144
  validateWorkItem(workItem);
80
145
 
81
- const currentWorkPath = getCurrentWorkPath();
82
- const jettypodDir = path.dirname(currentWorkPath);
146
+ const { createOrUpdateSession } = require('./worktree-sessions');
147
+ const { execSync } = require('child_process');
148
+ const worktreePath = process.cwd();
83
149
 
150
+ // Get current branch name
151
+ let branchName;
84
152
  try {
85
- // Ensure .jettypod directory exists
86
- if (!fs.existsSync(jettypodDir)) {
87
- fs.mkdirSync(jettypodDir, { recursive: true });
88
- }
89
-
90
- // Check write permission
91
- if (fs.existsSync(jettypodDir)) {
92
- fs.accessSync(jettypodDir, fs.constants.W_OK);
93
- }
94
-
95
- fs.writeFileSync(currentWorkPath, JSON.stringify(workItem, null, 2));
153
+ branchName = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8' }).trim();
96
154
  } catch (err) {
97
- if (err.code === 'EACCES') {
98
- throw new Error(`No write permission for directory: ${jettypodDir}`);
99
- }
100
- throw new Error(`Failed to write current work file: ${err.message}`);
155
+ branchName = 'unknown';
101
156
  }
157
+
158
+ // Create or update worktree session in database
159
+ await createOrUpdateSession(worktreePath, workItem.id, branchName);
102
160
  }
103
161
 
104
162
  /**
105
- * Clear current work item file
106
- * @throws {Error} If file cannot be deleted due to permissions
163
+ * Clear current work item from worktree session database
164
+ * @throws {Error} If session cannot be deleted
107
165
  */
108
- function clearCurrentWork() {
109
- const currentWorkPath = getCurrentWorkPath();
110
-
111
- if (!fs.existsSync(currentWorkPath)) {
112
- return;
113
- }
166
+ async function clearCurrentWork() {
167
+ const { deleteSession } = require('./worktree-sessions');
168
+ const worktreePath = process.cwd();
114
169
 
115
170
  try {
116
- fs.unlinkSync(currentWorkPath);
171
+ await deleteSession(worktreePath);
117
172
  } catch (err) {
118
- if (err.code === 'EACCES') {
119
- throw new Error(`No permission to delete current work file: ${currentWorkPath}`);
120
- }
121
- throw new Error(`Failed to clear current work file: ${err.message}`);
173
+ throw new Error(`Failed to clear current work session: ${err.message}`);
122
174
  }
123
175
  }
124
176
 
@@ -0,0 +1,84 @@
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
+ };
@@ -100,9 +100,34 @@ async function createWorktree(workItem, options = {}) {
100
100
  });
101
101
 
102
102
  // Step 2: Create git worktree
103
- // Always branch from 'main' to avoid nested worktrees
103
+ // Branch from the default branch (main, master, etc.) to avoid nested worktrees
104
104
  try {
105
- execSync(`git worktree add -b "${branchName}" "${worktreePath}" main`, {
105
+ // Detect the default branch name
106
+ let defaultBranch;
107
+ try {
108
+ // Try to get the default branch from git config
109
+ defaultBranch = execSync('git symbolic-ref refs/remotes/origin/HEAD', {
110
+ cwd: gitRoot,
111
+ encoding: 'utf8',
112
+ stdio: ['pipe', 'pipe', 'pipe']
113
+ }).trim().replace('refs/remotes/origin/', '');
114
+ } catch {
115
+ // Fallback: check which common branch names exist
116
+ try {
117
+ execSync('git rev-parse --verify main', { cwd: gitRoot, stdio: 'pipe' });
118
+ defaultBranch = 'main';
119
+ } catch {
120
+ try {
121
+ execSync('git rev-parse --verify master', { cwd: gitRoot, stdio: 'pipe' });
122
+ defaultBranch = 'master';
123
+ } catch {
124
+ // Last resort: use HEAD
125
+ defaultBranch = 'HEAD';
126
+ }
127
+ }
128
+ }
129
+
130
+ execSync(`git worktree add -b "${branchName}" "${worktreePath}" ${defaultBranch}`, {
106
131
  cwd: gitRoot,
107
132
  stdio: 'pipe'
108
133
  });
@@ -0,0 +1,186 @@
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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jettypod",
3
- "version": "4.1.4",
3
+ "version": "4.1.6",
4
4
  "description": "AI-powered development workflow manager with TDD, BDD, and automatic test generation",
5
5
  "main": "jettypod.js",
6
6
  "bin": {