devbonzai 2.1.6 → 2.1.8
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/cli.js +40 -0
- package/package.json +1 -1
- package/templates/config.js +18 -0
- package/templates/handlers/analyze_prompt.js +118 -0
- package/templates/handlers/delete.js +20 -0
- package/templates/handlers/git-churn.js +44 -0
- package/templates/handlers/index.js +25 -0
- package/templates/handlers/list.js +16 -0
- package/templates/handlers/move.js +35 -0
- package/templates/handlers/open-cursor.js +108 -0
- package/templates/handlers/prompt_agent.js +181 -0
- package/templates/handlers/prompt_agent_stream.js +247 -0
- package/templates/handlers/read.js +120 -0
- package/templates/handlers/revert_job.js +31 -0
- package/templates/handlers/shutdown.js +16 -0
- package/templates/handlers/write.js +19 -0
- package/templates/handlers/write_dir.js +20 -0
- package/templates/receiver.js +31 -1784
- package/templates/utils/fileList.js +96 -0
- package/templates/utils/ignore.js +53 -0
- package/templates/utils/parsers.js +720 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
const { spawn, execSync } = require('child_process');
|
|
2
|
+
const { ROOT } = require('../config');
|
|
3
|
+
|
|
4
|
+
function promptAgentStreamHandler(req, res) {
|
|
5
|
+
console.log('🔵 [prompt_agent_stream] Endpoint hit');
|
|
6
|
+
const { prompt } = req.body;
|
|
7
|
+
console.log('🔵 [prompt_agent_stream] Received prompt:', prompt ? `${prompt.substring(0, 50)}...` : 'none');
|
|
8
|
+
|
|
9
|
+
if (!prompt || typeof prompt !== 'string') {
|
|
10
|
+
console.log('❌ [prompt_agent_stream] Error: prompt required');
|
|
11
|
+
return res.status(400).json({ error: 'prompt required' });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Set up SSE headers
|
|
15
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
16
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
17
|
+
res.setHeader('Connection', 'keep-alive');
|
|
18
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
19
|
+
res.flushHeaders();
|
|
20
|
+
|
|
21
|
+
// Helper to send SSE events with robustness checks
|
|
22
|
+
const sendEvent = (type, data) => {
|
|
23
|
+
try {
|
|
24
|
+
// Check if response is still writable - try to send even if clientDisconnected flag is set
|
|
25
|
+
// because the response stream might still be open
|
|
26
|
+
if (res.destroyed || res.closed) {
|
|
27
|
+
console.log(`⚠️ [prompt_agent_stream] Response already closed, cannot send ${type} event`);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
res.write(`data: ${JSON.stringify({ type, ...data })}\n\n`);
|
|
31
|
+
return true;
|
|
32
|
+
} catch (e) {
|
|
33
|
+
console.log(`⚠️ [prompt_agent_stream] Error sending ${type} event:`, e.message);
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Capture beforeCommit
|
|
39
|
+
let beforeCommit = '';
|
|
40
|
+
try {
|
|
41
|
+
beforeCommit = execSync('git rev-parse HEAD', { cwd: ROOT }).toString().trim();
|
|
42
|
+
console.log('🔵 [prompt_agent_stream] beforeCommit:', beforeCommit);
|
|
43
|
+
} catch (e) {
|
|
44
|
+
console.log('⚠️ [prompt_agent_stream] Could not get beforeCommit:', e.message);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Capture initial state of modified files
|
|
48
|
+
const initiallyModifiedFiles = new Set();
|
|
49
|
+
try {
|
|
50
|
+
const initialStatus = execSync('git status --short', { cwd: ROOT }).toString();
|
|
51
|
+
initialStatus.split('\n').filter(Boolean).forEach(line => {
|
|
52
|
+
const filePath = line.substring(3).trim();
|
|
53
|
+
if (filePath) initiallyModifiedFiles.add(filePath);
|
|
54
|
+
});
|
|
55
|
+
} catch (e) {
|
|
56
|
+
// Ignore
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Send starting event
|
|
60
|
+
sendEvent('start', { beforeCommit });
|
|
61
|
+
|
|
62
|
+
// Set up file change tracking with real-time updates
|
|
63
|
+
const changedFiles = new Set();
|
|
64
|
+
const pollInterval = setInterval(() => {
|
|
65
|
+
try {
|
|
66
|
+
const status = execSync('git status --short', { cwd: ROOT }).toString();
|
|
67
|
+
status.split('\n').filter(Boolean).forEach(line => {
|
|
68
|
+
const filePath = line.substring(3).trim();
|
|
69
|
+
if (filePath && !initiallyModifiedFiles.has(filePath)) {
|
|
70
|
+
if (!changedFiles.has(filePath)) {
|
|
71
|
+
changedFiles.add(filePath);
|
|
72
|
+
console.log('📁 [prompt_agent_stream] File changed:', filePath);
|
|
73
|
+
// Send real-time update to client
|
|
74
|
+
sendEvent('file_changed', { path: filePath });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
} catch (e) {
|
|
79
|
+
// Ignore git status errors
|
|
80
|
+
}
|
|
81
|
+
}, 500);
|
|
82
|
+
|
|
83
|
+
const timeoutMs = parseInt(req.body.timeout) || 5 * 60 * 1000;
|
|
84
|
+
let timeoutId = null;
|
|
85
|
+
let responseSent = false;
|
|
86
|
+
|
|
87
|
+
const args = ['--print', '--force', '--workspace', '.', prompt];
|
|
88
|
+
|
|
89
|
+
console.log('🔵 [prompt_agent_stream] Spawning cursor-agent process...');
|
|
90
|
+
const proc = spawn(
|
|
91
|
+
'cursor-agent',
|
|
92
|
+
args,
|
|
93
|
+
{
|
|
94
|
+
cwd: ROOT,
|
|
95
|
+
env: process.env,
|
|
96
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
console.log('🔵 [prompt_agent_stream] Process spawned, PID:', proc.pid);
|
|
101
|
+
|
|
102
|
+
let stdout = '';
|
|
103
|
+
let stderr = '';
|
|
104
|
+
|
|
105
|
+
timeoutId = setTimeout(() => {
|
|
106
|
+
if (!responseSent && proc && !proc.killed) {
|
|
107
|
+
console.log('⏱️ [prompt_agent_stream] Timeout reached');
|
|
108
|
+
clearInterval(pollInterval);
|
|
109
|
+
proc.kill('SIGTERM');
|
|
110
|
+
|
|
111
|
+
setTimeout(() => {
|
|
112
|
+
if (!proc.killed) proc.kill('SIGKILL');
|
|
113
|
+
}, 5000);
|
|
114
|
+
|
|
115
|
+
// Always try to send complete event when timeout occurs
|
|
116
|
+
// Only skip if we've already sent it
|
|
117
|
+
if (!responseSent) {
|
|
118
|
+
try {
|
|
119
|
+
// Check if response is still writable - try to send even if clientDisconnected flag is set
|
|
120
|
+
// because the response stream might still be open
|
|
121
|
+
if (res.destroyed || res.closed) {
|
|
122
|
+
console.log('⚠️ [prompt_agent_stream] Response already closed, cannot send timeout events');
|
|
123
|
+
} else {
|
|
124
|
+
responseSent = true;
|
|
125
|
+
sendEvent('error', {
|
|
126
|
+
error: 'Process timeout',
|
|
127
|
+
message: `cursor-agent exceeded timeout of ${timeoutMs / 1000} seconds`
|
|
128
|
+
});
|
|
129
|
+
sendEvent('complete', {
|
|
130
|
+
code: -1,
|
|
131
|
+
stdout,
|
|
132
|
+
stderr,
|
|
133
|
+
changedFiles: Array.from(changedFiles),
|
|
134
|
+
beforeCommit,
|
|
135
|
+
afterCommit: ''
|
|
136
|
+
});
|
|
137
|
+
// Send stop event after complete
|
|
138
|
+
sendEvent('stop', {});
|
|
139
|
+
res.end();
|
|
140
|
+
console.log('✅ [prompt_agent_stream] Sent timeout error, complete, and stop events');
|
|
141
|
+
}
|
|
142
|
+
} catch (e) {
|
|
143
|
+
console.log('⚠️ [prompt_agent_stream] Error sending timeout events:', e.message);
|
|
144
|
+
// Don't set responseSent = true on error, in case we can retry
|
|
145
|
+
// But realistically, if there's an error, the connection is probably dead
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
console.log('⚠️ [prompt_agent_stream] Timeout events already sent, skipping');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}, timeoutMs);
|
|
152
|
+
|
|
153
|
+
proc.stdout.on('data', (d) => {
|
|
154
|
+
stdout += d.toString();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
proc.stderr.on('data', (d) => {
|
|
158
|
+
stderr += d.toString();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
proc.on('error', (error) => {
|
|
162
|
+
console.log('❌ [prompt_agent_stream] Process error:', error.message);
|
|
163
|
+
clearInterval(pollInterval);
|
|
164
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
165
|
+
|
|
166
|
+
// Always try to send error event when process error occurs
|
|
167
|
+
// Only skip if we've already sent it
|
|
168
|
+
if (!responseSent) {
|
|
169
|
+
try {
|
|
170
|
+
// Check if response is still writable - try to send even if clientDisconnected flag is set
|
|
171
|
+
// because the response stream might still be open
|
|
172
|
+
if (res.destroyed || res.closed) {
|
|
173
|
+
console.log('⚠️ [prompt_agent_stream] Response already closed, cannot send error event');
|
|
174
|
+
} else {
|
|
175
|
+
responseSent = true;
|
|
176
|
+
sendEvent('error', { error: error.message });
|
|
177
|
+
// Send stop event after error
|
|
178
|
+
sendEvent('stop', {});
|
|
179
|
+
res.end();
|
|
180
|
+
console.log('✅ [prompt_agent_stream] Sent error and stop events');
|
|
181
|
+
}
|
|
182
|
+
} catch (e) {
|
|
183
|
+
console.log('⚠️ [prompt_agent_stream] Error sending error event:', e.message);
|
|
184
|
+
// Don't set responseSent = true on error, in case we can retry
|
|
185
|
+
// But realistically, if there's an error, the connection is probably dead
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
console.log('⚠️ [prompt_agent_stream] Error event already sent, skipping');
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
proc.on('close', (code, signal) => {
|
|
193
|
+
console.log('🔵 [prompt_agent_stream] Process closed with code:', code);
|
|
194
|
+
clearInterval(pollInterval);
|
|
195
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
196
|
+
|
|
197
|
+
let afterCommit = '';
|
|
198
|
+
try {
|
|
199
|
+
afterCommit = execSync('git rev-parse HEAD', { cwd: ROOT }).toString().trim();
|
|
200
|
+
} catch (e) {
|
|
201
|
+
// Ignore
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Always try to send complete event when process finishes
|
|
205
|
+
// Check actual response stream state rather than relying on responseSent flag
|
|
206
|
+
// Only skip if we've already sent it (responseSent flag prevents duplicates)
|
|
207
|
+
if (!responseSent) {
|
|
208
|
+
try {
|
|
209
|
+
// Check if response is still writable - check actual stream state
|
|
210
|
+
if (res.destroyed || res.closed) {
|
|
211
|
+
console.log('⚠️ [prompt_agent_stream] Response already closed, cannot send complete event');
|
|
212
|
+
} else {
|
|
213
|
+
// Send events and only set flag after successful send
|
|
214
|
+
sendEvent('complete', {
|
|
215
|
+
code,
|
|
216
|
+
stdout,
|
|
217
|
+
stderr,
|
|
218
|
+
changedFiles: Array.from(changedFiles),
|
|
219
|
+
beforeCommit,
|
|
220
|
+
afterCommit
|
|
221
|
+
});
|
|
222
|
+
// Send stop event after complete
|
|
223
|
+
sendEvent('stop', {});
|
|
224
|
+
res.end();
|
|
225
|
+
responseSent = true;
|
|
226
|
+
console.log('✅ [prompt_agent_stream] Sent complete and stop events');
|
|
227
|
+
}
|
|
228
|
+
} catch (e) {
|
|
229
|
+
console.log('⚠️ [prompt_agent_stream] Error sending complete event:', e.message);
|
|
230
|
+
// Don't set responseSent = true on error, in case we can retry
|
|
231
|
+
// But realistically, if there's an error, the connection is probably dead
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
console.log('⚠️ [prompt_agent_stream] Complete event already sent, skipping');
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Handle client disconnect - DON'T kill the process, let it complete
|
|
239
|
+
req.on('close', () => {
|
|
240
|
+
console.log('🔵 [prompt_agent_stream] Client disconnected (process continues in background)');
|
|
241
|
+
// Don't kill the process - let it complete
|
|
242
|
+
// Don't set responseSent here - let proc.on('close') check actual stream state
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
module.exports = promptAgentStreamHandler;
|
|
247
|
+
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { ROOT } = require('../config');
|
|
4
|
+
const { extractPythonFunctions, extractJavaScriptFunctions, extractVueFunctions } = require('../utils/parsers');
|
|
5
|
+
|
|
6
|
+
function readHandler(req, res) {
|
|
7
|
+
try {
|
|
8
|
+
const requestedPath = req.query.path || '';
|
|
9
|
+
const filePath = path.join(ROOT, requestedPath);
|
|
10
|
+
|
|
11
|
+
if (!filePath.startsWith(ROOT)) {
|
|
12
|
+
return res.status(400).send('Invalid path');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Helper function to find and return content from parse result
|
|
16
|
+
const findAndReturn = (parseResult, name, type) => {
|
|
17
|
+
if (type === 'function') {
|
|
18
|
+
const target = parseResult.functions.find(f => f.name === name);
|
|
19
|
+
if (target) return target.content;
|
|
20
|
+
} else if (type === 'method') {
|
|
21
|
+
// Method name format: ClassName.methodName
|
|
22
|
+
for (const cls of parseResult.classes) {
|
|
23
|
+
const method = cls.methods.find(m => m.name === name);
|
|
24
|
+
if (method) return method.content;
|
|
25
|
+
}
|
|
26
|
+
} else if (type === 'class') {
|
|
27
|
+
const target = parseResult.classes.find(c => c.name === name);
|
|
28
|
+
if (target) return target.content;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Check if this is a virtual file request (.function, .method, or .class)
|
|
34
|
+
if (requestedPath.endsWith('.function') || requestedPath.endsWith('.method') || requestedPath.endsWith('.class')) {
|
|
35
|
+
// Traverse up the path to find the actual source file
|
|
36
|
+
let currentPath = filePath;
|
|
37
|
+
let sourceFilePath = null;
|
|
38
|
+
let parser = null;
|
|
39
|
+
|
|
40
|
+
// Keep going up until we find a source file (.py, .js, .jsx, .ts, .tsx, .vue)
|
|
41
|
+
while (currentPath !== ROOT && currentPath !== path.dirname(currentPath)) {
|
|
42
|
+
const stat = fs.existsSync(currentPath) ? fs.statSync(currentPath) : null;
|
|
43
|
+
|
|
44
|
+
// Check if current path is a file with a supported extension
|
|
45
|
+
if (stat && stat.isFile()) {
|
|
46
|
+
if (currentPath.endsWith('.py')) {
|
|
47
|
+
parser = extractPythonFunctions;
|
|
48
|
+
sourceFilePath = currentPath;
|
|
49
|
+
break;
|
|
50
|
+
} else if (currentPath.endsWith('.js') || currentPath.endsWith('.jsx') ||
|
|
51
|
+
currentPath.endsWith('.ts') || currentPath.endsWith('.tsx')) {
|
|
52
|
+
parser = extractJavaScriptFunctions;
|
|
53
|
+
sourceFilePath = currentPath;
|
|
54
|
+
break;
|
|
55
|
+
} else if (currentPath.endsWith('.vue')) {
|
|
56
|
+
parser = extractVueFunctions;
|
|
57
|
+
sourceFilePath = currentPath;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Move up one level
|
|
63
|
+
const parentPath = path.dirname(currentPath);
|
|
64
|
+
if (parentPath === currentPath) break; // Reached root
|
|
65
|
+
currentPath = parentPath;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!sourceFilePath || !parser) {
|
|
69
|
+
return res.status(404).send('Source file not found for virtual file');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Extract the requested item name from the requested path
|
|
73
|
+
let itemName = '';
|
|
74
|
+
let itemType = '';
|
|
75
|
+
|
|
76
|
+
if (requestedPath.endsWith('.function')) {
|
|
77
|
+
itemName = path.basename(requestedPath, '.function');
|
|
78
|
+
itemType = 'function';
|
|
79
|
+
} else if (requestedPath.endsWith('.method')) {
|
|
80
|
+
itemName = path.basename(requestedPath, '.method');
|
|
81
|
+
itemType = 'method';
|
|
82
|
+
} else if (requestedPath.endsWith('.class')) {
|
|
83
|
+
itemName = path.basename(requestedPath, '.class');
|
|
84
|
+
itemType = 'class';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check if the source file exists
|
|
88
|
+
try {
|
|
89
|
+
if (!fs.existsSync(sourceFilePath)) {
|
|
90
|
+
return res.status(404).send('Source file not found');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Parse the file
|
|
94
|
+
const parseResult = parser(sourceFilePath);
|
|
95
|
+
|
|
96
|
+
// Find and return the content
|
|
97
|
+
const content = findAndReturn(parseResult, itemName, itemType);
|
|
98
|
+
|
|
99
|
+
if (!content) {
|
|
100
|
+
return res.status(404).send(`${itemType} '${itemName}' not found in file`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return res.json({ content });
|
|
104
|
+
} catch (e) {
|
|
105
|
+
const errorType = requestedPath.endsWith('.function') ? 'function' :
|
|
106
|
+
requestedPath.endsWith('.method') ? 'method' : 'class';
|
|
107
|
+
return res.status(500).send('Error reading ' + errorType + ': ' + e.message);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Regular file read
|
|
112
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
113
|
+
res.json({ content });
|
|
114
|
+
} catch (e) {
|
|
115
|
+
res.status(500).send(e.message);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = readHandler;
|
|
120
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const { execSync } = require('child_process');
|
|
2
|
+
const { ROOT } = require('../config');
|
|
3
|
+
|
|
4
|
+
function revertJobHandler(req, res) {
|
|
5
|
+
console.log('🔵 [revert_job] Endpoint hit');
|
|
6
|
+
const { beforeCommit } = req.body;
|
|
7
|
+
|
|
8
|
+
if (!beforeCommit || typeof beforeCommit !== 'string') {
|
|
9
|
+
console.log('❌ [revert_job] Error: beforeCommit required');
|
|
10
|
+
return res.status(400).json({ error: 'beforeCommit required' });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Validate commit hash format (basic sanitization to prevent command injection)
|
|
14
|
+
if (!/^[a-f0-9]{7,40}$/i.test(beforeCommit)) {
|
|
15
|
+
console.log('❌ [revert_job] Error: invalid commit hash format');
|
|
16
|
+
return res.status(400).json({ error: 'Invalid commit hash format' });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
console.log('🔵 [revert_job] Resetting to commit:', beforeCommit);
|
|
21
|
+
execSync(`git reset --hard ${beforeCommit}`, { cwd: ROOT });
|
|
22
|
+
console.log('✅ [revert_job] Successfully reverted to commit:', beforeCommit);
|
|
23
|
+
res.json({ success: true });
|
|
24
|
+
} catch (e) {
|
|
25
|
+
console.log('❌ [revert_job] Error:', e.message);
|
|
26
|
+
res.status(500).json({ error: e.message });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = revertJobHandler;
|
|
31
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
function shutdownHandler(req, res) {
|
|
2
|
+
console.log('🛑 Shutdown endpoint called - terminating server...');
|
|
3
|
+
|
|
4
|
+
res.json({
|
|
5
|
+
success: true,
|
|
6
|
+
message: 'Server shutting down...'
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// Close the server gracefully
|
|
10
|
+
setTimeout(() => {
|
|
11
|
+
process.exit(0);
|
|
12
|
+
}, 100); // Small delay to ensure response is sent
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = shutdownHandler;
|
|
16
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { ROOT } = require('../config');
|
|
4
|
+
|
|
5
|
+
function writeHandler(req, res) {
|
|
6
|
+
try {
|
|
7
|
+
const filePath = path.join(ROOT, req.body.path || '');
|
|
8
|
+
if (!filePath.startsWith(ROOT)) {
|
|
9
|
+
return res.status(400).send('Invalid path');
|
|
10
|
+
}
|
|
11
|
+
fs.writeFileSync(filePath, req.body.content, 'utf8');
|
|
12
|
+
res.json({ status: 'ok' });
|
|
13
|
+
} catch (e) {
|
|
14
|
+
res.status(500).send(e.message);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = writeHandler;
|
|
19
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { ROOT } = require('../config');
|
|
4
|
+
|
|
5
|
+
function writeDirHandler(req, res) {
|
|
6
|
+
try {
|
|
7
|
+
const dirPath = path.join(ROOT, req.body.path || '');
|
|
8
|
+
if (!dirPath.startsWith(ROOT)) {
|
|
9
|
+
return res.status(400).send('Invalid path');
|
|
10
|
+
}
|
|
11
|
+
// Create directory recursively (creates parent directories if they don't exist)
|
|
12
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
13
|
+
res.json({ status: 'ok' });
|
|
14
|
+
} catch (e) {
|
|
15
|
+
res.status(500).send(e.message);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = writeDirHandler;
|
|
20
|
+
|