-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstorage.ts
More file actions
377 lines (329 loc) · 11.4 KB
/
Copy pathstorage.ts
File metadata and controls
377 lines (329 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
/**
* Storage Management Commands
* Commands for managing centralized storage
*/
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import * as readline from 'node:readline';
import {
ensureStorageDirectory,
getStorageFilePaths,
getStoragePath,
loadMetadata,
type RepositoryMetadata,
saveMetadata,
} from '@prosdevlab/dev-agent-core';
import chalk from 'chalk';
import { Command } from 'commander';
import ora from 'ora';
import { loadConfig } from '../utils/config.js';
import { formatBytes, getDirectorySize } from '../utils/file.js';
import { logger } from '../utils/logger.js';
import { printStorageInfo } from '../utils/output.js';
/**
* Detect existing project-local indexes
*/
async function detectLocalIndexes(repositoryPath: string): Promise<{
vectors: string | null;
indexerState: string | null;
githubState: string | null;
}> {
const localDevAgentDir = path.join(repositoryPath, '.dev-agent');
const vectorsPath = path.join(localDevAgentDir, 'vectors.lance');
const indexerStatePath = path.join(localDevAgentDir, 'indexer-state.json');
const githubStatePath = path.join(localDevAgentDir, 'github-state.json');
const result = {
vectors: null as string | null,
indexerState: null as string | null,
githubState: null as string | null,
};
try {
await fs.access(vectorsPath);
result.vectors = vectorsPath;
} catch {
// Not found
}
try {
await fs.access(indexerStatePath);
result.indexerState = indexerStatePath;
} catch {
// Not found
}
try {
await fs.access(githubStatePath);
result.githubState = githubStatePath;
} catch {
// Not found
}
return result;
}
/**
* Prompt user for confirmation
*/
function askConfirmation(message: string): Promise<boolean> {
return new Promise((resolve) => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
rl.question(`${message} (y/N): `, (answer) => {
rl.close();
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
});
});
}
/**
* Storage command group
*/
const storageCommand = new Command('storage')
.description('Manage centralized storage for repository indexes')
.addHelpText(
'after',
`
Examples:
$ dev storage info Show storage location and size
$ dev storage migrate Migrate from old storage layout
Storage Location:
All indexed data is stored in ~/.dev-agent/indexes/
Each repository gets its own subdirectory based on path hash
What's Stored:
• metadata.json Repository metadata
Vector data is stored in Antfly (local search backend).
`
);
/**
* Migrate command - Move local indexes to centralized storage
*/
storageCommand
.command('migrate')
.description('Migrate project-local indexes to centralized storage')
.option('-f, --force', 'Skip confirmation prompt', false)
.option('--dry-run', 'Show what would be migrated without actually moving files', false)
.action(async (options) => {
const spinner = ora('Detecting local indexes...').start();
try {
// Load config
const config = await loadConfig();
const repositoryPath = config?.repository?.path || config?.repositoryPath || process.cwd();
const resolvedRepoPath = path.resolve(repositoryPath);
// Detect local indexes
const localIndexes = await detectLocalIndexes(resolvedRepoPath);
// Check if there's anything to migrate
const hasLocalIndexes =
localIndexes.vectors || localIndexes.indexerState || localIndexes.githubState;
if (!hasLocalIndexes) {
spinner.succeed('No local indexes found to migrate');
logger.log('');
logger.log('All indexes are already using centralized storage.');
return;
}
// Get centralized storage path
const storagePath = await getStoragePath(resolvedRepoPath);
await ensureStorageDirectory(storagePath);
const filePaths = getStorageFilePaths(storagePath);
// Check if centralized storage already exists
let centralizedExists = false;
try {
await fs.access(filePaths.vectors);
centralizedExists = true;
} catch {
// Doesn't exist yet
}
spinner.stop();
// Show what will be migrated
logger.log('');
logger.log(chalk.bold('📦 Local Indexes Found:'));
logger.log('');
let totalSize = 0;
const filesToMigrate: Array<{ from: string; to: string; size: number }> = [];
if (localIndexes.vectors) {
const size = await getDirectorySize(localIndexes.vectors);
totalSize += size;
filesToMigrate.push({
from: localIndexes.vectors,
to: filePaths.vectors,
size,
});
logger.log(` ${chalk.cyan('Vector store:')} ${localIndexes.vectors}`);
logger.log(` ${chalk.gray(`→ ${filePaths.vectors}`)}`);
logger.log(` ${chalk.gray(`Size: ${formatBytes(size)}`)}`);
}
if (localIndexes.indexerState) {
const stat = await fs.stat(localIndexes.indexerState);
totalSize += stat.size;
filesToMigrate.push({
from: localIndexes.indexerState,
to: filePaths.indexerState,
size: stat.size,
});
logger.log(` ${chalk.cyan('Indexer state:')} ${localIndexes.indexerState}`);
logger.log(` ${chalk.gray(`→ ${filePaths.indexerState}`)}`);
logger.log(` ${chalk.gray(`Size: ${formatBytes(stat.size)}`)}`);
}
if (localIndexes.githubState) {
const stat = await fs.stat(localIndexes.githubState);
totalSize += stat.size;
filesToMigrate.push({
from: localIndexes.githubState,
to: filePaths.githubState,
size: stat.size,
});
logger.log(` ${chalk.cyan('GitHub state:')} ${localIndexes.githubState}`);
logger.log(` ${chalk.gray(`→ ${filePaths.githubState}`)}`);
logger.log(` ${chalk.gray(`Size: ${formatBytes(stat.size)}`)}`);
}
logger.log('');
logger.log(` ${chalk.bold('Total size:')} ${formatBytes(totalSize)}`);
logger.log(` ${chalk.bold('Storage location:')} ${storagePath}`);
logger.log('');
if (centralizedExists) {
logger.warn('⚠️ Centralized storage already exists!');
logger.log('Migration will merge/overwrite existing indexes.');
logger.log('');
}
// Dry run mode
if (options.dryRun) {
logger.log(chalk.yellow('🔍 DRY RUN MODE - No files will be moved'));
logger.log('');
logger.log('To actually migrate, run without --dry-run flag.');
return;
}
// Confirm unless --force
if (!options.force) {
logger.warn('This will move indexes to centralized storage.');
logger.log('');
const confirmed = await askConfirmation('Continue with migration?');
if (!confirmed) {
logger.log('Migration cancelled.');
logger.log(`Run with ${chalk.yellow('--force')} to skip this prompt.`);
return;
}
}
// Perform migration
spinner.start('Migrating indexes...');
for (const file of filesToMigrate) {
try {
// Ensure target directory exists
await fs.mkdir(path.dirname(file.to), { recursive: true });
// Move file/directory
await fs.rename(file.from, file.to);
spinner.text = `Migrated ${path.basename(file.from)}`;
} catch (error) {
spinner.fail(`Failed to migrate ${path.basename(file.from)}`);
logger.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
// Continue with other files
}
}
// Create/update metadata
try {
const existingMetadata = await loadMetadata(storagePath);
await saveMetadata(storagePath, resolvedRepoPath, {
...existingMetadata,
migrated: {
timestamp: new Date().toISOString(),
from: resolvedRepoPath,
},
});
} catch (error) {
logger.debug(`Failed to update metadata: ${error}`);
}
// Clean up empty .dev-agent directory
try {
const localDevAgentDir = path.join(resolvedRepoPath, '.dev-agent');
const entries = await fs.readdir(localDevAgentDir);
if (entries.length === 0) {
await fs.rmdir(localDevAgentDir);
}
} catch {
// Ignore errors
}
spinner.succeed(chalk.green('Migration completed successfully!'));
logger.log('');
logger.log(`✓ Indexes migrated to: ${chalk.cyan(storagePath)}`);
logger.log(`✓ ${formatBytes(totalSize)} moved to centralized storage`);
logger.log('');
logger.log('Local indexes have been moved. Your repository is now clean!');
logger.log('');
} catch (error) {
spinner.fail('Migration failed');
logger.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
});
/**
* Info command - Show storage information
*/
storageCommand
.command('info')
.description('Show storage information and repository list')
.action(async () => {
const spinner = ora('Loading storage information...').start();
try {
// Load config
const config = await loadConfig();
const repositoryPath = config?.repository?.path || config?.repositoryPath || process.cwd();
const resolvedRepoPath = path.resolve(repositoryPath);
// Get centralized storage path
const storagePath = await getStoragePath(resolvedRepoPath);
const filePaths = getStorageFilePaths(storagePath);
spinner.stop();
// Check if storage exists
let storageExists = false;
let totalSize = 0;
try {
await fs.access(storagePath);
storageExists = true;
totalSize = await getDirectorySize(storagePath);
} catch {
// Storage doesn't exist yet
}
// Collect file information
const fileList = [
{ name: 'Vector Store', path: filePaths.vectors },
{ name: 'Indexer State', path: filePaths.indexerState },
{ name: 'GitHub State', path: filePaths.githubState },
{ name: 'Metadata', path: filePaths.metadata },
];
const files = await Promise.all(
fileList.map(async (file) => {
try {
const stat = await fs.stat(file.path);
const size = stat.isDirectory() ? await getDirectorySize(file.path) : stat.size;
return {
name: file.name,
path: file.path,
size,
exists: true,
};
} catch {
return {
name: file.name,
path: file.path,
size: null,
exists: false,
};
}
})
);
// Load metadata if available
let metadata: RepositoryMetadata | null = null;
try {
metadata = await loadMetadata(storagePath);
} catch {
// Metadata not available
}
// Print using new output format
printStorageInfo({
storagePath,
status: storageExists ? 'active' : 'not-initialized',
totalSize,
files,
metadata: metadata || undefined,
});
} catch (error) {
spinner.fail('Failed to load storage information');
logger.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
});
export { storageCommand };