code-squad-cli 1.2.16 → 1.2.17
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/dist/flip/index.js +159 -36
- package/dist/flip/routes/cancel.d.ts +3 -0
- package/dist/flip/routes/cancel.js +10 -7
- package/dist/flip/routes/changes.d.ts +8 -0
- package/dist/flip/routes/changes.js +26 -0
- package/dist/flip/routes/file.js +23 -4
- package/dist/flip/routes/files.js +28 -7
- package/dist/flip/routes/git.js +30 -9
- package/dist/flip/routes/ping.d.ts +6 -0
- package/dist/flip/routes/ping.js +14 -0
- package/dist/flip/routes/session.d.ts +14 -0
- package/dist/flip/routes/session.js +54 -0
- package/dist/flip/routes/submit.js +8 -6
- package/dist/flip/server/Server.d.ts +40 -6
- package/dist/flip/server/Server.js +151 -46
- package/dist/flip/session/SessionManager.d.ts +69 -0
- package/dist/flip/session/SessionManager.js +163 -0
- package/dist/flip-ui/dist/assets/{index-B2PjDe6F.js → index-KAtdqB2p.js} +58 -58
- package/dist/flip-ui/dist/index.html +1 -1
- package/dist/index.js +597 -151
- package/package.json +1 -1
- package/dist/flip/events/SSEManager.d.ts +0 -10
- package/dist/flip/events/SSEManager.js +0 -28
- package/dist/flip/events/types.d.ts +0 -11
- package/dist/flip/events/types.js +0 -1
- package/dist/flip/routes/events.d.ts +0 -3
- package/dist/flip/routes/events.js +0 -26
|
@@ -1,11 +1,45 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { SessionManager } from '../session/SessionManager.js';
|
|
2
|
+
/** Server identification string for ping endpoint */
|
|
3
|
+
export declare const SERVER_ID = "csq-flip";
|
|
4
|
+
export interface ServerOptions {
|
|
5
|
+
/** Session timeout in ms. Default: 30000 (30 seconds) */
|
|
6
|
+
sessionTimeoutMs?: number;
|
|
7
|
+
/** Idle timeout in ms. Server shuts down if no session is registered within this time. 0 = no timeout. */
|
|
8
|
+
idleTimeout?: number;
|
|
4
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Singleton flip server that manages multiple sessions
|
|
12
|
+
*/
|
|
5
13
|
export declare class Server {
|
|
6
|
-
private cwd;
|
|
7
14
|
private port;
|
|
8
|
-
|
|
9
|
-
|
|
15
|
+
private options;
|
|
16
|
+
private app;
|
|
17
|
+
private server;
|
|
18
|
+
private sessionManager;
|
|
19
|
+
private resolveShutdown;
|
|
20
|
+
constructor(port: number, options?: ServerOptions);
|
|
21
|
+
/**
|
|
22
|
+
* Start the server and return a promise that resolves when server shuts down
|
|
23
|
+
*/
|
|
24
|
+
run(): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Shutdown the server gracefully
|
|
27
|
+
*/
|
|
28
|
+
shutdown(): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Get the session manager
|
|
31
|
+
*/
|
|
32
|
+
getSessionManager(): SessionManager | null;
|
|
10
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Find an available port starting from the preferred port
|
|
36
|
+
*/
|
|
11
37
|
export declare function findFreePort(preferred: number, maxPort?: number): Promise<number>;
|
|
38
|
+
/**
|
|
39
|
+
* Check if a csq-flip server is running on the given port
|
|
40
|
+
*/
|
|
41
|
+
export declare function isFlipServerRunning(port: number, timeoutMs?: number): Promise<boolean>;
|
|
42
|
+
/**
|
|
43
|
+
* Find an existing csq-flip server in the port range
|
|
44
|
+
*/
|
|
45
|
+
export declare function findExistingServer(startPort: number, endPort: number): Promise<number | null>;
|
|
@@ -8,67 +8,129 @@ import { gitRouter } from '../routes/git.js';
|
|
|
8
8
|
import { submitRouter } from '../routes/submit.js';
|
|
9
9
|
import { cancelRouter } from '../routes/cancel.js';
|
|
10
10
|
import { createStaticRouter } from '../routes/static.js';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
11
|
+
import { createPingRouter } from '../routes/ping.js';
|
|
12
|
+
import { createSessionRouter } from '../routes/session.js';
|
|
13
|
+
import { createChangesRouter } from '../routes/changes.js';
|
|
14
|
+
import { SessionManager } from '../session/SessionManager.js';
|
|
15
|
+
const log = (...args) => console.error(...args);
|
|
16
|
+
/** Server identification string for ping endpoint */
|
|
17
|
+
export const SERVER_ID = 'csq-flip';
|
|
18
|
+
/**
|
|
19
|
+
* Singleton flip server that manages multiple sessions
|
|
20
|
+
*/
|
|
14
21
|
export class Server {
|
|
15
|
-
cwd;
|
|
16
22
|
port;
|
|
17
|
-
|
|
18
|
-
|
|
23
|
+
options;
|
|
24
|
+
app = null;
|
|
25
|
+
server = null;
|
|
26
|
+
sessionManager = null;
|
|
27
|
+
resolveShutdown = null;
|
|
28
|
+
constructor(port, options = {}) {
|
|
19
29
|
this.port = port;
|
|
30
|
+
this.options = {
|
|
31
|
+
sessionTimeoutMs: options.sessionTimeoutMs ?? 30000,
|
|
32
|
+
idleTimeout: options.idleTimeout ?? 0,
|
|
33
|
+
};
|
|
20
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Start the server and return a promise that resolves when server shuts down
|
|
37
|
+
*/
|
|
21
38
|
async run() {
|
|
22
39
|
return new Promise((resolve) => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
resolve: null,
|
|
26
|
-
};
|
|
27
|
-
const app = express();
|
|
40
|
+
this.resolveShutdown = resolve;
|
|
41
|
+
this.app = express();
|
|
28
42
|
// Middleware
|
|
29
|
-
app.use(cors());
|
|
30
|
-
app.use(express.json());
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
sseManager.broadcast({ type: 'files-changed' });
|
|
39
|
-
});
|
|
40
|
-
fileWatcher.onFileChanged((path) => {
|
|
41
|
-
sseManager.broadcast({ type: 'file-changed', path });
|
|
42
|
-
});
|
|
43
|
-
fileWatcher.onGitChanged(() => {
|
|
44
|
-
sseManager.broadcast({ type: 'git-changed' });
|
|
43
|
+
this.app.use(cors());
|
|
44
|
+
this.app.use(express.json());
|
|
45
|
+
// Initialize session manager
|
|
46
|
+
this.sessionManager = new SessionManager({
|
|
47
|
+
sessionTimeoutMs: this.options.sessionTimeoutMs,
|
|
48
|
+
onAllSessionsGone: () => {
|
|
49
|
+
log('[Server] All sessions gone, shutting down...');
|
|
50
|
+
this.shutdown();
|
|
51
|
+
},
|
|
45
52
|
});
|
|
46
|
-
|
|
47
|
-
|
|
53
|
+
this.sessionManager.start();
|
|
54
|
+
// Store session manager in app locals
|
|
55
|
+
this.app.locals.sessionManager = this.sessionManager;
|
|
48
56
|
// API routes
|
|
49
|
-
app.use('/api/
|
|
50
|
-
app.use('/api/
|
|
51
|
-
app.use('/api/
|
|
52
|
-
app.use('/api/
|
|
53
|
-
app.use('/api/
|
|
54
|
-
app.use('/api/
|
|
57
|
+
this.app.use('/api/ping', createPingRouter());
|
|
58
|
+
this.app.use('/api/session', createSessionRouter(this.sessionManager));
|
|
59
|
+
this.app.use('/api/changes', createChangesRouter(this.sessionManager));
|
|
60
|
+
this.app.use('/api/files', filesRouter);
|
|
61
|
+
this.app.use('/api/file', fileRouter);
|
|
62
|
+
this.app.use('/api/git', gitRouter);
|
|
63
|
+
this.app.use('/api/submit', submitRouter);
|
|
64
|
+
this.app.use('/api/cancel', cancelRouter);
|
|
55
65
|
// Static files (fallback to web-ui dist)
|
|
56
|
-
app.use(createStaticRouter());
|
|
57
|
-
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
sseManager.closeAll();
|
|
63
|
-
await new Promise(res => server.close(() => res()));
|
|
64
|
-
resolve(output);
|
|
66
|
+
this.app.use(createStaticRouter());
|
|
67
|
+
this.server = http.createServer(this.app);
|
|
68
|
+
// Handle SIGINT/SIGTERM for graceful shutdown
|
|
69
|
+
const signalHandler = () => {
|
|
70
|
+
log('\nShutting down...');
|
|
71
|
+
this.shutdown();
|
|
65
72
|
};
|
|
66
|
-
|
|
67
|
-
|
|
73
|
+
process.on('SIGINT', signalHandler);
|
|
74
|
+
process.on('SIGTERM', signalHandler);
|
|
75
|
+
// Idle timeout - shutdown if no session is registered
|
|
76
|
+
let idleTimer = null;
|
|
77
|
+
if (this.options.idleTimeout && this.options.idleTimeout > 0) {
|
|
78
|
+
idleTimer = setTimeout(() => {
|
|
79
|
+
if (!this.sessionManager?.hasSessions) {
|
|
80
|
+
log('\nNo session registered, shutting down...');
|
|
81
|
+
this.shutdown();
|
|
82
|
+
}
|
|
83
|
+
}, this.options.idleTimeout);
|
|
84
|
+
// Clear timeout when first session is registered
|
|
85
|
+
// This is handled in session registration
|
|
86
|
+
}
|
|
87
|
+
// Store idle timer for cleanup
|
|
88
|
+
this.app.locals.idleTimer = idleTimer;
|
|
89
|
+
this.app.locals.clearIdleTimer = () => {
|
|
90
|
+
if (idleTimer) {
|
|
91
|
+
clearTimeout(idleTimer);
|
|
92
|
+
idleTimer = null;
|
|
93
|
+
this.app.locals.idleTimer = null;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
this.server.listen(this.port, '127.0.0.1', () => {
|
|
97
|
+
log(`[Server] Running at http://localhost:${this.port}`);
|
|
68
98
|
});
|
|
69
99
|
});
|
|
70
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Shutdown the server gracefully
|
|
103
|
+
*/
|
|
104
|
+
async shutdown() {
|
|
105
|
+
if (!this.server)
|
|
106
|
+
return;
|
|
107
|
+
// Clear idle timer if exists
|
|
108
|
+
if (this.app?.locals.idleTimer) {
|
|
109
|
+
clearTimeout(this.app.locals.idleTimer);
|
|
110
|
+
}
|
|
111
|
+
// Stop session manager (stops all watchers)
|
|
112
|
+
if (this.sessionManager) {
|
|
113
|
+
await this.sessionManager.stop();
|
|
114
|
+
}
|
|
115
|
+
// Close HTTP server
|
|
116
|
+
await new Promise(res => {
|
|
117
|
+
this.server.close(() => res());
|
|
118
|
+
});
|
|
119
|
+
log('[Server] Shutdown complete');
|
|
120
|
+
if (this.resolveShutdown) {
|
|
121
|
+
this.resolveShutdown();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Get the session manager
|
|
126
|
+
*/
|
|
127
|
+
getSessionManager() {
|
|
128
|
+
return this.sessionManager;
|
|
129
|
+
}
|
|
71
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Find an available port starting from the preferred port
|
|
133
|
+
*/
|
|
72
134
|
export async function findFreePort(preferred, maxPort = 65535) {
|
|
73
135
|
return new Promise((resolve, reject) => {
|
|
74
136
|
if (preferred > maxPort) {
|
|
@@ -88,3 +150,46 @@ export async function findFreePort(preferred, maxPort = 65535) {
|
|
|
88
150
|
});
|
|
89
151
|
});
|
|
90
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Check if a csq-flip server is running on the given port
|
|
155
|
+
*/
|
|
156
|
+
export async function isFlipServerRunning(port, timeoutMs = 1000) {
|
|
157
|
+
return new Promise((resolve) => {
|
|
158
|
+
const req = http.request({
|
|
159
|
+
hostname: '127.0.0.1',
|
|
160
|
+
port,
|
|
161
|
+
path: '/api/ping',
|
|
162
|
+
method: 'GET',
|
|
163
|
+
timeout: timeoutMs,
|
|
164
|
+
}, (res) => {
|
|
165
|
+
let data = '';
|
|
166
|
+
res.on('data', chunk => data += chunk);
|
|
167
|
+
res.on('end', () => {
|
|
168
|
+
try {
|
|
169
|
+
const json = JSON.parse(data);
|
|
170
|
+
resolve(json.id === SERVER_ID);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
resolve(false);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
req.on('error', () => resolve(false));
|
|
178
|
+
req.on('timeout', () => {
|
|
179
|
+
req.destroy();
|
|
180
|
+
resolve(false);
|
|
181
|
+
});
|
|
182
|
+
req.end();
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Find an existing csq-flip server in the port range
|
|
187
|
+
*/
|
|
188
|
+
export async function findExistingServer(startPort, endPort) {
|
|
189
|
+
for (let port = startPort; port <= endPort; port++) {
|
|
190
|
+
if (await isFlipServerRunning(port)) {
|
|
191
|
+
return port;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { FileWatcher } from '../watcher/FileWatcher.js';
|
|
2
|
+
export interface Session {
|
|
3
|
+
/** Session ID (tty path, e.g., /dev/ttys001) */
|
|
4
|
+
id: string;
|
|
5
|
+
/** Working directory for this session */
|
|
6
|
+
cwd: string;
|
|
7
|
+
/** FileWatcher instance for this session */
|
|
8
|
+
watcher: FileWatcher;
|
|
9
|
+
/** Last activity timestamp (ms since epoch) */
|
|
10
|
+
lastActivity: number;
|
|
11
|
+
/** Changed files since last poll (for polling-based sync) */
|
|
12
|
+
pendingChanges: PendingChanges;
|
|
13
|
+
}
|
|
14
|
+
export interface PendingChanges {
|
|
15
|
+
filesChanged: boolean;
|
|
16
|
+
gitChanged: boolean;
|
|
17
|
+
changedFiles: Set<string>;
|
|
18
|
+
}
|
|
19
|
+
export interface SessionManagerOptions {
|
|
20
|
+
/** Session timeout in ms. Sessions are cleaned up after this period of inactivity. */
|
|
21
|
+
sessionTimeoutMs: number;
|
|
22
|
+
/** Callback when all sessions are gone */
|
|
23
|
+
onAllSessionsGone?: () => void;
|
|
24
|
+
}
|
|
25
|
+
export declare class SessionManager {
|
|
26
|
+
private sessions;
|
|
27
|
+
private options;
|
|
28
|
+
private cleanupInterval;
|
|
29
|
+
constructor(options: SessionManagerOptions);
|
|
30
|
+
/**
|
|
31
|
+
* Start the session cleanup timer
|
|
32
|
+
*/
|
|
33
|
+
start(): void;
|
|
34
|
+
/**
|
|
35
|
+
* Stop the session manager and clean up all sessions
|
|
36
|
+
*/
|
|
37
|
+
stop(): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Register a new session or update existing one
|
|
40
|
+
*/
|
|
41
|
+
registerSession(sessionId: string, cwd: string): Session;
|
|
42
|
+
/**
|
|
43
|
+
* Get a session by ID and update activity
|
|
44
|
+
*/
|
|
45
|
+
getSession(sessionId: string): Session | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* Touch session to update activity timestamp (called on every API request)
|
|
48
|
+
*/
|
|
49
|
+
touchSession(sessionId: string): void;
|
|
50
|
+
/**
|
|
51
|
+
* Unregister a session (e.g., on cancel/submit)
|
|
52
|
+
*/
|
|
53
|
+
unregisterSession(sessionId: string): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Get pending changes for a session and clear them
|
|
56
|
+
*/
|
|
57
|
+
consumePendingChanges(sessionId: string): PendingChanges | null;
|
|
58
|
+
/**
|
|
59
|
+
* Get number of active sessions
|
|
60
|
+
*/
|
|
61
|
+
get sessionCount(): number;
|
|
62
|
+
/**
|
|
63
|
+
* Check if any sessions exist
|
|
64
|
+
*/
|
|
65
|
+
get hasSessions(): boolean;
|
|
66
|
+
private createWatcher;
|
|
67
|
+
private createEmptyPendingChanges;
|
|
68
|
+
private cleanupTimedOutSessions;
|
|
69
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { FileWatcher } from '../watcher/FileWatcher.js';
|
|
2
|
+
const log = (...args) => console.error(...args);
|
|
3
|
+
export class SessionManager {
|
|
4
|
+
sessions = new Map();
|
|
5
|
+
options;
|
|
6
|
+
cleanupInterval = null;
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.options = options;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Start the session cleanup timer
|
|
12
|
+
*/
|
|
13
|
+
start() {
|
|
14
|
+
// Check for timed out sessions every 5 seconds
|
|
15
|
+
this.cleanupInterval = setInterval(() => {
|
|
16
|
+
this.cleanupTimedOutSessions();
|
|
17
|
+
}, 5000);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Stop the session manager and clean up all sessions
|
|
21
|
+
*/
|
|
22
|
+
async stop() {
|
|
23
|
+
if (this.cleanupInterval) {
|
|
24
|
+
clearInterval(this.cleanupInterval);
|
|
25
|
+
this.cleanupInterval = null;
|
|
26
|
+
}
|
|
27
|
+
// Stop all watchers
|
|
28
|
+
const stopPromises = Array.from(this.sessions.values()).map(session => session.watcher.stop());
|
|
29
|
+
await Promise.all(stopPromises);
|
|
30
|
+
this.sessions.clear();
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Register a new session or update existing one
|
|
34
|
+
*/
|
|
35
|
+
registerSession(sessionId, cwd) {
|
|
36
|
+
const existing = this.sessions.get(sessionId);
|
|
37
|
+
if (existing) {
|
|
38
|
+
// Update activity timestamp
|
|
39
|
+
existing.lastActivity = Date.now();
|
|
40
|
+
// If cwd changed, need to restart watcher
|
|
41
|
+
if (existing.cwd !== cwd) {
|
|
42
|
+
existing.watcher.stop();
|
|
43
|
+
existing.cwd = cwd;
|
|
44
|
+
existing.watcher = this.createWatcher(sessionId, cwd);
|
|
45
|
+
existing.pendingChanges = this.createEmptyPendingChanges();
|
|
46
|
+
}
|
|
47
|
+
return existing;
|
|
48
|
+
}
|
|
49
|
+
// Create new session
|
|
50
|
+
const watcher = this.createWatcher(sessionId, cwd);
|
|
51
|
+
const session = {
|
|
52
|
+
id: sessionId,
|
|
53
|
+
cwd,
|
|
54
|
+
watcher,
|
|
55
|
+
lastActivity: Date.now(),
|
|
56
|
+
pendingChanges: this.createEmptyPendingChanges(),
|
|
57
|
+
};
|
|
58
|
+
this.sessions.set(sessionId, session);
|
|
59
|
+
log(`[SessionManager] Session registered: ${sessionId} (cwd: ${cwd})`);
|
|
60
|
+
return session;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Get a session by ID and update activity
|
|
64
|
+
*/
|
|
65
|
+
getSession(sessionId) {
|
|
66
|
+
const session = this.sessions.get(sessionId);
|
|
67
|
+
if (session) {
|
|
68
|
+
session.lastActivity = Date.now();
|
|
69
|
+
}
|
|
70
|
+
return session;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Touch session to update activity timestamp (called on every API request)
|
|
74
|
+
*/
|
|
75
|
+
touchSession(sessionId) {
|
|
76
|
+
const session = this.sessions.get(sessionId);
|
|
77
|
+
if (session) {
|
|
78
|
+
session.lastActivity = Date.now();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Unregister a session (e.g., on cancel/submit)
|
|
83
|
+
*/
|
|
84
|
+
async unregisterSession(sessionId) {
|
|
85
|
+
const session = this.sessions.get(sessionId);
|
|
86
|
+
if (session) {
|
|
87
|
+
await session.watcher.stop();
|
|
88
|
+
this.sessions.delete(sessionId);
|
|
89
|
+
log(`[SessionManager] Session unregistered: ${sessionId}`);
|
|
90
|
+
if (this.sessions.size === 0 && this.options.onAllSessionsGone) {
|
|
91
|
+
this.options.onAllSessionsGone();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Get pending changes for a session and clear them
|
|
97
|
+
*/
|
|
98
|
+
consumePendingChanges(sessionId) {
|
|
99
|
+
const session = this.sessions.get(sessionId);
|
|
100
|
+
if (!session)
|
|
101
|
+
return null;
|
|
102
|
+
const changes = { ...session.pendingChanges };
|
|
103
|
+
changes.changedFiles = new Set(session.pendingChanges.changedFiles);
|
|
104
|
+
// Reset pending changes
|
|
105
|
+
session.pendingChanges = this.createEmptyPendingChanges();
|
|
106
|
+
return changes;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Get number of active sessions
|
|
110
|
+
*/
|
|
111
|
+
get sessionCount() {
|
|
112
|
+
return this.sessions.size;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Check if any sessions exist
|
|
116
|
+
*/
|
|
117
|
+
get hasSessions() {
|
|
118
|
+
return this.sessions.size > 0;
|
|
119
|
+
}
|
|
120
|
+
createWatcher(sessionId, cwd) {
|
|
121
|
+
const watcher = new FileWatcher({ cwd });
|
|
122
|
+
watcher.onFilesChanged(() => {
|
|
123
|
+
const session = this.sessions.get(sessionId);
|
|
124
|
+
if (session) {
|
|
125
|
+
session.pendingChanges.filesChanged = true;
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
watcher.onFileChanged((filePath) => {
|
|
129
|
+
const session = this.sessions.get(sessionId);
|
|
130
|
+
if (session) {
|
|
131
|
+
session.pendingChanges.changedFiles.add(filePath);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
watcher.onGitChanged(() => {
|
|
135
|
+
const session = this.sessions.get(sessionId);
|
|
136
|
+
if (session) {
|
|
137
|
+
session.pendingChanges.gitChanged = true;
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
watcher.start();
|
|
141
|
+
return watcher;
|
|
142
|
+
}
|
|
143
|
+
createEmptyPendingChanges() {
|
|
144
|
+
return {
|
|
145
|
+
filesChanged: false,
|
|
146
|
+
gitChanged: false,
|
|
147
|
+
changedFiles: new Set(),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
cleanupTimedOutSessions() {
|
|
151
|
+
const now = Date.now();
|
|
152
|
+
const timedOut = [];
|
|
153
|
+
for (const [sessionId, session] of this.sessions) {
|
|
154
|
+
if (now - session.lastActivity > this.options.sessionTimeoutMs) {
|
|
155
|
+
timedOut.push(sessionId);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
for (const sessionId of timedOut) {
|
|
159
|
+
log(`[SessionManager] Session timed out: ${sessionId}`);
|
|
160
|
+
this.unregisterSession(sessionId);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|