@burdenoff/vibe-agent 1.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.
- package/.env.example +8 -0
- package/LICENSE +22 -0
- package/README.md +290 -0
- package/dist/app.d.ts +15 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +445 -0
- package/dist/app.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1043 -0
- package/dist/cli.js.map +1 -0
- package/dist/db/schema.d.ts +145 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +536 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +61 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/ModuleAuth.d.ts +61 -0
- package/dist/middleware/ModuleAuth.d.ts.map +1 -0
- package/dist/middleware/ModuleAuth.js +220 -0
- package/dist/middleware/ModuleAuth.js.map +1 -0
- package/dist/middleware/auth.d.ts +3 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +11 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/migrations/remove-notes-prompts.d.ts +13 -0
- package/dist/migrations/remove-notes-prompts.d.ts.map +1 -0
- package/dist/migrations/remove-notes-prompts.js +148 -0
- package/dist/migrations/remove-notes-prompts.js.map +1 -0
- package/dist/routes/bookmarks.d.ts +3 -0
- package/dist/routes/bookmarks.d.ts.map +1 -0
- package/dist/routes/bookmarks.js +186 -0
- package/dist/routes/bookmarks.js.map +1 -0
- package/dist/routes/config.d.ts +3 -0
- package/dist/routes/config.d.ts.map +1 -0
- package/dist/routes/config.js +108 -0
- package/dist/routes/config.js.map +1 -0
- package/dist/routes/files.d.ts +3 -0
- package/dist/routes/files.d.ts.map +1 -0
- package/dist/routes/files.js +471 -0
- package/dist/routes/files.js.map +1 -0
- package/dist/routes/git.d.ts +3 -0
- package/dist/routes/git.d.ts.map +1 -0
- package/dist/routes/git.js +498 -0
- package/dist/routes/git.js.map +1 -0
- package/dist/routes/moduleRegistry.d.ts +41 -0
- package/dist/routes/moduleRegistry.d.ts.map +1 -0
- package/dist/routes/moduleRegistry.js +356 -0
- package/dist/routes/moduleRegistry.js.map +1 -0
- package/dist/routes/notifications.d.ts +3 -0
- package/dist/routes/notifications.d.ts.map +1 -0
- package/dist/routes/notifications.js +250 -0
- package/dist/routes/notifications.js.map +1 -0
- package/dist/routes/port-forward.d.ts +3 -0
- package/dist/routes/port-forward.d.ts.map +1 -0
- package/dist/routes/port-forward.js +205 -0
- package/dist/routes/port-forward.js.map +1 -0
- package/dist/routes/projects.d.ts +3 -0
- package/dist/routes/projects.d.ts.map +1 -0
- package/dist/routes/projects.js +442 -0
- package/dist/routes/projects.js.map +1 -0
- package/dist/routes/ssh.d.ts +3 -0
- package/dist/routes/ssh.d.ts.map +1 -0
- package/dist/routes/ssh.js +192 -0
- package/dist/routes/ssh.js.map +1 -0
- package/dist/routes/tasks.d.ts +3 -0
- package/dist/routes/tasks.d.ts.map +1 -0
- package/dist/routes/tasks.js +183 -0
- package/dist/routes/tasks.js.map +1 -0
- package/dist/routes/tmux.d.ts +3 -0
- package/dist/routes/tmux.d.ts.map +1 -0
- package/dist/routes/tmux.js +1191 -0
- package/dist/routes/tmux.js.map +1 -0
- package/dist/routes/tunnel.d.ts +25 -0
- package/dist/routes/tunnel.d.ts.map +1 -0
- package/dist/routes/tunnel.js +449 -0
- package/dist/routes/tunnel.js.map +1 -0
- package/dist/services/ModulePermissions.d.ts +100 -0
- package/dist/services/ModulePermissions.d.ts.map +1 -0
- package/dist/services/ModulePermissions.js +312 -0
- package/dist/services/ModulePermissions.js.map +1 -0
- package/dist/services/ModuleRegistryService.d.ts +152 -0
- package/dist/services/ModuleRegistryService.d.ts.map +1 -0
- package/dist/services/ModuleRegistryService.js +522 -0
- package/dist/services/ModuleRegistryService.js.map +1 -0
- package/dist/services/agent.service.d.ts +19 -0
- package/dist/services/agent.service.d.ts.map +1 -0
- package/dist/services/agent.service.js +88 -0
- package/dist/services/agent.service.js.map +1 -0
- package/dist/services/bootstrap.d.ts +22 -0
- package/dist/services/bootstrap.d.ts.map +1 -0
- package/dist/services/bootstrap.js +206 -0
- package/dist/services/bootstrap.js.map +1 -0
- package/dist/services/service-manager.d.ts +50 -0
- package/dist/services/service-manager.d.ts.map +1 -0
- package/dist/services/service-manager.js +382 -0
- package/dist/services/service-manager.js.map +1 -0
- package/package.json +107 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
+
import { Client } from 'ssh2';
|
|
3
|
+
import { createServer } from 'net';
|
|
4
|
+
import { readFileSync } from 'fs';
|
|
5
|
+
// Store active connections and servers
|
|
6
|
+
const activeConnections = new Map();
|
|
7
|
+
export const portForwardRoutes = async (fastify) => {
|
|
8
|
+
// Get all port forwards
|
|
9
|
+
fastify.get('/', async (_request, _reply) => {
|
|
10
|
+
const portForwards = fastify.db.getAllPortForwards();
|
|
11
|
+
return { portForwards };
|
|
12
|
+
});
|
|
13
|
+
// Create new port forward
|
|
14
|
+
fastify.post('/', async (request, reply) => {
|
|
15
|
+
const { localPort, remoteHost, remotePort, connectionId } = request.body;
|
|
16
|
+
try {
|
|
17
|
+
// Check if port is already in use
|
|
18
|
+
const existing = fastify.db.getAllPortForwards()
|
|
19
|
+
.find(pf => pf.localPort === localPort && pf.status === 'active');
|
|
20
|
+
if (existing) {
|
|
21
|
+
return reply.code(409).send({ error: 'Local port is already in use' });
|
|
22
|
+
}
|
|
23
|
+
// Get SSH connection config
|
|
24
|
+
const connectionConfig = fastify.db.getSSHConnection(connectionId);
|
|
25
|
+
if (!connectionConfig) {
|
|
26
|
+
return reply.code(404).send({ error: 'SSH connection not found' });
|
|
27
|
+
}
|
|
28
|
+
const portForward = fastify.db.createPortForward({
|
|
29
|
+
id: uuidv4(),
|
|
30
|
+
localPort,
|
|
31
|
+
remoteHost,
|
|
32
|
+
remotePort,
|
|
33
|
+
serverName: connectionConfig.serverName, // Keep for backward compatibility
|
|
34
|
+
connectionId,
|
|
35
|
+
status: 'inactive'
|
|
36
|
+
});
|
|
37
|
+
return { portForward };
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
return reply.code(500).send({
|
|
41
|
+
error: 'Failed to create port forward',
|
|
42
|
+
details: error instanceof Error ? error.message : 'Unknown error'
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
// Start port forwarding
|
|
47
|
+
fastify.post('/:id/start', async (request, reply) => {
|
|
48
|
+
const { id } = request.params;
|
|
49
|
+
try {
|
|
50
|
+
const portForward = fastify.db.getPortForward(id);
|
|
51
|
+
if (!portForward) {
|
|
52
|
+
return reply.code(404).send({ error: 'Port forward not found' });
|
|
53
|
+
}
|
|
54
|
+
if (portForward.status === 'active') {
|
|
55
|
+
return reply.code(400).send({ error: 'Port forward is already active' });
|
|
56
|
+
}
|
|
57
|
+
// Get SSH connection config
|
|
58
|
+
const connectionConfig = portForward.connectionId
|
|
59
|
+
? fastify.db.getSSHConnection(portForward.connectionId)
|
|
60
|
+
: fastify.db.getSSHConnectionByName(portForward.serverName);
|
|
61
|
+
if (!connectionConfig) {
|
|
62
|
+
return reply.code(404).send({ error: 'SSH connection not found' });
|
|
63
|
+
}
|
|
64
|
+
const sshClient = new Client();
|
|
65
|
+
// Create local server
|
|
66
|
+
const server = createServer((localSocket) => {
|
|
67
|
+
sshClient.forwardOut('localhost', portForward.localPort, portForward.remoteHost, portForward.remotePort, (err, stream) => {
|
|
68
|
+
if (err) {
|
|
69
|
+
localSocket.end();
|
|
70
|
+
console.error('Forward error:', err);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
localSocket.pipe(stream).pipe(localSocket);
|
|
74
|
+
localSocket.on('close', () => {
|
|
75
|
+
stream.end();
|
|
76
|
+
});
|
|
77
|
+
stream.on('close', () => {
|
|
78
|
+
localSocket.end();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
return new Promise((resolve) => {
|
|
83
|
+
sshClient.on('ready', () => {
|
|
84
|
+
server.listen(portForward.localPort, () => {
|
|
85
|
+
// Store active connection
|
|
86
|
+
activeConnections.set(id, { client: sshClient, server });
|
|
87
|
+
// Update database
|
|
88
|
+
fastify.db.updatePortForward(id, {
|
|
89
|
+
status: 'active',
|
|
90
|
+
connectionId: id
|
|
91
|
+
});
|
|
92
|
+
// Emit event
|
|
93
|
+
fastify.io.emit('portforward:started', { id, localPort: portForward.localPort });
|
|
94
|
+
resolve({
|
|
95
|
+
success: true,
|
|
96
|
+
message: `Port forwarding started on localhost:${portForward.localPort}`
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
server.on('error', (err) => {
|
|
100
|
+
sshClient.end();
|
|
101
|
+
resolve(reply.code(500).send({
|
|
102
|
+
error: 'Failed to start local server',
|
|
103
|
+
details: err.message
|
|
104
|
+
}));
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
sshClient.on('error', (err) => {
|
|
108
|
+
resolve(reply.code(500).send({
|
|
109
|
+
error: 'SSH connection failed',
|
|
110
|
+
details: err.message
|
|
111
|
+
}));
|
|
112
|
+
});
|
|
113
|
+
// Connect with appropriate auth method
|
|
114
|
+
const connectConfig = {
|
|
115
|
+
host: connectionConfig.host,
|
|
116
|
+
port: connectionConfig.port,
|
|
117
|
+
username: connectionConfig.username,
|
|
118
|
+
};
|
|
119
|
+
if (connectionConfig.privateKeyPath) {
|
|
120
|
+
connectConfig.privateKey = readFileSync(connectionConfig.privateKeyPath);
|
|
121
|
+
}
|
|
122
|
+
else if (connectionConfig.password) {
|
|
123
|
+
connectConfig.password = connectionConfig.password;
|
|
124
|
+
}
|
|
125
|
+
sshClient.connect(connectConfig);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
return reply.code(500).send({
|
|
130
|
+
error: 'Failed to start port forward',
|
|
131
|
+
details: error instanceof Error ? error.message : 'Unknown error'
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
// Stop port forwarding
|
|
136
|
+
fastify.post('/:id/stop', async (request, reply) => {
|
|
137
|
+
const { id } = request.params;
|
|
138
|
+
try {
|
|
139
|
+
const portForward = fastify.db.getPortForward(id);
|
|
140
|
+
if (!portForward) {
|
|
141
|
+
return reply.code(404).send({ error: 'Port forward not found' });
|
|
142
|
+
}
|
|
143
|
+
if (portForward.status !== 'active') {
|
|
144
|
+
return reply.code(400).send({ error: 'Port forward is not active' });
|
|
145
|
+
}
|
|
146
|
+
// Get active connection
|
|
147
|
+
const active = activeConnections.get(id);
|
|
148
|
+
if (active) {
|
|
149
|
+
active.server.close();
|
|
150
|
+
active.client.end();
|
|
151
|
+
activeConnections.delete(id);
|
|
152
|
+
}
|
|
153
|
+
// Update database
|
|
154
|
+
fastify.db.updatePortForward(id, {
|
|
155
|
+
status: 'inactive',
|
|
156
|
+
connectionId: undefined
|
|
157
|
+
});
|
|
158
|
+
// Emit event
|
|
159
|
+
fastify.io.emit('portforward:stopped', { id, localPort: portForward.localPort });
|
|
160
|
+
return { success: true };
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
return reply.code(500).send({
|
|
164
|
+
error: 'Failed to stop port forward',
|
|
165
|
+
details: error instanceof Error ? error.message : 'Unknown error'
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
// Delete port forward
|
|
170
|
+
fastify.delete('/:id', async (request, reply) => {
|
|
171
|
+
const { id } = request.params;
|
|
172
|
+
try {
|
|
173
|
+
const portForward = fastify.db.getPortForward(id);
|
|
174
|
+
if (!portForward) {
|
|
175
|
+
return reply.code(404).send({ error: 'Port forward not found' });
|
|
176
|
+
}
|
|
177
|
+
// Stop if active
|
|
178
|
+
if (portForward.status === 'active') {
|
|
179
|
+
const active = activeConnections.get(id);
|
|
180
|
+
if (active) {
|
|
181
|
+
active.server.close();
|
|
182
|
+
active.client.end();
|
|
183
|
+
activeConnections.delete(id);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
fastify.db.deletePortForward(id);
|
|
187
|
+
return { success: true };
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
return reply.code(500).send({
|
|
191
|
+
error: 'Failed to delete port forward',
|
|
192
|
+
details: error instanceof Error ? error.message : 'Unknown error'
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
// Cleanup on server close
|
|
197
|
+
fastify.addHook('onClose', async () => {
|
|
198
|
+
for (const [, { client, server }] of activeConnections) {
|
|
199
|
+
server.close();
|
|
200
|
+
client.end();
|
|
201
|
+
}
|
|
202
|
+
activeConnections.clear();
|
|
203
|
+
});
|
|
204
|
+
};
|
|
205
|
+
//# sourceMappingURL=port-forward.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"port-forward.js","sourceRoot":"","sources":["../../src/routes/port-forward.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projects.d.ts","sourceRoot":"","sources":["../../src/routes/projects.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAgK7C,eAAO,MAAM,aAAa,EAAE,kBA8K3B,CAAC"}
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
// Security function to check if a path is allowed for scanning
|
|
4
|
+
function isAllowedPath(scanPath) {
|
|
5
|
+
const normalizedPath = path.resolve(scanPath);
|
|
6
|
+
// List of forbidden paths (system directories)
|
|
7
|
+
const forbiddenPaths = [
|
|
8
|
+
'/System',
|
|
9
|
+
'/usr',
|
|
10
|
+
'/bin',
|
|
11
|
+
'/sbin',
|
|
12
|
+
'/etc',
|
|
13
|
+
'/var/root',
|
|
14
|
+
'/private/var/root',
|
|
15
|
+
'/Library/Application Support',
|
|
16
|
+
'/private/var/db',
|
|
17
|
+
'/private/var/install',
|
|
18
|
+
'/private/var/tmp',
|
|
19
|
+
'/tmp',
|
|
20
|
+
'/Windows',
|
|
21
|
+
'/Program Files',
|
|
22
|
+
'/Program Files (x86)',
|
|
23
|
+
'C:\\Windows',
|
|
24
|
+
'C:\\Program Files',
|
|
25
|
+
'C:\\Program Files (x86)',
|
|
26
|
+
];
|
|
27
|
+
// Check if the path starts with any forbidden directory
|
|
28
|
+
for (const forbiddenPath of forbiddenPaths) {
|
|
29
|
+
if (normalizedPath.startsWith(forbiddenPath)) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Only allow paths under common user directories
|
|
34
|
+
const allowedPrefixes = [
|
|
35
|
+
'/Users',
|
|
36
|
+
'/home',
|
|
37
|
+
'/opt',
|
|
38
|
+
process.env.HOME || '',
|
|
39
|
+
'C:\\Users',
|
|
40
|
+
'D:\\',
|
|
41
|
+
'E:\\',
|
|
42
|
+
].filter(Boolean);
|
|
43
|
+
return allowedPrefixes.some(prefix => normalizedPath.startsWith(prefix));
|
|
44
|
+
}
|
|
45
|
+
// Function to parse .gitignore patterns
|
|
46
|
+
function parseGitignore(gitignoreContent) {
|
|
47
|
+
return gitignoreContent
|
|
48
|
+
.split('\n')
|
|
49
|
+
.map(line => line.trim())
|
|
50
|
+
.filter(line => line && !line.startsWith('#'))
|
|
51
|
+
.map(line => {
|
|
52
|
+
// Remove leading slash
|
|
53
|
+
if (line.startsWith('/')) {
|
|
54
|
+
line = line.slice(1);
|
|
55
|
+
}
|
|
56
|
+
return line;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
// Function to check if a path should be ignored based on .gitignore patterns
|
|
60
|
+
function shouldIgnoreByGitignore(relativePath, patterns, _basePath) {
|
|
61
|
+
for (const pattern of patterns) {
|
|
62
|
+
// Simple pattern matching - could be enhanced with proper glob matching
|
|
63
|
+
if (pattern.endsWith('/')) {
|
|
64
|
+
// Directory pattern
|
|
65
|
+
const dirPattern = pattern.slice(0, -1);
|
|
66
|
+
if (relativePath === dirPattern || relativePath.startsWith(dirPattern + '/')) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else if (pattern.includes('*')) {
|
|
71
|
+
// Wildcard pattern - basic implementation
|
|
72
|
+
const regexPattern = pattern
|
|
73
|
+
.replace(/\./g, '\\.')
|
|
74
|
+
.replace(/\*/g, '.*');
|
|
75
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
76
|
+
if (regex.test(relativePath)) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
// Exact match or starts with pattern
|
|
82
|
+
if (relativePath === pattern || relativePath.startsWith(pattern + '/')) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
// Function to collect .gitignore patterns from all parent directories
|
|
90
|
+
async function collectGitignorePatterns(startPath, targetPath) {
|
|
91
|
+
const patterns = new Map();
|
|
92
|
+
let currentPath = targetPath;
|
|
93
|
+
while (currentPath.startsWith(startPath)) {
|
|
94
|
+
try {
|
|
95
|
+
const gitignorePath = path.join(currentPath, '.gitignore');
|
|
96
|
+
const gitignoreContent = await fs.readFile(gitignorePath, 'utf-8');
|
|
97
|
+
const pathPatterns = parseGitignore(gitignoreContent);
|
|
98
|
+
if (pathPatterns.length > 0) {
|
|
99
|
+
patterns.set(currentPath, pathPatterns);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
// .gitignore doesn't exist in this directory, continue
|
|
104
|
+
}
|
|
105
|
+
const parentPath = path.dirname(currentPath);
|
|
106
|
+
if (parentPath === currentPath)
|
|
107
|
+
break; // Reached root
|
|
108
|
+
currentPath = parentPath;
|
|
109
|
+
}
|
|
110
|
+
return patterns;
|
|
111
|
+
}
|
|
112
|
+
export const projectRoutes = async (fastify) => {
|
|
113
|
+
// Scan local projects
|
|
114
|
+
fastify.post('/scan', async (request, reply) => {
|
|
115
|
+
const { recursive = false, scanPath, respectGitignore = true } = request.body;
|
|
116
|
+
try {
|
|
117
|
+
console.log('Scanning projects...', { recursive, scanPath, respectGitignore });
|
|
118
|
+
// Require scanPath to be provided - no default scanning
|
|
119
|
+
if (!scanPath) {
|
|
120
|
+
return reply.code(400).send({
|
|
121
|
+
error: 'scanPath is required',
|
|
122
|
+
success: false
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
// Security check - prevent directory traversal
|
|
126
|
+
const normalizedPath = path.normalize(scanPath);
|
|
127
|
+
if (normalizedPath.includes('..')) {
|
|
128
|
+
return reply.code(403).send({
|
|
129
|
+
error: 'Directory traversal not allowed',
|
|
130
|
+
success: false
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
// Additional security checks
|
|
134
|
+
if (!isAllowedPath(normalizedPath)) {
|
|
135
|
+
return reply.code(403).send({
|
|
136
|
+
error: 'Access to this path is not allowed',
|
|
137
|
+
success: false
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
const projects = await scanForProjects(normalizedPath, recursive, respectGitignore);
|
|
141
|
+
// Emit project scan event
|
|
142
|
+
fastify.io.emit('projects:scanned', {
|
|
143
|
+
path: normalizedPath,
|
|
144
|
+
count: projects.length,
|
|
145
|
+
recursive
|
|
146
|
+
});
|
|
147
|
+
return {
|
|
148
|
+
projects,
|
|
149
|
+
success: true,
|
|
150
|
+
count: projects.length
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
console.error('Error scanning projects:', error);
|
|
155
|
+
return reply.code(500).send({
|
|
156
|
+
error: 'Failed to scan projects',
|
|
157
|
+
details: error instanceof Error ? error.message : 'Unknown error',
|
|
158
|
+
success: false
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
// Scan remote projects
|
|
163
|
+
fastify.post('/scan-remote', async (request, reply) => {
|
|
164
|
+
const { recursive = false, scanPath, respectGitignore = true } = request.body;
|
|
165
|
+
try {
|
|
166
|
+
console.log('Scanning remote projects...', { recursive, scanPath, respectGitignore });
|
|
167
|
+
// Require scanPath to be provided - no default scanning
|
|
168
|
+
if (!scanPath) {
|
|
169
|
+
return reply.code(400).send({
|
|
170
|
+
error: 'scanPath is required for remote scanning',
|
|
171
|
+
success: false
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
// Security check - prevent directory traversal
|
|
175
|
+
const normalizedPath = path.normalize(scanPath);
|
|
176
|
+
if (normalizedPath.includes('..')) {
|
|
177
|
+
return reply.code(403).send({
|
|
178
|
+
error: 'Directory traversal not allowed',
|
|
179
|
+
success: false
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
// Additional security checks
|
|
183
|
+
if (!isAllowedPath(normalizedPath)) {
|
|
184
|
+
return reply.code(403).send({
|
|
185
|
+
error: 'Access to this path is not allowed',
|
|
186
|
+
success: false
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
// For now, return empty array as this requires SSH setup
|
|
190
|
+
// TODO: Implement remote scanning via SSH
|
|
191
|
+
const projects = [];
|
|
192
|
+
// Emit remote project scan event
|
|
193
|
+
fastify.io.emit('projects:remote-scanned', {
|
|
194
|
+
path: normalizedPath,
|
|
195
|
+
count: projects.length,
|
|
196
|
+
recursive
|
|
197
|
+
});
|
|
198
|
+
return {
|
|
199
|
+
projects,
|
|
200
|
+
success: true,
|
|
201
|
+
count: projects.length,
|
|
202
|
+
message: 'Remote scanning not yet implemented'
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
console.error('Error scanning remote projects:', error);
|
|
207
|
+
return reply.code(500).send({
|
|
208
|
+
error: 'Failed to scan remote projects',
|
|
209
|
+
details: error instanceof Error ? error.message : 'Unknown error',
|
|
210
|
+
success: false
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
// Get all projects (mock for now)
|
|
215
|
+
fastify.get('/', async (request, reply) => {
|
|
216
|
+
const { includeNested } = request.query;
|
|
217
|
+
try {
|
|
218
|
+
// For now, return empty array as this would come from database
|
|
219
|
+
const projects = [];
|
|
220
|
+
return projects;
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
console.error('Error getting projects:', error);
|
|
224
|
+
return reply.code(500).send({
|
|
225
|
+
error: 'Failed to get projects',
|
|
226
|
+
details: error instanceof Error ? error.message : 'Unknown error'
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
// Create project (mock for now)
|
|
231
|
+
fastify.post('/', async (request, reply) => {
|
|
232
|
+
const projectData = request.body;
|
|
233
|
+
try {
|
|
234
|
+
// For now, just return success
|
|
235
|
+
const id = `project-${Date.now()}`;
|
|
236
|
+
return {
|
|
237
|
+
id,
|
|
238
|
+
success: true
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
console.error('Error creating project:', error);
|
|
243
|
+
return reply.code(500).send({
|
|
244
|
+
error: 'Failed to create project',
|
|
245
|
+
details: error instanceof Error ? error.message : 'Unknown error'
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
// Update project (mock for now)
|
|
250
|
+
fastify.put('/:id', async (request, reply) => {
|
|
251
|
+
const { id } = request.params;
|
|
252
|
+
const updates = request.body;
|
|
253
|
+
try {
|
|
254
|
+
// For now, just return success
|
|
255
|
+
return {
|
|
256
|
+
success: true,
|
|
257
|
+
changes: 1
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
console.error('Error updating project:', error);
|
|
262
|
+
return reply.code(500).send({
|
|
263
|
+
error: 'Failed to update project',
|
|
264
|
+
details: error instanceof Error ? error.message : 'Unknown error'
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
};
|
|
269
|
+
// Helper function to scan for projects
|
|
270
|
+
async function scanForProjects(basePath, recursive, respectGitignore = true) {
|
|
271
|
+
const projects = [];
|
|
272
|
+
// Collect .gitignore patterns if respectGitignore is enabled
|
|
273
|
+
let gitignorePatterns = new Map();
|
|
274
|
+
if (respectGitignore) {
|
|
275
|
+
gitignorePatterns = await collectGitignorePatterns(basePath, basePath);
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
const entries = await fs.readdir(basePath, { withFileTypes: true });
|
|
279
|
+
for (const entry of entries) {
|
|
280
|
+
if (!entry.isDirectory())
|
|
281
|
+
continue;
|
|
282
|
+
const fullPath = path.join(basePath, entry.name);
|
|
283
|
+
// Skip hidden directories and node_modules
|
|
284
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
// Check if directory should be ignored by .gitignore
|
|
288
|
+
if (respectGitignore && gitignorePatterns.size > 0) {
|
|
289
|
+
let shouldIgnore = false;
|
|
290
|
+
// Check against all collected .gitignore patterns
|
|
291
|
+
for (const [patternBasePath, patterns] of Array.from(gitignorePatterns)) {
|
|
292
|
+
if (fullPath.startsWith(patternBasePath)) {
|
|
293
|
+
const relativePath = path.relative(patternBasePath, fullPath);
|
|
294
|
+
if (shouldIgnoreByGitignore(relativePath, patterns, patternBasePath)) {
|
|
295
|
+
shouldIgnore = true;
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (shouldIgnore) {
|
|
301
|
+
console.log(`Skipping directory ${entry.name} due to .gitignore`);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// Check if this directory is a project
|
|
306
|
+
const project = await detectProject(fullPath, entry.name);
|
|
307
|
+
if (project) {
|
|
308
|
+
projects.push(project);
|
|
309
|
+
}
|
|
310
|
+
// If recursive, scan subdirectories
|
|
311
|
+
if (recursive) {
|
|
312
|
+
try {
|
|
313
|
+
const subProjects = await scanForProjects(fullPath, true, respectGitignore);
|
|
314
|
+
projects.push(...subProjects);
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
// Ignore errors in subdirectories
|
|
318
|
+
console.warn(`Failed to scan subdirectory ${fullPath}:`, error);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
console.error(`Failed to scan directory ${basePath}:`, error);
|
|
325
|
+
}
|
|
326
|
+
return projects;
|
|
327
|
+
}
|
|
328
|
+
// Helper function to detect if a directory is a project
|
|
329
|
+
async function detectProject(dirPath, dirName) {
|
|
330
|
+
try {
|
|
331
|
+
const files = await fs.readdir(dirPath);
|
|
332
|
+
// Check for common project indicators
|
|
333
|
+
let hasPackageJson = files.includes('package.json');
|
|
334
|
+
const hasPyproject = files.includes('pyproject.toml');
|
|
335
|
+
const hasRequirements = files.includes('requirements.txt');
|
|
336
|
+
const hasCargoToml = files.includes('Cargo.toml');
|
|
337
|
+
const hasGitDir = files.includes('.git');
|
|
338
|
+
const hasDockerfile = files.includes('Dockerfile');
|
|
339
|
+
const hasMakefile = files.includes('Makefile');
|
|
340
|
+
// If it doesn't look like a project, skip it
|
|
341
|
+
if (!hasPackageJson && !hasPyproject && !hasRequirements && !hasCargoToml && !hasGitDir) {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
let projectType = 'unknown';
|
|
345
|
+
let vitePort;
|
|
346
|
+
// Determine project type
|
|
347
|
+
if (hasPackageJson) {
|
|
348
|
+
try {
|
|
349
|
+
const packageJsonPath = path.join(dirPath, 'package.json');
|
|
350
|
+
const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8');
|
|
351
|
+
// Check if the file is empty or invalid
|
|
352
|
+
if (!packageJsonContent.trim()) {
|
|
353
|
+
console.warn(`Empty package.json file found at ${packageJsonPath}`);
|
|
354
|
+
hasPackageJson = false;
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
const packageJson = JSON.parse(packageJsonContent);
|
|
358
|
+
if (packageJson.dependencies?.vite || packageJson.devDependencies?.vite) {
|
|
359
|
+
projectType = 'vite';
|
|
360
|
+
// Try to detect port from vite config or scripts
|
|
361
|
+
vitePort = extractVitePort(packageJson);
|
|
362
|
+
}
|
|
363
|
+
else if (packageJson.dependencies?.react || packageJson.devDependencies?.react) {
|
|
364
|
+
projectType = 'react';
|
|
365
|
+
}
|
|
366
|
+
else if (packageJson.dependencies?.next || packageJson.devDependencies?.next) {
|
|
367
|
+
projectType = 'nextjs';
|
|
368
|
+
}
|
|
369
|
+
else if (packageJson.dependencies?.express) {
|
|
370
|
+
projectType = 'express';
|
|
371
|
+
}
|
|
372
|
+
else if (packageJson.dependencies?.fastify) {
|
|
373
|
+
projectType = 'fastify';
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
projectType = 'nodejs';
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
console.warn(`Failed to parse package.json in ${dirPath}:`, error);
|
|
382
|
+
projectType = 'nodejs';
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
else if (hasPyproject || hasRequirements) {
|
|
386
|
+
projectType = 'python';
|
|
387
|
+
}
|
|
388
|
+
else if (hasCargoToml) {
|
|
389
|
+
projectType = 'rust';
|
|
390
|
+
}
|
|
391
|
+
// Get directory stats
|
|
392
|
+
const stats = await fs.stat(dirPath);
|
|
393
|
+
// Create project object
|
|
394
|
+
const project = {
|
|
395
|
+
id: `local-${Buffer.from(dirPath).toString('base64').slice(0, 8)}-${Date.now()}`,
|
|
396
|
+
name: dirName,
|
|
397
|
+
path: dirPath,
|
|
398
|
+
type: projectType,
|
|
399
|
+
lastUpdated: stats.mtime.getTime(),
|
|
400
|
+
lastFileChange: stats.mtime.getTime(),
|
|
401
|
+
status: 'INACTIVE',
|
|
402
|
+
isRemote: false,
|
|
403
|
+
tags: [projectType]
|
|
404
|
+
};
|
|
405
|
+
if (vitePort) {
|
|
406
|
+
project.vitePort = vitePort;
|
|
407
|
+
}
|
|
408
|
+
// Check if it's a git repository
|
|
409
|
+
if (hasGitDir) {
|
|
410
|
+
try {
|
|
411
|
+
const gitConfigPath = path.join(dirPath, '.git', 'config');
|
|
412
|
+
const gitConfig = await fs.readFile(gitConfigPath, 'utf-8');
|
|
413
|
+
const remoteMatch = gitConfig.match(/url = (.+)/);
|
|
414
|
+
if (remoteMatch) {
|
|
415
|
+
project.repository = remoteMatch[1].trim();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
// Ignore git config errors
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return project;
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
console.error(`Failed to detect project in ${dirPath}:`, error);
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
// Helper function to extract Vite port from package.json
|
|
430
|
+
function extractVitePort(packageJson) {
|
|
431
|
+
// Check dev script for port
|
|
432
|
+
const devScript = packageJson.scripts?.dev;
|
|
433
|
+
if (devScript && typeof devScript === 'string') {
|
|
434
|
+
const portMatch = devScript.match(/--port[=\s]+(\d+)/);
|
|
435
|
+
if (portMatch) {
|
|
436
|
+
return parseInt(portMatch[1], 10);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
// Default Vite port
|
|
440
|
+
return undefined;
|
|
441
|
+
}
|
|
442
|
+
//# sourceMappingURL=projects.js.map
|