@evolve.labs/devflow 0.8.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 (106) hide show
  1. package/.claude/commands/agents/architect.md +1162 -0
  2. package/.claude/commands/agents/architect.meta.yaml +124 -0
  3. package/.claude/commands/agents/builder.md +1432 -0
  4. package/.claude/commands/agents/builder.meta.yaml +117 -0
  5. package/.claude/commands/agents/chronicler.md +633 -0
  6. package/.claude/commands/agents/chronicler.meta.yaml +217 -0
  7. package/.claude/commands/agents/guardian.md +456 -0
  8. package/.claude/commands/agents/guardian.meta.yaml +127 -0
  9. package/.claude/commands/agents/strategist.md +483 -0
  10. package/.claude/commands/agents/strategist.meta.yaml +158 -0
  11. package/.claude/commands/agents/system-designer.md +1137 -0
  12. package/.claude/commands/agents/system-designer.meta.yaml +156 -0
  13. package/.claude/commands/devflow-help.md +93 -0
  14. package/.claude/commands/devflow-status.md +60 -0
  15. package/.claude/commands/quick/create-adr.md +82 -0
  16. package/.claude/commands/quick/new-feature.md +57 -0
  17. package/.claude/commands/quick/security-check.md +54 -0
  18. package/.claude/commands/quick/system-design.md +58 -0
  19. package/.claude_project +52 -0
  20. package/.devflow/agents/architect.meta.yaml +122 -0
  21. package/.devflow/agents/builder.meta.yaml +116 -0
  22. package/.devflow/agents/chronicler.meta.yaml +222 -0
  23. package/.devflow/agents/guardian.meta.yaml +127 -0
  24. package/.devflow/agents/strategist.meta.yaml +158 -0
  25. package/.devflow/agents/system-designer.meta.yaml +265 -0
  26. package/.devflow/project.yaml +242 -0
  27. package/.gitignore-template +84 -0
  28. package/LICENSE +21 -0
  29. package/README.md +249 -0
  30. package/bin/devflow.js +54 -0
  31. package/lib/autopilot.js +235 -0
  32. package/lib/autopilotConstants.js +213 -0
  33. package/lib/constants.js +95 -0
  34. package/lib/init.js +200 -0
  35. package/lib/update.js +181 -0
  36. package/lib/utils.js +157 -0
  37. package/lib/web.js +119 -0
  38. package/package.json +57 -0
  39. package/web/CHANGELOG.md +192 -0
  40. package/web/README.md +156 -0
  41. package/web/app/api/autopilot/execute/route.ts +102 -0
  42. package/web/app/api/autopilot/terminal-execute/route.ts +124 -0
  43. package/web/app/api/files/route.ts +280 -0
  44. package/web/app/api/files/tree/route.ts +160 -0
  45. package/web/app/api/git/route.ts +201 -0
  46. package/web/app/api/health/route.ts +94 -0
  47. package/web/app/api/project/open/route.ts +134 -0
  48. package/web/app/api/search/route.ts +247 -0
  49. package/web/app/api/specs/route.ts +405 -0
  50. package/web/app/api/terminal/route.ts +222 -0
  51. package/web/app/globals.css +160 -0
  52. package/web/app/ide/layout.tsx +43 -0
  53. package/web/app/ide/page.tsx +216 -0
  54. package/web/app/layout.tsx +34 -0
  55. package/web/app/page.tsx +303 -0
  56. package/web/components/agents/AgentIcons.tsx +281 -0
  57. package/web/components/autopilot/AutopilotConfigModal.tsx +245 -0
  58. package/web/components/autopilot/AutopilotPanel.tsx +299 -0
  59. package/web/components/dashboard/DashboardPanel.tsx +393 -0
  60. package/web/components/editor/Breadcrumbs.tsx +134 -0
  61. package/web/components/editor/EditorPanel.tsx +120 -0
  62. package/web/components/editor/EditorTabs.tsx +229 -0
  63. package/web/components/editor/MarkdownPreview.tsx +154 -0
  64. package/web/components/editor/MermaidDiagram.tsx +113 -0
  65. package/web/components/editor/MonacoEditor.tsx +177 -0
  66. package/web/components/editor/TabContextMenu.tsx +207 -0
  67. package/web/components/git/GitPanel.tsx +534 -0
  68. package/web/components/layout/Shell.tsx +15 -0
  69. package/web/components/layout/StatusBar.tsx +100 -0
  70. package/web/components/modals/CommandPalette.tsx +393 -0
  71. package/web/components/modals/GlobalSearch.tsx +348 -0
  72. package/web/components/modals/QuickOpen.tsx +241 -0
  73. package/web/components/modals/RecentFiles.tsx +208 -0
  74. package/web/components/projects/ProjectSelector.tsx +147 -0
  75. package/web/components/settings/SettingItem.tsx +150 -0
  76. package/web/components/settings/SettingsPanel.tsx +323 -0
  77. package/web/components/specs/SpecsPanel.tsx +1091 -0
  78. package/web/components/terminal/TerminalPanel.tsx +683 -0
  79. package/web/components/ui/ContextMenu.tsx +182 -0
  80. package/web/components/ui/LoadingSpinner.tsx +66 -0
  81. package/web/components/ui/ResizeHandle.tsx +110 -0
  82. package/web/components/ui/Skeleton.tsx +108 -0
  83. package/web/components/ui/SkipLinks.tsx +37 -0
  84. package/web/components/ui/Toaster.tsx +57 -0
  85. package/web/hooks/useFocusTrap.ts +141 -0
  86. package/web/hooks/useKeyboardShortcuts.ts +169 -0
  87. package/web/hooks/useListNavigation.ts +237 -0
  88. package/web/lib/autopilotConstants.ts +213 -0
  89. package/web/lib/constants/agents.ts +67 -0
  90. package/web/lib/git.ts +339 -0
  91. package/web/lib/ptyManager.ts +191 -0
  92. package/web/lib/specsParser.ts +299 -0
  93. package/web/lib/stores/autopilotStore.ts +288 -0
  94. package/web/lib/stores/fileStore.ts +550 -0
  95. package/web/lib/stores/gitStore.ts +386 -0
  96. package/web/lib/stores/projectStore.ts +196 -0
  97. package/web/lib/stores/settingsStore.ts +126 -0
  98. package/web/lib/stores/specsStore.ts +297 -0
  99. package/web/lib/stores/uiStore.ts +175 -0
  100. package/web/lib/types/index.ts +177 -0
  101. package/web/lib/utils.ts +98 -0
  102. package/web/next.config.js +50 -0
  103. package/web/package.json +54 -0
  104. package/web/postcss.config.js +6 -0
  105. package/web/tailwind.config.ts +68 -0
  106. package/web/tsconfig.json +41 -0
