@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.
- package/.claude/commands/agents/architect.md +1162 -0
- package/.claude/commands/agents/architect.meta.yaml +124 -0
- package/.claude/commands/agents/builder.md +1432 -0
- package/.claude/commands/agents/builder.meta.yaml +117 -0
- package/.claude/commands/agents/chronicler.md +633 -0
- package/.claude/commands/agents/chronicler.meta.yaml +217 -0
- package/.claude/commands/agents/guardian.md +456 -0
- package/.claude/commands/agents/guardian.meta.yaml +127 -0
- package/.claude/commands/agents/strategist.md +483 -0
- package/.claude/commands/agents/strategist.meta.yaml +158 -0
- package/.claude/commands/agents/system-designer.md +1137 -0
- package/.claude/commands/agents/system-designer.meta.yaml +156 -0
- package/.claude/commands/devflow-help.md +93 -0
- package/.claude/commands/devflow-status.md +60 -0
- package/.claude/commands/quick/create-adr.md +82 -0
- package/.claude/commands/quick/new-feature.md +57 -0
- package/.claude/commands/quick/security-check.md +54 -0
- package/.claude/commands/quick/system-design.md +58 -0
- package/.claude_project +52 -0
- package/.devflow/agents/architect.meta.yaml +122 -0
- package/.devflow/agents/builder.meta.yaml +116 -0
- package/.devflow/agents/chronicler.meta.yaml +222 -0
- package/.devflow/agents/guardian.meta.yaml +127 -0
- package/.devflow/agents/strategist.meta.yaml +158 -0
- package/.devflow/agents/system-designer.meta.yaml +265 -0
- package/.devflow/project.yaml +242 -0
- package/.gitignore-template +84 -0
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/bin/devflow.js +54 -0
- package/lib/autopilot.js +235 -0
- package/lib/autopilotConstants.js +213 -0
- package/lib/constants.js +95 -0
- package/lib/init.js +200 -0
- package/lib/update.js +181 -0
- package/lib/utils.js +157 -0
- package/lib/web.js +119 -0
- package/package.json +57 -0
- package/web/CHANGELOG.md +192 -0
- package/web/README.md +156 -0
- package/web/app/api/autopilot/execute/route.ts +102 -0
- package/web/app/api/autopilot/terminal-execute/route.ts +124 -0
- package/web/app/api/files/route.ts +280 -0
- package/web/app/api/files/tree/route.ts +160 -0
- package/web/app/api/git/route.ts +201 -0
- package/web/app/api/health/route.ts +94 -0
- package/web/app/api/project/open/route.ts +134 -0
- package/web/app/api/search/route.ts +247 -0
- package/web/app/api/specs/route.ts +405 -0
- package/web/app/api/terminal/route.ts +222 -0
- package/web/app/globals.css +160 -0
- package/web/app/ide/layout.tsx +43 -0
- package/web/app/ide/page.tsx +216 -0
- package/web/app/layout.tsx +34 -0
- package/web/app/page.tsx +303 -0
- package/web/components/agents/AgentIcons.tsx +281 -0
- package/web/components/autopilot/AutopilotConfigModal.tsx +245 -0
- package/web/components/autopilot/AutopilotPanel.tsx +299 -0
- package/web/components/dashboard/DashboardPanel.tsx +393 -0
- package/web/components/editor/Breadcrumbs.tsx +134 -0
- package/web/components/editor/EditorPanel.tsx +120 -0
- package/web/components/editor/EditorTabs.tsx +229 -0
- package/web/components/editor/MarkdownPreview.tsx +154 -0
- package/web/components/editor/MermaidDiagram.tsx +113 -0
- package/web/components/editor/MonacoEditor.tsx +177 -0
- package/web/components/editor/TabContextMenu.tsx +207 -0
- package/web/components/git/GitPanel.tsx +534 -0
- package/web/components/layout/Shell.tsx +15 -0
- package/web/components/layout/StatusBar.tsx +100 -0
- package/web/components/modals/CommandPalette.tsx +393 -0
- package/web/components/modals/GlobalSearch.tsx +348 -0
- package/web/components/modals/QuickOpen.tsx +241 -0
- package/web/components/modals/RecentFiles.tsx +208 -0
- package/web/components/projects/ProjectSelector.tsx +147 -0
- package/web/components/settings/SettingItem.tsx +150 -0
- package/web/components/settings/SettingsPanel.tsx +323 -0
- package/web/components/specs/SpecsPanel.tsx +1091 -0
- package/web/components/terminal/TerminalPanel.tsx +683 -0
- package/web/components/ui/ContextMenu.tsx +182 -0
- package/web/components/ui/LoadingSpinner.tsx +66 -0
- package/web/components/ui/ResizeHandle.tsx +110 -0
- package/web/components/ui/Skeleton.tsx +108 -0
- package/web/components/ui/SkipLinks.tsx +37 -0
- package/web/components/ui/Toaster.tsx +57 -0
- package/web/hooks/useFocusTrap.ts +141 -0
- package/web/hooks/useKeyboardShortcuts.ts +169 -0
- package/web/hooks/useListNavigation.ts +237 -0
- package/web/lib/autopilotConstants.ts +213 -0
- package/web/lib/constants/agents.ts +67 -0
- package/web/lib/git.ts +339 -0
- package/web/lib/ptyManager.ts +191 -0
- package/web/lib/specsParser.ts +299 -0
- package/web/lib/stores/autopilotStore.ts +288 -0
- package/web/lib/stores/fileStore.ts +550 -0
- package/web/lib/stores/gitStore.ts +386 -0
- package/web/lib/stores/projectStore.ts +196 -0
- package/web/lib/stores/settingsStore.ts +126 -0
- package/web/lib/stores/specsStore.ts +297 -0
- package/web/lib/stores/uiStore.ts +175 -0
- package/web/lib/types/index.ts +177 -0
- package/web/lib/utils.ts +98 -0
- package/web/next.config.js +50 -0
- package/web/package.json +54 -0
- package/web/postcss.config.js +6 -0
- package/web/tailwind.config.ts +68 -0
- 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
|
+
}
|