mstro-app 0.1.47
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/LICENSE +21 -0
- package/README.md +177 -0
- package/bin/commands/config.js +145 -0
- package/bin/commands/login.js +313 -0
- package/bin/commands/logout.js +75 -0
- package/bin/commands/status.js +197 -0
- package/bin/commands/whoami.js +161 -0
- package/bin/configure-claude.js +298 -0
- package/bin/mstro.js +581 -0
- package/bin/postinstall.js +45 -0
- package/bin/release.sh +110 -0
- package/dist/server/cli/headless/claude-invoker.d.ts +17 -0
- package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -0
- package/dist/server/cli/headless/claude-invoker.js +311 -0
- package/dist/server/cli/headless/claude-invoker.js.map +1 -0
- package/dist/server/cli/headless/index.d.ts +13 -0
- package/dist/server/cli/headless/index.d.ts.map +1 -0
- package/dist/server/cli/headless/index.js +10 -0
- package/dist/server/cli/headless/index.js.map +1 -0
- package/dist/server/cli/headless/mcp-config.d.ts +11 -0
- package/dist/server/cli/headless/mcp-config.d.ts.map +1 -0
- package/dist/server/cli/headless/mcp-config.js +76 -0
- package/dist/server/cli/headless/mcp-config.js.map +1 -0
- package/dist/server/cli/headless/output-utils.d.ts +33 -0
- package/dist/server/cli/headless/output-utils.d.ts.map +1 -0
- package/dist/server/cli/headless/output-utils.js +101 -0
- package/dist/server/cli/headless/output-utils.js.map +1 -0
- package/dist/server/cli/headless/prompt-utils.d.ts +21 -0
- package/dist/server/cli/headless/prompt-utils.d.ts.map +1 -0
- package/dist/server/cli/headless/prompt-utils.js +84 -0
- package/dist/server/cli/headless/prompt-utils.js.map +1 -0
- package/dist/server/cli/headless/runner.d.ts +24 -0
- package/dist/server/cli/headless/runner.d.ts.map +1 -0
- package/dist/server/cli/headless/runner.js +99 -0
- package/dist/server/cli/headless/runner.js.map +1 -0
- package/dist/server/cli/headless/types.d.ts +106 -0
- package/dist/server/cli/headless/types.d.ts.map +1 -0
- package/dist/server/cli/headless/types.js +4 -0
- package/dist/server/cli/headless/types.js.map +1 -0
- package/dist/server/cli/improvisation-session-manager.d.ts +155 -0
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -0
- package/dist/server/cli/improvisation-session-manager.js +415 -0
- package/dist/server/cli/improvisation-session-manager.js.map +1 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +386 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/mcp/bouncer-cli.d.ts +3 -0
- package/dist/server/mcp/bouncer-cli.d.ts.map +1 -0
- package/dist/server/mcp/bouncer-cli.js +99 -0
- package/dist/server/mcp/bouncer-cli.js.map +1 -0
- package/dist/server/mcp/bouncer-integration.d.ts +36 -0
- package/dist/server/mcp/bouncer-integration.d.ts.map +1 -0
- package/dist/server/mcp/bouncer-integration.js +301 -0
- package/dist/server/mcp/bouncer-integration.js.map +1 -0
- package/dist/server/mcp/security-audit.d.ts +52 -0
- package/dist/server/mcp/security-audit.d.ts.map +1 -0
- package/dist/server/mcp/security-audit.js +118 -0
- package/dist/server/mcp/security-audit.js.map +1 -0
- package/dist/server/mcp/security-patterns.d.ts +73 -0
- package/dist/server/mcp/security-patterns.d.ts.map +1 -0
- package/dist/server/mcp/security-patterns.js +247 -0
- package/dist/server/mcp/security-patterns.js.map +1 -0
- package/dist/server/mcp/server.d.ts +3 -0
- package/dist/server/mcp/server.d.ts.map +1 -0
- package/dist/server/mcp/server.js +146 -0
- package/dist/server/mcp/server.js.map +1 -0
- package/dist/server/routes/files.d.ts +9 -0
- package/dist/server/routes/files.d.ts.map +1 -0
- package/dist/server/routes/files.js +24 -0
- package/dist/server/routes/files.js.map +1 -0
- package/dist/server/routes/improvise.d.ts +3 -0
- package/dist/server/routes/improvise.d.ts.map +1 -0
- package/dist/server/routes/improvise.js +72 -0
- package/dist/server/routes/improvise.js.map +1 -0
- package/dist/server/routes/index.d.ts +10 -0
- package/dist/server/routes/index.d.ts.map +1 -0
- package/dist/server/routes/index.js +12 -0
- package/dist/server/routes/index.js.map +1 -0
- package/dist/server/routes/instances.d.ts +10 -0
- package/dist/server/routes/instances.d.ts.map +1 -0
- package/dist/server/routes/instances.js +47 -0
- package/dist/server/routes/instances.js.map +1 -0
- package/dist/server/routes/notifications.d.ts +3 -0
- package/dist/server/routes/notifications.d.ts.map +1 -0
- package/dist/server/routes/notifications.js +136 -0
- package/dist/server/routes/notifications.js.map +1 -0
- package/dist/server/services/analytics.d.ts +56 -0
- package/dist/server/services/analytics.d.ts.map +1 -0
- package/dist/server/services/analytics.js +240 -0
- package/dist/server/services/analytics.js.map +1 -0
- package/dist/server/services/auth.d.ts +26 -0
- package/dist/server/services/auth.d.ts.map +1 -0
- package/dist/server/services/auth.js +71 -0
- package/dist/server/services/auth.js.map +1 -0
- package/dist/server/services/client-id.d.ts +10 -0
- package/dist/server/services/client-id.d.ts.map +1 -0
- package/dist/server/services/client-id.js +61 -0
- package/dist/server/services/client-id.js.map +1 -0
- package/dist/server/services/credentials.d.ts +39 -0
- package/dist/server/services/credentials.d.ts.map +1 -0
- package/dist/server/services/credentials.js +110 -0
- package/dist/server/services/credentials.js.map +1 -0
- package/dist/server/services/files.d.ts +119 -0
- package/dist/server/services/files.d.ts.map +1 -0
- package/dist/server/services/files.js +560 -0
- package/dist/server/services/files.js.map +1 -0
- package/dist/server/services/instances.d.ts +52 -0
- package/dist/server/services/instances.d.ts.map +1 -0
- package/dist/server/services/instances.js +241 -0
- package/dist/server/services/instances.js.map +1 -0
- package/dist/server/services/pathUtils.d.ts +47 -0
- package/dist/server/services/pathUtils.d.ts.map +1 -0
- package/dist/server/services/pathUtils.js +124 -0
- package/dist/server/services/pathUtils.js.map +1 -0
- package/dist/server/services/platform.d.ts +72 -0
- package/dist/server/services/platform.d.ts.map +1 -0
- package/dist/server/services/platform.js +368 -0
- package/dist/server/services/platform.js.map +1 -0
- package/dist/server/services/sentry.d.ts +5 -0
- package/dist/server/services/sentry.d.ts.map +1 -0
- package/dist/server/services/sentry.js +71 -0
- package/dist/server/services/sentry.js.map +1 -0
- package/dist/server/services/terminal/pty-manager.d.ts +149 -0
- package/dist/server/services/terminal/pty-manager.d.ts.map +1 -0
- package/dist/server/services/terminal/pty-manager.js +377 -0
- package/dist/server/services/terminal/pty-manager.js.map +1 -0
- package/dist/server/services/terminal/tmux-manager.d.ts +82 -0
- package/dist/server/services/terminal/tmux-manager.d.ts.map +1 -0
- package/dist/server/services/terminal/tmux-manager.js +352 -0
- package/dist/server/services/terminal/tmux-manager.js.map +1 -0
- package/dist/server/services/websocket/autocomplete.d.ts +50 -0
- package/dist/server/services/websocket/autocomplete.d.ts.map +1 -0
- package/dist/server/services/websocket/autocomplete.js +361 -0
- package/dist/server/services/websocket/autocomplete.js.map +1 -0
- package/dist/server/services/websocket/file-utils.d.ts +44 -0
- package/dist/server/services/websocket/file-utils.d.ts.map +1 -0
- package/dist/server/services/websocket/file-utils.js +272 -0
- package/dist/server/services/websocket/file-utils.js.map +1 -0
- package/dist/server/services/websocket/handler.d.ts +246 -0
- package/dist/server/services/websocket/handler.d.ts.map +1 -0
- package/dist/server/services/websocket/handler.js +1771 -0
- package/dist/server/services/websocket/handler.js.map +1 -0
- package/dist/server/services/websocket/index.d.ts +11 -0
- package/dist/server/services/websocket/index.d.ts.map +1 -0
- package/dist/server/services/websocket/index.js +14 -0
- package/dist/server/services/websocket/index.js.map +1 -0
- package/dist/server/services/websocket/types.d.ts +214 -0
- package/dist/server/services/websocket/types.d.ts.map +1 -0
- package/dist/server/services/websocket/types.js +4 -0
- package/dist/server/services/websocket/types.js.map +1 -0
- package/dist/server/utils/agent-manager.d.ts +69 -0
- package/dist/server/utils/agent-manager.d.ts.map +1 -0
- package/dist/server/utils/agent-manager.js +269 -0
- package/dist/server/utils/agent-manager.js.map +1 -0
- package/dist/server/utils/paths.d.ts +25 -0
- package/dist/server/utils/paths.d.ts.map +1 -0
- package/dist/server/utils/paths.js +38 -0
- package/dist/server/utils/paths.js.map +1 -0
- package/dist/server/utils/port-manager.d.ts +10 -0
- package/dist/server/utils/port-manager.d.ts.map +1 -0
- package/dist/server/utils/port-manager.js +60 -0
- package/dist/server/utils/port-manager.js.map +1 -0
- package/dist/server/utils/port.d.ts +26 -0
- package/dist/server/utils/port.d.ts.map +1 -0
- package/dist/server/utils/port.js +83 -0
- package/dist/server/utils/port.js.map +1 -0
- package/hooks/bouncer.sh +138 -0
- package/package.json +74 -0
- package/server/README.md +191 -0
- package/server/cli/headless/claude-invoker.ts +415 -0
- package/server/cli/headless/index.ts +39 -0
- package/server/cli/headless/mcp-config.ts +87 -0
- package/server/cli/headless/output-utils.ts +109 -0
- package/server/cli/headless/prompt-utils.ts +108 -0
- package/server/cli/headless/runner.ts +133 -0
- package/server/cli/headless/types.ts +118 -0
- package/server/cli/improvisation-session-manager.ts +531 -0
- package/server/index.ts +456 -0
- package/server/mcp/README.md +122 -0
- package/server/mcp/bouncer-cli.ts +127 -0
- package/server/mcp/bouncer-integration.ts +430 -0
- package/server/mcp/security-audit.ts +180 -0
- package/server/mcp/security-patterns.ts +290 -0
- package/server/mcp/server.ts +174 -0
- package/server/routes/files.ts +29 -0
- package/server/routes/improvise.ts +82 -0
- package/server/routes/index.ts +13 -0
- package/server/routes/instances.ts +54 -0
- package/server/routes/notifications.ts +158 -0
- package/server/services/analytics.ts +277 -0
- package/server/services/auth.ts +80 -0
- package/server/services/client-id.ts +68 -0
- package/server/services/credentials.ts +134 -0
- package/server/services/files.ts +710 -0
- package/server/services/instances.ts +275 -0
- package/server/services/pathUtils.ts +158 -0
- package/server/services/platform.test.ts +1314 -0
- package/server/services/platform.ts +435 -0
- package/server/services/sentry.ts +81 -0
- package/server/services/terminal/pty-manager.ts +464 -0
- package/server/services/terminal/tmux-manager.ts +426 -0
- package/server/services/websocket/autocomplete.ts +438 -0
- package/server/services/websocket/file-utils.ts +305 -0
- package/server/services/websocket/handler.test.ts +20 -0
- package/server/services/websocket/handler.ts +2047 -0
- package/server/services/websocket/index.ts +40 -0
- package/server/services/websocket/types.ts +339 -0
- package/server/tsconfig.json +19 -0
- package/server/utils/agent-manager.ts +323 -0
- package/server/utils/paths.ts +45 -0
- package/server/utils/port-manager.ts +70 -0
- package/server/utils/port.ts +102 -0
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Mstro, Inc. All rights reserved.
|
|
2
|
+
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* File Service
|
|
6
|
+
*
|
|
7
|
+
* Handles file autocomplete, directory operations, and file explorer features.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
existsSync,
|
|
12
|
+
mkdirSync,
|
|
13
|
+
readdirSync,
|
|
14
|
+
renameSync,
|
|
15
|
+
rmdirSync,
|
|
16
|
+
statSync,
|
|
17
|
+
unlinkSync,
|
|
18
|
+
writeFileSync
|
|
19
|
+
} from 'node:fs'
|
|
20
|
+
import { dirname, join } from 'node:path'
|
|
21
|
+
import {
|
|
22
|
+
containsDangerousPatterns,
|
|
23
|
+
validateBothPathsWithinWorkingDir,
|
|
24
|
+
validatePathWithinWorkingDir
|
|
25
|
+
} from './pathUtils.js'
|
|
26
|
+
|
|
27
|
+
export interface FileItem {
|
|
28
|
+
name: string
|
|
29
|
+
path: string
|
|
30
|
+
isDirectory: boolean
|
|
31
|
+
score?: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Internal type for scored file items (score is required)
|
|
36
|
+
*/
|
|
37
|
+
interface ScoredFileItem {
|
|
38
|
+
name: string
|
|
39
|
+
path: string
|
|
40
|
+
isDirectory: boolean
|
|
41
|
+
score: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Directory entry with metadata for file explorer
|
|
46
|
+
*/
|
|
47
|
+
export interface DirectoryEntry {
|
|
48
|
+
name: string
|
|
49
|
+
path: string
|
|
50
|
+
type: 'file' | 'directory'
|
|
51
|
+
size?: number
|
|
52
|
+
modifiedAt?: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Result type for file operations
|
|
57
|
+
*/
|
|
58
|
+
export interface FileOperationResult {
|
|
59
|
+
success: boolean
|
|
60
|
+
path?: string
|
|
61
|
+
error?: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Result type for list directory operation
|
|
66
|
+
*/
|
|
67
|
+
export interface ListDirectoryResult {
|
|
68
|
+
success: boolean
|
|
69
|
+
entries?: DirectoryEntry[]
|
|
70
|
+
error?: string
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export class FileService {
|
|
74
|
+
private workingDirectory: string
|
|
75
|
+
private skipDirs = ['node_modules', '.git', '.mstro', 'dist', 'build', '.next', 'coverage', '.vscode', '.idea']
|
|
76
|
+
|
|
77
|
+
constructor(workingDirectory: string) {
|
|
78
|
+
this.workingDirectory = workingDirectory
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get all files recursively with optional filtering
|
|
83
|
+
*/
|
|
84
|
+
getAllFiles(
|
|
85
|
+
_baseDir: 'working' | 'scores' = 'working',
|
|
86
|
+
filter?: string,
|
|
87
|
+
limit: number = 10
|
|
88
|
+
): FileItem[] {
|
|
89
|
+
// Note: 'scores' baseDir kept for API compatibility, but maps to working directory
|
|
90
|
+
const directory = this.workingDirectory
|
|
91
|
+
|
|
92
|
+
let files = this.scanDirectory(directory, directory)
|
|
93
|
+
|
|
94
|
+
// Apply filter if provided
|
|
95
|
+
if (filter) {
|
|
96
|
+
files = this.filterAndScore(files, filter)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Sort by score if filtered, otherwise by type and name
|
|
100
|
+
if (filter) {
|
|
101
|
+
files.sort((a, b) => (b.score || 0) - (a.score || 0))
|
|
102
|
+
} else {
|
|
103
|
+
files.sort((a, b) => {
|
|
104
|
+
if (a.isDirectory && !b.isDirectory) return -1
|
|
105
|
+
if (!a.isDirectory && b.isDirectory) return 1
|
|
106
|
+
return a.name.localeCompare(b.name)
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return files.slice(0, limit)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Recursively scan directory
|
|
115
|
+
*/
|
|
116
|
+
private scanDirectory(
|
|
117
|
+
dir: string,
|
|
118
|
+
baseDir: string,
|
|
119
|
+
results: FileItem[] = []
|
|
120
|
+
): FileItem[] {
|
|
121
|
+
try {
|
|
122
|
+
// Skip common ignore directories
|
|
123
|
+
const dirName = dir.split('/').pop() || ''
|
|
124
|
+
if (this.skipDirs.includes(dirName)) {
|
|
125
|
+
return results
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const entries = readdirSync(dir, { withFileTypes: true })
|
|
129
|
+
|
|
130
|
+
for (const entry of entries) {
|
|
131
|
+
// Skip hidden files and ignore directories
|
|
132
|
+
if (entry.name.startsWith('.') || this.skipDirs.includes(entry.name)) {
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const fullPath = join(dir, entry.name)
|
|
137
|
+
const relativePath = fullPath.replace(`${baseDir}/`, '')
|
|
138
|
+
|
|
139
|
+
results.push({
|
|
140
|
+
name: entry.name,
|
|
141
|
+
path: relativePath,
|
|
142
|
+
isDirectory: entry.isDirectory()
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
// Recursively search directories (with depth limit)
|
|
146
|
+
if (entry.isDirectory() && results.length < 1000) {
|
|
147
|
+
this.scanDirectory(fullPath, baseDir, results)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} catch (_error) {
|
|
151
|
+
// Skip directories we can't read
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return results
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Filter files and assign relevance scores
|
|
159
|
+
*/
|
|
160
|
+
private filterAndScore(files: FileItem[], filter: string): FileItem[] {
|
|
161
|
+
const filterLower = filter.toLowerCase()
|
|
162
|
+
|
|
163
|
+
const scoredFiles: ScoredFileItem[] = []
|
|
164
|
+
|
|
165
|
+
for (const file of files) {
|
|
166
|
+
const nameLower = file.name.toLowerCase()
|
|
167
|
+
const pathLower = file.path.toLowerCase()
|
|
168
|
+
|
|
169
|
+
let score = 0
|
|
170
|
+
|
|
171
|
+
// Highest priority: starts with filter
|
|
172
|
+
if (nameLower.startsWith(filterLower)) {
|
|
173
|
+
score = 1000
|
|
174
|
+
}
|
|
175
|
+
// High priority: word in path starts with filter
|
|
176
|
+
else if (pathLower.split('/').some(part => part.startsWith(filterLower))) {
|
|
177
|
+
score = 500
|
|
178
|
+
}
|
|
179
|
+
// Medium priority: contains filter
|
|
180
|
+
else if (nameLower.includes(filterLower)) {
|
|
181
|
+
score = 100
|
|
182
|
+
}
|
|
183
|
+
// Low priority: path contains filter
|
|
184
|
+
else if (pathLower.includes(filterLower)) {
|
|
185
|
+
score = 10
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
continue // Doesn't match, skip
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Boost score for shorter paths (prefer files closer to root)
|
|
192
|
+
score += (50 - Math.min(50, file.path.split('/').length * 5))
|
|
193
|
+
|
|
194
|
+
scoredFiles.push({
|
|
195
|
+
name: file.name,
|
|
196
|
+
path: file.path,
|
|
197
|
+
isDirectory: file.isDirectory,
|
|
198
|
+
score
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return scoredFiles
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Get file statistics
|
|
207
|
+
*/
|
|
208
|
+
getFileStats(path: string, _baseDir: 'working' | 'scores' = 'working') {
|
|
209
|
+
try {
|
|
210
|
+
// Note: 'scores' baseDir kept for API compatibility, but maps to working directory
|
|
211
|
+
const fullPath = join(this.workingDirectory, path)
|
|
212
|
+
const stats = statSync(fullPath)
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
size: stats.size,
|
|
216
|
+
created: stats.birthtime,
|
|
217
|
+
modified: stats.mtime,
|
|
218
|
+
isDirectory: stats.isDirectory(),
|
|
219
|
+
isFile: stats.isFile()
|
|
220
|
+
}
|
|
221
|
+
} catch (_error) {
|
|
222
|
+
return null
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ============================================================================
|
|
228
|
+
// File Explorer Functions (standalone exports for WebSocket handler)
|
|
229
|
+
// ============================================================================
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* List immediate children of a directory.
|
|
233
|
+
* Returns entries sorted: directories first, then files, alphabetically.
|
|
234
|
+
*
|
|
235
|
+
* @param dirPath - Directory path (relative to workingDir or absolute)
|
|
236
|
+
* @param workingDir - Working directory boundary
|
|
237
|
+
* @param showHidden - Whether to show hidden files (starting with .)
|
|
238
|
+
* @returns ListDirectoryResult with entries array or error
|
|
239
|
+
*/
|
|
240
|
+
export function listDirectory(
|
|
241
|
+
dirPath: string,
|
|
242
|
+
workingDir: string,
|
|
243
|
+
showHidden: boolean = false
|
|
244
|
+
): ListDirectoryResult {
|
|
245
|
+
// Check for dangerous patterns first
|
|
246
|
+
if (containsDangerousPatterns(dirPath)) {
|
|
247
|
+
console.error(`[FileService] SECURITY: Dangerous pattern in path: "${dirPath}"`)
|
|
248
|
+
return {
|
|
249
|
+
success: false,
|
|
250
|
+
error: 'Invalid path: contains dangerous patterns'
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Validate path is within working directory
|
|
255
|
+
const validation = validatePathWithinWorkingDir(dirPath, workingDir)
|
|
256
|
+
if (!validation.valid) {
|
|
257
|
+
return {
|
|
258
|
+
success: false,
|
|
259
|
+
error: validation.error
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const resolvedPath = validation.resolvedPath
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
// Check if path exists and is a directory
|
|
267
|
+
if (!existsSync(resolvedPath)) {
|
|
268
|
+
return {
|
|
269
|
+
success: false,
|
|
270
|
+
error: 'Directory not found'
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const stats = statSync(resolvedPath)
|
|
275
|
+
if (!stats.isDirectory()) {
|
|
276
|
+
return {
|
|
277
|
+
success: false,
|
|
278
|
+
error: 'Path is not a directory'
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Read directory entries
|
|
283
|
+
const entries = readdirSync(resolvedPath, { withFileTypes: true })
|
|
284
|
+
|
|
285
|
+
// Map to DirectoryEntry objects
|
|
286
|
+
const directoryEntries: DirectoryEntry[] = entries
|
|
287
|
+
.filter(entry => {
|
|
288
|
+
// Filter hidden files unless showHidden is true
|
|
289
|
+
if (!showHidden && entry.name.startsWith('.')) {
|
|
290
|
+
return false
|
|
291
|
+
}
|
|
292
|
+
return true
|
|
293
|
+
})
|
|
294
|
+
.map(entry => {
|
|
295
|
+
const entryPath = join(resolvedPath, entry.name)
|
|
296
|
+
const isDir = entry.isDirectory()
|
|
297
|
+
|
|
298
|
+
// Get stats for size and modification time
|
|
299
|
+
let size: number | undefined
|
|
300
|
+
let modifiedAt: string | undefined
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
const entryStats = statSync(entryPath)
|
|
304
|
+
modifiedAt = entryStats.mtime.toISOString()
|
|
305
|
+
if (!isDir) {
|
|
306
|
+
size = entryStats.size
|
|
307
|
+
}
|
|
308
|
+
} catch {
|
|
309
|
+
// Skip stats if we can't read them
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Return path relative to workingDir for consistency
|
|
313
|
+
const relativePath = entryPath.replace(`${workingDir}/`, '')
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
name: entry.name,
|
|
317
|
+
path: relativePath,
|
|
318
|
+
type: isDir ? 'directory' : 'file',
|
|
319
|
+
size,
|
|
320
|
+
modifiedAt
|
|
321
|
+
} as DirectoryEntry
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
// Sort: directories first, then files, alphabetically within each group
|
|
325
|
+
directoryEntries.sort((a, b) => {
|
|
326
|
+
if (a.type === 'directory' && b.type === 'file') return -1
|
|
327
|
+
if (a.type === 'file' && b.type === 'directory') return 1
|
|
328
|
+
return a.name.localeCompare(b.name)
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
success: true,
|
|
333
|
+
entries: directoryEntries
|
|
334
|
+
}
|
|
335
|
+
} catch (error: any) {
|
|
336
|
+
// Handle permission errors gracefully
|
|
337
|
+
if (error.code === 'EACCES') {
|
|
338
|
+
return {
|
|
339
|
+
success: false,
|
|
340
|
+
error: 'Permission denied'
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
console.error('[FileService] Error listing directory:', error)
|
|
345
|
+
return {
|
|
346
|
+
success: false,
|
|
347
|
+
error: error.message || 'Failed to list directory'
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Write content to a file.
|
|
354
|
+
* Creates parent directories if needed.
|
|
355
|
+
*
|
|
356
|
+
* @param filePath - File path (relative to workingDir or absolute)
|
|
357
|
+
* @param content - Content to write
|
|
358
|
+
* @param workingDir - Working directory boundary
|
|
359
|
+
* @returns FileOperationResult
|
|
360
|
+
*/
|
|
361
|
+
export function writeFile(
|
|
362
|
+
filePath: string,
|
|
363
|
+
content: string,
|
|
364
|
+
workingDir: string
|
|
365
|
+
): FileOperationResult {
|
|
366
|
+
// Check for dangerous patterns first
|
|
367
|
+
if (containsDangerousPatterns(filePath)) {
|
|
368
|
+
console.error(`[FileService] SECURITY: Dangerous pattern in path: "${filePath}"`)
|
|
369
|
+
return {
|
|
370
|
+
success: false,
|
|
371
|
+
error: 'Invalid path: contains dangerous patterns'
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Validate path is within working directory
|
|
376
|
+
const validation = validatePathWithinWorkingDir(filePath, workingDir)
|
|
377
|
+
if (!validation.valid) {
|
|
378
|
+
return {
|
|
379
|
+
success: false,
|
|
380
|
+
error: validation.error
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const resolvedPath = validation.resolvedPath
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
// Create parent directories if they don't exist
|
|
388
|
+
const parentDir = dirname(resolvedPath)
|
|
389
|
+
if (!existsSync(parentDir)) {
|
|
390
|
+
mkdirSync(parentDir, { recursive: true })
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Check if target is a directory (can't write to directory)
|
|
394
|
+
if (existsSync(resolvedPath)) {
|
|
395
|
+
const stats = statSync(resolvedPath)
|
|
396
|
+
if (stats.isDirectory()) {
|
|
397
|
+
return {
|
|
398
|
+
success: false,
|
|
399
|
+
error: 'Cannot write to a directory'
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Write the file
|
|
405
|
+
writeFileSync(resolvedPath, content, 'utf-8')
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
success: true,
|
|
409
|
+
path: resolvedPath.replace(`${workingDir}/`, '')
|
|
410
|
+
}
|
|
411
|
+
} catch (error: any) {
|
|
412
|
+
console.error('[FileService] Error writing file:', error)
|
|
413
|
+
return {
|
|
414
|
+
success: false,
|
|
415
|
+
error: error.message || 'Failed to write file'
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Create an empty file.
|
|
422
|
+
* Returns error if file already exists.
|
|
423
|
+
*
|
|
424
|
+
* @param filePath - File path (relative to workingDir or absolute)
|
|
425
|
+
* @param workingDir - Working directory boundary
|
|
426
|
+
* @returns FileOperationResult
|
|
427
|
+
*/
|
|
428
|
+
export function createFile(
|
|
429
|
+
filePath: string,
|
|
430
|
+
workingDir: string
|
|
431
|
+
): FileOperationResult {
|
|
432
|
+
// Check for dangerous patterns first
|
|
433
|
+
if (containsDangerousPatterns(filePath)) {
|
|
434
|
+
console.error(`[FileService] SECURITY: Dangerous pattern in path: "${filePath}"`)
|
|
435
|
+
return {
|
|
436
|
+
success: false,
|
|
437
|
+
error: 'Invalid path: contains dangerous patterns'
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Validate path is within working directory
|
|
442
|
+
const validation = validatePathWithinWorkingDir(filePath, workingDir)
|
|
443
|
+
if (!validation.valid) {
|
|
444
|
+
return {
|
|
445
|
+
success: false,
|
|
446
|
+
error: validation.error
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const resolvedPath = validation.resolvedPath
|
|
451
|
+
|
|
452
|
+
try {
|
|
453
|
+
// Check if file already exists
|
|
454
|
+
if (existsSync(resolvedPath)) {
|
|
455
|
+
return {
|
|
456
|
+
success: false,
|
|
457
|
+
error: 'File already exists'
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Create parent directories if needed
|
|
462
|
+
const parentDir = dirname(resolvedPath)
|
|
463
|
+
if (!existsSync(parentDir)) {
|
|
464
|
+
mkdirSync(parentDir, { recursive: true })
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Create empty file
|
|
468
|
+
writeFileSync(resolvedPath, '', 'utf-8')
|
|
469
|
+
|
|
470
|
+
return {
|
|
471
|
+
success: true,
|
|
472
|
+
path: resolvedPath.replace(`${workingDir}/`, '')
|
|
473
|
+
}
|
|
474
|
+
} catch (error: any) {
|
|
475
|
+
console.error('[FileService] Error creating file:', error)
|
|
476
|
+
return {
|
|
477
|
+
success: false,
|
|
478
|
+
error: error.message || 'Failed to create file'
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Create a directory (recursively if needed).
|
|
485
|
+
* Returns error if directory already exists.
|
|
486
|
+
*
|
|
487
|
+
* @param dirPath - Directory path (relative to workingDir or absolute)
|
|
488
|
+
* @param workingDir - Working directory boundary
|
|
489
|
+
* @returns FileOperationResult
|
|
490
|
+
*/
|
|
491
|
+
export function createDirectory(
|
|
492
|
+
dirPath: string,
|
|
493
|
+
workingDir: string
|
|
494
|
+
): FileOperationResult {
|
|
495
|
+
// Check for dangerous patterns first
|
|
496
|
+
if (containsDangerousPatterns(dirPath)) {
|
|
497
|
+
console.error(`[FileService] SECURITY: Dangerous pattern in path: "${dirPath}"`)
|
|
498
|
+
return {
|
|
499
|
+
success: false,
|
|
500
|
+
error: 'Invalid path: contains dangerous patterns'
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Validate path is within working directory
|
|
505
|
+
const validation = validatePathWithinWorkingDir(dirPath, workingDir)
|
|
506
|
+
if (!validation.valid) {
|
|
507
|
+
return {
|
|
508
|
+
success: false,
|
|
509
|
+
error: validation.error
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const resolvedPath = validation.resolvedPath
|
|
514
|
+
|
|
515
|
+
try {
|
|
516
|
+
// Check if path already exists
|
|
517
|
+
if (existsSync(resolvedPath)) {
|
|
518
|
+
const stats = statSync(resolvedPath)
|
|
519
|
+
if (stats.isDirectory()) {
|
|
520
|
+
return {
|
|
521
|
+
success: false,
|
|
522
|
+
error: 'Directory already exists'
|
|
523
|
+
}
|
|
524
|
+
} else {
|
|
525
|
+
return {
|
|
526
|
+
success: false,
|
|
527
|
+
error: 'A file with that name already exists'
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Create directory recursively
|
|
533
|
+
mkdirSync(resolvedPath, { recursive: true })
|
|
534
|
+
|
|
535
|
+
return {
|
|
536
|
+
success: true,
|
|
537
|
+
path: resolvedPath.replace(`${workingDir}/`, '')
|
|
538
|
+
}
|
|
539
|
+
} catch (error: any) {
|
|
540
|
+
console.error('[FileService] Error creating directory:', error)
|
|
541
|
+
return {
|
|
542
|
+
success: false,
|
|
543
|
+
error: error.message || 'Failed to create directory'
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Delete a file or EMPTY directory.
|
|
550
|
+
* For safety, only deletes empty directories.
|
|
551
|
+
*
|
|
552
|
+
* CRITICAL: Validates path is within workingDir to prevent path traversal.
|
|
553
|
+
*
|
|
554
|
+
* @param targetPath - Path to delete (relative to workingDir or absolute)
|
|
555
|
+
* @param workingDir - Working directory boundary
|
|
556
|
+
* @returns FileOperationResult
|
|
557
|
+
*/
|
|
558
|
+
export function deleteFile(
|
|
559
|
+
targetPath: string,
|
|
560
|
+
workingDir: string
|
|
561
|
+
): FileOperationResult {
|
|
562
|
+
// Check for dangerous patterns first
|
|
563
|
+
if (containsDangerousPatterns(targetPath)) {
|
|
564
|
+
console.error(`[FileService] SECURITY: Dangerous pattern in path: "${targetPath}"`)
|
|
565
|
+
return {
|
|
566
|
+
success: false,
|
|
567
|
+
error: 'Invalid path: contains dangerous patterns'
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Validate path is within working directory
|
|
572
|
+
const validation = validatePathWithinWorkingDir(targetPath, workingDir)
|
|
573
|
+
if (!validation.valid) {
|
|
574
|
+
return {
|
|
575
|
+
success: false,
|
|
576
|
+
error: validation.error
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const resolvedPath = validation.resolvedPath
|
|
581
|
+
|
|
582
|
+
// Additional safety: prevent deleting the working directory itself
|
|
583
|
+
if (resolvedPath === workingDir || resolvedPath === `${workingDir}/`) {
|
|
584
|
+
console.error(`[FileService] SECURITY: Attempted to delete working directory: "${resolvedPath}"`)
|
|
585
|
+
return {
|
|
586
|
+
success: false,
|
|
587
|
+
error: 'Cannot delete the working directory'
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
try {
|
|
592
|
+
// Check if path exists
|
|
593
|
+
if (!existsSync(resolvedPath)) {
|
|
594
|
+
return {
|
|
595
|
+
success: false,
|
|
596
|
+
error: 'File or directory not found'
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const stats = statSync(resolvedPath)
|
|
601
|
+
|
|
602
|
+
if (stats.isDirectory()) {
|
|
603
|
+
// Only delete empty directories for safety
|
|
604
|
+
const contents = readdirSync(resolvedPath)
|
|
605
|
+
if (contents.length > 0) {
|
|
606
|
+
return {
|
|
607
|
+
success: false,
|
|
608
|
+
error: 'Directory is not empty. Only empty directories can be deleted.'
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
rmdirSync(resolvedPath)
|
|
613
|
+
} else {
|
|
614
|
+
unlinkSync(resolvedPath)
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return {
|
|
618
|
+
success: true,
|
|
619
|
+
path: resolvedPath.replace(`${workingDir}/`, '')
|
|
620
|
+
}
|
|
621
|
+
} catch (error: any) {
|
|
622
|
+
console.error('[FileService] Error deleting file:', error)
|
|
623
|
+
return {
|
|
624
|
+
success: false,
|
|
625
|
+
error: error.message || 'Failed to delete'
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Rename or move a file/directory.
|
|
632
|
+
* Both source and destination must be within workingDir.
|
|
633
|
+
*
|
|
634
|
+
* @param oldPath - Current path (relative to workingDir or absolute)
|
|
635
|
+
* @param newPath - New path (relative to workingDir or absolute)
|
|
636
|
+
* @param workingDir - Working directory boundary
|
|
637
|
+
* @returns FileOperationResult
|
|
638
|
+
*/
|
|
639
|
+
export function renameFile(
|
|
640
|
+
oldPath: string,
|
|
641
|
+
newPath: string,
|
|
642
|
+
workingDir: string
|
|
643
|
+
): FileOperationResult {
|
|
644
|
+
// Check for dangerous patterns in both paths
|
|
645
|
+
if (containsDangerousPatterns(oldPath)) {
|
|
646
|
+
console.error(`[FileService] SECURITY: Dangerous pattern in source path: "${oldPath}"`)
|
|
647
|
+
return {
|
|
648
|
+
success: false,
|
|
649
|
+
error: 'Invalid source path: contains dangerous patterns'
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (containsDangerousPatterns(newPath)) {
|
|
654
|
+
console.error(`[FileService] SECURITY: Dangerous pattern in destination path: "${newPath}"`)
|
|
655
|
+
return {
|
|
656
|
+
success: false,
|
|
657
|
+
error: 'Invalid destination path: contains dangerous patterns'
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Validate both paths are within working directory
|
|
662
|
+
const validation = validateBothPathsWithinWorkingDir(oldPath, newPath, workingDir)
|
|
663
|
+
if (!validation.valid) {
|
|
664
|
+
return {
|
|
665
|
+
success: false,
|
|
666
|
+
error: validation.error
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const resolvedOldPath = validation.resolvedSourcePath
|
|
671
|
+
const resolvedNewPath = validation.resolvedDestPath
|
|
672
|
+
|
|
673
|
+
try {
|
|
674
|
+
// Check if source exists
|
|
675
|
+
if (!existsSync(resolvedOldPath)) {
|
|
676
|
+
return {
|
|
677
|
+
success: false,
|
|
678
|
+
error: 'Source file or directory not found'
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Check if destination already exists
|
|
683
|
+
if (existsSync(resolvedNewPath)) {
|
|
684
|
+
return {
|
|
685
|
+
success: false,
|
|
686
|
+
error: 'Destination already exists'
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Create parent directories for destination if needed
|
|
691
|
+
const parentDir = dirname(resolvedNewPath)
|
|
692
|
+
if (!existsSync(parentDir)) {
|
|
693
|
+
mkdirSync(parentDir, { recursive: true })
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Perform the rename
|
|
697
|
+
renameSync(resolvedOldPath, resolvedNewPath)
|
|
698
|
+
|
|
699
|
+
return {
|
|
700
|
+
success: true,
|
|
701
|
+
path: resolvedNewPath.replace(`${workingDir}/`, '')
|
|
702
|
+
}
|
|
703
|
+
} catch (error: any) {
|
|
704
|
+
console.error('[FileService] Error renaming file:', error)
|
|
705
|
+
return {
|
|
706
|
+
success: false,
|
|
707
|
+
error: error.message || 'Failed to rename'
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|