@sylphx/flow 1.0.1 → 1.0.3
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/CHANGELOG.md +12 -0
- package/package.json +10 -9
- package/src/commands/codebase-command.ts +168 -0
- package/src/commands/flow-command.ts +1137 -0
- package/src/commands/flow-orchestrator.ts +296 -0
- package/src/commands/hook-command.ts +444 -0
- package/src/commands/init-command.ts +92 -0
- package/src/commands/init-core.ts +322 -0
- package/src/commands/knowledge-command.ts +161 -0
- package/src/commands/run-command.ts +120 -0
- package/src/components/benchmark-monitor.tsx +331 -0
- package/src/components/reindex-progress.tsx +261 -0
- package/src/composables/functional/index.ts +14 -0
- package/src/composables/functional/useEnvironment.ts +171 -0
- package/src/composables/functional/useFileSystem.ts +139 -0
- package/src/composables/index.ts +5 -0
- package/src/composables/useEnv.ts +13 -0
- package/src/composables/useRuntimeConfig.ts +27 -0
- package/src/composables/useTargetConfig.ts +45 -0
- package/src/config/ai-config.ts +376 -0
- package/src/config/constants.ts +35 -0
- package/src/config/index.ts +27 -0
- package/src/config/rules.ts +43 -0
- package/src/config/servers.ts +371 -0
- package/src/config/targets.ts +126 -0
- package/src/core/agent-loader.ts +141 -0
- package/src/core/agent-manager.ts +174 -0
- package/src/core/ai-sdk.ts +603 -0
- package/src/core/app-factory.ts +381 -0
- package/src/core/builtin-agents.ts +9 -0
- package/src/core/command-system.ts +550 -0
- package/src/core/config-system.ts +550 -0
- package/src/core/connection-pool.ts +390 -0
- package/src/core/di-container.ts +155 -0
- package/src/core/error-handling.ts +519 -0
- package/src/core/formatting/bytes.test.ts +115 -0
- package/src/core/formatting/bytes.ts +64 -0
- package/src/core/functional/async.ts +313 -0
- package/src/core/functional/either.ts +109 -0
- package/src/core/functional/error-handler.ts +135 -0
- package/src/core/functional/error-types.ts +311 -0
- package/src/core/functional/index.ts +19 -0
- package/src/core/functional/option.ts +142 -0
- package/src/core/functional/pipe.ts +189 -0
- package/src/core/functional/result.ts +204 -0
- package/src/core/functional/validation.ts +138 -0
- package/src/core/headless-display.ts +96 -0
- package/src/core/index.ts +6 -0
- package/src/core/installers/file-installer.ts +303 -0
- package/src/core/installers/mcp-installer.ts +213 -0
- package/src/core/interfaces/index.ts +22 -0
- package/src/core/interfaces/repository.interface.ts +91 -0
- package/src/core/interfaces/service.interface.ts +133 -0
- package/src/core/interfaces.ts +129 -0
- package/src/core/loop-controller.ts +200 -0
- package/src/core/result.ts +351 -0
- package/src/core/rule-loader.ts +147 -0
- package/src/core/rule-manager.ts +240 -0
- package/src/core/service-config.ts +252 -0
- package/src/core/session-service.ts +121 -0
- package/src/core/state-detector.ts +389 -0
- package/src/core/storage-factory.ts +115 -0
- package/src/core/stream-handler.ts +288 -0
- package/src/core/target-manager.ts +161 -0
- package/src/core/type-utils.ts +427 -0
- package/src/core/unified-storage.ts +456 -0
- package/src/core/upgrade-manager.ts +300 -0
- package/src/core/validation/limit.test.ts +155 -0
- package/src/core/validation/limit.ts +46 -0
- package/src/core/validation/query.test.ts +44 -0
- package/src/core/validation/query.ts +20 -0
- package/src/db/auto-migrate.ts +322 -0
- package/src/db/base-database-client.ts +144 -0
- package/src/db/cache-db.ts +218 -0
- package/src/db/cache-schema.ts +75 -0
- package/src/db/database.ts +70 -0
- package/src/db/index.ts +252 -0
- package/src/db/memory-db.ts +153 -0
- package/src/db/memory-schema.ts +29 -0
- package/src/db/schema.ts +289 -0
- package/src/db/session-repository.ts +733 -0
- package/src/domains/codebase/index.ts +5 -0
- package/src/domains/codebase/tools.ts +139 -0
- package/src/domains/index.ts +8 -0
- package/src/domains/knowledge/index.ts +10 -0
- package/src/domains/knowledge/resources.ts +537 -0
- package/src/domains/knowledge/tools.ts +174 -0
- package/src/domains/utilities/index.ts +6 -0
- package/src/domains/utilities/time/index.ts +5 -0
- package/src/domains/utilities/time/tools.ts +291 -0
- package/src/index.ts +211 -0
- package/src/services/agent-service.ts +273 -0
- package/src/services/claude-config-service.ts +252 -0
- package/src/services/config-service.ts +258 -0
- package/src/services/evaluation-service.ts +271 -0
- package/src/services/functional/evaluation-logic.ts +296 -0
- package/src/services/functional/file-processor.ts +273 -0
- package/src/services/functional/index.ts +12 -0
- package/src/services/index.ts +13 -0
- package/src/services/mcp-service.ts +432 -0
- package/src/services/memory.service.ts +476 -0
- package/src/services/search/base-indexer.ts +156 -0
- package/src/services/search/codebase-indexer-types.ts +38 -0
- package/src/services/search/codebase-indexer.ts +647 -0
- package/src/services/search/embeddings-provider.ts +455 -0
- package/src/services/search/embeddings.ts +316 -0
- package/src/services/search/functional-indexer.ts +323 -0
- package/src/services/search/index.ts +27 -0
- package/src/services/search/indexer.ts +380 -0
- package/src/services/search/knowledge-indexer.ts +422 -0
- package/src/services/search/semantic-search.ts +244 -0
- package/src/services/search/tfidf.ts +559 -0
- package/src/services/search/unified-search-service.ts +888 -0
- package/src/services/smart-config-service.ts +385 -0
- package/src/services/storage/cache-storage.ts +487 -0
- package/src/services/storage/drizzle-storage.ts +581 -0
- package/src/services/storage/index.ts +15 -0
- package/src/services/storage/lancedb-vector-storage.ts +494 -0
- package/src/services/storage/memory-storage.ts +268 -0
- package/src/services/storage/separated-storage.ts +467 -0
- package/src/services/storage/vector-storage.ts +13 -0
- package/src/shared/agents/index.ts +63 -0
- package/src/shared/files/index.ts +99 -0
- package/src/shared/index.ts +32 -0
- package/src/shared/logging/index.ts +24 -0
- package/src/shared/processing/index.ts +153 -0
- package/src/shared/types/index.ts +25 -0
- package/src/targets/claude-code.ts +574 -0
- package/src/targets/functional/claude-code-logic.ts +185 -0
- package/src/targets/functional/index.ts +6 -0
- package/src/targets/opencode.ts +529 -0
- package/src/types/agent.types.ts +32 -0
- package/src/types/api/batch.ts +108 -0
- package/src/types/api/errors.ts +118 -0
- package/src/types/api/index.ts +55 -0
- package/src/types/api/requests.ts +76 -0
- package/src/types/api/responses.ts +180 -0
- package/src/types/api/websockets.ts +85 -0
- package/src/types/api.types.ts +9 -0
- package/src/types/benchmark.ts +49 -0
- package/src/types/cli.types.ts +87 -0
- package/src/types/common.types.ts +35 -0
- package/src/types/database.types.ts +510 -0
- package/src/types/mcp-config.types.ts +448 -0
- package/src/types/mcp.types.ts +69 -0
- package/src/types/memory-types.ts +63 -0
- package/src/types/provider.types.ts +28 -0
- package/src/types/rule.types.ts +24 -0
- package/src/types/session.types.ts +214 -0
- package/src/types/target-config.types.ts +295 -0
- package/src/types/target.types.ts +140 -0
- package/src/types/todo.types.ts +25 -0
- package/src/types.ts +40 -0
- package/src/utils/advanced-tokenizer.ts +191 -0
- package/src/utils/agent-enhancer.ts +114 -0
- package/src/utils/ai-model-fetcher.ts +19 -0
- package/src/utils/async-file-operations.ts +516 -0
- package/src/utils/audio-player.ts +345 -0
- package/src/utils/cli-output.ts +266 -0
- package/src/utils/codebase-helpers.ts +211 -0
- package/src/utils/console-ui.ts +79 -0
- package/src/utils/database-errors.ts +140 -0
- package/src/utils/debug-logger.ts +49 -0
- package/src/utils/error-handler.ts +53 -0
- package/src/utils/file-operations.ts +310 -0
- package/src/utils/file-scanner.ts +259 -0
- package/src/utils/functional/array.ts +355 -0
- package/src/utils/functional/index.ts +15 -0
- package/src/utils/functional/object.ts +279 -0
- package/src/utils/functional/string.ts +281 -0
- package/src/utils/functional.ts +543 -0
- package/src/utils/help.ts +20 -0
- package/src/utils/immutable-cache.ts +106 -0
- package/src/utils/index.ts +78 -0
- package/src/utils/jsonc.ts +158 -0
- package/src/utils/logger.ts +396 -0
- package/src/utils/mcp-config.ts +249 -0
- package/src/utils/memory-tui.ts +414 -0
- package/src/utils/models-dev.ts +91 -0
- package/src/utils/notifications.ts +169 -0
- package/src/utils/object-utils.ts +51 -0
- package/src/utils/parallel-operations.ts +487 -0
- package/src/utils/paths.ts +143 -0
- package/src/utils/process-manager.ts +155 -0
- package/src/utils/prompts.ts +120 -0
- package/src/utils/search-tool-builder.ts +214 -0
- package/src/utils/secret-utils.ts +179 -0
- package/src/utils/security.ts +537 -0
- package/src/utils/session-manager.ts +168 -0
- package/src/utils/session-title.ts +87 -0
- package/src/utils/settings.ts +182 -0
- package/src/utils/simplified-errors.ts +410 -0
- package/src/utils/sync-utils.ts +159 -0
- package/src/utils/target-config.ts +570 -0
- package/src/utils/target-utils.ts +394 -0
- package/src/utils/template-engine.ts +94 -0
- package/src/utils/test-audio.ts +71 -0
- package/src/utils/todo-context.ts +46 -0
- package/src/utils/token-counter.ts +288 -0
- package/dist/index.d.ts +0 -10
- package/dist/index.js +0 -59554
- package/dist/lancedb.linux-x64-gnu-b7f0jgsz.node +0 -0
- package/dist/lancedb.linux-x64-musl-tgcv22rx.node +0 -0
- package/dist/shared/chunk-25dwp0dp.js +0 -89
- package/dist/shared/chunk-3pjb6063.js +0 -208
- package/dist/shared/chunk-4d6ydpw7.js +0 -2854
- package/dist/shared/chunk-4wjcadjk.js +0 -225
- package/dist/shared/chunk-5j4w74t6.js +0 -30
- package/dist/shared/chunk-5j8m3dh3.js +0 -58
- package/dist/shared/chunk-5thh3qem.js +0 -91
- package/dist/shared/chunk-6g9xy73m.js +0 -252
- package/dist/shared/chunk-7eq34c42.js +0 -23
- package/dist/shared/chunk-c2gwgx3r.js +0 -115
- package/dist/shared/chunk-cjd3mk4c.js +0 -1320
- package/dist/shared/chunk-g5cv6703.js +0 -368
- package/dist/shared/chunk-hpkhykhq.js +0 -574
- package/dist/shared/chunk-m2322pdk.js +0 -122
- package/dist/shared/chunk-nd5fdvaq.js +0 -26
- package/dist/shared/chunk-pgd3m6zf.js +0 -108
- package/dist/shared/chunk-qk8n91hw.js +0 -494
- package/dist/shared/chunk-rkkn8szp.js +0 -16855
- package/dist/shared/chunk-t16rfxh0.js +0 -61
- package/dist/shared/chunk-t4fbfa5v.js +0 -19
- package/dist/shared/chunk-t77h86w6.js +0 -276
- package/dist/shared/chunk-v0ez4aef.js +0 -71
- package/dist/shared/chunk-v29j2r3s.js +0 -32051
- package/dist/shared/chunk-vfbc6ew5.js +0 -765
- package/dist/shared/chunk-vmeqwm1c.js +0 -204
- package/dist/shared/chunk-x66eh37x.js +0 -137
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standardized file operations utilities
|
|
3
|
+
* Provides consistent async file operations with proper error handling
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'node:fs/promises';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { pathSecurity } from './security.js';
|
|
9
|
+
import { formatFileSize as formatFileSizeCore } from '../core/formatting/bytes.js';
|
|
10
|
+
|
|
11
|
+
export interface FileReadOptions {
|
|
12
|
+
encoding?: BufferEncoding;
|
|
13
|
+
fallback?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface FileWriteOptions {
|
|
17
|
+
encoding?: BufferEncoding;
|
|
18
|
+
createDir?: boolean;
|
|
19
|
+
backup?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface FileCopyOptions {
|
|
23
|
+
overwrite?: boolean;
|
|
24
|
+
createDir?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface FileInfo {
|
|
28
|
+
exists: boolean;
|
|
29
|
+
isFile: boolean;
|
|
30
|
+
isDirectory: boolean;
|
|
31
|
+
size?: number;
|
|
32
|
+
mtime?: Date;
|
|
33
|
+
atime?: Date;
|
|
34
|
+
ctime?: Date;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Safely read a file with encoding and fallback options
|
|
39
|
+
*/
|
|
40
|
+
export async function readFileSafe(
|
|
41
|
+
filePath: string,
|
|
42
|
+
options: FileReadOptions = {}
|
|
43
|
+
): Promise<string | null> {
|
|
44
|
+
const { encoding = 'utf8', fallback } = options;
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const content = await fs.readFile(filePath, encoding);
|
|
48
|
+
return content;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT' && fallback !== undefined) {
|
|
51
|
+
return fallback;
|
|
52
|
+
}
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Safely write a file with directory creation and backup options
|
|
59
|
+
*/
|
|
60
|
+
export async function writeFileSafe(
|
|
61
|
+
filePath: string,
|
|
62
|
+
content: string,
|
|
63
|
+
options: FileWriteOptions = {}
|
|
64
|
+
): Promise<void> {
|
|
65
|
+
const { encoding = 'utf8', createDir = true, backup = false } = options;
|
|
66
|
+
|
|
67
|
+
// Validate path security
|
|
68
|
+
pathSecurity.validatePath(filePath);
|
|
69
|
+
|
|
70
|
+
// Create directory if needed
|
|
71
|
+
if (createDir) {
|
|
72
|
+
const dir = path.dirname(filePath);
|
|
73
|
+
await ensureDirectory(dir);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Create backup if requested and file exists
|
|
77
|
+
if (backup && (await fileExists(filePath))) {
|
|
78
|
+
const backupPath = `${filePath}.backup.${Date.now()}`;
|
|
79
|
+
await fs.copyFile(filePath, backupPath);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
await fs.writeFile(filePath, content, encoding);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Check if a file or directory exists
|
|
87
|
+
*/
|
|
88
|
+
export async function fileExists(filePath: string): Promise<boolean> {
|
|
89
|
+
try {
|
|
90
|
+
await fs.access(filePath);
|
|
91
|
+
return true;
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get comprehensive file information
|
|
99
|
+
*/
|
|
100
|
+
export async function getFileInfo(filePath: string): Promise<FileInfo> {
|
|
101
|
+
try {
|
|
102
|
+
const stats = await fs.stat(filePath);
|
|
103
|
+
return {
|
|
104
|
+
exists: true,
|
|
105
|
+
isFile: stats.isFile(),
|
|
106
|
+
isDirectory: stats.isDirectory(),
|
|
107
|
+
size: stats.size,
|
|
108
|
+
mtime: stats.mtime,
|
|
109
|
+
atime: stats.atime,
|
|
110
|
+
ctime: stats.ctime,
|
|
111
|
+
};
|
|
112
|
+
} catch {
|
|
113
|
+
return {
|
|
114
|
+
exists: false,
|
|
115
|
+
isFile: false,
|
|
116
|
+
isDirectory: false,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Ensure a directory exists, creating it if necessary
|
|
123
|
+
*/
|
|
124
|
+
export async function ensureDirectory(dirPath: string): Promise<void> {
|
|
125
|
+
try {
|
|
126
|
+
await fs.access(dirPath);
|
|
127
|
+
} catch {
|
|
128
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Safely copy a file with options
|
|
134
|
+
*/
|
|
135
|
+
export async function copyFileSafe(
|
|
136
|
+
sourcePath: string,
|
|
137
|
+
destPath: string,
|
|
138
|
+
options: FileCopyOptions = {}
|
|
139
|
+
): Promise<void> {
|
|
140
|
+
const { overwrite = false, createDir = true } = options;
|
|
141
|
+
|
|
142
|
+
// Validate path security
|
|
143
|
+
pathSecurity.validatePath(sourcePath);
|
|
144
|
+
pathSecurity.validatePath(destPath);
|
|
145
|
+
|
|
146
|
+
// Check if source exists
|
|
147
|
+
if (!(await fileExists(sourcePath))) {
|
|
148
|
+
throw new Error(`Source file does not exist: ${sourcePath}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check if destination exists and overwrite is false
|
|
152
|
+
if (!overwrite && (await fileExists(destPath))) {
|
|
153
|
+
throw new Error(`Destination file already exists: ${destPath}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Create destination directory if needed
|
|
157
|
+
if (createDir) {
|
|
158
|
+
const destDir = path.dirname(destPath);
|
|
159
|
+
await ensureDirectory(destDir);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
await fs.copyFile(sourcePath, destPath);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Safely delete a file or directory
|
|
167
|
+
*/
|
|
168
|
+
export async function deletePathSafe(targetPath: string): Promise<void> {
|
|
169
|
+
// Validate path security
|
|
170
|
+
pathSecurity.validatePath(targetPath);
|
|
171
|
+
|
|
172
|
+
if (!(await fileExists(targetPath))) {
|
|
173
|
+
return; // Already deleted
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const info = await getFileInfo(targetPath);
|
|
177
|
+
|
|
178
|
+
if (info.isDirectory) {
|
|
179
|
+
await fs.rm(targetPath, { recursive: true, force: true });
|
|
180
|
+
} else {
|
|
181
|
+
await fs.unlink(targetPath);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Read directory contents safely
|
|
187
|
+
*/
|
|
188
|
+
export async function readDirectorySafe(
|
|
189
|
+
dirPath: string,
|
|
190
|
+
options: { recursive?: boolean; includeFiles?: boolean; includeDirectories?: boolean } = {}
|
|
191
|
+
): Promise<string[]> {
|
|
192
|
+
const { recursive = false, includeFiles = true, includeDirectories = true } = options;
|
|
193
|
+
|
|
194
|
+
// Validate path security
|
|
195
|
+
pathSecurity.validatePath(dirPath);
|
|
196
|
+
|
|
197
|
+
if (!(await fileExists(dirPath))) {
|
|
198
|
+
throw new Error(`Directory does not exist: ${dirPath}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const info = await getFileInfo(dirPath);
|
|
202
|
+
if (!info.isDirectory) {
|
|
203
|
+
throw new Error(`Path is not a directory: ${dirPath}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (recursive) {
|
|
207
|
+
const results: string[] = [];
|
|
208
|
+
const items = await fs.readdir(dirPath, { withFileTypes: true });
|
|
209
|
+
|
|
210
|
+
for (const item of items) {
|
|
211
|
+
const fullPath = path.join(dirPath, item.name);
|
|
212
|
+
|
|
213
|
+
if (item.isDirectory() && includeDirectories) {
|
|
214
|
+
results.push(fullPath);
|
|
215
|
+
const subResults = await readDirectorySafe(fullPath, options);
|
|
216
|
+
results.push(...subResults);
|
|
217
|
+
} else if (item.isFile() && includeFiles) {
|
|
218
|
+
results.push(fullPath);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return results;
|
|
223
|
+
}
|
|
224
|
+
const items = await fs.readdir(dirPath, { withFileTypes: true });
|
|
225
|
+
return items
|
|
226
|
+
.filter((item) => {
|
|
227
|
+
if (item.isFile() && includeFiles) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
if (item.isDirectory() && includeDirectories) {
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
return false;
|
|
234
|
+
})
|
|
235
|
+
.map((item) => path.join(dirPath, item.name));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Find files matching patterns in a directory
|
|
240
|
+
*/
|
|
241
|
+
export async function findFiles(
|
|
242
|
+
dirPath: string,
|
|
243
|
+
patterns: string[],
|
|
244
|
+
options: { recursive?: boolean; caseSensitive?: boolean } = {}
|
|
245
|
+
): Promise<string[]> {
|
|
246
|
+
const { recursive = true, caseSensitive = true } = options;
|
|
247
|
+
|
|
248
|
+
const allFiles = await readDirectorySafe(dirPath, {
|
|
249
|
+
recursive,
|
|
250
|
+
includeFiles: true,
|
|
251
|
+
includeDirectories: false,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const regexPatterns = patterns.map((pattern) => {
|
|
255
|
+
const regexPattern = pattern.replace(/\./g, '\\.').replace(/\*/g, '.*').replace(/\?/g, '.');
|
|
256
|
+
return new RegExp(regexPattern, caseSensitive ? '' : 'i');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
return allFiles.filter((filePath) => {
|
|
260
|
+
const fileName = path.basename(filePath);
|
|
261
|
+
return regexPatterns.some((regex) => regex.test(fileName));
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Move a file safely with validation
|
|
267
|
+
*/
|
|
268
|
+
export async function moveFileSafe(
|
|
269
|
+
sourcePath: string,
|
|
270
|
+
destPath: string,
|
|
271
|
+
options: { overwrite?: boolean; createDir?: boolean } = {}
|
|
272
|
+
): Promise<void> {
|
|
273
|
+
const { overwrite = false, createDir = true } = options;
|
|
274
|
+
|
|
275
|
+
// Copy first, then delete original
|
|
276
|
+
await copyFileSafe(sourcePath, destPath, { overwrite, createDir });
|
|
277
|
+
await deletePathSafe(sourcePath);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Get file size in human readable format
|
|
282
|
+
*/
|
|
283
|
+
export function formatFileSize(bytes: number): string {
|
|
284
|
+
return formatFileSizeCore(bytes);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Validate file path against security constraints
|
|
289
|
+
*/
|
|
290
|
+
export function validateFilePath(filePath: string, allowedBasePaths?: string[]): boolean {
|
|
291
|
+
try {
|
|
292
|
+
pathSecurity.validatePath(filePath);
|
|
293
|
+
|
|
294
|
+
if (allowedBasePaths) {
|
|
295
|
+
const resolved = path.resolve(filePath);
|
|
296
|
+
const isAllowed = allowedBasePaths.some((basePath) => {
|
|
297
|
+
const resolvedBase = path.resolve(basePath);
|
|
298
|
+
return resolved.startsWith(resolvedBase);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
if (!isAllowed) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return true;
|
|
307
|
+
} catch {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Scanner
|
|
3
|
+
* Scan project files for @file auto-completion with caching
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readdir, stat, readFile as fsReadFile, writeFile } from 'node:fs/promises';
|
|
7
|
+
import { join, relative } from 'node:path';
|
|
8
|
+
import { readFile } from 'node:fs/promises';
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
|
|
11
|
+
export interface FileInfo {
|
|
12
|
+
path: string;
|
|
13
|
+
relativePath: string;
|
|
14
|
+
size: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Default ignore patterns
|
|
18
|
+
const DEFAULT_IGNORE = [
|
|
19
|
+
'node_modules',
|
|
20
|
+
'.git',
|
|
21
|
+
'dist',
|
|
22
|
+
'build',
|
|
23
|
+
'.next',
|
|
24
|
+
'.vercel',
|
|
25
|
+
'.turbo',
|
|
26
|
+
'coverage',
|
|
27
|
+
'.cache',
|
|
28
|
+
'.sylphx',
|
|
29
|
+
'bun.lock',
|
|
30
|
+
'package-lock.json',
|
|
31
|
+
'yarn.lock',
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Load .gitignore patterns
|
|
36
|
+
*/
|
|
37
|
+
async function loadGitignore(rootPath: string): Promise<Set<string>> {
|
|
38
|
+
const patterns = new Set<string>(DEFAULT_IGNORE);
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const gitignorePath = join(rootPath, '.gitignore');
|
|
42
|
+
const content = await readFile(gitignorePath, 'utf8');
|
|
43
|
+
|
|
44
|
+
// Parse gitignore file
|
|
45
|
+
for (const line of content.split('\n')) {
|
|
46
|
+
const trimmed = line.trim();
|
|
47
|
+
// Skip empty lines and comments
|
|
48
|
+
if (trimmed && !trimmed.startsWith('#')) {
|
|
49
|
+
// Remove trailing slashes
|
|
50
|
+
const pattern = trimmed.endsWith('/') ? trimmed.slice(0, -1) : trimmed;
|
|
51
|
+
patterns.add(pattern);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// No .gitignore file, use defaults only
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return patterns;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Check if path should be ignored
|
|
63
|
+
*/
|
|
64
|
+
function shouldIgnore(relativePath: string, patterns: Set<string>): boolean {
|
|
65
|
+
// Check if any part of the path matches ignore patterns
|
|
66
|
+
const parts = relativePath.split('/');
|
|
67
|
+
|
|
68
|
+
for (const pattern of patterns) {
|
|
69
|
+
// Exact match
|
|
70
|
+
if (relativePath === pattern) return true;
|
|
71
|
+
|
|
72
|
+
// Directory match
|
|
73
|
+
if (parts.includes(pattern)) return true;
|
|
74
|
+
|
|
75
|
+
// Glob pattern (basic support for *)
|
|
76
|
+
if (pattern.includes('*')) {
|
|
77
|
+
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
|
78
|
+
if (regex.test(relativePath)) return true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Recursively scan directory for files (parallelized)
|
|
87
|
+
*/
|
|
88
|
+
async function scanDirectory(
|
|
89
|
+
dirPath: string,
|
|
90
|
+
rootPath: string,
|
|
91
|
+
patterns: Set<string>,
|
|
92
|
+
results: FileInfo[] = []
|
|
93
|
+
): Promise<FileInfo[]> {
|
|
94
|
+
try {
|
|
95
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
96
|
+
|
|
97
|
+
// Separate files and directories for parallel processing
|
|
98
|
+
const files: typeof entries = [];
|
|
99
|
+
const directories: typeof entries = [];
|
|
100
|
+
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
const fullPath = join(dirPath, entry.name);
|
|
103
|
+
const relativePath = relative(rootPath, fullPath);
|
|
104
|
+
|
|
105
|
+
// Skip ignored paths
|
|
106
|
+
if (shouldIgnore(relativePath, patterns)) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (entry.isDirectory()) {
|
|
111
|
+
directories.push(entry);
|
|
112
|
+
} else if (entry.isFile()) {
|
|
113
|
+
files.push(entry);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Process files in parallel (no need to stat each one individually)
|
|
118
|
+
// Use entry.isFile() which we already know, skip stat() call for performance
|
|
119
|
+
const fileResults = files.map((entry) => {
|
|
120
|
+
const fullPath = join(dirPath, entry.name);
|
|
121
|
+
const relativePath = relative(rootPath, fullPath);
|
|
122
|
+
return {
|
|
123
|
+
path: fullPath,
|
|
124
|
+
relativePath,
|
|
125
|
+
size: 0, // We skip stat() for performance, size not critical for autocomplete
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
results.push(...fileResults);
|
|
129
|
+
|
|
130
|
+
// Process subdirectories in parallel
|
|
131
|
+
if (directories.length > 0) {
|
|
132
|
+
const subdirResults = await Promise.all(
|
|
133
|
+
directories.map((entry) => {
|
|
134
|
+
const fullPath = join(dirPath, entry.name);
|
|
135
|
+
return scanDirectory(fullPath, rootPath, patterns, []);
|
|
136
|
+
})
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Flatten results from all subdirectories
|
|
140
|
+
for (const subdirResult of subdirResults) {
|
|
141
|
+
results.push(...subdirResult);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} catch (error) {
|
|
145
|
+
// Skip directories we can't read
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return results;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Cache for scanned files
|
|
152
|
+
const CACHE_DIR = join(homedir(), '.sylphx', 'cache');
|
|
153
|
+
const CACHE_VERSION = 1;
|
|
154
|
+
|
|
155
|
+
interface ScanCache {
|
|
156
|
+
version: number;
|
|
157
|
+
rootPath: string;
|
|
158
|
+
timestamp: number;
|
|
159
|
+
files: FileInfo[];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get cache file path for a project
|
|
164
|
+
*/
|
|
165
|
+
function getCachePath(rootPath: string): string {
|
|
166
|
+
// Use hash of root path as cache filename
|
|
167
|
+
const hash = Buffer.from(rootPath).toString('base64').replace(/[/+=]/g, '_');
|
|
168
|
+
return join(CACHE_DIR, `filescan-${hash}.json`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Load cached file list if valid
|
|
173
|
+
*/
|
|
174
|
+
async function loadCache(rootPath: string): Promise<FileInfo[] | null> {
|
|
175
|
+
try {
|
|
176
|
+
const cachePath = getCachePath(rootPath);
|
|
177
|
+
const content = await fsReadFile(cachePath, 'utf8');
|
|
178
|
+
const cache: ScanCache = JSON.parse(content);
|
|
179
|
+
|
|
180
|
+
// Validate cache
|
|
181
|
+
if (cache.version !== CACHE_VERSION || cache.rootPath !== rootPath) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Check if cache is still fresh (less than 5 minutes old)
|
|
186
|
+
const age = Date.now() - cache.timestamp;
|
|
187
|
+
const MAX_CACHE_AGE = 5 * 60 * 1000; // 5 minutes
|
|
188
|
+
if (age > MAX_CACHE_AGE) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return cache.files;
|
|
193
|
+
} catch {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Save file list to cache
|
|
200
|
+
*/
|
|
201
|
+
async function saveCache(rootPath: string, files: FileInfo[]): Promise<void> {
|
|
202
|
+
try {
|
|
203
|
+
const cachePath = getCachePath(rootPath);
|
|
204
|
+
const cache: ScanCache = {
|
|
205
|
+
version: CACHE_VERSION,
|
|
206
|
+
rootPath,
|
|
207
|
+
timestamp: Date.now(),
|
|
208
|
+
files,
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// Ensure cache directory exists
|
|
212
|
+
const { mkdir } = await import('node:fs/promises');
|
|
213
|
+
await mkdir(CACHE_DIR, { recursive: true });
|
|
214
|
+
|
|
215
|
+
// Write cache file
|
|
216
|
+
await writeFile(cachePath, JSON.stringify(cache), 'utf8');
|
|
217
|
+
} catch (error) {
|
|
218
|
+
// Ignore cache write errors
|
|
219
|
+
console.warn('Failed to write file scanner cache:', error);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Scan project files with caching
|
|
225
|
+
* Returns list of files respecting .gitignore
|
|
226
|
+
*/
|
|
227
|
+
export async function scanProjectFiles(rootPath: string): Promise<FileInfo[]> {
|
|
228
|
+
// Try to load from cache first
|
|
229
|
+
const cached = await loadCache(rootPath);
|
|
230
|
+
if (cached) {
|
|
231
|
+
return cached;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Cache miss or stale, scan filesystem
|
|
235
|
+
const patterns = await loadGitignore(rootPath);
|
|
236
|
+
const files = await scanDirectory(rootPath, rootPath, patterns);
|
|
237
|
+
|
|
238
|
+
// Sort by path for consistent ordering
|
|
239
|
+
files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
240
|
+
|
|
241
|
+
// Save to cache for next time
|
|
242
|
+
saveCache(rootPath, files).catch(() => {
|
|
243
|
+
// Ignore cache save errors
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return files;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Filter files by query string
|
|
251
|
+
*/
|
|
252
|
+
export function filterFiles(files: FileInfo[], query: string): FileInfo[] {
|
|
253
|
+
if (!query) return files;
|
|
254
|
+
|
|
255
|
+
const lowerQuery = query.toLowerCase();
|
|
256
|
+
return files.filter((file) =>
|
|
257
|
+
file.relativePath.toLowerCase().includes(lowerQuery)
|
|
258
|
+
);
|
|
259
|
+
}
|