deckide 3.0.0

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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +192 -0
  3. package/apps/server/dist/config.js +77 -0
  4. package/apps/server/dist/index.js +5 -0
  5. package/apps/server/dist/middleware/auth.js +78 -0
  6. package/apps/server/dist/middleware/cors.js +26 -0
  7. package/apps/server/dist/middleware/security.js +16 -0
  8. package/apps/server/dist/pty-client.js +177 -0
  9. package/apps/server/dist/pty-daemon.js +246 -0
  10. package/apps/server/dist/routes/decks.js +95 -0
  11. package/apps/server/dist/routes/files.js +221 -0
  12. package/apps/server/dist/routes/git.js +775 -0
  13. package/apps/server/dist/routes/settings.js +95 -0
  14. package/apps/server/dist/routes/terminals.js +239 -0
  15. package/apps/server/dist/routes/workspaces.js +83 -0
  16. package/apps/server/dist/server.js +257 -0
  17. package/apps/server/dist/types.js +1 -0
  18. package/apps/server/dist/utils/database.js +136 -0
  19. package/apps/server/dist/utils/error.js +28 -0
  20. package/apps/server/dist/utils/path.js +98 -0
  21. package/apps/server/dist/utils/shell.js +4 -0
  22. package/apps/server/dist/websocket.js +207 -0
  23. package/apps/server/package.json +26 -0
  24. package/apps/web/dist/assets/index-C-usl0y0.css +32 -0
  25. package/apps/web/dist/assets/index-CibzlLP5.js +129 -0
  26. package/apps/web/dist/index.html +13 -0
  27. package/bin/deckide.js +79 -0
  28. package/package.json +77 -0
  29. package/packages/shared/dist/types.d.ts +124 -0
  30. package/packages/shared/dist/types.d.ts.map +1 -0
  31. package/packages/shared/dist/types.js +3 -0
  32. package/packages/shared/dist/types.js.map +1 -0
  33. package/packages/shared/dist/utils-node.d.ts +22 -0
  34. package/packages/shared/dist/utils-node.d.ts.map +1 -0
  35. package/packages/shared/dist/utils-node.js +35 -0
  36. package/packages/shared/dist/utils-node.js.map +1 -0
  37. package/packages/shared/dist/utils.d.ts +90 -0
  38. package/packages/shared/dist/utils.d.ts.map +1 -0
  39. package/packages/shared/dist/utils.js +186 -0
  40. package/packages/shared/dist/utils.js.map +1 -0
  41. package/packages/shared/package.json +16 -0
