@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,201 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import {
3
+ getGitStatus,
4
+ getGitLog,
5
+ getGitBranches,
6
+ getGitDiff,
7
+ gitStage,
8
+ gitUnstage,
9
+ gitCommit,
10
+ gitPush,
11
+ gitPull,
12
+ gitCheckout,
13
+ gitCreateBranch,
14
+ gitDiscard,
15
+ gitInit,
16
+ getRemotes,
17
+ } from '@/lib/git';
18
+
19
+ // GET - Get git status, log, branches, or diff
20
+ export async function GET(request: NextRequest) {
21
+ const { searchParams } = new URL(request.url);
22
+ const action = searchParams.get('action') || 'status';
23
+ const projectPath = searchParams.get('projectPath');
24
+ const file = searchParams.get('file');
25
+ const staged = searchParams.get('staged') === 'true';
26
+
27
+ if (!projectPath) {
28
+ return NextResponse.json(
29
+ { error: 'projectPath is required' },
30
+ { status: 400 }
31
+ );
32
+ }
33
+
34
+ try {
35
+ switch (action) {
36
+ case 'status': {
37
+ const status = await getGitStatus(projectPath);
38
+ return NextResponse.json(status);
39
+ }
40
+
41
+ case 'log': {
42
+ const maxCount = parseInt(searchParams.get('maxCount') || '50', 10);
43
+ const log = await getGitLog(projectPath, maxCount);
44
+ return NextResponse.json({ commits: log });
45
+ }
46
+
47
+ case 'branches': {
48
+ const branches = await getGitBranches(projectPath);
49
+ return NextResponse.json({ branches });
50
+ }
51
+
52
+ case 'diff': {
53
+ const diff = await getGitDiff(projectPath, file || undefined, staged);
54
+ return NextResponse.json({ diff });
55
+ }
56
+
57
+ case 'remotes': {
58
+ const remotes = await getRemotes(projectPath);
59
+ return NextResponse.json({ remotes });
60
+ }
61
+
62
+ default:
63
+ return NextResponse.json(
64
+ { error: 'Invalid action' },
65
+ { status: 400 }
66
+ );
67
+ }
68
+ } catch (error) {
69
+ console.error('Git API error:', error);
70
+ return NextResponse.json(
71
+ { error: 'Git operation failed' },
72
+ { status: 500 }
73
+ );
74
+ }
75
+ }
76
+
77
+ // POST - Perform git operations
78
+ export async function POST(request: NextRequest) {
79
+ try {
80
+ const body = await request.json();
81
+ const { action, projectPath, files, message, remote, branch } = body;
82
+
83
+ if (!projectPath) {
84
+ return NextResponse.json(
85
+ { error: 'projectPath is required' },
86
+ { status: 400 }
87
+ );
88
+ }
89
+
90
+ switch (action) {
91
+ case 'stage': {
92
+ if (!files || !Array.isArray(files) || files.length === 0) {
93
+ return NextResponse.json(
94
+ { error: 'files array is required' },
95
+ { status: 400 }
96
+ );
97
+ }
98
+ const success = await gitStage(projectPath, files);
99
+ return NextResponse.json({ success });
100
+ }
101
+
102
+ case 'unstage': {
103
+ if (!files || !Array.isArray(files) || files.length === 0) {
104
+ return NextResponse.json(
105
+ { error: 'files array is required' },
106
+ { status: 400 }
107
+ );
108
+ }
109
+ const success = await gitUnstage(projectPath, files);
110
+ return NextResponse.json({ success });
111
+ }
112
+
113
+ case 'commit': {
114
+ if (!message) {
115
+ return NextResponse.json(
116
+ { error: 'message is required' },
117
+ { status: 400 }
118
+ );
119
+ }
120
+ const result = await gitCommit(projectPath, message);
121
+ return NextResponse.json(result);
122
+ }
123
+
124
+ case 'push': {
125
+ const result = await gitPush(projectPath, remote || 'origin', branch);
126
+ return NextResponse.json(result);
127
+ }
128
+
129
+ case 'pull': {
130
+ const result = await gitPull(projectPath, remote || 'origin', branch);
131
+ return NextResponse.json(result);
132
+ }
133
+
134
+ case 'checkout': {
135
+ if (!branch) {
136
+ return NextResponse.json(
137
+ { error: 'branch is required' },
138
+ { status: 400 }
139
+ );
140
+ }
141
+ const result = await gitCheckout(projectPath, branch);
142
+ return NextResponse.json(result);
143
+ }
144
+
145
+ case 'createBranch': {
146
+ if (!branch) {
147
+ return NextResponse.json(
148
+ { error: 'branch is required' },
149
+ { status: 400 }
150
+ );
151
+ }
152
+ const checkout = body.checkout !== false;
153
+ const result = await gitCreateBranch(projectPath, branch, checkout);
154
+ return NextResponse.json(result);
155
+ }
156
+
157
+ case 'discard': {
158
+ if (!files || !Array.isArray(files) || files.length === 0) {
159
+ return NextResponse.json(
160
+ { error: 'files array is required' },
161
+ { status: 400 }
162
+ );
163
+ }
164
+ const success = await gitDiscard(projectPath, files);
165
+ return NextResponse.json({ success });
166
+ }
167
+
168
+ case 'init': {
169
+ const result = await gitInit(projectPath);
170
+ return NextResponse.json(result);
171
+ }
172
+
173
+ case 'stageAll': {
174
+ const success = await gitStage(projectPath, ['.']);
175
+ return NextResponse.json({ success });
176
+ }
177
+
178
+ case 'unstageAll': {
179
+ const status = await getGitStatus(projectPath);
180
+ const stagedFiles = status.staged.map(f => f.path);
181
+ if (stagedFiles.length > 0) {
182
+ const success = await gitUnstage(projectPath, stagedFiles);
183
+ return NextResponse.json({ success });
184
+ }
185
+ return NextResponse.json({ success: true });
186
+ }
187
+
188
+ default:
189
+ return NextResponse.json(
190
+ { error: 'Invalid action' },
191
+ { status: 400 }
192
+ );
193
+ }
194
+ } catch (error) {
195
+ console.error('Git API error:', error);
196
+ return NextResponse.json(
197
+ { error: 'Git operation failed' },
198
+ { status: 500 }
199
+ );
200
+ }
201
+ }
@@ -0,0 +1,94 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { exec } from 'child_process';
3
+ import { promisify } from 'util';
4
+ import fs from 'fs/promises';
5
+ import path from 'path';
6
+
7
+ const execAsync = promisify(exec);
8
+
9
+ interface HealthStatus {
10
+ claudeCli: {
11
+ installed: boolean;
12
+ authenticated: boolean;
13
+ version?: string;
14
+ error?: string;
15
+ };
16
+ project: {
17
+ valid: boolean;
18
+ hasDevflow: boolean;
19
+ hasClaudeProject: boolean;
20
+ };
21
+ system: {
22
+ platform: string;
23
+ nodeVersion: string;
24
+ };
25
+ }
26
+
27
+ /**
28
+ * GET /api/health?projectPath=/path
29
+ * Check system health status
30
+ */
31
+ export async function GET(req: NextRequest) {
32
+ const { searchParams } = new URL(req.url);
33
+ const projectPath = searchParams.get('projectPath');
34
+
35
+ const status: HealthStatus = {
36
+ claudeCli: {
37
+ installed: false,
38
+ authenticated: false,
39
+ },
40
+ project: {
41
+ valid: false,
42
+ hasDevflow: false,
43
+ hasClaudeProject: false,
44
+ },
45
+ system: {
46
+ platform: process.platform,
47
+ nodeVersion: process.version,
48
+ },
49
+ };
50
+
51
+ // Check Claude CLI (skip whoami to avoid slowness)
52
+ try {
53
+ const { stdout } = await execAsync('claude --version', { timeout: 5000 });
54
+ status.claudeCli.installed = true;
55
+ status.claudeCli.version = stdout.trim();
56
+ status.claudeCli.authenticated = true; // Assume authenticated if CLI is installed
57
+ } catch (error) {
58
+ status.claudeCli.error = 'Claude CLI not found. Install from https://claude.ai/cli';
59
+ }
60
+
61
+ // Check project if path provided
62
+ if (projectPath && !projectPath.includes('..')) {
63
+ try {
64
+ await fs.access(projectPath);
65
+ status.project.valid = true;
66
+
67
+ // Check .devflow in current path or parent directory
68
+ try {
69
+ await fs.access(path.join(projectPath, '.devflow'));
70
+ status.project.hasDevflow = true;
71
+ } catch {
72
+ // Try parent directory
73
+ try {
74
+ await fs.access(path.join(projectPath, '..', '.devflow'));
75
+ status.project.hasDevflow = true;
76
+ } catch {}
77
+ }
78
+
79
+ // Check .claude_project in current path or parent directory
80
+ try {
81
+ await fs.access(path.join(projectPath, '.claude_project'));
82
+ status.project.hasClaudeProject = true;
83
+ } catch {
84
+ // Try parent directory
85
+ try {
86
+ await fs.access(path.join(projectPath, '..', '.claude_project'));
87
+ status.project.hasClaudeProject = true;
88
+ } catch {}
89
+ }
90
+ } catch {}
91
+ }
92
+
93
+ return NextResponse.json(status);
94
+ }
@@ -0,0 +1,134 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+
5
+ interface ProjectInfo {
6
+ path: string;
7
+ name: string;
8
+ isValid: boolean;
9
+ hasDevflow: boolean;
10
+ hasClaudeProject: boolean;
11
+ stats: {
12
+ specs: number;
13
+ stories: number;
14
+ adrs: number;
15
+ agents: number;
16
+ };
17
+ }
18
+
19
+ /**
20
+ * POST /api/project/open
21
+ * Opens a DevFlow project and returns its metadata
22
+ */
23
+ export async function POST(req: NextRequest) {
24
+ try {
25
+ const { path: projectPath } = await req.json();
26
+
27
+ // Validate path
28
+ if (!projectPath || typeof projectPath !== 'string') {
29
+ return NextResponse.json(
30
+ { error: 'Project path is required' },
31
+ { status: 400 }
32
+ );
33
+ }
34
+
35
+ // Security: Check for path traversal
36
+ if (projectPath.includes('..')) {
37
+ return NextResponse.json(
38
+ { error: 'Invalid path' },
39
+ { status: 400 }
40
+ );
41
+ }
42
+
43
+ // Check if path exists
44
+ try {
45
+ await fs.access(projectPath);
46
+ } catch {
47
+ return NextResponse.json(
48
+ { error: 'Path does not exist' },
49
+ { status: 404 }
50
+ );
51
+ }
52
+
53
+ // Check if it's a directory
54
+ const stat = await fs.stat(projectPath);
55
+ if (!stat.isDirectory()) {
56
+ return NextResponse.json(
57
+ { error: 'Path is not a directory' },
58
+ { status: 400 }
59
+ );
60
+ }
61
+
62
+ // Check for DevFlow markers
63
+ let hasDevflow = false;
64
+ let hasClaudeProject = false;
65
+
66
+ try {
67
+ await fs.access(path.join(projectPath, '.devflow'));
68
+ hasDevflow = true;
69
+ } catch {}
70
+
71
+ try {
72
+ await fs.access(path.join(projectPath, '.claude_project'));
73
+ hasClaudeProject = true;
74
+ } catch {}
75
+
76
+ // Get stats
77
+ const stats = {
78
+ specs: 0,
79
+ stories: 0,
80
+ adrs: 0,
81
+ agents: 0,
82
+ };
83
+
84
+ // Count stories
85
+ try {
86
+ const storiesPath = path.join(projectPath, 'docs', 'planning', 'stories');
87
+ const stories = await fs.readdir(storiesPath);
88
+ stats.stories = stories.filter(f => f.endsWith('.md') && f !== '.gitkeep').length;
89
+ } catch {}
90
+
91
+ // Count ADRs
92
+ try {
93
+ const adrsPath = path.join(projectPath, 'docs', 'decisions');
94
+ const adrs = await fs.readdir(adrsPath);
95
+ stats.adrs = adrs.filter(f => f.endsWith('.md') && !f.startsWith('000-')).length;
96
+ } catch {}
97
+
98
+ // Count agents
99
+ try {
100
+ const agentsPath = path.join(projectPath, '.devflow', 'agents');
101
+ const agents = await fs.readdir(agentsPath);
102
+ stats.agents = agents.filter(f => f.endsWith('.md')).length;
103
+ } catch {}
104
+
105
+ // Count specs (planning/*.md)
106
+ try {
107
+ const planningPath = path.join(projectPath, 'docs', 'planning');
108
+ const files = await fs.readdir(planningPath);
109
+ stats.specs = files.filter(f => f.endsWith('.md')).length;
110
+ } catch {}
111
+
112
+ const projectName = path.basename(projectPath);
113
+
114
+ const project: ProjectInfo = {
115
+ path: projectPath,
116
+ name: projectName,
117
+ isValid: hasDevflow || hasClaudeProject,
118
+ hasDevflow,
119
+ hasClaudeProject,
120
+ stats,
121
+ };
122
+
123
+ return NextResponse.json({
124
+ success: true,
125
+ project,
126
+ });
127
+ } catch (error) {
128
+ console.error('Error opening project:', error);
129
+ return NextResponse.json(
130
+ { error: 'Failed to open project' },
131
+ { status: 500 }
132
+ );
133
+ }
134
+ }
@@ -0,0 +1,247 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { promises as fs } from 'fs';
3
+ import path from 'path';
4
+
5
+ interface SearchResult {
6
+ filePath: string;
7
+ relativePath: string;
8
+ fileName: string;
9
+ matches: {
10
+ line: number;
11
+ content: string;
12
+ matchStart: number;
13
+ matchEnd: number;
14
+ }[];
15
+ }
16
+
17
+ interface FileResult {
18
+ filePath: string;
19
+ relativePath: string;
20
+ fileName: string;
21
+ extension: string;
22
+ }
23
+
24
+ // Directories to ignore
25
+ const IGNORE_DIRS = [
26
+ 'node_modules',
27
+ '.git',
28
+ '.next',
29
+ 'dist',
30
+ 'build',
31
+ '.turbo',
32
+ 'coverage',
33
+ '__pycache__',
34
+ '.cache',
35
+ ];
36
+
37
+ // File extensions to search
38
+ const SEARCHABLE_EXTENSIONS = [
39
+ '.md',
40
+ '.txt',
41
+ '.ts',
42
+ '.tsx',
43
+ '.js',
44
+ '.jsx',
45
+ '.json',
46
+ '.yaml',
47
+ '.yml',
48
+ '.css',
49
+ '.scss',
50
+ '.html',
51
+ '.py',
52
+ '.go',
53
+ '.rs',
54
+ '.java',
55
+ '.c',
56
+ '.cpp',
57
+ '.h',
58
+ '.sh',
59
+ '.env',
60
+ '.toml',
61
+ ];
62
+
63
+ async function getAllFiles(
64
+ dir: string,
65
+ basePath: string,
66
+ files: FileResult[] = []
67
+ ): Promise<FileResult[]> {
68
+ try {
69
+ const entries = await fs.readdir(dir, { withFileTypes: true });
70
+
71
+ for (const entry of entries) {
72
+ const fullPath = path.join(dir, entry.name);
73
+ const relativePath = path.relative(basePath, fullPath);
74
+
75
+ if (entry.isDirectory()) {
76
+ if (!IGNORE_DIRS.includes(entry.name) && !entry.name.startsWith('.')) {
77
+ await getAllFiles(fullPath, basePath, files);
78
+ }
79
+ } else {
80
+ const ext = path.extname(entry.name).toLowerCase();
81
+ if (SEARCHABLE_EXTENSIONS.includes(ext) || entry.name.startsWith('.')) {
82
+ files.push({
83
+ filePath: fullPath,
84
+ relativePath,
85
+ fileName: entry.name,
86
+ extension: ext,
87
+ });
88
+ }
89
+ }
90
+ }
91
+ } catch {
92
+ // Ignore permission errors
93
+ }
94
+
95
+ return files;
96
+ }
97
+
98
+ async function searchInFile(
99
+ file: FileResult,
100
+ query: string,
101
+ caseSensitive: boolean,
102
+ isRegex: boolean
103
+ ): Promise<SearchResult | null> {
104
+ try {
105
+ const content = await fs.readFile(file.filePath, 'utf-8');
106
+ const lines = content.split('\n');
107
+ const matches: SearchResult['matches'] = [];
108
+
109
+ let searchPattern: RegExp;
110
+ try {
111
+ if (isRegex) {
112
+ searchPattern = new RegExp(query, caseSensitive ? 'g' : 'gi');
113
+ } else {
114
+ const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
115
+ searchPattern = new RegExp(escapedQuery, caseSensitive ? 'g' : 'gi');
116
+ }
117
+ } catch {
118
+ // Invalid regex, return null
119
+ return null;
120
+ }
121
+
122
+ for (let i = 0; i < lines.length; i++) {
123
+ const line = lines[i];
124
+ let match: RegExpExecArray | null;
125
+ searchPattern.lastIndex = 0;
126
+
127
+ while ((match = searchPattern.exec(line)) !== null) {
128
+ matches.push({
129
+ line: i + 1,
130
+ content: line.slice(0, 200), // Limit line length
131
+ matchStart: match.index,
132
+ matchEnd: match.index + match[0].length,
133
+ });
134
+
135
+ // Prevent infinite loop for zero-length matches
136
+ if (match[0].length === 0) break;
137
+ }
138
+ }
139
+
140
+ if (matches.length > 0) {
141
+ return {
142
+ filePath: file.filePath,
143
+ relativePath: file.relativePath,
144
+ fileName: file.fileName,
145
+ matches: matches.slice(0, 50), // Limit matches per file
146
+ };
147
+ }
148
+
149
+ return null;
150
+ } catch {
151
+ return null;
152
+ }
153
+ }
154
+
155
+ /**
156
+ * GET /api/search - Search for files or content
157
+ */
158
+ export async function GET(req: NextRequest) {
159
+ try {
160
+ const searchParams = req.nextUrl.searchParams;
161
+ const projectPath = searchParams.get('projectPath');
162
+ const query = searchParams.get('query');
163
+ const type = searchParams.get('type') || 'files'; // 'files' or 'content'
164
+ const caseSensitive = searchParams.get('caseSensitive') === 'true';
165
+ const isRegex = searchParams.get('regex') === 'true';
166
+ const limit = parseInt(searchParams.get('limit') || '100', 10);
167
+
168
+ if (!projectPath) {
169
+ return NextResponse.json(
170
+ { error: 'projectPath is required' },
171
+ { status: 400 }
172
+ );
173
+ }
174
+
175
+ // Security: Validate path
176
+ if (projectPath.includes('..')) {
177
+ return NextResponse.json({ error: 'Invalid path' }, { status: 400 });
178
+ }
179
+
180
+ // Get all files
181
+ const allFiles = await getAllFiles(projectPath, projectPath);
182
+
183
+ if (type === 'files') {
184
+ // Quick Open - search file names
185
+ let results = allFiles;
186
+
187
+ if (query && query.trim()) {
188
+ const lowerQuery = query.toLowerCase();
189
+ results = allFiles
190
+ .filter((file) => {
191
+ const lowerPath = file.relativePath.toLowerCase();
192
+ const lowerName = file.fileName.toLowerCase();
193
+ return lowerPath.includes(lowerQuery) || lowerName.includes(lowerQuery);
194
+ })
195
+ .sort((a, b) => {
196
+ // Prioritize exact filename matches
197
+ const aNameMatch = a.fileName.toLowerCase().includes(lowerQuery);
198
+ const bNameMatch = b.fileName.toLowerCase().includes(lowerQuery);
199
+ if (aNameMatch && !bNameMatch) return -1;
200
+ if (!aNameMatch && bNameMatch) return 1;
201
+
202
+ // Then by path length (shorter = better)
203
+ return a.relativePath.length - b.relativePath.length;
204
+ });
205
+ }
206
+
207
+ return NextResponse.json({
208
+ type: 'files',
209
+ results: results.slice(0, limit),
210
+ total: results.length,
211
+ });
212
+ } else if (type === 'content') {
213
+ // Global Search - search file contents
214
+ if (!query || !query.trim()) {
215
+ return NextResponse.json({
216
+ type: 'content',
217
+ results: [],
218
+ total: 0,
219
+ });
220
+ }
221
+
222
+ const searchPromises = allFiles.map((file) =>
223
+ searchInFile(file, query, caseSensitive, isRegex)
224
+ );
225
+
226
+ const searchResults = await Promise.all(searchPromises);
227
+ const validResults = searchResults.filter(
228
+ (result): result is SearchResult => result !== null
229
+ );
230
+
231
+ // Sort by number of matches
232
+ validResults.sort((a, b) => b.matches.length - a.matches.length);
233
+
234
+ return NextResponse.json({
235
+ type: 'content',
236
+ results: validResults.slice(0, limit),
237
+ total: validResults.length,
238
+ totalMatches: validResults.reduce((sum, r) => sum + r.matches.length, 0),
239
+ });
240
+ }
241
+
242
+ return NextResponse.json({ error: 'Invalid type' }, { status: 400 });
243
+ } catch (error) {
244
+ console.error('Search error:', error);
245
+ return NextResponse.json({ error: 'Search failed' }, { status: 500 });
246
+ }
247
+ }