code-squad-cli 1.0.8 → 1.1.1
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.d.ts +1 -0
- package/dist/flip/index.js +246 -0
- package/dist/flip/output/autopaste.d.ts +5 -0
- package/dist/flip/output/autopaste.js +80 -0
- package/dist/flip/output/clipboard.d.ts +4 -0
- package/dist/flip/output/clipboard.js +7 -0
- package/dist/flip/output/formatter.d.ts +13 -0
- package/dist/flip/output/formatter.js +18 -0
- package/dist/flip/output/index.d.ts +4 -0
- package/dist/flip/output/index.js +3 -0
- package/dist/flip/routes/cancel.d.ts +6 -0
- package/dist/flip/routes/cancel.js +13 -0
- package/dist/flip/routes/file.d.ts +8 -0
- package/dist/flip/routes/file.js +77 -0
- package/dist/flip/routes/files.d.ts +16 -0
- package/dist/flip/routes/files.js +102 -0
- package/dist/flip/routes/git.d.ts +11 -0
- package/dist/flip/routes/git.js +83 -0
- package/dist/flip/routes/static.d.ts +2 -0
- package/dist/flip/routes/static.js +35 -0
- package/dist/flip/routes/submit.d.ts +20 -0
- package/dist/flip/routes/submit.js +42 -0
- package/dist/flip/server/Server.d.ts +11 -0
- package/dist/flip/server/Server.js +63 -0
- package/dist/flip-ui/dist/assets/index-2_ZJL2eQ.css +10 -0
- package/dist/flip-ui/dist/assets/index-BGxWXtBJ.js +66 -0
- package/dist/flip-ui/dist/index.html +13 -0
- package/dist/index.js +711 -22
- package/package.json +14 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runFlip(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { Server, findFreePort } from './server/Server.js';
|
|
2
|
+
import open from 'open';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
import { confirm } from '@inquirer/prompts';
|
|
8
|
+
import clipboardy from 'clipboardy';
|
|
9
|
+
const DEFAULT_PORT = 51234;
|
|
10
|
+
function formatTime() {
|
|
11
|
+
const now = new Date();
|
|
12
|
+
const hours = String(now.getHours()).padStart(2, '0');
|
|
13
|
+
const mins = String(now.getMinutes()).padStart(2, '0');
|
|
14
|
+
const secs = String(now.getSeconds()).padStart(2, '0');
|
|
15
|
+
return `${hours}:${mins}:${secs}`;
|
|
16
|
+
}
|
|
17
|
+
export async function runFlip(args) {
|
|
18
|
+
// Parse --session flag from anywhere in args
|
|
19
|
+
let sessionId;
|
|
20
|
+
const sessionIdx = args.indexOf('--session');
|
|
21
|
+
if (sessionIdx !== -1 && args[sessionIdx + 1]) {
|
|
22
|
+
sessionId = args[sessionIdx + 1];
|
|
23
|
+
}
|
|
24
|
+
// Filter out --session and its value for further parsing
|
|
25
|
+
const filteredArgs = args.filter((arg, idx) => {
|
|
26
|
+
if (arg === '--session')
|
|
27
|
+
return false;
|
|
28
|
+
if (idx > 0 && args[idx - 1] === '--session')
|
|
29
|
+
return false;
|
|
30
|
+
return true;
|
|
31
|
+
});
|
|
32
|
+
// Parse subcommand
|
|
33
|
+
let command;
|
|
34
|
+
let pathArg;
|
|
35
|
+
if (filteredArgs.length > 0) {
|
|
36
|
+
switch (filteredArgs[0]) {
|
|
37
|
+
case 'serve':
|
|
38
|
+
command = 'serve';
|
|
39
|
+
pathArg = filteredArgs[1];
|
|
40
|
+
break;
|
|
41
|
+
case 'open':
|
|
42
|
+
command = 'open';
|
|
43
|
+
break;
|
|
44
|
+
case 'setup':
|
|
45
|
+
command = 'setup';
|
|
46
|
+
break;
|
|
47
|
+
case '--help':
|
|
48
|
+
case '-h':
|
|
49
|
+
printUsage();
|
|
50
|
+
return;
|
|
51
|
+
default:
|
|
52
|
+
command = 'oneshot';
|
|
53
|
+
pathArg = filteredArgs[0];
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
command = 'oneshot';
|
|
59
|
+
}
|
|
60
|
+
const cwd = pathArg ? path.resolve(pathArg) : process.cwd();
|
|
61
|
+
switch (command) {
|
|
62
|
+
case 'setup': {
|
|
63
|
+
await setupHotkey();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
case 'serve': {
|
|
67
|
+
// Daemon mode: start server, keep running
|
|
68
|
+
const port = await findFreePort(DEFAULT_PORT);
|
|
69
|
+
console.log(`Server running at http://localhost:${port}`);
|
|
70
|
+
console.log('Press Ctrl+C to stop');
|
|
71
|
+
console.log('');
|
|
72
|
+
console.log('To open browser, run: csq flip open');
|
|
73
|
+
console.log(`Or use hotkey to open: open http://localhost:${port}`);
|
|
74
|
+
// Run server in loop (restarts after each submit/cancel)
|
|
75
|
+
while (true) {
|
|
76
|
+
const server = new Server(cwd, port);
|
|
77
|
+
const result = await server.run();
|
|
78
|
+
if (result) {
|
|
79
|
+
console.log(`[${formatTime()}] Submitted ${result.length} characters`);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
console.log(`[${formatTime()}] Cancelled`);
|
|
83
|
+
}
|
|
84
|
+
console.log(`[${formatTime()}] Ready for next session...`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
case 'open': {
|
|
88
|
+
// Just open browser to existing server
|
|
89
|
+
const url = `http://localhost:${DEFAULT_PORT}`;
|
|
90
|
+
console.log(`Opening ${url} in browser...`);
|
|
91
|
+
try {
|
|
92
|
+
await open(url);
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
console.error('Failed to open browser:', e);
|
|
96
|
+
console.error('Is the server running? Start with: csq flip serve');
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
case 'oneshot':
|
|
101
|
+
default: {
|
|
102
|
+
// Original behavior: start server, open browser, exit after submit/cancel
|
|
103
|
+
const port = await findFreePort(DEFAULT_PORT);
|
|
104
|
+
const url = sessionId
|
|
105
|
+
? `http://localhost:${port}?session=${sessionId}`
|
|
106
|
+
: `http://localhost:${port}`;
|
|
107
|
+
console.log(`Opening ${url} in browser...`);
|
|
108
|
+
try {
|
|
109
|
+
await open(url);
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
console.error('Failed to open browser:', e);
|
|
113
|
+
console.log(`Please open ${url} manually`);
|
|
114
|
+
}
|
|
115
|
+
const server = new Server(cwd, port);
|
|
116
|
+
const result = await server.run();
|
|
117
|
+
if (result) {
|
|
118
|
+
console.log(`\nSubmitted ${result.length} characters`);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
console.log('\nCancelled');
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function printUsage() {
|
|
128
|
+
console.error('Usage: csq flip [command] [options]');
|
|
129
|
+
console.error('');
|
|
130
|
+
console.error('Commands:');
|
|
131
|
+
console.error(' serve [path] Start server in daemon mode (keeps running)');
|
|
132
|
+
console.error(' open Open browser to existing server');
|
|
133
|
+
console.error(' setup Setup iTerm2 hotkey');
|
|
134
|
+
console.error(' (no command) Start server + open browser (one-shot mode)');
|
|
135
|
+
console.error('');
|
|
136
|
+
console.error('Options:');
|
|
137
|
+
console.error(' path Directory to serve (default: current directory)');
|
|
138
|
+
console.error(' --session <uuid> Session ID for paste-back tracking');
|
|
139
|
+
}
|
|
140
|
+
async function setupHotkey() {
|
|
141
|
+
const configDir = path.join(os.homedir(), '.config', 'flip');
|
|
142
|
+
const applescriptPath = path.join(configDir, 'flip.applescript');
|
|
143
|
+
const shPath = path.join(configDir, 'flip.sh');
|
|
144
|
+
// Get node path
|
|
145
|
+
let nodePath;
|
|
146
|
+
try {
|
|
147
|
+
nodePath = execSync('which node', { encoding: 'utf-8' }).trim();
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
nodePath = '/usr/local/bin/node';
|
|
151
|
+
}
|
|
152
|
+
// Get csq path (this script's location)
|
|
153
|
+
const csqPath = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../index.js');
|
|
154
|
+
// Create config directory
|
|
155
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
156
|
+
// Write AppleScript
|
|
157
|
+
const applescriptContent = `#!/usr/bin/osascript
|
|
158
|
+
|
|
159
|
+
-- flip hotkey script
|
|
160
|
+
-- Generated by: csq flip setup
|
|
161
|
+
|
|
162
|
+
tell application "iTerm2"
|
|
163
|
+
tell current session of current window
|
|
164
|
+
set originalSessionId to id
|
|
165
|
+
set sessionUUID to do shell script "uuidgen"
|
|
166
|
+
set currentPath to variable named "path"
|
|
167
|
+
do shell script "echo '" & originalSessionId & "' > /tmp/flip-view-session-" & sessionUUID
|
|
168
|
+
do shell script "nohup ${nodePath} ${csqPath} flip --session " & sessionUUID & " " & quoted form of currentPath & " > /tmp/flip.log 2>&1 &"
|
|
169
|
+
end tell
|
|
170
|
+
end tell
|
|
171
|
+
|
|
172
|
+
return ""`;
|
|
173
|
+
fs.writeFileSync(applescriptPath, applescriptContent);
|
|
174
|
+
fs.chmodSync(applescriptPath, '755');
|
|
175
|
+
// Write shell wrapper
|
|
176
|
+
const shContent = `#!/bin/bash
|
|
177
|
+
osascript ${applescriptPath} > /dev/null 2>&1
|
|
178
|
+
`;
|
|
179
|
+
fs.writeFileSync(shPath, shContent);
|
|
180
|
+
fs.chmodSync(shPath, '755');
|
|
181
|
+
console.log('');
|
|
182
|
+
console.log('┌─────────────────────────────────────────────────┐');
|
|
183
|
+
console.log('│ Flip Hotkey Setup Wizard │');
|
|
184
|
+
console.log('└─────────────────────────────────────────────────┘');
|
|
185
|
+
console.log('');
|
|
186
|
+
console.log('✓ Scripts generated:');
|
|
187
|
+
console.log(` ${shPath}`);
|
|
188
|
+
console.log('');
|
|
189
|
+
// Step 1: Open iTerm2 Settings
|
|
190
|
+
const openSettings = await confirm({
|
|
191
|
+
message: 'Open iTerm2 Settings? (Keys → Key Bindings)',
|
|
192
|
+
default: true,
|
|
193
|
+
});
|
|
194
|
+
if (openSettings) {
|
|
195
|
+
try {
|
|
196
|
+
execSync(`osascript -e 'tell application "iTerm2" to activate' -e 'tell application "System Events" to keystroke "," using command down'`);
|
|
197
|
+
console.log('');
|
|
198
|
+
console.log(' → iTerm2 Settings opened. Navigate to: Keys → Key Bindings');
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
console.log(' → Could not open settings automatically. Open manually: iTerm2 → Settings → Keys → Key Bindings');
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
console.log('');
|
|
205
|
+
// Step 2: Guide through adding key binding
|
|
206
|
+
const ready = await confirm({
|
|
207
|
+
message: 'Ready to add Key Binding? (Click + button in Key Bindings tab)',
|
|
208
|
+
default: true,
|
|
209
|
+
});
|
|
210
|
+
if (!ready) {
|
|
211
|
+
console.log('');
|
|
212
|
+
console.log('Run `csq flip setup` again when ready.');
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
console.log('');
|
|
216
|
+
console.log('┌─────────────────────────────────────────────────┐');
|
|
217
|
+
console.log('│ Configure the Key Binding: │');
|
|
218
|
+
console.log('├─────────────────────────────────────────────────┤');
|
|
219
|
+
console.log('│ 1. Keyboard Shortcut: Press your hotkey │');
|
|
220
|
+
console.log('│ (e.g., ⌘⇧F) │');
|
|
221
|
+
console.log('│ │');
|
|
222
|
+
console.log('│ 2. Action: Select "Run Coprocess" │');
|
|
223
|
+
console.log('│ │');
|
|
224
|
+
console.log('│ 3. Command: Paste the path (copied below) │');
|
|
225
|
+
console.log('└─────────────────────────────────────────────────┘');
|
|
226
|
+
console.log('');
|
|
227
|
+
// Copy path to clipboard
|
|
228
|
+
await clipboardy.write(shPath);
|
|
229
|
+
console.log(`✓ Copied to clipboard: ${shPath}`);
|
|
230
|
+
console.log('');
|
|
231
|
+
await confirm({
|
|
232
|
+
message: 'Done configuring? (Paste the path and click OK)',
|
|
233
|
+
default: true,
|
|
234
|
+
});
|
|
235
|
+
console.log('');
|
|
236
|
+
console.log('┌─────────────────────────────────────────────────┐');
|
|
237
|
+
console.log('│ Setup Complete! │');
|
|
238
|
+
console.log('├─────────────────────────────────────────────────┤');
|
|
239
|
+
console.log('│ Usage: │');
|
|
240
|
+
console.log('│ 1. Focus on any terminal panel │');
|
|
241
|
+
console.log('│ 2. Press your hotkey → browser opens │');
|
|
242
|
+
console.log('│ 3. Select files, add comments │');
|
|
243
|
+
console.log('│ 4. Submit → text appears in terminal │');
|
|
244
|
+
console.log('└─────────────────────────────────────────────────┘');
|
|
245
|
+
console.log('');
|
|
246
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
/**
|
|
6
|
+
* Schedule automatic paste to the original iTerm session
|
|
7
|
+
* @param sessionId The UUID that identifies this flip-view session
|
|
8
|
+
*/
|
|
9
|
+
export async function schedulePaste(sessionId) {
|
|
10
|
+
if (process.platform !== 'darwin') {
|
|
11
|
+
console.error('Auto-paste not supported on this platform. Please paste manually (Ctrl+V).');
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
await pasteToOriginalSession(sessionId);
|
|
15
|
+
}
|
|
16
|
+
async function pasteToOriginalSession(sessionId) {
|
|
17
|
+
// Read the original iTerm session ID from session-specific temp file
|
|
18
|
+
const sessionFile = path.join(os.tmpdir(), `flip-view-session-${sessionId}`);
|
|
19
|
+
let itermSessionId;
|
|
20
|
+
try {
|
|
21
|
+
itermSessionId = fs.readFileSync(sessionFile, 'utf-8').trim();
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// Session file not found
|
|
25
|
+
}
|
|
26
|
+
let script;
|
|
27
|
+
if (itermSessionId) {
|
|
28
|
+
// Find the original session by ID and paste to it
|
|
29
|
+
script = `
|
|
30
|
+
tell application "iTerm2"
|
|
31
|
+
set found to false
|
|
32
|
+
repeat with w in windows
|
|
33
|
+
repeat with t in tabs of w
|
|
34
|
+
repeat with s in sessions of t
|
|
35
|
+
if id of s is "${itermSessionId}" then
|
|
36
|
+
set found to true
|
|
37
|
+
select s
|
|
38
|
+
tell s to write text (the clipboard)
|
|
39
|
+
tell s to write text ""
|
|
40
|
+
return
|
|
41
|
+
end if
|
|
42
|
+
end repeat
|
|
43
|
+
end repeat
|
|
44
|
+
end repeat
|
|
45
|
+
|
|
46
|
+
if not found then
|
|
47
|
+
display notification "Original session closed. Output copied to clipboard." with title "flip"
|
|
48
|
+
end if
|
|
49
|
+
end tell
|
|
50
|
+
`;
|
|
51
|
+
// Cleanup the session file after use
|
|
52
|
+
try {
|
|
53
|
+
fs.unlinkSync(sessionFile);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Ignore cleanup errors
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
// Fallback: paste to current iTerm session
|
|
61
|
+
script = `
|
|
62
|
+
tell application "iTerm2"
|
|
63
|
+
activate
|
|
64
|
+
delay 0.1
|
|
65
|
+
tell current session of current window
|
|
66
|
+
write text (the clipboard)
|
|
67
|
+
write text ""
|
|
68
|
+
end tell
|
|
69
|
+
end tell
|
|
70
|
+
`;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
execSync(`osascript -e '${script.replace(/'/g, "'\"'\"'")}'`, {
|
|
74
|
+
stdio: 'pipe',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
console.error('Failed to paste to iTerm:', e);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Comment interface compatible with @code-squad/core Comment
|
|
3
|
+
*/
|
|
4
|
+
export interface CommentLike {
|
|
5
|
+
file: string;
|
|
6
|
+
line: number;
|
|
7
|
+
endLine?: number;
|
|
8
|
+
text: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Format Comment-like entities for AI agent consumption
|
|
12
|
+
*/
|
|
13
|
+
export declare function formatComments(comments: CommentLike[]): string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format Comment-like entities for AI agent consumption
|
|
3
|
+
*/
|
|
4
|
+
export function formatComments(comments) {
|
|
5
|
+
if (comments.length === 0) {
|
|
6
|
+
return '';
|
|
7
|
+
}
|
|
8
|
+
let output = 'The user has annotated the following code locations:\n\n';
|
|
9
|
+
for (const comment of comments) {
|
|
10
|
+
const lineRange = comment.endLine && comment.endLine !== comment.line
|
|
11
|
+
? `${comment.line}-${comment.endLine}`
|
|
12
|
+
: `${comment.line}`;
|
|
13
|
+
const location = `${comment.file}:${lineRange}`;
|
|
14
|
+
output += `- ${location} -> "${comment.text}"\n`;
|
|
15
|
+
}
|
|
16
|
+
output += '\nPlease review these comments and address them.';
|
|
17
|
+
return output;
|
|
18
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
const router = Router();
|
|
3
|
+
// POST /api/cancel
|
|
4
|
+
router.post('/', (req, res) => {
|
|
5
|
+
const state = req.app.locals.state;
|
|
6
|
+
// Send response before shutdown
|
|
7
|
+
res.json({ status: 'ok' });
|
|
8
|
+
// Trigger shutdown without output
|
|
9
|
+
if (state.resolve) {
|
|
10
|
+
state.resolve(null);
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
export { router as cancelRouter };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
const router = Router();
|
|
5
|
+
function detectLanguage(filePath) {
|
|
6
|
+
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
7
|
+
const languageMap = {
|
|
8
|
+
rs: 'rust',
|
|
9
|
+
js: 'javascript',
|
|
10
|
+
ts: 'typescript',
|
|
11
|
+
tsx: 'tsx',
|
|
12
|
+
jsx: 'jsx',
|
|
13
|
+
py: 'python',
|
|
14
|
+
go: 'go',
|
|
15
|
+
java: 'java',
|
|
16
|
+
c: 'c',
|
|
17
|
+
cpp: 'cpp',
|
|
18
|
+
cc: 'cpp',
|
|
19
|
+
cxx: 'cpp',
|
|
20
|
+
h: 'cpp',
|
|
21
|
+
hpp: 'cpp',
|
|
22
|
+
md: 'markdown',
|
|
23
|
+
json: 'json',
|
|
24
|
+
yaml: 'yaml',
|
|
25
|
+
yml: 'yaml',
|
|
26
|
+
toml: 'toml',
|
|
27
|
+
html: 'html',
|
|
28
|
+
css: 'css',
|
|
29
|
+
scss: 'scss',
|
|
30
|
+
sh: 'bash',
|
|
31
|
+
bash: 'bash',
|
|
32
|
+
sql: 'sql',
|
|
33
|
+
rb: 'ruby',
|
|
34
|
+
swift: 'swift',
|
|
35
|
+
kt: 'kotlin',
|
|
36
|
+
kts: 'kotlin',
|
|
37
|
+
xml: 'xml',
|
|
38
|
+
vue: 'vue',
|
|
39
|
+
};
|
|
40
|
+
return languageMap[ext] || 'plaintext';
|
|
41
|
+
}
|
|
42
|
+
// GET /api/file?path=<path>
|
|
43
|
+
router.get('/', (req, res) => {
|
|
44
|
+
const state = req.app.locals.state;
|
|
45
|
+
const relativePath = req.query.path;
|
|
46
|
+
if (!relativePath) {
|
|
47
|
+
res.status(400).json({ error: 'Missing path parameter' });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const filePath = path.join(state.cwd, relativePath);
|
|
51
|
+
// Security check: ensure path is within cwd
|
|
52
|
+
const resolvedPath = path.resolve(filePath);
|
|
53
|
+
const resolvedCwd = path.resolve(state.cwd);
|
|
54
|
+
if (!resolvedPath.startsWith(resolvedCwd)) {
|
|
55
|
+
res.status(403).json({ error: 'Access denied' });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const content = fs.readFileSync(resolvedPath, 'utf-8');
|
|
60
|
+
const language = detectLanguage(relativePath);
|
|
61
|
+
const response = {
|
|
62
|
+
path: relativePath,
|
|
63
|
+
content,
|
|
64
|
+
language,
|
|
65
|
+
};
|
|
66
|
+
res.json(response);
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
if (err.code === 'ENOENT') {
|
|
70
|
+
res.status(404).json({ error: 'File not found' });
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
res.status(500).json({ error: 'Failed to read file' });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
export { router as fileRouter };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Router as IRouter } from 'express';
|
|
2
|
+
export interface FileNode {
|
|
3
|
+
path: string;
|
|
4
|
+
name: string;
|
|
5
|
+
type: 'file' | 'directory';
|
|
6
|
+
children?: FileNode[];
|
|
7
|
+
}
|
|
8
|
+
export interface FilesResponse {
|
|
9
|
+
root: string;
|
|
10
|
+
tree: FileNode[];
|
|
11
|
+
}
|
|
12
|
+
export interface FlatFilesResponse {
|
|
13
|
+
files: string[];
|
|
14
|
+
}
|
|
15
|
+
declare const router: IRouter;
|
|
16
|
+
export { router as filesRouter };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
const router = Router();
|
|
5
|
+
// Default ignore patterns (similar to .gitignore behavior)
|
|
6
|
+
const DEFAULT_IGNORES = [
|
|
7
|
+
'node_modules',
|
|
8
|
+
'.git',
|
|
9
|
+
'.svn',
|
|
10
|
+
'.hg',
|
|
11
|
+
'dist',
|
|
12
|
+
'build',
|
|
13
|
+
'out',
|
|
14
|
+
'.cache',
|
|
15
|
+
'.next',
|
|
16
|
+
'.nuxt',
|
|
17
|
+
'coverage',
|
|
18
|
+
'__pycache__',
|
|
19
|
+
'.pytest_cache',
|
|
20
|
+
'target',
|
|
21
|
+
'Cargo.lock',
|
|
22
|
+
'package-lock.json',
|
|
23
|
+
'pnpm-lock.yaml',
|
|
24
|
+
'yarn.lock',
|
|
25
|
+
'.DS_Store',
|
|
26
|
+
];
|
|
27
|
+
function shouldIgnore(name) {
|
|
28
|
+
if (name.startsWith('.') && name !== '.gitignore' && name !== '.env.example') {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
return DEFAULT_IGNORES.includes(name);
|
|
32
|
+
}
|
|
33
|
+
function buildFileTree(rootPath, currentPath, maxDepth, depth = 0) {
|
|
34
|
+
if (depth > maxDepth)
|
|
35
|
+
return [];
|
|
36
|
+
const entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
|
37
|
+
const nodes = [];
|
|
38
|
+
// Sort: directories first, then alphabetically
|
|
39
|
+
entries.sort((a, b) => {
|
|
40
|
+
if (a.isDirectory() && !b.isDirectory())
|
|
41
|
+
return -1;
|
|
42
|
+
if (!a.isDirectory() && b.isDirectory())
|
|
43
|
+
return 1;
|
|
44
|
+
return a.name.localeCompare(b.name);
|
|
45
|
+
});
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
if (shouldIgnore(entry.name))
|
|
48
|
+
continue;
|
|
49
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
50
|
+
const relativePath = path.relative(rootPath, fullPath);
|
|
51
|
+
const node = {
|
|
52
|
+
path: relativePath,
|
|
53
|
+
name: entry.name,
|
|
54
|
+
type: entry.isDirectory() ? 'directory' : 'file',
|
|
55
|
+
};
|
|
56
|
+
if (entry.isDirectory()) {
|
|
57
|
+
node.children = buildFileTree(rootPath, fullPath, maxDepth, depth + 1);
|
|
58
|
+
}
|
|
59
|
+
nodes.push(node);
|
|
60
|
+
}
|
|
61
|
+
return nodes;
|
|
62
|
+
}
|
|
63
|
+
function collectFlatFiles(rootPath, currentPath, maxDepth, depth = 0) {
|
|
64
|
+
if (depth > maxDepth)
|
|
65
|
+
return [];
|
|
66
|
+
const entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
|
67
|
+
const files = [];
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
if (shouldIgnore(entry.name))
|
|
70
|
+
continue;
|
|
71
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
72
|
+
const relativePath = path.relative(rootPath, fullPath);
|
|
73
|
+
if (entry.isFile()) {
|
|
74
|
+
files.push(relativePath);
|
|
75
|
+
}
|
|
76
|
+
else if (entry.isDirectory()) {
|
|
77
|
+
files.push(...collectFlatFiles(rootPath, fullPath, maxDepth, depth + 1));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return files;
|
|
81
|
+
}
|
|
82
|
+
// GET /api/files - Get file tree structure
|
|
83
|
+
router.get('/', (req, res) => {
|
|
84
|
+
const state = req.app.locals.state;
|
|
85
|
+
const tree = buildFileTree(state.cwd, state.cwd, 10);
|
|
86
|
+
const response = {
|
|
87
|
+
root: state.cwd,
|
|
88
|
+
tree,
|
|
89
|
+
};
|
|
90
|
+
res.json(response);
|
|
91
|
+
});
|
|
92
|
+
// GET /api/files/flat - Get flat list of all files
|
|
93
|
+
router.get('/flat', (req, res) => {
|
|
94
|
+
const state = req.app.locals.state;
|
|
95
|
+
const files = collectFlatFiles(state.cwd, state.cwd, 10);
|
|
96
|
+
files.sort();
|
|
97
|
+
const response = {
|
|
98
|
+
files,
|
|
99
|
+
};
|
|
100
|
+
res.json(response);
|
|
101
|
+
});
|
|
102
|
+
export { router as filesRouter };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Router as IRouter } from 'express';
|
|
2
|
+
export interface UnstagedFile {
|
|
3
|
+
path: string;
|
|
4
|
+
status: string;
|
|
5
|
+
}
|
|
6
|
+
export interface GitStatusResponse {
|
|
7
|
+
isGitRepo: boolean;
|
|
8
|
+
unstaged: UnstagedFile[];
|
|
9
|
+
}
|
|
10
|
+
declare const router: IRouter;
|
|
11
|
+
export { router as gitRouter };
|