@@ -0,0 +1,102 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { execSync } from 'child_process';
3
+ import path from 'node:path';
4
+ import {
5
+ type AgentName,
6
+ isValidAgent,
7
+ buildPrompt,
8
+ loadAgentDefinition,
9
+ AGENT_TIMEOUTS,
10
+ TASK_TRACKING_AGENTS,
11
+ autoUpdateSpecTasks,
12
+ } from '@/lib/autopilotConstants';
13
+
14
+ /**
15
+ * Autopilot Execute API - Executa uma única fase via Claude CLI --print (execSync fallback)
16
+ */
17
+
18
+ interface ExecuteRequest {
19
+ agent: string;
20
+ specContent: string;
21
+ specFilePath?: string;
22
+ previousOutputs: string[];
23
+ projectPath: string;
24
+ }
25
+
26
+ export async function POST(req: NextRequest) {
27
+ let body: ExecuteRequest;
28
+
29
+ try {
30
+ body = await req.json();
31
+ } catch {
32
+ return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
33
+ }
34
+
35
+ const { agent, specContent, specFilePath, previousOutputs, projectPath } = body;
36
+
37
+ if (!agent || !specContent || !projectPath) {
38
+ return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
39
+ }
40
+
41
+ if (!isValidAgent(agent)) {
42
+ return NextResponse.json({ error: `Unknown agent: ${agent}` }, { status: 400 });
43
+ }
44
+
45
+ // Validate projectPath - must be absolute and not escape via traversal
46
+ const resolved = path.resolve(projectPath);
47
+ if (resolved !== projectPath || projectPath.includes('..')) {
48
+ return NextResponse.json({ error: 'Invalid project path' }, { status: 400 });
49
+ }
50
+
51
+ // Load the full agent definition from the project's .claude/commands/agents/
52
+ const agentDefinition = await loadAgentDefinition(resolved, agent);
53
+ const fullPrompt = buildPrompt(agent, specContent, previousOutputs || [], agentDefinition);
54
+ const timeout = AGENT_TIMEOUTS[agent];
55
+
56
+ try {
57
+ const output = execSync('claude --print', {
58
+ cwd: resolved,
59
+ input: fullPrompt,
60
+ encoding: 'utf-8',
61
+ timeout: timeout * 1000,
62
+ maxBuffer: 2 * 1024 * 1024,
63
+ });
64
+
65
+ const trimmedOutput = output.trim();
66
+
67
+ // Auto-update spec tasks if this agent produces trackable output
68
+ let tasksCompleted: string[] = [];
69
+ if (
70
+ specFilePath &&
71
+ TASK_TRACKING_AGENTS.includes(agent) &&
72
+ trimmedOutput.length > 0
73
+ ) {
74
+ const resolvedSpec = path.resolve(specFilePath);
75
+ if (resolvedSpec === specFilePath && !specFilePath.includes('..')) {
76
+ tasksCompleted = await autoUpdateSpecTasks(resolvedSpec, trimmedOutput);
77
+ }
78
+ }
79
+
80
+ return NextResponse.json({
81
+ success: true,
82
+ output: trimmedOutput,
83
+ agent,
84
+ tasksCompleted,
85
+ });
86
+ } catch (error) {
87
+ console.error(`Autopilot ${agent} error:`, error);
88
+
89
+ let errorMessage = 'Execution failed';
90
+ if (error instanceof Error) {
91
+ if (error.message.includes('ETIMEDOUT') || error.message.includes('timeout')) {
92
+ errorMessage = `Timeout: ${agent} exceeded ${timeout}s`;
93
+ } else if (error.message.includes('ENOENT')) {
94
+ errorMessage = 'Claude CLI not found. Install with: npm i -g @anthropic-ai/claude-code';
95
+ } else {
96
+ errorMessage = error.message.slice(0, 500);
97
+ }
98
+ }
99
+
100
+ return NextResponse.json({ error: errorMessage }, { status: 500 });
101
+ }
102
+ }
@@ -0,0 +1,124 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { promises as fs } from 'fs';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { ptyManager } from '@/lib/ptyManager';
6
+ import {
7
+ isValidAgent,
8
+ buildPrompt,
9
+ loadAgentDefinition,
10
+ AGENT_TIMEOUTS,
11
+ TASK_TRACKING_AGENTS,
12
+ PHASE_DONE_MARKER,
13
+ autoUpdateSpecTasks,
14
+ } from '@/lib/autopilotConstants';
15
+
16
+ /**
17
+ * Autopilot Terminal Execute API
18
+ * Runs an agent phase through the terminal PTY (streaming output visible in xterm.js).
19
+ */
20
+
21
+ interface TerminalExecuteRequest {
22
+ sessionId: string;
23
+ agent: string;
24
+ specContent: string;
25
+ specFilePath?: string;
26
+ previousOutputs: string[];
27
+ projectPath: string;
28
+ }
29
+
30
+ export async function POST(req: NextRequest) {
31
+ let body: TerminalExecuteRequest;
32
+
33
+ try {
34
+ body = await req.json();
35
+ } catch {
36
+ return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
37
+ }
38
+
39
+ const { sessionId, agent, specContent, specFilePath, previousOutputs, projectPath } = body;
40
+
41
+ if (!sessionId || !agent || !specContent || !projectPath) {
42
+ return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
43
+ }
44
+
45
+ if (!isValidAgent(agent)) {
46
+ return NextResponse.json({ error: `Unknown agent: ${agent}` }, { status: 400 });
47
+ }
48
+
49
+ // Validate projectPath
50
+ const resolved = path.resolve(projectPath);
51
+ if (resolved !== projectPath || projectPath.includes('..')) {
52
+ return NextResponse.json({ error: 'Invalid project path' }, { status: 400 });
53
+ }
54
+
55
+ // Verify terminal session exists
56
+ const session = ptyManager.getSession(sessionId);
57
+ if (!session) {
58
+ return NextResponse.json({ error: 'Terminal session not found' }, { status: 404 });
59
+ }
60
+
61
+ // Load full agent definition from the project's .claude/commands/agents/
62
+ const agentDefinition = await loadAgentDefinition(resolved, agent);
63
+ const fullPrompt = buildPrompt(agent, specContent, previousOutputs || [], agentDefinition);
64
+ const timeoutSec = AGENT_TIMEOUTS[agent];
65
+ const timeoutMs = timeoutSec * 1000;
66
+
67
+ // Write prompt to temp file (avoid shell escaping issues)
68
+ const tmpDir = os.tmpdir();
69
+ const tmpFile = path.join(tmpDir, `devflow-autopilot-${sessionId}-${agent}-${Date.now()}.md`);
70
+
71
+ try {
72
+ await fs.writeFile(tmpFile, fullPrompt, 'utf-8');
73
+
74
+ // Arm the collector before writing the command
75
+ const collectorPromise = ptyManager.armAutopilotCollector(sessionId, timeoutMs + 5000);
76
+
77
+ // Write the command to the PTY
78
+ // Uses: claude --print < tmpfile ; echo marker
79
+ // The marker includes $? to capture the exit code of claude
80
+ const command = `cd ${JSON.stringify(resolved)} && claude --print < ${JSON.stringify(tmpFile)}; echo "${PHASE_DONE_MARKER}$?___"\n`;
81
+ ptyManager.write(sessionId, command);
82
+
83
+ // Wait for the collector to detect the marker
84
+ const result = await collectorPromise;
85
+
86
+ // Clean up temp file
87
+ await fs.unlink(tmpFile).catch(() => {});
88
+
89
+ const trimmedOutput = result.output.trim();
90
+
91
+ // Auto-update spec tasks
92
+ let tasksCompleted: string[] = [];
93
+ if (
94
+ specFilePath &&
95
+ TASK_TRACKING_AGENTS.includes(agent) &&
96
+ trimmedOutput.length > 0
97
+ ) {
98
+ const resolvedSpec = path.resolve(specFilePath);
99
+ if (resolvedSpec === specFilePath && !specFilePath.includes('..')) {
100
+ tasksCompleted = await autoUpdateSpecTasks(resolvedSpec, trimmedOutput);
101
+ }
102
+ }
103
+
104
+ return NextResponse.json({
105
+ success: result.exitCode === 0,
106
+ output: trimmedOutput,
107
+ exitCode: result.exitCode,
108
+ agent,
109
+ tasksCompleted,
110
+ error: result.exitCode !== 0 ? `Agent ${agent} exited with code ${result.exitCode}` : undefined,
111
+ });
112
+ } catch (error) {
113
+ // Clean up temp file on error
114
+ await fs.unlink(tmpFile).catch(() => {});
115
+
116
+ // Disarm collector if still active
117
+ ptyManager.disarmAutopilotCollector(sessionId);
118
+
119
+ console.error(`Autopilot terminal-execute ${agent} error:`, error);
120
+
121
+ const errorMessage = error instanceof Error ? error.message : 'Execution failed';
122
+ return NextResponse.json({ error: errorMessage, success: false }, { status: 500 });
123
+ }
124
+ }
@@ -0,0 +1,280 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+
5
+ /**
6
+ * GET /api/files?path=/path/to/file
7
+ * Read file content
8
+ */
9
+ export async function GET(req: NextRequest) {
10
+ try {
11
+ const { searchParams } = new URL(req.url);
12
+ const filePath = searchParams.get('path');
13
+
14
+ if (!filePath) {
15
+ return NextResponse.json(
16
+ { error: 'File path is required' },
17
+ { status: 400 }
18
+ );
19
+ }
20
+
21
+ // Security check
22
+ if (filePath.includes('..')) {
23
+ return NextResponse.json(
24
+ { error: 'Invalid path' },
25
+ { status: 400 }
26
+ );
27
+ }
28
+
29
+ try {
30
+ const stat = await fs.stat(filePath);
31
+
32
+ if (!stat.isFile()) {
33
+ return NextResponse.json(
34
+ { error: 'Path is not a file' },
35
+ { status: 400 }
36
+ );
37
+ }
38
+
39
+ // Check file size (limit to 5MB)
40
+ if (stat.size > 5 * 1024 * 1024) {
41
+ return NextResponse.json(
42
+ { error: 'File too large (max 5MB)' },
43
+ { status: 400 }
44
+ );
45
+ }
46
+
47
+ const content = await fs.readFile(filePath, 'utf-8');
48
+
49
+ return NextResponse.json({
50
+ path: filePath,
51
+ content,
52
+ size: stat.size,
53
+ modifiedAt: stat.mtime.toISOString(),
54
+ });
55
+ } catch (error) {
56
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
57
+ return NextResponse.json(
58
+ { error: 'File not found' },
59
+ { status: 404 }
60
+ );
61
+ }
62
+ throw error;
63
+ }
64
+ } catch (error) {
65
+ console.error('Error reading file:', error);
66
+ return NextResponse.json(
67
+ { error: 'Failed to read file' },
68
+ { status: 500 }
69
+ );
70
+ }
71
+ }
72
+
73
+ /**
74
+ * PUT /api/files
75
+ * Write file content
76
+ */
77
+ export async function PUT(req: NextRequest) {
78
+ try {
79
+ const { path: filePath, content } = await req.json();
80
+
81
+ if (!filePath || typeof content !== 'string') {
82
+ return NextResponse.json(
83
+ { error: 'Path and content are required' },
84
+ { status: 400 }
85
+ );
86
+ }
87
+
88
+ // Security check
89
+ if (filePath.includes('..')) {
90
+ return NextResponse.json(
91
+ { error: 'Invalid path' },
92
+ { status: 400 }
93
+ );
94
+ }
95
+
96
+ await fs.writeFile(filePath, content, 'utf-8');
97
+
98
+ const stat = await fs.stat(filePath);
99
+
100
+ return NextResponse.json({
101
+ success: true,
102
+ path: filePath,
103
+ modifiedAt: stat.mtime.toISOString(),
104
+ });
105
+ } catch (error) {
106
+ console.error('Error writing file:', error);
107
+ return NextResponse.json(
108
+ { error: 'Failed to write file' },
109
+ { status: 500 }
110
+ );
111
+ }
112
+ }
113
+
114
+ /**
115
+ * POST /api/files
116
+ * Create new file or directory
117
+ */
118
+ export async function POST(req: NextRequest) {
119
+ try {
120
+ const { path: filePath, type, content = '' } = await req.json();
121
+
122
+ if (!filePath || !type) {
123
+ return NextResponse.json(
124
+ { error: 'Path and type are required' },
125
+ { status: 400 }
126
+ );
127
+ }
128
+
129
+ // Security check
130
+ if (filePath.includes('..')) {
131
+ return NextResponse.json(
132
+ { error: 'Invalid path' },
133
+ { status: 400 }
134
+ );
135
+ }
136
+
137
+ // Check if already exists
138
+ try {
139
+ await fs.access(filePath);
140
+ return NextResponse.json(
141
+ { error: 'Path already exists' },
142
+ { status: 409 }
143
+ );
144
+ } catch {
145
+ // Path doesn't exist, good to create
146
+ }
147
+
148
+ if (type === 'directory') {
149
+ await fs.mkdir(filePath, { recursive: true });
150
+ } else {
151
+ // Ensure parent directory exists
152
+ const parentDir = path.dirname(filePath);
153
+ await fs.mkdir(parentDir, { recursive: true });
154
+ await fs.writeFile(filePath, content, 'utf-8');
155
+ }
156
+
157
+ return NextResponse.json({
158
+ success: true,
159
+ path: filePath,
160
+ type,
161
+ });
162
+ } catch (error) {
163
+ console.error('Error creating file:', error);
164
+ return NextResponse.json(
165
+ { error: 'Failed to create file' },
166
+ { status: 500 }
167
+ );
168
+ }
169
+ }
170
+
171
+ /**
172
+ * PATCH /api/files
173
+ * Rename file or directory
174
+ */
175
+ export async function PATCH(req: NextRequest) {
176
+ try {
177
+ const { oldPath, newPath } = await req.json();
178
+
179
+ if (!oldPath || !newPath) {
180
+ return NextResponse.json(
181
+ { error: 'Old path and new path are required' },
182
+ { status: 400 }
183
+ );
184
+ }
185
+
186
+ // Security check
187
+ if (oldPath.includes('..') || newPath.includes('..')) {
188
+ return NextResponse.json(
189
+ { error: 'Invalid path' },
190
+ { status: 400 }
191
+ );
192
+ }
193
+
194
+ // Check if old path exists
195
+ try {
196
+ await fs.access(oldPath);
197
+ } catch {
198
+ return NextResponse.json(
199
+ { error: 'Source path not found' },
200
+ { status: 404 }
201
+ );
202
+ }
203
+
204
+ // Check if new path already exists
205
+ try {
206
+ await fs.access(newPath);
207
+ return NextResponse.json(
208
+ { error: 'Destination path already exists' },
209
+ { status: 409 }
210
+ );
211
+ } catch {
212
+ // Path doesn't exist, good to rename
213
+ }
214
+
215
+ await fs.rename(oldPath, newPath);
216
+
217
+ return NextResponse.json({
218
+ success: true,
219
+ oldPath,
220
+ newPath,
221
+ });
222
+ } catch (error) {
223
+ console.error('Error renaming file:', error);
224
+ return NextResponse.json(
225
+ { error: 'Failed to rename file' },
226
+ { status: 500 }
227
+ );
228
+ }
229
+ }
230
+
231
+ /**
232
+ * DELETE /api/files
233
+ * Delete file or directory
234
+ */
235
+ export async function DELETE(req: NextRequest) {
236
+ try {
237
+ const { path: filePath } = await req.json();
238
+
239
+ if (!filePath) {
240
+ return NextResponse.json(
241
+ { error: 'Path is required' },
242
+ { status: 400 }
243
+ );
244
+ }
245
+
246
+ // Security check
247
+ if (filePath.includes('..')) {
248
+ return NextResponse.json(
249
+ { error: 'Invalid path' },
250
+ { status: 400 }
251
+ );
252
+ }
253
+
254
+ const stat = await fs.stat(filePath);
255
+
256
+ if (stat.isDirectory()) {
257
+ await fs.rm(filePath, { recursive: true });
258
+ } else {
259
+ await fs.unlink(filePath);
260
+ }
261
+
262
+ return NextResponse.json({
263
+ success: true,
264
+ path: filePath,
265
+ });
266
+ } catch (error) {
267
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
268
+ return NextResponse.json(
269
+ { error: 'Path not found' },
270
+ { status: 404 }
271
+ );
272
+ }
273
+
274
+ console.error('Error deleting file:', error);
275
+ return NextResponse.json(
276
+ { error: 'Failed to delete file' },
277
+ { status: 500 }
278
+ );
279
+ }
280
+ }
@@ -0,0 +1,160 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+
5
+ interface FileNode {
6
+ name: string;
7
+ path: string;
8
+ type: 'file' | 'directory';
9
+ extension?: string;
10
+ children?: FileNode[];
11
+ }
12
+
13
+ // Folders to prioritize (shown first)
14
+ const PRIORITY_FOLDERS = ['.devflow', 'docs'];
15
+
16
+ // Files/folders to exclude
17
+ const EXCLUDED = [
18
+ 'node_modules',
19
+ '.git',
20
+ '.DS_Store',
21
+ '.next',
22
+ 'dist',
23
+ 'build',
24
+ '__pycache__',
25
+ '.pytest_cache',
26
+ '.venv',
27
+ 'venv',
28
+ ];
29
+
30
+ /**
31
+ * GET /api/files/tree?path=/project/path
32
+ * Returns file tree for a project
33
+ */
34
+ export async function GET(req: NextRequest) {
35
+ try {
36
+ const { searchParams } = new URL(req.url);
37
+ const projectPath = searchParams.get('path');
38
+
39
+ if (!projectPath) {
40
+ return NextResponse.json(
41
+ { error: 'Project path is required' },
42
+ { status: 400 }
43
+ );
44
+ }
45
+
46
+ // Security check
47
+ if (projectPath.includes('..')) {
48
+ return NextResponse.json(
49
+ { error: 'Invalid path' },
50
+ { status: 400 }
51
+ );
52
+ }
53
+
54
+ const root = await buildFileTree(projectPath, projectPath);
55
+
56
+ return NextResponse.json({ root });
57
+ } catch (error) {
58
+ console.error('Error building file tree:', error);
59
+ return NextResponse.json(
60
+ { error: 'Failed to build file tree' },
61
+ { status: 500 }
62
+ );
63
+ }
64
+ }
65
+
66
+ async function buildFileTree(
67
+ basePath: string,
68
+ currentPath: string,
69
+ depth: number = 0
70
+ ): Promise<FileNode> {
71
+ const stat = await fs.stat(currentPath);
72
+ const name = path.basename(currentPath);
73
+ const relativePath = path.relative(basePath, currentPath) || '.';
74
+
75
+ if (stat.isFile()) {
76
+ const ext = path.extname(name).slice(1);
77
+ return {
78
+ name,
79
+ path: currentPath,
80
+ type: 'file',
81
+ extension: ext || undefined,
82
+ };
83
+ }
84
+
85
+ // Directory
86
+ let children: FileNode[] = [];
87
+
88
+ // Limit depth to prevent huge trees
89
+ if (depth < 10) {
90
+ try {
91
+ const entries = await fs.readdir(currentPath);
92
+
93
+ // Filter and sort entries
94
+ const filteredEntries = entries.filter(
95
+ (entry) => !EXCLUDED.includes(entry) && !entry.startsWith('.')
96
+ );
97
+
98
+ // Include priority hidden folders
99
+ const priorityHidden = entries.filter(
100
+ (entry) => PRIORITY_FOLDERS.includes(entry)
101
+ );
102
+
103
+ const allEntries = [...priorityHidden, ...filteredEntries];
104
+ const uniqueEntries = [...new Set(allEntries)];
105
+
106
+ // Sort: directories first, then priority folders, then alphabetically
107
+ const sorted = await sortEntries(currentPath, uniqueEntries);
108
+
109
+ children = await Promise.all(
110
+ sorted.map((entry) =>
111
+ buildFileTree(basePath, path.join(currentPath, entry), depth + 1)
112
+ )
113
+ );
114
+ } catch (error) {
115
+ console.error(`Error reading directory ${currentPath}:`, error);
116
+ }
117
+ }
118
+
119
+ return {
120
+ name,
121
+ path: currentPath,
122
+ type: 'directory',
123
+ children,
124
+ };
125
+ }
126
+
127
+ async function sortEntries(
128
+ dirPath: string,
129
+ entries: string[]
130
+ ): Promise<string[]> {
131
+ const withTypes = await Promise.all(
132
+ entries.map(async (entry) => {
133
+ try {
134
+ const stat = await fs.stat(path.join(dirPath, entry));
135
+ return {
136
+ name: entry,
137
+ isDirectory: stat.isDirectory(),
138
+ isPriority: PRIORITY_FOLDERS.includes(entry),
139
+ };
140
+ } catch {
141
+ return { name: entry, isDirectory: false, isPriority: false };
142
+ }
143
+ })
144
+ );
145
+
146
+ return withTypes
147
+ .sort((a, b) => {
148
+ // Priority folders first
149
+ if (a.isPriority && !b.isPriority) return -1;
150
+ if (!a.isPriority && b.isPriority) return 1;
151
+
152
+ // Directories before files
153
+ if (a.isDirectory && !b.isDirectory) return -1;
154
+ if (!a.isDirectory && b.isDirectory) return 1;
155
+
156
+ // Alphabetically
157
+ return a.name.localeCompare(b.name);
158
+ })
159
+ .map((e) => e.name);
160
+ }