jettypod 4.4.65 → 4.4.66

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/jettypod.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const config = require('./lib/config');
6
+ const wsServer = require('./lib/ws-server');
6
7
  // getModeBehaviorContent removed - skills now provide all mode guidance
7
8
 
8
9
  // CRITICAL: Calculate and cache the REAL git root BEFORE any worktree operations
@@ -2125,6 +2126,16 @@ switch (command) {
2125
2126
  }
2126
2127
  }
2127
2128
 
2129
+ // Start WebSocket server for real-time updates
2130
+ const WS_PORT = 8080;
2131
+ const { getDbPath } = require('./lib/database');
2132
+ try {
2133
+ await wsServer.start(WS_PORT, { dbPath: getDbPath() });
2134
+ } catch (err) {
2135
+ // WebSocket server failed to start (port in use?) - continue without it
2136
+ console.log('⚠️ WebSocket server unavailable (real-time updates disabled)');
2137
+ }
2138
+
2128
2139
  // Start dashboard in background with project path
2129
2140
  console.log('🚀 Starting dashboard...');
2130
2141
  const dashboardProcess = spawn('npm', ['run', 'start', '--', '-p', String(availablePort)], {
@@ -2133,7 +2144,8 @@ switch (command) {
2133
2144
  stdio: 'ignore',
2134
2145
  env: {
2135
2146
  ...process.env,
2136
- JETTYPOD_PROJECT_PATH: process.cwd()
2147
+ JETTYPOD_PROJECT_PATH: process.cwd(),
2148
+ JETTYPOD_WS_PORT: String(WS_PORT)
2137
2149
  }
2138
2150
  });
2139
2151
  dashboardProcess.unref();
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Database change watcher for triggering WebSocket broadcasts.
3
+ *
4
+ * Watches the .jettypod/work.db file and its WAL file for modifications
5
+ * and calls a callback when changes are detected.
6
+ *
7
+ * Usage:
8
+ * const dbWatcher = require('./lib/db-watcher');
9
+ * dbWatcher.start((changeType) => {
10
+ * wsServer.broadcast({ type: 'db_change' });
11
+ * });
12
+ * dbWatcher.stop();
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ let lastMtimes = { db: null, wal: null };
19
+ let pollInterval = null;
20
+ let onChange = null;
21
+ let watchedPath = null;
22
+
23
+ // Polling interval in milliseconds
24
+ const POLL_MS = 50;
25
+
26
+ /**
27
+ * Start watching the database file for changes
28
+ * @param {Function} callback - Called when database changes detected
29
+ * @param {string} dbPath - Path to database file (default: .jettypod/work.db)
30
+ */
31
+ function start(callback, dbPath = null) {
32
+ if (pollInterval) {
33
+ return; // Already watching
34
+ }
35
+
36
+ onChange = callback;
37
+ watchedPath = dbPath || path.join(process.cwd(), '.jettypod', 'work.db');
38
+
39
+ // Check if file exists
40
+ if (!fs.existsSync(watchedPath)) {
41
+ return;
42
+ }
43
+
44
+ // Get initial mtimes for both db and WAL file
45
+ try {
46
+ const dbStats = fs.statSync(watchedPath);
47
+ lastMtimes.db = dbStats.mtimeMs;
48
+
49
+ // Also check WAL file (SQLite in WAL mode writes here first)
50
+ const walPath = watchedPath + '-wal';
51
+ if (fs.existsSync(walPath)) {
52
+ const walStats = fs.statSync(walPath);
53
+ lastMtimes.wal = walStats.mtimeMs;
54
+ }
55
+ } catch {
56
+ return;
57
+ }
58
+
59
+ // Use polling - most reliable for SQLite files across platforms
60
+ pollInterval = setInterval(() => {
61
+ try {
62
+ let changed = false;
63
+
64
+ // Check main db file
65
+ const dbStats = fs.statSync(watchedPath);
66
+ if (dbStats.mtimeMs !== lastMtimes.db) {
67
+ lastMtimes.db = dbStats.mtimeMs;
68
+ changed = true;
69
+ }
70
+
71
+ // Check WAL file (where SQLite writes first in WAL mode)
72
+ const walPath = watchedPath + '-wal';
73
+ if (fs.existsSync(walPath)) {
74
+ const walStats = fs.statSync(walPath);
75
+ if (walStats.mtimeMs !== lastMtimes.wal) {
76
+ lastMtimes.wal = walStats.mtimeMs;
77
+ changed = true;
78
+ }
79
+ }
80
+
81
+ if (changed && onChange) {
82
+ onChange('change');
83
+ }
84
+ } catch {
85
+ // File might be temporarily locked during writes
86
+ }
87
+ }, POLL_MS);
88
+ }
89
+
90
+ /**
91
+ * Stop watching the database file
92
+ */
93
+ function stop() {
94
+ if (pollInterval) {
95
+ clearInterval(pollInterval);
96
+ pollInterval = null;
97
+ }
98
+
99
+ onChange = null;
100
+ lastMtimes = { db: null, wal: null };
101
+ watchedPath = null;
102
+ }
103
+
104
+ /**
105
+ * Check if currently watching
106
+ * @returns {boolean}
107
+ */
108
+ function isWatching() {
109
+ return pollInterval !== null;
110
+ }
111
+
112
+ module.exports = {
113
+ start,
114
+ stop,
115
+ isWatching,
116
+ };
@@ -1592,6 +1592,11 @@ async function mergeWork(options = {}) {
1592
1592
 
1593
1593
  // Clean up worktree if it exists
1594
1594
  if (worktree && worktree.worktree_path && fs.existsSync(worktree.worktree_path)) {
1595
+ // Check if shell CWD is inside the worktree being deleted
1596
+ const shellCwd = process.cwd();
1597
+ const worktreePath = path.resolve(worktree.worktree_path);
1598
+ const cwdWillBeInvalid = shellCwd.startsWith(worktreePath);
1599
+
1595
1600
  console.log('Cleaning up worktree...');
1596
1601
  try {
1597
1602
  // Remove the git worktree
@@ -1613,6 +1618,13 @@ async function mergeWork(options = {}) {
1613
1618
  });
1614
1619
 
1615
1620
  console.log('✅ Worktree cleaned up');
1621
+
1622
+ // Warn if shell CWD was inside deleted worktree
1623
+ if (cwdWillBeInvalid) {
1624
+ console.log('');
1625
+ console.log('⚠️ Your shell was inside the deleted worktree.');
1626
+ console.log(` Run this to fix: cd ${gitRoot}`);
1627
+ }
1616
1628
  } catch (worktreeErr) {
1617
1629
  console.warn(`Warning: Failed to clean up worktree: ${worktreeErr.message}`);
1618
1630
  // Non-fatal - continue with merge success
@@ -1886,6 +1898,11 @@ async function testsMerge(featureId) {
1886
1898
  }
1887
1899
 
1888
1900
  // Clean up the worktree
1901
+ // Check if shell CWD is inside the worktree being deleted
1902
+ const shellCwd = process.cwd();
1903
+ const resolvedWorktreePath = path.resolve(worktreePath);
1904
+ const cwdWillBeInvalid = shellCwd.startsWith(resolvedWorktreePath);
1905
+
1889
1906
  try {
1890
1907
  execSync(`git worktree remove "${worktreePath}" --force`, {
1891
1908
  cwd: gitRoot,
@@ -1893,6 +1910,13 @@ async function testsMerge(featureId) {
1893
1910
  stdio: 'pipe'
1894
1911
  });
1895
1912
  console.log('✅ Removed worktree directory');
1913
+
1914
+ // Warn if shell CWD was inside deleted worktree
1915
+ if (cwdWillBeInvalid) {
1916
+ console.log('');
1917
+ console.log('⚠️ Your shell was inside the deleted worktree.');
1918
+ console.log(` Run this to fix: cd ${gitRoot}`);
1919
+ }
1896
1920
  } catch (err) {
1897
1921
  console.log('⚠️ Failed to remove worktree (non-fatal):', err.message);
1898
1922
  }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * WebSocket server for broadcasting database changes to connected dashboard clients.
3
+ *
4
+ * Usage:
5
+ * const wsServer = require('./lib/ws-server');
6
+ * await wsServer.start(); // Start on port 8080 and watch for db changes
7
+ * wsServer.broadcast({ type: 'db_change' }); // Manual broadcast to all clients
8
+ * await wsServer.stop(); // Graceful shutdown
9
+ */
10
+
11
+ const { WebSocketServer } = require('ws');
12
+ const dbWatcher = require('./db-watcher');
13
+
14
+ const DEFAULT_PORT = 8080;
15
+
16
+ let wss = null;
17
+ const clients = new Set();
18
+
19
+ /**
20
+ * Start the WebSocket server
21
+ * @param {number} port - Port to listen on (default: 8080)
22
+ * @param {object} options - Options { dbPath: string }
23
+ * @returns {Promise<void>}
24
+ */
25
+ async function start(port = DEFAULT_PORT, options = {}) {
26
+ if (wss) {
27
+ return; // Already running
28
+ }
29
+
30
+ return new Promise((resolve, reject) => {
31
+ wss = new WebSocketServer({ port });
32
+
33
+ wss.on('listening', () => {
34
+ // Start watching database for changes
35
+ dbWatcher.start(() => {
36
+ broadcast({ type: 'db_change', timestamp: Date.now() });
37
+ }, options.dbPath);
38
+ resolve();
39
+ });
40
+
41
+ wss.on('error', (error) => {
42
+ wss = null;
43
+ reject(error);
44
+ });
45
+
46
+ wss.on('connection', (ws) => {
47
+ clients.add(ws);
48
+
49
+ // Send connected confirmation
50
+ ws.send(JSON.stringify({ type: 'connected', timestamp: Date.now() }));
51
+
52
+ ws.on('close', () => {
53
+ clients.delete(ws);
54
+ });
55
+
56
+ ws.on('error', () => {
57
+ clients.delete(ws);
58
+ });
59
+ });
60
+ });
61
+ }
62
+
63
+ /**
64
+ * Stop the WebSocket server
65
+ * @returns {Promise<void>}
66
+ */
67
+ async function stop() {
68
+ // Stop database watcher
69
+ dbWatcher.stop();
70
+
71
+ if (!wss) {
72
+ return;
73
+ }
74
+
75
+ return new Promise((resolve) => {
76
+ // Close all client connections
77
+ for (const client of clients) {
78
+ client.close();
79
+ }
80
+ clients.clear();
81
+
82
+ // Close the server
83
+ wss.close(() => {
84
+ wss = null;
85
+ resolve();
86
+ });
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Broadcast a message to all connected clients
92
+ * @param {object} message - Message to broadcast (will be JSON stringified)
93
+ */
94
+ function broadcast(message) {
95
+ const data = JSON.stringify(message);
96
+ for (const client of clients) {
97
+ if (client.readyState === 1) { // WebSocket.OPEN
98
+ client.send(data);
99
+ }
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Get the number of connected clients
105
+ * @returns {number}
106
+ */
107
+ function getClientCount() {
108
+ return clients.size;
109
+ }
110
+
111
+ /**
112
+ * Check if the server is running
113
+ * @returns {boolean}
114
+ */
115
+ function isRunning() {
116
+ return wss !== null;
117
+ }
118
+
119
+ module.exports = {
120
+ start,
121
+ stop,
122
+ broadcast,
123
+ getClientCount,
124
+ isRunning,
125
+ DEFAULT_PORT,
126
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jettypod",
3
- "version": "4.4.65",
3
+ "version": "4.4.66",
4
4
  "description": "AI-powered development workflow manager with TDD, BDD, and automatic test generation",
5
5
  "main": "jettypod.js",
6
6
  "bin": {