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
package/dist/flip/index.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { Server, findFreePort } from './server/Server.js';
|
|
1
|
+
import { Server, findFreePort, findExistingServer } from './server/Server.js';
|
|
2
2
|
import open from 'open';
|
|
3
3
|
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import http from 'http';
|
|
4
7
|
import { execSync } from 'child_process';
|
|
5
8
|
import { copyToClipboard } from './output/clipboard.js';
|
|
6
9
|
const DEFAULT_PORT = 51234;
|
|
10
|
+
const MAX_PORT = 51240;
|
|
7
11
|
// Use stderr for info messages (stdout goes to terminal input in coprocess mode)
|
|
8
12
|
const log = (...args) => console.error(...args);
|
|
9
13
|
function formatTime() {
|
|
@@ -13,6 +17,51 @@ function formatTime() {
|
|
|
13
17
|
const secs = String(now.getSeconds()).padStart(2, '0');
|
|
14
18
|
return `${hours}:${mins}:${secs}`;
|
|
15
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Generate a unique session ID from the current tty
|
|
22
|
+
*/
|
|
23
|
+
function getSessionId() {
|
|
24
|
+
try {
|
|
25
|
+
const tty = execSync('tty', { encoding: 'utf-8' }).trim();
|
|
26
|
+
return tty;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Fallback to random ID if tty not available
|
|
30
|
+
return `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Register a session with the server
|
|
35
|
+
*/
|
|
36
|
+
async function registerSession(port, sessionId, cwd) {
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
const data = JSON.stringify({ session_id: sessionId, cwd });
|
|
39
|
+
const req = http.request({
|
|
40
|
+
hostname: '127.0.0.1',
|
|
41
|
+
port,
|
|
42
|
+
path: '/api/session/register',
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: {
|
|
45
|
+
'Content-Type': 'application/json',
|
|
46
|
+
'Content-Length': Buffer.byteLength(data),
|
|
47
|
+
},
|
|
48
|
+
timeout: 5000,
|
|
49
|
+
}, (res) => {
|
|
50
|
+
let body = '';
|
|
51
|
+
res.on('data', chunk => body += chunk);
|
|
52
|
+
res.on('end', () => {
|
|
53
|
+
resolve(res.statusCode === 200);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
req.on('error', () => resolve(false));
|
|
57
|
+
req.on('timeout', () => {
|
|
58
|
+
req.destroy();
|
|
59
|
+
resolve(false);
|
|
60
|
+
});
|
|
61
|
+
req.write(data);
|
|
62
|
+
req.end();
|
|
63
|
+
});
|
|
64
|
+
}
|
|
16
65
|
export async function runFlip(args) {
|
|
17
66
|
// Parse --session flag from anywhere in args
|
|
18
67
|
let sessionId;
|
|
@@ -57,52 +106,88 @@ export async function runFlip(args) {
|
|
|
57
106
|
command = 'oneshot';
|
|
58
107
|
}
|
|
59
108
|
const cwd = pathArg ? path.resolve(pathArg) : process.cwd();
|
|
109
|
+
// Get or generate session ID
|
|
110
|
+
const finalSessionId = sessionId || getSessionId();
|
|
60
111
|
switch (command) {
|
|
61
112
|
case 'setup': {
|
|
62
113
|
await setupHotkey();
|
|
63
114
|
return;
|
|
64
115
|
}
|
|
65
116
|
case 'serve': {
|
|
66
|
-
// Daemon mode: start server, keep running
|
|
67
|
-
|
|
68
|
-
|
|
117
|
+
// Daemon mode: start or reuse server, keep running
|
|
118
|
+
let port = await findExistingServer(DEFAULT_PORT, MAX_PORT);
|
|
119
|
+
if (port) {
|
|
120
|
+
log(`[${formatTime()}] Found existing server at port ${port}`);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
port = await findFreePort(DEFAULT_PORT, MAX_PORT);
|
|
124
|
+
log(`[${formatTime()}] Starting new server at port ${port}`);
|
|
125
|
+
// Start server in background (non-blocking)
|
|
126
|
+
const server = new Server(port, { sessionTimeoutMs: 4000 });
|
|
127
|
+
server.run().catch(err => {
|
|
128
|
+
log(`[${formatTime()}] Server error:`, err);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
log(`[${formatTime()}] Server running at http://localhost:${port}`);
|
|
69
132
|
log('Press Ctrl+C to stop');
|
|
70
133
|
log('');
|
|
71
134
|
log('To open browser, run: csq flip open');
|
|
72
135
|
log(`Or use hotkey to open: open http://localhost:${port}`);
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
const server = new Server(cwd, port);
|
|
76
|
-
const result = await server.run();
|
|
77
|
-
if (result) {
|
|
78
|
-
log(`[${formatTime()}] Submitted ${result.length} characters`);
|
|
79
|
-
}
|
|
80
|
-
else {
|
|
81
|
-
log(`[${formatTime()}] Cancelled`);
|
|
82
|
-
}
|
|
83
|
-
log(`[${formatTime()}] Ready for next session...`);
|
|
84
|
-
}
|
|
136
|
+
// Keep the process alive
|
|
137
|
+
await new Promise(() => { });
|
|
85
138
|
}
|
|
86
139
|
case 'open': {
|
|
87
140
|
// Just open browser to existing server
|
|
88
|
-
const
|
|
141
|
+
const port = await findExistingServer(DEFAULT_PORT, MAX_PORT);
|
|
142
|
+
if (!port) {
|
|
143
|
+
log('No csq flip server running.');
|
|
144
|
+
log('Start with: csq flip serve');
|
|
145
|
+
log('Or use: csq flip (one-shot mode)');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
// Register session
|
|
149
|
+
if (!await registerSession(port, finalSessionId, cwd)) {
|
|
150
|
+
log('Failed to register session with server');
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const url = `http://localhost:${port}?session=${encodeURIComponent(finalSessionId)}`;
|
|
89
154
|
log(`Opening ${url} in browser...`);
|
|
90
155
|
try {
|
|
91
156
|
await open(url);
|
|
92
157
|
}
|
|
93
158
|
catch (e) {
|
|
94
159
|
log('Failed to open browser:', e);
|
|
95
|
-
log(
|
|
160
|
+
log(`Please open ${url} manually`);
|
|
96
161
|
}
|
|
97
162
|
break;
|
|
98
163
|
}
|
|
99
164
|
case 'oneshot':
|
|
100
165
|
default: {
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
166
|
+
// One-shot mode: find or start server, register session, open browser
|
|
167
|
+
let port = await findExistingServer(DEFAULT_PORT, MAX_PORT);
|
|
168
|
+
let serverPromise = null;
|
|
169
|
+
if (!port) {
|
|
170
|
+
port = await findFreePort(DEFAULT_PORT, MAX_PORT);
|
|
171
|
+
log(`Starting server at http://localhost:${port}...`);
|
|
172
|
+
// Start server with idle timeout
|
|
173
|
+
const server = new Server(port, {
|
|
174
|
+
sessionTimeoutMs: 4000, // 4s (polling is 2s, so 2 missed polls = dead)
|
|
175
|
+
idleTimeout: 5000,
|
|
176
|
+
});
|
|
177
|
+
// Run server and save promise
|
|
178
|
+
serverPromise = server.run();
|
|
179
|
+
// Wait a bit for server to start
|
|
180
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
log(`Using existing server at http://localhost:${port}`);
|
|
184
|
+
}
|
|
185
|
+
// Register session
|
|
186
|
+
if (!await registerSession(port, finalSessionId, cwd)) {
|
|
187
|
+
log('Failed to register session with server');
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const url = `http://localhost:${port}?session=${encodeURIComponent(finalSessionId)}`;
|
|
106
191
|
log(`Opening ${url} in browser...`);
|
|
107
192
|
try {
|
|
108
193
|
await open(url);
|
|
@@ -111,13 +196,12 @@ export async function runFlip(args) {
|
|
|
111
196
|
log('Failed to open browser:', e);
|
|
112
197
|
log(`Please open ${url} manually`);
|
|
113
198
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
log(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
log('\nCancelled');
|
|
199
|
+
// If we started the server, wait for it to shutdown
|
|
200
|
+
if (serverPromise) {
|
|
201
|
+
log(`Session: ${finalSessionId}`);
|
|
202
|
+
log('Waiting for session to complete... (Ctrl+C to exit)');
|
|
203
|
+
await serverPromise;
|
|
204
|
+
log(`[${formatTime()}] Done`);
|
|
121
205
|
}
|
|
122
206
|
break;
|
|
123
207
|
}
|
|
@@ -129,12 +213,12 @@ function printUsage() {
|
|
|
129
213
|
console.error('Commands:');
|
|
130
214
|
console.error(' serve [path] Start server in daemon mode (keeps running)');
|
|
131
215
|
console.error(' open Open browser to existing server');
|
|
132
|
-
console.error(' setup Setup
|
|
216
|
+
console.error(' setup Setup hotkey in shell config');
|
|
133
217
|
console.error(' (no command) Start server + open browser (one-shot mode)');
|
|
134
218
|
console.error('');
|
|
135
219
|
console.error('Options:');
|
|
136
220
|
console.error(' path Directory to serve (default: current directory)');
|
|
137
|
-
console.error(' --session <
|
|
221
|
+
console.error(' --session <id> Session ID for paste-back tracking (auto-generated from tty)');
|
|
138
222
|
}
|
|
139
223
|
async function setupHotkey() {
|
|
140
224
|
let nodePath;
|
|
@@ -144,8 +228,47 @@ async function setupHotkey() {
|
|
|
144
228
|
catch {
|
|
145
229
|
nodePath = '/usr/local/bin/node';
|
|
146
230
|
}
|
|
147
|
-
|
|
148
|
-
|
|
231
|
+
let csqPath;
|
|
232
|
+
try {
|
|
233
|
+
csqPath = execSync('which csq', { encoding: 'utf-8' }).trim();
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
csqPath = new URL(import.meta.url).pathname;
|
|
237
|
+
}
|
|
238
|
+
// Wrapper script that gets cwd from iTerm2 and passes to csq flip
|
|
239
|
+
const nodeDir = path.dirname(nodePath);
|
|
240
|
+
const wrapperScript = `#!/bin/bash
|
|
241
|
+
# Add node to PATH (coprocess doesn't inherit shell PATH)
|
|
242
|
+
export PATH="${nodeDir}:$PATH"
|
|
243
|
+
|
|
244
|
+
# Get current directory from iTerm2
|
|
245
|
+
CWD=$(osascript -e 'tell application "iTerm2" to tell current session of current tab of current window to return variable named "session.path"' 2>/dev/null)
|
|
246
|
+
|
|
247
|
+
# Fallback: try to get from tty
|
|
248
|
+
if [ -z "$CWD" ] || [ "$CWD" = "missing value" ]; then
|
|
249
|
+
TTY=$(osascript -e 'tell application "iTerm2" to tell current session of current tab of current window to return tty' 2>/dev/null)
|
|
250
|
+
if [ -n "$TTY" ]; then
|
|
251
|
+
PID=$(lsof -t "$TTY" 2>/dev/null | head -1)
|
|
252
|
+
if [ -n "$PID" ]; then
|
|
253
|
+
CWD=$(lsof -a -d cwd -p "$PID" -Fn 2>/dev/null | grep ^n | cut -c2-)
|
|
254
|
+
fi
|
|
255
|
+
fi
|
|
256
|
+
fi
|
|
257
|
+
|
|
258
|
+
# Run csq flip with cwd
|
|
259
|
+
if [ -n "$CWD" ] && [ "$CWD" != "missing value" ]; then
|
|
260
|
+
exec ${csqPath} flip "$CWD"
|
|
261
|
+
else
|
|
262
|
+
exec ${csqPath} flip
|
|
263
|
+
fi
|
|
264
|
+
`;
|
|
265
|
+
// Save wrapper script
|
|
266
|
+
const scriptDir = path.join(os.homedir(), '.config', 'csq');
|
|
267
|
+
const scriptPath = path.join(scriptDir, 'flip-hotkey.sh');
|
|
268
|
+
if (!fs.existsSync(scriptDir)) {
|
|
269
|
+
fs.mkdirSync(scriptDir, { recursive: true });
|
|
270
|
+
}
|
|
271
|
+
fs.writeFileSync(scriptPath, wrapperScript, { mode: 0o755 });
|
|
149
272
|
console.log('');
|
|
150
273
|
console.log('┌─────────────────────────────────────────────────┐');
|
|
151
274
|
console.log('│ Flip Hotkey Setup (iTerm2) │');
|
|
@@ -157,10 +280,10 @@ async function setupHotkey() {
|
|
|
157
280
|
console.log('4. Action: "Run Coprocess"');
|
|
158
281
|
console.log('5. Command (아래 자동 복사됨):');
|
|
159
282
|
console.log('');
|
|
160
|
-
console.log(` ${
|
|
283
|
+
console.log(` ${scriptPath}`);
|
|
161
284
|
console.log('');
|
|
162
285
|
try {
|
|
163
|
-
await copyToClipboard(
|
|
286
|
+
await copyToClipboard(scriptPath);
|
|
164
287
|
console.log(' (Copied to clipboard!)');
|
|
165
288
|
}
|
|
166
289
|
catch {
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
const router = Router();
|
|
3
3
|
// POST /api/cancel
|
|
4
|
-
router.post('/', (req, res) => {
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
state.resolve(null);
|
|
4
|
+
router.post('/', async (req, res) => {
|
|
5
|
+
const sessionManager = req.app.locals.sessionManager;
|
|
6
|
+
const body = req.body;
|
|
7
|
+
if (!body.session_id) {
|
|
8
|
+
res.status(400).json({ error: 'Missing session_id' });
|
|
9
|
+
return;
|
|
11
10
|
}
|
|
11
|
+
// Send response before cleanup
|
|
12
|
+
res.json({ status: 'ok' });
|
|
13
|
+
// Unregister the session
|
|
14
|
+
await sessionManager.unregisterSession(body.session_id);
|
|
12
15
|
});
|
|
13
16
|
export { router as cancelRouter };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Router as IRouter } from 'express';
|
|
2
|
+
import { SessionManager } from '../session/SessionManager.js';
|
|
3
|
+
export interface ChangesResponse {
|
|
4
|
+
filesChanged: boolean;
|
|
5
|
+
gitChanged: boolean;
|
|
6
|
+
changedFiles: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function createChangesRouter(sessionManager: SessionManager): IRouter;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
export function createChangesRouter(sessionManager) {
|
|
3
|
+
const router = Router();
|
|
4
|
+
// GET /api/changes?session_id=xxx - Get pending changes for a session (polling endpoint)
|
|
5
|
+
router.get('/', (req, res) => {
|
|
6
|
+
const sessionId = req.query.session_id;
|
|
7
|
+
if (!sessionId) {
|
|
8
|
+
res.status(400).json({ error: 'Missing session_id' });
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
// Touch session to update activity
|
|
12
|
+
sessionManager.touchSession(sessionId);
|
|
13
|
+
const changes = sessionManager.consumePendingChanges(sessionId);
|
|
14
|
+
if (!changes) {
|
|
15
|
+
res.status(404).json({ error: 'Session not found' });
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const response = {
|
|
19
|
+
filesChanged: changes.filesChanged,
|
|
20
|
+
gitChanged: changes.gitChanged,
|
|
21
|
+
changedFiles: Array.from(changes.changedFiles),
|
|
22
|
+
};
|
|
23
|
+
res.json(response);
|
|
24
|
+
});
|
|
25
|
+
return router;
|
|
26
|
+
}
|
package/dist/flip/routes/file.js
CHANGED
|
@@ -127,18 +127,37 @@ function detectLanguage(filePath) {
|
|
|
127
127
|
}
|
|
128
128
|
return 'text';
|
|
129
129
|
}
|
|
130
|
-
|
|
130
|
+
/**
|
|
131
|
+
* Get cwd from session_id query parameter
|
|
132
|
+
*/
|
|
133
|
+
function getCwdFromSession(req, res) {
|
|
134
|
+
const sessionId = req.query.session_id;
|
|
135
|
+
if (!sessionId) {
|
|
136
|
+
res.status(400).json({ error: 'Missing session_id parameter' });
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const sessionManager = req.app.locals.sessionManager;
|
|
140
|
+
const session = sessionManager.getSession(sessionId);
|
|
141
|
+
if (!session) {
|
|
142
|
+
res.status(404).json({ error: 'Session not found' });
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
return session.cwd;
|
|
146
|
+
}
|
|
147
|
+
// GET /api/file?session_id=xxx&path=<path>
|
|
131
148
|
router.get('/', (req, res) => {
|
|
132
|
-
const
|
|
149
|
+
const cwd = getCwdFromSession(req, res);
|
|
150
|
+
if (!cwd)
|
|
151
|
+
return;
|
|
133
152
|
const relativePath = req.query.path;
|
|
134
153
|
if (!relativePath) {
|
|
135
154
|
res.status(400).json({ error: 'Missing path parameter' });
|
|
136
155
|
return;
|
|
137
156
|
}
|
|
138
|
-
const filePath = path.join(
|
|
157
|
+
const filePath = path.join(cwd, relativePath);
|
|
139
158
|
// Security check: ensure path is within cwd
|
|
140
159
|
const resolvedPath = path.resolve(filePath);
|
|
141
|
-
const resolvedCwd = path.resolve(
|
|
160
|
+
const resolvedCwd = path.resolve(cwd);
|
|
142
161
|
if (!resolvedPath.startsWith(resolvedCwd)) {
|
|
143
162
|
res.status(403).json({ error: 'Access denied' });
|
|
144
163
|
return;
|
|
@@ -66,20 +66,41 @@ function collectFlatFiles(rootPath, currentPath, maxDepth, depth = 0, result = {
|
|
|
66
66
|
}
|
|
67
67
|
return result;
|
|
68
68
|
}
|
|
69
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Get cwd from session_id query parameter
|
|
71
|
+
*/
|
|
72
|
+
function getCwdFromSession(req, res) {
|
|
73
|
+
const sessionId = req.query.session_id;
|
|
74
|
+
if (!sessionId) {
|
|
75
|
+
res.status(400).json({ error: 'Missing session_id parameter' });
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const sessionManager = req.app.locals.sessionManager;
|
|
79
|
+
const session = sessionManager.getSession(sessionId);
|
|
80
|
+
if (!session) {
|
|
81
|
+
res.status(404).json({ error: 'Session not found' });
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
return session.cwd;
|
|
85
|
+
}
|
|
86
|
+
// GET /api/files?session_id=xxx - Get file tree structure
|
|
70
87
|
router.get('/', (req, res) => {
|
|
71
|
-
const
|
|
72
|
-
|
|
88
|
+
const cwd = getCwdFromSession(req, res);
|
|
89
|
+
if (!cwd)
|
|
90
|
+
return;
|
|
91
|
+
const tree = buildFileTree(cwd, cwd, 10);
|
|
73
92
|
const response = {
|
|
74
|
-
root:
|
|
93
|
+
root: cwd,
|
|
75
94
|
tree,
|
|
76
95
|
};
|
|
77
96
|
res.json(response);
|
|
78
97
|
});
|
|
79
|
-
// GET /api/files/flat - Get flat list of all files with mtime
|
|
98
|
+
// GET /api/files/flat?session_id=xxx - Get flat list of all files with mtime
|
|
80
99
|
router.get('/flat', (req, res) => {
|
|
81
|
-
const
|
|
82
|
-
|
|
100
|
+
const cwd = getCwdFromSession(req, res);
|
|
101
|
+
if (!cwd)
|
|
102
|
+
return;
|
|
103
|
+
const result = collectFlatFiles(cwd, cwd, 10);
|
|
83
104
|
// Sort files by path for consistent display
|
|
84
105
|
result.files.sort((a, b) => a.path.localeCompare(b.path));
|
|
85
106
|
result.filteredDirs.sort();
|
package/dist/flip/routes/git.js
CHANGED
|
@@ -116,14 +116,33 @@ function createAddedFileDiff(content) {
|
|
|
116
116
|
lines: diffLines,
|
|
117
117
|
}];
|
|
118
118
|
}
|
|
119
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Get cwd from session_id query parameter
|
|
121
|
+
*/
|
|
122
|
+
function getCwdFromSession(req, res) {
|
|
123
|
+
const sessionId = req.query.session_id;
|
|
124
|
+
if (!sessionId) {
|
|
125
|
+
res.status(400).json({ error: 'Missing session_id parameter' });
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
const sessionManager = req.app.locals.sessionManager;
|
|
129
|
+
const session = sessionManager.getSession(sessionId);
|
|
130
|
+
if (!session) {
|
|
131
|
+
res.status(404).json({ error: 'Session not found' });
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return session.cwd;
|
|
135
|
+
}
|
|
136
|
+
// GET /api/git/status?session_id=xxx
|
|
120
137
|
router.get('/status', (req, res) => {
|
|
121
|
-
const
|
|
138
|
+
const cwd = getCwdFromSession(req, res);
|
|
139
|
+
if (!cwd)
|
|
140
|
+
return;
|
|
122
141
|
// Check if it's a git repository
|
|
123
142
|
let isGitRepo = false;
|
|
124
143
|
try {
|
|
125
144
|
execSync('git rev-parse --git-dir', {
|
|
126
|
-
cwd
|
|
145
|
+
cwd,
|
|
127
146
|
stdio: 'pipe',
|
|
128
147
|
});
|
|
129
148
|
isGitRepo = true;
|
|
@@ -143,7 +162,7 @@ router.get('/status', (req, res) => {
|
|
|
143
162
|
let unstaged = [];
|
|
144
163
|
try {
|
|
145
164
|
const output = execSync('git status --porcelain', {
|
|
146
|
-
cwd
|
|
165
|
+
cwd,
|
|
147
166
|
encoding: 'utf-8',
|
|
148
167
|
});
|
|
149
168
|
unstaged = parseGitStatus(output);
|
|
@@ -157,20 +176,22 @@ router.get('/status', (req, res) => {
|
|
|
157
176
|
};
|
|
158
177
|
res.json(response);
|
|
159
178
|
});
|
|
160
|
-
// GET /api/git/diff?path=<file>
|
|
179
|
+
// GET /api/git/diff?session_id=xxx&path=<file>
|
|
161
180
|
router.get('/diff', (req, res) => {
|
|
162
|
-
const
|
|
181
|
+
const cwd = getCwdFromSession(req, res);
|
|
182
|
+
if (!cwd)
|
|
183
|
+
return;
|
|
163
184
|
const relativePath = req.query.path;
|
|
164
185
|
if (!relativePath) {
|
|
165
186
|
res.status(400).json({ error: 'Missing path parameter' });
|
|
166
187
|
return;
|
|
167
188
|
}
|
|
168
|
-
const fullPath = path.join(
|
|
189
|
+
const fullPath = path.join(cwd, relativePath);
|
|
169
190
|
// Check git status for this file
|
|
170
191
|
let fileStatus = 'modified';
|
|
171
192
|
try {
|
|
172
193
|
const statusOutput = execSync(`git status --porcelain -- "${relativePath}"`, {
|
|
173
|
-
cwd
|
|
194
|
+
cwd,
|
|
174
195
|
encoding: 'utf-8',
|
|
175
196
|
});
|
|
176
197
|
if (statusOutput.startsWith('??')) {
|
|
@@ -205,7 +226,7 @@ router.get('/diff', (req, res) => {
|
|
|
205
226
|
// Get diff for tracked file
|
|
206
227
|
try {
|
|
207
228
|
const diffOutput = execSync(`git diff --no-color -- "${relativePath}"`, {
|
|
208
|
-
cwd
|
|
229
|
+
cwd,
|
|
209
230
|
encoding: 'utf-8',
|
|
210
231
|
});
|
|
211
232
|
if (!diffOutput.trim()) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { SERVER_ID } from '../server/Server.js';
|
|
3
|
+
export function createPingRouter() {
|
|
4
|
+
const router = Router();
|
|
5
|
+
// GET /api/ping - Server identification for discovery
|
|
6
|
+
router.get('/', (_req, res) => {
|
|
7
|
+
const response = {
|
|
8
|
+
id: SERVER_ID,
|
|
9
|
+
version: '1.0.0',
|
|
10
|
+
};
|
|
11
|
+
res.json(response);
|
|
12
|
+
});
|
|
13
|
+
return router;
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Router as IRouter } from 'express';
|
|
2
|
+
import { SessionManager } from '../session/SessionManager.js';
|
|
3
|
+
export interface RegisterSessionRequest {
|
|
4
|
+
session_id: string;
|
|
5
|
+
cwd: string;
|
|
6
|
+
}
|
|
7
|
+
export interface RegisterSessionResponse {
|
|
8
|
+
status: string;
|
|
9
|
+
session_id: string;
|
|
10
|
+
}
|
|
11
|
+
export interface UnregisterSessionResponse {
|
|
12
|
+
status: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function createSessionRouter(sessionManager: SessionManager): IRouter;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
export function createSessionRouter(sessionManager) {
|
|
3
|
+
const router = Router();
|
|
4
|
+
// POST /api/session/register - Register a new session
|
|
5
|
+
router.post('/register', (req, res) => {
|
|
6
|
+
const body = req.body;
|
|
7
|
+
if (!body.session_id || !body.cwd) {
|
|
8
|
+
res.status(400).json({ error: 'Missing session_id or cwd' });
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
// Clear idle timer on first session registration
|
|
12
|
+
if (!sessionManager.hasSessions && req.app.locals.clearIdleTimer) {
|
|
13
|
+
req.app.locals.clearIdleTimer();
|
|
14
|
+
}
|
|
15
|
+
sessionManager.registerSession(body.session_id, body.cwd);
|
|
16
|
+
const response = {
|
|
17
|
+
status: 'ok',
|
|
18
|
+
session_id: body.session_id,
|
|
19
|
+
};
|
|
20
|
+
res.json(response);
|
|
21
|
+
});
|
|
22
|
+
// POST /api/session/unregister - Unregister a session
|
|
23
|
+
router.post('/unregister', async (req, res) => {
|
|
24
|
+
const body = req.body;
|
|
25
|
+
if (!body.session_id) {
|
|
26
|
+
res.status(400).json({ error: 'Missing session_id' });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
await sessionManager.unregisterSession(body.session_id);
|
|
30
|
+
const response = {
|
|
31
|
+
status: 'ok',
|
|
32
|
+
};
|
|
33
|
+
res.json(response);
|
|
34
|
+
});
|
|
35
|
+
// GET /api/session/info?session_id=xxx - Get session info
|
|
36
|
+
router.get('/info', (req, res) => {
|
|
37
|
+
const sessionId = req.query.session_id;
|
|
38
|
+
if (!sessionId) {
|
|
39
|
+
res.status(400).json({ error: 'Missing session_id' });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const session = sessionManager.getSession(sessionId);
|
|
43
|
+
if (!session) {
|
|
44
|
+
res.status(404).json({ error: 'Session not found' });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
res.json({
|
|
48
|
+
session_id: session.id,
|
|
49
|
+
cwd: session.cwd,
|
|
50
|
+
lastActivity: session.lastActivity,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
return router;
|
|
54
|
+
}
|
|
@@ -3,8 +3,12 @@ import { formatComments, copyToClipboard, schedulePaste } from '../output/index.
|
|
|
3
3
|
const router = Router();
|
|
4
4
|
// POST /api/submit
|
|
5
5
|
router.post('/', async (req, res) => {
|
|
6
|
-
const
|
|
6
|
+
const sessionManager = req.app.locals.sessionManager;
|
|
7
7
|
const body = req.body;
|
|
8
|
+
if (!body.session_id) {
|
|
9
|
+
res.status(400).json({ error: 'Missing session_id' });
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
8
12
|
if (!body.items || body.items.length === 0) {
|
|
9
13
|
res.status(400).json({ error: 'No items to submit' });
|
|
10
14
|
return;
|
|
@@ -39,11 +43,9 @@ router.post('/', async (req, res) => {
|
|
|
39
43
|
console.error('Failed to schedule paste:', e);
|
|
40
44
|
}
|
|
41
45
|
}
|
|
42
|
-
// Send response before
|
|
46
|
+
// Send response before cleanup
|
|
43
47
|
res.json({ status: 'ok' });
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
state.resolve(formatted);
|
|
47
|
-
}
|
|
48
|
+
// Unregister the session
|
|
49
|
+
await sessionManager.unregisterSession(body.session_id);
|
|
48
50
|
});
|
|
49
51
|
export { router as submitRouter };
|