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 +13 -1
- package/lib/db-watcher.js +116 -0
- package/lib/work-commands/index.js +24 -0
- package/lib/ws-server.js +126 -0
- package/package.json +1 -1
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
|
}
|
package/lib/ws-server.js
ADDED
|
@@ -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
|
+
};
|