@@ -0,0 +1,246 @@
1
+ /**
2
+ * PTY Daemon - standalone process that manages PTY sessions.
3
+ *
4
+ * Spawned as a detached child by the main server so it outlives server restarts.
5
+ * Communicates with the server over a local TCP socket using newline-delimited JSON.
6
+ *
7
+ * Protocol (Server → Daemon):
8
+ * { type:"create", id, shell, shellArgs, cwd, env, cols, rows }
9
+ * { type:"input", id, data }
10
+ * { type:"resize", id, cols, rows }
11
+ * { type:"kill", id }
12
+ * { type:"attach", id, bufferOffset } -- start streaming data for this terminal
13
+ * { type:"list" } -- list active terminal IDs
14
+ * { type:"shutdown" } -- terminate daemon and all PTYs
15
+ *
16
+ * Protocol (Daemon → Server):
17
+ * { type:"created", id }
18
+ * { type:"data", id, data } -- only for attached terminals
19
+ * { type:"exit", id, code }
20
+ * { type:"list_result", terminals: [{id, bufferLength}] }
21
+ * { type:"shutdown_ack" }
22
+ * { type:"error", id?, message }
23
+ */
24
+ import net from 'node:net';
25
+ import fs from 'node:fs';
26
+ import { spawn } from 'node-pty';
27
+ const DAEMON_INFO_PATH = process.env.DAEMON_INFO_PATH;
28
+ const BUFFER_LIMIT = Number(process.env.TERMINAL_BUFFER_LIMIT || 500000);
29
+ if (!DAEMON_INFO_PATH) {
30
+ console.error('[pty-daemon] DAEMON_INFO_PATH env var is required');
31
+ process.exit(1);
32
+ }
33
+ const sessions = new Map();
34
+ // IDs the server is currently subscribed to (receives live data for)
35
+ const attached = new Set();
36
+ let serverSocket = null;
37
+ let shuttingDown = false;
38
+ function appendBuffer(session, data) {
39
+ const combined = session.buffer + data;
40
+ session.buffer =
41
+ combined.length > BUFFER_LIMIT
42
+ ? combined.slice(combined.length - BUFFER_LIMIT)
43
+ : combined;
44
+ }
45
+ function sendToServer(msg) {
46
+ if (serverSocket && !serverSocket.destroyed) {
47
+ try {
48
+ serverSocket.write(JSON.stringify(msg) + '\n');
49
+ }
50
+ catch {
51
+ // Socket may have just closed
52
+ }
53
+ }
54
+ }
55
+ function handleMessage(msg) {
56
+ switch (msg.type) {
57
+ case 'create': {
58
+ const { id, shell, shellArgs = [], cwd, env, cols = 120, rows = 32 } = msg;
59
+ if (sessions.has(id)) {
60
+ // Already exists - just ack
61
+ sendToServer({ type: 'created', id });
62
+ return;
63
+ }
64
+ try {
65
+ const isWindows = process.platform === 'win32';
66
+ const term = spawn(shell, shellArgs, {
67
+ cwd,
68
+ cols,
69
+ rows,
70
+ env,
71
+ encoding: 'utf8',
72
+ ...(isWindows ? { useConpty: true } : {}),
73
+ });
74
+ const session = { id, term, buffer: '' };
75
+ sessions.set(id, session);
76
+ term.onData((data) => {
77
+ appendBuffer(session, data);
78
+ if (attached.has(id)) {
79
+ sendToServer({ type: 'data', id, data });
80
+ }
81
+ });
82
+ term.onExit(({ exitCode }) => {
83
+ sessions.delete(id);
84
+ attached.delete(id);
85
+ sendToServer({ type: 'exit', id, code: exitCode });
86
+ });
87
+ sendToServer({ type: 'created', id });
88
+ console.log(`[pty-daemon] Created terminal ${id} (pid=${term.pid})`);
89
+ }
90
+ catch (err) {
91
+ console.error(`[pty-daemon] Failed to create terminal ${id}:`, err);
92
+ sendToServer({ type: 'error', id, message: String(err) });
93
+ }
94
+ break;
95
+ }
96
+ case 'input': {
97
+ const session = sessions.get(msg.id);
98
+ if (session) {
99
+ try {
100
+ session.term.write(msg.data);
101
+ }
102
+ catch { /* terminal may be dying */ }
103
+ }
104
+ break;
105
+ }
106
+ case 'resize': {
107
+ const session = sessions.get(msg.id);
108
+ if (session) {
109
+ try {
110
+ session.term.resize(msg.cols, msg.rows);
111
+ }
112
+ catch { /* terminal may be dying */ }
113
+ }
114
+ break;
115
+ }
116
+ case 'kill': {
117
+ const session = sessions.get(msg.id);
118
+ if (session) {
119
+ sessions.delete(msg.id);
120
+ attached.delete(msg.id);
121
+ try {
122
+ session.term.kill();
123
+ }
124
+ catch { /* already dead */ }
125
+ console.log(`[pty-daemon] Killed terminal ${msg.id}`);
126
+ }
127
+ break;
128
+ }
129
+ case 'attach': {
130
+ const session = sessions.get(msg.id);
131
+ if (!session) {
132
+ sendToServer({ type: 'error', id: msg.id, message: 'Terminal not found' });
133
+ return;
134
+ }
135
+ attached.add(msg.id);
136
+ // Send buffered output the server hasn't seen yet
137
+ const offset = Number(msg.bufferOffset) || 0;
138
+ const delta = offset > 0 ? session.buffer.slice(offset) : session.buffer;
139
+ if (delta) {
140
+ sendToServer({ type: 'data', id: msg.id, data: delta });
141
+ }
142
+ break;
143
+ }
144
+ case 'list': {
145
+ const terminals = Array.from(sessions.entries()).map(([id, s]) => ({
146
+ id,
147
+ bufferLength: s.buffer.length,
148
+ }));
149
+ sendToServer({ type: 'list_result', terminals });
150
+ break;
151
+ }
152
+ case 'shutdown': {
153
+ sendToServer({ type: 'shutdown_ack' });
154
+ // ackを送信しきるため少し待ってから終了
155
+ setTimeout(() => {
156
+ shutdown();
157
+ }, 10);
158
+ break;
159
+ }
160
+ default:
161
+ console.warn(`[pty-daemon] Unknown message type: ${msg.type}`);
162
+ }
163
+ }
164
+ // TCP server - accepts one connection from the main server at a time
165
+ const tcpServer = net.createServer((socket) => {
166
+ // Close previous connection if server reconnects
167
+ if (serverSocket && !serverSocket.destroyed) {
168
+ serverSocket.destroy();
169
+ }
170
+ serverSocket = socket;
171
+ attached.clear(); // New connection starts with no subscriptions
172
+ console.log('[pty-daemon] Main server connected');
173
+ let lineBuf = '';
174
+ socket.on('data', (chunk) => {
175
+ lineBuf += chunk.toString('utf8');
176
+ const lines = lineBuf.split('\n');
177
+ lineBuf = lines.pop();
178
+ for (const line of lines) {
179
+ if (!line.trim())
180
+ continue;
181
+ try {
182
+ handleMessage(JSON.parse(line));
183
+ }
184
+ catch (err) {
185
+ console.error('[pty-daemon] Failed to parse message:', err);
186
+ }
187
+ }
188
+ });
189
+ socket.on('close', () => {
190
+ if (serverSocket === socket) {
191
+ serverSocket = null;
192
+ attached.clear();
193
+ console.log('[pty-daemon] Main server disconnected');
194
+ }
195
+ });
196
+ socket.on('error', (err) => {
197
+ console.error('[pty-daemon] Socket error:', err.message);
198
+ });
199
+ });
200
+ tcpServer.on('error', (err) => {
201
+ console.error('[pty-daemon] TCP server error:', err);
202
+ process.exit(1);
203
+ });
204
+ // Listen on a random available port, then write info file for the server to find us
205
+ tcpServer.listen(0, '127.0.0.1', () => {
206
+ const addr = tcpServer.address();
207
+ const info = { pid: process.pid, port: addr.port };
208
+ fs.writeFileSync(DAEMON_INFO_PATH, JSON.stringify(info));
209
+ console.log(`[pty-daemon] Running on port ${addr.port} (pid ${process.pid})`);
210
+ });
211
+ function shutdown() {
212
+ if (shuttingDown) {
213
+ return;
214
+ }
215
+ shuttingDown = true;
216
+ console.log('[pty-daemon] Shutting down...');
217
+ sessions.forEach(({ term }) => {
218
+ try {
219
+ term.kill();
220
+ }
221
+ catch { /* ignore */ }
222
+ });
223
+ sessions.clear();
224
+ try {
225
+ serverSocket?.destroy();
226
+ }
227
+ catch { /* ignore */ }
228
+ try {
229
+ tcpServer.close();
230
+ }
231
+ catch { /* ignore */ }
232
+ try {
233
+ fs.unlinkSync(DAEMON_INFO_PATH);
234
+ }
235
+ catch { /* ignore */ }
236
+ process.exit(0);
237
+ }
238
+ process.on('SIGTERM', shutdown);
239
+ process.on('SIGINT', shutdown);
240
+ // Keep the daemon alive even on unexpected errors
241
+ process.on('uncaughtException', (err) => {
242
+ console.error('[pty-daemon] Uncaught exception (continuing):', err);
243
+ });
244
+ process.on('unhandledRejection', (err) => {
245
+ console.error('[pty-daemon] Unhandled rejection (continuing):', err);
246
+ });
@@ -0,0 +1,95 @@
1
+ import crypto from 'node:crypto';
2
+ import { Hono } from 'hono';
3
+ import { createHttpError, handleError, readJson } from '../utils/error.js';
4
+ import { requireWorkspace } from './workspaces.js';
5
+ export function createDeckRouter(db, workspaces, decks) {
6
+ const router = new Hono();
7
+ const insertDeck = db.prepare('INSERT INTO decks (id, name, root, workspace_id, created_at) VALUES (?, ?, ?, ?, ?)');
8
+ function createDeck(name, workspaceId) {
9
+ const workspace = requireWorkspace(workspaces, workspaceId);
10
+ const deck = {
11
+ id: crypto.randomUUID(),
12
+ name: name || `${workspace.name} ${Array.from(decks.values()).filter((d) => d.workspaceId === workspaceId).length + 1}`,
13
+ root: workspace.path,
14
+ workspaceId,
15
+ createdAt: new Date().toISOString()
16
+ };
17
+ insertDeck.run(deck.id, deck.name, deck.root, deck.workspaceId, deck.createdAt);
18
+ decks.set(deck.id, deck);
19
+ return deck;
20
+ }
21
+ router.get('/', (c) => {
22
+ return c.json(Array.from(decks.values()));
23
+ });
24
+ router.post('/', async (c) => {
25
+ try {
26
+ const body = await readJson(c);
27
+ const workspaceId = body?.workspaceId;
28
+ if (!workspaceId) {
29
+ throw createHttpError('workspaceId is required', 400);
30
+ }
31
+ const deck = createDeck(body?.name, workspaceId);
32
+ return c.json(deck, 201);
33
+ }
34
+ catch (error) {
35
+ return handleError(c, error);
36
+ }
37
+ });
38
+ const updateSortOrderStmt = db.prepare('UPDATE decks SET sort_order = ? WHERE id = ?');
39
+ router.put('/order', async (c) => {
40
+ try {
41
+ const body = await readJson(c);
42
+ const deckIds = body?.deckIds;
43
+ if (!Array.isArray(deckIds)) {
44
+ throw createHttpError('deckIds array is required', 400);
45
+ }
46
+ db.exec('BEGIN TRANSACTION');
47
+ try {
48
+ deckIds.forEach((id, index) => {
49
+ updateSortOrderStmt.run(index, id);
50
+ });
51
+ db.exec('COMMIT');
52
+ }
53
+ catch (err) {
54
+ db.exec('ROLLBACK');
55
+ throw err;
56
+ }
57
+ // Re-order the in-memory map to match
58
+ const entries = deckIds
59
+ .filter((id) => decks.has(id))
60
+ .map((id) => [id, decks.get(id)]);
61
+ // Add any decks not in the list at the end
62
+ for (const [id, deck] of decks) {
63
+ if (!deckIds.includes(id)) {
64
+ entries.push([id, deck]);
65
+ }
66
+ }
67
+ decks.clear();
68
+ for (const [id, deck] of entries) {
69
+ decks.set(id, deck);
70
+ }
71
+ return c.json({ success: true });
72
+ }
73
+ catch (error) {
74
+ return handleError(c, error);
75
+ }
76
+ });
77
+ const deleteDeckStmt = db.prepare('DELETE FROM decks WHERE id = ?');
78
+ const deleteTerminalsByDeckStmt = db.prepare('DELETE FROM terminals WHERE deck_id = ?');
79
+ router.delete('/:id', (c) => {
80
+ try {
81
+ const deckId = c.req.param('id');
82
+ if (!decks.has(deckId)) {
83
+ throw createHttpError('Deck not found', 404);
84
+ }
85
+ deleteTerminalsByDeckStmt.run(deckId);
86
+ deleteDeckStmt.run(deckId);
87
+ decks.delete(deckId);
88
+ return c.json({ deleted: true });
89
+ }
90
+ catch (error) {
91
+ return handleError(c, error);
92
+ }
93
+ });
94
+ return router;
95
+ }
@@ -0,0 +1,221 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { Hono } from 'hono';
4
+ import { MAX_FILE_SIZE, DEFAULT_ROOT } from '../config.js';
5
+ import { createHttpError, handleError, readJson } from '../utils/error.js';
6
+ import { resolveSafePath, normalizeWorkspacePath } from '../utils/path.js';
7
+ import { requireWorkspace } from './workspaces.js';
8
+ import { sortFileEntries } from '@deck-ide/shared/utils-node';
9
+ function mapFileEntry(entry, normalizedBase) {
10
+ const entryPath = normalizedBase ? `${normalizedBase}/${entry.name}` : entry.name;
11
+ return {
12
+ name: entry.name,
13
+ path: entryPath,
14
+ type: (entry.isDirectory() ? 'dir' : 'file')
15
+ };
16
+ }
17
+ export function createFileRouter(workspaces) {
18
+ const router = new Hono();
19
+ router.get('/files', async (c) => {
20
+ try {
21
+ const workspaceId = c.req.query('workspaceId');
22
+ if (!workspaceId) {
23
+ throw createHttpError('workspaceId is required', 400);
24
+ }
25
+ const workspace = requireWorkspace(workspaces, workspaceId);
26
+ const requestedPath = c.req.query('path') || '';
27
+ const target = await resolveSafePath(workspace.path, requestedPath);
28
+ const stats = await fs.stat(target);
29
+ if (!stats.isDirectory()) {
30
+ throw createHttpError('Path is not a directory', 400);
31
+ }
32
+ const entries = await fs.readdir(target, { withFileTypes: true });
33
+ const normalizedBase = requestedPath.replace(/\\/g, '/');
34
+ const mapped = entries.map((entry) => mapFileEntry(entry, normalizedBase));
35
+ const sorted = sortFileEntries(mapped);
36
+ return c.json(sorted);
37
+ }
38
+ catch (error) {
39
+ return handleError(c, error);
40
+ }
41
+ });
42
+ router.get('/preview', async (c) => {
43
+ try {
44
+ const rootInput = c.req.query('path') || DEFAULT_ROOT;
45
+ const requestedPath = c.req.query('subpath') || '';
46
+ const rootPath = normalizeWorkspacePath(rootInput);
47
+ // When workspaces exist, restrict browsing to registered workspace paths
48
+ if (workspaces.size > 0) {
49
+ const isAllowed = [...workspaces.values()].some(ws => rootPath === ws.path || rootPath.startsWith(ws.path + path.sep));
50
+ if (!isAllowed) {
51
+ throw createHttpError('Path outside registered workspaces', 403);
52
+ }
53
+ }
54
+ const target = await resolveSafePath(rootPath, requestedPath);
55
+ const stats = await fs.stat(target);
56
+ if (!stats.isDirectory()) {
57
+ throw createHttpError('Path is not a directory', 400);
58
+ }
59
+ const entries = await fs.readdir(target, { withFileTypes: true });
60
+ const normalizedBase = String(requestedPath || '').replace(/\\/g, '/');
61
+ const mapped = entries.map((entry) => mapFileEntry(entry, normalizedBase));
62
+ const sorted = sortFileEntries(mapped);
63
+ return c.json(sorted);
64
+ }
65
+ catch (error) {
66
+ return handleError(c, error);
67
+ }
68
+ });
69
+ router.get('/file', async (c) => {
70
+ try {
71
+ const workspaceId = c.req.query('workspaceId');
72
+ if (!workspaceId) {
73
+ throw createHttpError('workspaceId is required', 400);
74
+ }
75
+ const workspace = requireWorkspace(workspaces, workspaceId);
76
+ const target = await resolveSafePath(workspace.path, c.req.query('path') || '');
77
+ const stats = await fs.stat(target);
78
+ if (stats.size > MAX_FILE_SIZE) {
79
+ throw createHttpError(`File too large (${Math.round(stats.size / 1024 / 1024)}MB). Maximum size is ${Math.round(MAX_FILE_SIZE / 1024 / 1024)}MB.`, 413);
80
+ }
81
+ const contents = await fs.readFile(target, 'utf8');
82
+ return c.json({ path: c.req.query('path'), contents });
83
+ }
84
+ catch (error) {
85
+ return handleError(c, error);
86
+ }
87
+ });
88
+ router.put('/file', async (c) => {
89
+ try {
90
+ const body = await readJson(c);
91
+ const workspaceId = body?.workspaceId;
92
+ if (!workspaceId) {
93
+ throw createHttpError('workspaceId is required', 400);
94
+ }
95
+ const workspace = requireWorkspace(workspaces, workspaceId);
96
+ const target = await resolveSafePath(workspace.path, body?.path || '');
97
+ const contents = body?.contents ?? '';
98
+ const contentSize = Buffer.byteLength(contents, 'utf8');
99
+ if (contentSize > MAX_FILE_SIZE) {
100
+ throw createHttpError(`Content size exceeds maximum allowed size of ${Math.round(MAX_FILE_SIZE / 1024 / 1024)}MB`, 413);
101
+ }
102
+ await fs.mkdir(path.dirname(target), { recursive: true });
103
+ await fs.writeFile(target, contents, 'utf8');
104
+ return c.json({ path: body?.path, saved: true });
105
+ }
106
+ catch (error) {
107
+ return handleError(c, error);
108
+ }
109
+ });
110
+ // Create new file
111
+ router.post('/file', async (c) => {
112
+ try {
113
+ const body = await readJson(c);
114
+ const workspaceId = body?.workspaceId;
115
+ if (!workspaceId) {
116
+ throw createHttpError('workspaceId is required', 400);
117
+ }
118
+ if (!body?.path) {
119
+ throw createHttpError('path is required', 400);
120
+ }
121
+ const workspace = requireWorkspace(workspaces, workspaceId);
122
+ const target = await resolveSafePath(workspace.path, body.path);
123
+ // Check if file already exists
124
+ try {
125
+ await fs.access(target);
126
+ throw createHttpError('File already exists', 409);
127
+ }
128
+ catch (err) {
129
+ if (err.code !== 'ENOENT') {
130
+ throw err;
131
+ }
132
+ }
133
+ const contents = body?.contents ?? '';
134
+ await fs.mkdir(path.dirname(target), { recursive: true });
135
+ await fs.writeFile(target, contents, 'utf8');
136
+ return c.json({ path: body.path, created: true });
137
+ }
138
+ catch (error) {
139
+ return handleError(c, error);
140
+ }
141
+ });
142
+ // Delete file
143
+ router.delete('/file', async (c) => {
144
+ try {
145
+ const workspaceId = c.req.query('workspaceId');
146
+ if (!workspaceId) {
147
+ throw createHttpError('workspaceId is required', 400);
148
+ }
149
+ const filePath = c.req.query('path');
150
+ if (!filePath) {
151
+ throw createHttpError('path is required', 400);
152
+ }
153
+ const workspace = requireWorkspace(workspaces, workspaceId);
154
+ const target = await resolveSafePath(workspace.path, filePath);
155
+ const stats = await fs.stat(target);
156
+ if (stats.isDirectory()) {
157
+ throw createHttpError('Path is a directory, use DELETE /dir instead', 400);
158
+ }
159
+ await fs.unlink(target);
160
+ return c.json({ path: filePath, deleted: true });
161
+ }
162
+ catch (error) {
163
+ return handleError(c, error);
164
+ }
165
+ });
166
+ // Create directory
167
+ router.post('/dir', async (c) => {
168
+ try {
169
+ const body = await readJson(c);
170
+ const workspaceId = body?.workspaceId;
171
+ if (!workspaceId) {
172
+ throw createHttpError('workspaceId is required', 400);
173
+ }
174
+ if (!body?.path) {
175
+ throw createHttpError('path is required', 400);
176
+ }
177
+ const workspace = requireWorkspace(workspaces, workspaceId);
178
+ const target = await resolveSafePath(workspace.path, body.path);
179
+ // Check if already exists
180
+ try {
181
+ await fs.access(target);
182
+ throw createHttpError('Directory already exists', 409);
183
+ }
184
+ catch (err) {
185
+ if (err.code !== 'ENOENT') {
186
+ throw err;
187
+ }
188
+ }
189
+ await fs.mkdir(target, { recursive: true });
190
+ return c.json({ path: body.path, created: true });
191
+ }
192
+ catch (error) {
193
+ return handleError(c, error);
194
+ }
195
+ });
196
+ // Delete directory
197
+ router.delete('/dir', async (c) => {
198
+ try {
199
+ const workspaceId = c.req.query('workspaceId');
200
+ if (!workspaceId) {
201
+ throw createHttpError('workspaceId is required', 400);
202
+ }
203
+ const dirPath = c.req.query('path');
204
+ if (!dirPath) {
205
+ throw createHttpError('path is required', 400);
206
+ }
207
+ const workspace = requireWorkspace(workspaces, workspaceId);
208
+ const target = await resolveSafePath(workspace.path, dirPath);
209
+ const stats = await fs.stat(target);
210
+ if (!stats.isDirectory()) {
211
+ throw createHttpError('Path is not a directory', 400);
212
+ }
213
+ await fs.rm(target, { recursive: true });
214
+ return c.json({ path: dirPath, deleted: true });
215
+ }
216
+ catch (error) {
217
+ return handleError(c, error);
218
+ }
219
+ });
220
+ return router;
221
+ }