@itz4blitz/agentful 1.2.0 → 1.3.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/README.md +28 -1
- package/bin/cli.js +11 -1055
- package/bin/hooks/block-file-creation.js +271 -0
- package/bin/hooks/product-spec-watcher.js +151 -0
- package/lib/index.js +0 -11
- package/lib/init.js +2 -21
- package/lib/parallel-execution.js +235 -0
- package/lib/presets.js +26 -4
- package/package.json +4 -7
- package/template/.claude/agents/architect.md +2 -2
- package/template/.claude/agents/backend.md +17 -30
- package/template/.claude/agents/frontend.md +17 -39
- package/template/.claude/agents/orchestrator.md +63 -4
- package/template/.claude/agents/product-analyzer.md +1 -1
- package/template/.claude/agents/tester.md +16 -29
- package/template/.claude/commands/agentful-generate.md +221 -14
- package/template/.claude/commands/agentful-init.md +621 -0
- package/template/.claude/commands/agentful-product.md +1 -1
- package/template/.claude/commands/agentful-start.md +99 -1
- package/template/.claude/product/EXAMPLES.md +2 -2
- package/template/.claude/product/index.md +1 -1
- package/template/.claude/settings.json +22 -0
- package/template/.claude/skills/research/SKILL.md +432 -0
- package/template/CLAUDE.md +5 -6
- package/template/bin/hooks/architect-drift-detector.js +242 -0
- package/template/bin/hooks/product-spec-watcher.js +151 -0
- package/version.json +1 -1
- package/bin/hooks/post-agent.js +0 -101
- package/bin/hooks/post-feature.js +0 -227
- package/bin/hooks/pre-agent.js +0 -118
- package/bin/hooks/pre-feature.js +0 -138
- package/lib/VALIDATION_README.md +0 -455
- package/lib/ci/claude-action-integration.js +0 -641
- package/lib/ci/index.js +0 -10
- package/lib/core/analyzer.js +0 -497
- package/lib/core/cli.js +0 -141
- package/lib/core/detectors/conventions.js +0 -342
- package/lib/core/detectors/framework.js +0 -276
- package/lib/core/detectors/index.js +0 -15
- package/lib/core/detectors/language.js +0 -199
- package/lib/core/detectors/patterns.js +0 -356
- package/lib/core/generator.js +0 -626
- package/lib/core/index.js +0 -9
- package/lib/core/output-parser.js +0 -458
- package/lib/core/storage.js +0 -515
- package/lib/core/templates.js +0 -556
- package/lib/pipeline/cli.js +0 -423
- package/lib/pipeline/engine.js +0 -928
- package/lib/pipeline/executor.js +0 -440
- package/lib/pipeline/index.js +0 -33
- package/lib/pipeline/integrations.js +0 -559
- package/lib/pipeline/schemas.js +0 -288
- package/lib/remote/client.js +0 -361
- package/lib/server/auth.js +0 -270
- package/lib/server/client-example.js +0 -190
- package/lib/server/executor.js +0 -477
- package/lib/server/index.js +0 -494
- package/lib/update-helpers.js +0 -505
- package/lib/validation.js +0 -460
package/lib/update-helpers.js
DELETED
|
@@ -1,505 +0,0 @@
|
|
|
1
|
-
import fs from 'fs/promises';
|
|
2
|
-
import crypto from 'crypto';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
|
|
5
|
-
const METADATA_FILE = '.agentful/update-metadata.json';
|
|
6
|
-
const BACKUP_DIR = '.agentful/backups';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Compute SHA256 hash of a file
|
|
10
|
-
* @param {string} filePath - Absolute path to file
|
|
11
|
-
* @returns {Promise<string>} Hash in format "sha256:..."
|
|
12
|
-
*/
|
|
13
|
-
export async function computeFileHash(filePath) {
|
|
14
|
-
try {
|
|
15
|
-
const content = await fs.readFile(filePath);
|
|
16
|
-
const hash = crypto.createHash('sha256').update(content).digest('hex');
|
|
17
|
-
return `sha256:${hash}`;
|
|
18
|
-
} catch (error) {
|
|
19
|
-
throw new Error(`Failed to compute hash for ${filePath}: ${error.message}`);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Get update metadata from .agentful/update-metadata.json
|
|
25
|
-
* @param {string} targetDir - Project directory
|
|
26
|
-
* @returns {Promise<Object|null>} Metadata object or null if doesn't exist
|
|
27
|
-
*/
|
|
28
|
-
export async function getMetadata(targetDir) {
|
|
29
|
-
const metadataPath = path.join(targetDir, METADATA_FILE);
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
const content = await fs.readFile(metadataPath, 'utf-8');
|
|
33
|
-
return JSON.parse(content);
|
|
34
|
-
} catch (error) {
|
|
35
|
-
if (error.code === 'ENOENT') {
|
|
36
|
-
// File doesn't exist - old installation or first time
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
throw new Error(`Failed to read metadata: ${error.message}`);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Save update metadata to .agentful/update-metadata.json
|
|
45
|
-
* @param {string} targetDir - Project directory
|
|
46
|
-
* @param {Object} metadata - Metadata object to save
|
|
47
|
-
* @returns {Promise<void>}
|
|
48
|
-
*/
|
|
49
|
-
export async function saveMetadata(targetDir, metadata) {
|
|
50
|
-
const metadataPath = path.join(targetDir, METADATA_FILE);
|
|
51
|
-
|
|
52
|
-
try {
|
|
53
|
-
// Ensure .agentful directory exists
|
|
54
|
-
await fs.mkdir(path.dirname(metadataPath), { recursive: true });
|
|
55
|
-
|
|
56
|
-
// Write metadata with pretty formatting
|
|
57
|
-
await fs.writeFile(
|
|
58
|
-
metadataPath,
|
|
59
|
-
JSON.stringify(metadata, null, 2),
|
|
60
|
-
'utf-8'
|
|
61
|
-
);
|
|
62
|
-
} catch (error) {
|
|
63
|
-
throw new Error(`Failed to save metadata: ${error.message}`);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Check if a file has been customized by comparing current hash vs metadata
|
|
69
|
-
* @param {string} targetDir - Project directory
|
|
70
|
-
* @param {string} relativePath - Relative path from project root (e.g., ".claude/agents/backend.md")
|
|
71
|
-
* @returns {Promise<{customized: boolean, reason: string}>}
|
|
72
|
-
*/
|
|
73
|
-
export async function isFileCustomized(targetDir, relativePath) {
|
|
74
|
-
// Get metadata
|
|
75
|
-
const metadata = await getMetadata(targetDir);
|
|
76
|
-
|
|
77
|
-
// If no metadata exists, assume all files are customized (safe default)
|
|
78
|
-
if (!metadata) {
|
|
79
|
-
return {
|
|
80
|
-
customized: true,
|
|
81
|
-
reason: 'no_metadata'
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// If file not in metadata, it was added by user
|
|
86
|
-
if (!metadata.files[relativePath]) {
|
|
87
|
-
return {
|
|
88
|
-
customized: true,
|
|
89
|
-
reason: 'user_added'
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Check if file exists
|
|
94
|
-
const filePath = path.join(targetDir, relativePath);
|
|
95
|
-
try {
|
|
96
|
-
await fs.access(filePath);
|
|
97
|
-
} catch {
|
|
98
|
-
// File was deleted by user
|
|
99
|
-
return {
|
|
100
|
-
customized: true,
|
|
101
|
-
reason: 'user_deleted'
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Compute current hash
|
|
106
|
-
try {
|
|
107
|
-
const currentHash = await computeFileHash(filePath);
|
|
108
|
-
const originalHash = metadata.files[relativePath].hash;
|
|
109
|
-
|
|
110
|
-
if (currentHash !== originalHash) {
|
|
111
|
-
return {
|
|
112
|
-
customized: true,
|
|
113
|
-
reason: 'modified'
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// File unchanged
|
|
118
|
-
return {
|
|
119
|
-
customized: false,
|
|
120
|
-
reason: 'unchanged'
|
|
121
|
-
};
|
|
122
|
-
} catch (error) {
|
|
123
|
-
// If we can't compute hash, err on safe side
|
|
124
|
-
return {
|
|
125
|
-
customized: true,
|
|
126
|
-
reason: 'hash_error'
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Get canonical file content from npm package template
|
|
133
|
-
* @param {string} relativePath - Relative path (e.g., ".claude/agents/backend.md" or "CLAUDE.md")
|
|
134
|
-
* @param {string} packageRoot - Root directory of npm package (usually __dirname/../)
|
|
135
|
-
* @returns {Promise<Buffer|null>} File content or null if not found
|
|
136
|
-
*/
|
|
137
|
-
export async function getCanonicalFile(relativePath, packageRoot) {
|
|
138
|
-
// Determine source location based on path
|
|
139
|
-
let sourcePath;
|
|
140
|
-
|
|
141
|
-
if (relativePath.startsWith('.claude/')) {
|
|
142
|
-
// Files from .claude/ directory
|
|
143
|
-
sourcePath = path.join(packageRoot, relativePath);
|
|
144
|
-
} else if (relativePath.startsWith('bin/hooks/')) {
|
|
145
|
-
// Hook files
|
|
146
|
-
sourcePath = path.join(packageRoot, relativePath);
|
|
147
|
-
} else if (relativePath === 'CLAUDE.md') {
|
|
148
|
-
// Template file
|
|
149
|
-
sourcePath = path.join(packageRoot, 'template', relativePath);
|
|
150
|
-
} else {
|
|
151
|
-
throw new Error(`Unknown file source for ${relativePath}`);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
try {
|
|
155
|
-
return await fs.readFile(sourcePath);
|
|
156
|
-
} catch (error) {
|
|
157
|
-
if (error.code === 'ENOENT') {
|
|
158
|
-
return null;
|
|
159
|
-
}
|
|
160
|
-
throw new Error(`Failed to read canonical file ${relativePath}: ${error.message}`);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Backup a file before updating
|
|
166
|
-
* @param {string} filePath - Absolute path to file to backup
|
|
167
|
-
* @param {string} targetDir - Project directory
|
|
168
|
-
* @returns {Promise<string>} Path to backup file
|
|
169
|
-
*/
|
|
170
|
-
export async function backupFile(filePath, targetDir) {
|
|
171
|
-
const backupDir = path.join(targetDir, BACKUP_DIR);
|
|
172
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
173
|
-
const relativePath = path.relative(targetDir, filePath);
|
|
174
|
-
const backupPath = path.join(
|
|
175
|
-
backupDir,
|
|
176
|
-
timestamp,
|
|
177
|
-
relativePath
|
|
178
|
-
);
|
|
179
|
-
|
|
180
|
-
try {
|
|
181
|
-
// Ensure backup directory exists
|
|
182
|
-
await fs.mkdir(path.dirname(backupPath), { recursive: true });
|
|
183
|
-
|
|
184
|
-
// Copy file to backup location
|
|
185
|
-
await fs.copyFile(filePath, backupPath);
|
|
186
|
-
|
|
187
|
-
return backupPath;
|
|
188
|
-
} catch (error) {
|
|
189
|
-
throw new Error(`Failed to backup ${filePath}: ${error.message}`);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Record file metadata after installation
|
|
195
|
-
* @param {string} targetDir - Project directory
|
|
196
|
-
* @param {string} relativePath - Relative path from project root
|
|
197
|
-
* @param {string} hash - File hash
|
|
198
|
-
* @param {string} version - Package version
|
|
199
|
-
* @returns {Promise<void>}
|
|
200
|
-
*/
|
|
201
|
-
export async function recordFileMetadata(targetDir, relativePath, hash, version) {
|
|
202
|
-
let metadata = await getMetadata(targetDir);
|
|
203
|
-
|
|
204
|
-
// Initialize metadata if doesn't exist
|
|
205
|
-
if (!metadata) {
|
|
206
|
-
metadata = {
|
|
207
|
-
installed_version: version,
|
|
208
|
-
installed_at: new Date().toISOString(),
|
|
209
|
-
files: {}
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Record file metadata
|
|
214
|
-
metadata.files[relativePath] = {
|
|
215
|
-
hash,
|
|
216
|
-
source: 'npm',
|
|
217
|
-
installed_at: new Date().toISOString()
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
// Save updated metadata
|
|
221
|
-
await saveMetadata(targetDir, metadata);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* Get list of all tracked files
|
|
226
|
-
* @param {string} targetDir - Project directory
|
|
227
|
-
* @returns {Promise<string[]>} Array of relative paths
|
|
228
|
-
*/
|
|
229
|
-
export async function getTrackedFiles(targetDir) {
|
|
230
|
-
const metadata = await getMetadata(targetDir);
|
|
231
|
-
|
|
232
|
-
if (!metadata || !metadata.files) {
|
|
233
|
-
return [];
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return Object.keys(metadata.files);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Remove file from tracking (e.g., when user deletes it intentionally)
|
|
241
|
-
* @param {string} targetDir - Project directory
|
|
242
|
-
* @param {string} relativePath - Relative path to remove
|
|
243
|
-
* @returns {Promise<void>}
|
|
244
|
-
*/
|
|
245
|
-
export async function untrackFile(targetDir, relativePath) {
|
|
246
|
-
const metadata = await getMetadata(targetDir);
|
|
247
|
-
|
|
248
|
-
if (!metadata || !metadata.files) {
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
delete metadata.files[relativePath];
|
|
253
|
-
await saveMetadata(targetDir, metadata);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Update file tracking after updating a file
|
|
258
|
-
* @param {string} targetDir - Project directory
|
|
259
|
-
* @param {string} relativePath - Relative path
|
|
260
|
-
* @param {string} newHash - New file hash
|
|
261
|
-
* @param {string} version - New package version
|
|
262
|
-
* @returns {Promise<void>}
|
|
263
|
-
*/
|
|
264
|
-
export async function updateFileTracking(targetDir, relativePath, newHash, version) {
|
|
265
|
-
const metadata = await getMetadata(targetDir);
|
|
266
|
-
|
|
267
|
-
if (!metadata) {
|
|
268
|
-
throw new Error('Cannot update tracking: no metadata exists');
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// Update file metadata
|
|
272
|
-
if (metadata.files[relativePath]) {
|
|
273
|
-
metadata.files[relativePath].hash = newHash;
|
|
274
|
-
metadata.files[relativePath].updated_at = new Date().toISOString();
|
|
275
|
-
metadata.files[relativePath].updated_to_version = version;
|
|
276
|
-
} else {
|
|
277
|
-
// File was added during update
|
|
278
|
-
metadata.files[relativePath] = {
|
|
279
|
-
hash: newHash,
|
|
280
|
-
source: 'npm',
|
|
281
|
-
installed_at: new Date().toISOString()
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Update package version
|
|
286
|
-
metadata.installed_version = version;
|
|
287
|
-
metadata.last_updated = new Date().toISOString();
|
|
288
|
-
|
|
289
|
-
await saveMetadata(targetDir, metadata);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Check if metadata exists for a project
|
|
294
|
-
* @param {string} targetDir - Project directory
|
|
295
|
-
* @returns {Promise<boolean>}
|
|
296
|
-
*/
|
|
297
|
-
export async function hasMetadata(targetDir) {
|
|
298
|
-
const metadata = await getMetadata(targetDir);
|
|
299
|
-
return metadata !== null;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Check if a file path represents user content that should never be auto-updated
|
|
304
|
-
* @param {string} relativePath - Relative file path
|
|
305
|
-
* @returns {boolean} True if user content, false otherwise
|
|
306
|
-
*/
|
|
307
|
-
export function isUserContent(relativePath) {
|
|
308
|
-
const userPatterns = [
|
|
309
|
-
/^\.claude\/product\//,
|
|
310
|
-
/^\.claude\/agents\/auto-generated\//,
|
|
311
|
-
/^\.agentful\/state\.json$/,
|
|
312
|
-
/^\.agentful\/completion\.json$/,
|
|
313
|
-
/^\.agentful\/decisions\.json$/,
|
|
314
|
-
/^\.agentful\/conversation/,
|
|
315
|
-
/\.local$/,
|
|
316
|
-
/\.backup$/
|
|
317
|
-
];
|
|
318
|
-
|
|
319
|
-
return userPatterns.some(pattern => pattern.test(relativePath));
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Check if a file is a core agentful file that can be updated
|
|
324
|
-
* @param {string} relativePath - Relative file path
|
|
325
|
-
* @returns {boolean} True if core file, false otherwise
|
|
326
|
-
*/
|
|
327
|
-
export function isCoreFile(relativePath) {
|
|
328
|
-
// Must be in a core directory
|
|
329
|
-
const corePrefixes = [
|
|
330
|
-
'.claude/agents/',
|
|
331
|
-
'.claude/skills/',
|
|
332
|
-
'.claude/commands/',
|
|
333
|
-
'CLAUDE.md'
|
|
334
|
-
];
|
|
335
|
-
|
|
336
|
-
const isInCoreDir = corePrefixes.some(prefix =>
|
|
337
|
-
relativePath.startsWith(prefix) || relativePath === prefix
|
|
338
|
-
);
|
|
339
|
-
|
|
340
|
-
// But not user content
|
|
341
|
-
return isInCoreDir && !isUserContent(relativePath);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Create a comprehensive backup of all agentful files
|
|
346
|
-
* @param {string} targetDir - Target project directory
|
|
347
|
-
* @param {string} reason - Reason for backup (e.g., 'update', 'force-update')
|
|
348
|
-
* @returns {Promise<string>} Path to backup directory
|
|
349
|
-
*/
|
|
350
|
-
export async function createFullBackup(targetDir, reason = 'update') {
|
|
351
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
|
|
352
|
-
const backupPath = path.join(targetDir, BACKUP_DIR, timestamp);
|
|
353
|
-
|
|
354
|
-
// Create backup directory
|
|
355
|
-
await fs.mkdir(backupPath, { recursive: true });
|
|
356
|
-
|
|
357
|
-
// Get all tracked files from metadata
|
|
358
|
-
const metadata = await getMetadata(targetDir);
|
|
359
|
-
let fileCount = 0;
|
|
360
|
-
const backedUpFiles = [];
|
|
361
|
-
|
|
362
|
-
if (metadata && metadata.files) {
|
|
363
|
-
for (const relativePath of Object.keys(metadata.files)) {
|
|
364
|
-
if (!isUserContent(relativePath)) {
|
|
365
|
-
const sourcePath = path.join(targetDir, relativePath);
|
|
366
|
-
const destPath = path.join(backupPath, relativePath);
|
|
367
|
-
|
|
368
|
-
try {
|
|
369
|
-
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
|
370
|
-
await fs.copyFile(sourcePath, destPath);
|
|
371
|
-
backedUpFiles.push(relativePath);
|
|
372
|
-
fileCount++;
|
|
373
|
-
} catch (error) {
|
|
374
|
-
console.warn(`Failed to backup ${relativePath}: ${error.message}`);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// Always backup metadata
|
|
381
|
-
try {
|
|
382
|
-
const metadataSource = path.join(targetDir, METADATA_FILE);
|
|
383
|
-
const metadataDest = path.join(backupPath, METADATA_FILE);
|
|
384
|
-
await fs.mkdir(path.dirname(metadataDest), { recursive: true });
|
|
385
|
-
await fs.copyFile(metadataSource, metadataDest);
|
|
386
|
-
fileCount++;
|
|
387
|
-
} catch {
|
|
388
|
-
// Metadata might not exist yet
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// Create backup manifest
|
|
392
|
-
const manifest = {
|
|
393
|
-
timestamp,
|
|
394
|
-
reason,
|
|
395
|
-
version: metadata?.version || 'unknown',
|
|
396
|
-
file_count: fileCount,
|
|
397
|
-
files: backedUpFiles,
|
|
398
|
-
created_at: new Date().toISOString()
|
|
399
|
-
};
|
|
400
|
-
|
|
401
|
-
await fs.writeFile(
|
|
402
|
-
path.join(backupPath, 'manifest.json'),
|
|
403
|
-
JSON.stringify(manifest, null, 2)
|
|
404
|
-
);
|
|
405
|
-
|
|
406
|
-
return backupPath;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* List available backups
|
|
411
|
-
* @param {string} targetDir - Target project directory
|
|
412
|
-
* @returns {Promise<Array>} List of backup info objects
|
|
413
|
-
*/
|
|
414
|
-
export async function listBackups(targetDir) {
|
|
415
|
-
const backupsPath = path.join(targetDir, BACKUP_DIR);
|
|
416
|
-
const backups = [];
|
|
417
|
-
|
|
418
|
-
try {
|
|
419
|
-
const dirs = await fs.readdir(backupsPath);
|
|
420
|
-
|
|
421
|
-
for (const dir of dirs) {
|
|
422
|
-
const manifestPath = path.join(backupsPath, dir, 'manifest.json');
|
|
423
|
-
try {
|
|
424
|
-
const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf-8'));
|
|
425
|
-
backups.push({
|
|
426
|
-
...manifest,
|
|
427
|
-
path: path.join(backupsPath, dir)
|
|
428
|
-
});
|
|
429
|
-
} catch {
|
|
430
|
-
// Skip directories without valid manifest
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// Sort by timestamp, newest first
|
|
435
|
-
backups.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
436
|
-
} catch {
|
|
437
|
-
// No backups directory or error reading
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
return backups;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
/**
|
|
444
|
-
* Restore files from a backup
|
|
445
|
-
* @param {string} targetDir - Target project directory
|
|
446
|
-
* @param {string} backupPath - Path to backup directory
|
|
447
|
-
* @returns {Promise<Object>} Restore results
|
|
448
|
-
*/
|
|
449
|
-
export async function restoreFromBackup(targetDir, backupPath) {
|
|
450
|
-
const manifestPath = path.join(backupPath, 'manifest.json');
|
|
451
|
-
const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf-8'));
|
|
452
|
-
|
|
453
|
-
let restoredCount = 0;
|
|
454
|
-
|
|
455
|
-
for (const relativePath of manifest.files) {
|
|
456
|
-
const sourcePath = path.join(backupPath, relativePath);
|
|
457
|
-
const destPath = path.join(targetDir, relativePath);
|
|
458
|
-
|
|
459
|
-
try {
|
|
460
|
-
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
|
461
|
-
await fs.copyFile(sourcePath, destPath);
|
|
462
|
-
restoredCount++;
|
|
463
|
-
} catch (error) {
|
|
464
|
-
console.error(`Failed to restore ${relativePath}: ${error.message}`);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// Restore metadata last
|
|
469
|
-
const metadataSource = path.join(backupPath, METADATA_FILE);
|
|
470
|
-
const metadataDest = path.join(targetDir, METADATA_FILE);
|
|
471
|
-
try {
|
|
472
|
-
await fs.copyFile(metadataSource, metadataDest);
|
|
473
|
-
} catch {
|
|
474
|
-
// Metadata might not exist in older backups
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
return {
|
|
478
|
-
restored: restoredCount,
|
|
479
|
-
total: manifest.files.length,
|
|
480
|
-
version: manifest.version
|
|
481
|
-
};
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
/**
|
|
485
|
-
* Initialize metadata structure (used during init)
|
|
486
|
-
* @param {string} targetDir - Project directory
|
|
487
|
-
* @param {string} version - Package version
|
|
488
|
-
* @returns {Promise<Object>} Initial metadata object
|
|
489
|
-
*/
|
|
490
|
-
export async function initializeMetadata(targetDir, version) {
|
|
491
|
-
const metadata = {
|
|
492
|
-
version: version,
|
|
493
|
-
installed_version: version,
|
|
494
|
-
installed_at: new Date().toISOString(),
|
|
495
|
-
last_update_check: null,
|
|
496
|
-
last_update_applied: null,
|
|
497
|
-
files: {},
|
|
498
|
-
file_hashes: {},
|
|
499
|
-
update_history: [],
|
|
500
|
-
customization_map: {}
|
|
501
|
-
};
|
|
502
|
-
|
|
503
|
-
await saveMetadata(targetDir, metadata);
|
|
504
|
-
return metadata;
|
|
505
|
-
}
|