@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.
Files changed (59) hide show
  1. package/README.md +28 -1
  2. package/bin/cli.js +11 -1055
  3. package/bin/hooks/block-file-creation.js +271 -0
  4. package/bin/hooks/product-spec-watcher.js +151 -0
  5. package/lib/index.js +0 -11
  6. package/lib/init.js +2 -21
  7. package/lib/parallel-execution.js +235 -0
  8. package/lib/presets.js +26 -4
  9. package/package.json +4 -7
  10. package/template/.claude/agents/architect.md +2 -2
  11. package/template/.claude/agents/backend.md +17 -30
  12. package/template/.claude/agents/frontend.md +17 -39
  13. package/template/.claude/agents/orchestrator.md +63 -4
  14. package/template/.claude/agents/product-analyzer.md +1 -1
  15. package/template/.claude/agents/tester.md +16 -29
  16. package/template/.claude/commands/agentful-generate.md +221 -14
  17. package/template/.claude/commands/agentful-init.md +621 -0
  18. package/template/.claude/commands/agentful-product.md +1 -1
  19. package/template/.claude/commands/agentful-start.md +99 -1
  20. package/template/.claude/product/EXAMPLES.md +2 -2
  21. package/template/.claude/product/index.md +1 -1
  22. package/template/.claude/settings.json +22 -0
  23. package/template/.claude/skills/research/SKILL.md +432 -0
  24. package/template/CLAUDE.md +5 -6
  25. package/template/bin/hooks/architect-drift-detector.js +242 -0
  26. package/template/bin/hooks/product-spec-watcher.js +151 -0
  27. package/version.json +1 -1
  28. package/bin/hooks/post-agent.js +0 -101
  29. package/bin/hooks/post-feature.js +0 -227
  30. package/bin/hooks/pre-agent.js +0 -118
  31. package/bin/hooks/pre-feature.js +0 -138
  32. package/lib/VALIDATION_README.md +0 -455
  33. package/lib/ci/claude-action-integration.js +0 -641
  34. package/lib/ci/index.js +0 -10
  35. package/lib/core/analyzer.js +0 -497
  36. package/lib/core/cli.js +0 -141
  37. package/lib/core/detectors/conventions.js +0 -342
  38. package/lib/core/detectors/framework.js +0 -276
  39. package/lib/core/detectors/index.js +0 -15
  40. package/lib/core/detectors/language.js +0 -199
  41. package/lib/core/detectors/patterns.js +0 -356
  42. package/lib/core/generator.js +0 -626
  43. package/lib/core/index.js +0 -9
  44. package/lib/core/output-parser.js +0 -458
  45. package/lib/core/storage.js +0 -515
  46. package/lib/core/templates.js +0 -556
  47. package/lib/pipeline/cli.js +0 -423
  48. package/lib/pipeline/engine.js +0 -928
  49. package/lib/pipeline/executor.js +0 -440
  50. package/lib/pipeline/index.js +0 -33
  51. package/lib/pipeline/integrations.js +0 -559
  52. package/lib/pipeline/schemas.js +0 -288
  53. package/lib/remote/client.js +0 -361
  54. package/lib/server/auth.js +0 -270
  55. package/lib/server/client-example.js +0 -190
  56. package/lib/server/executor.js +0 -477
  57. package/lib/server/index.js +0 -494
  58. package/lib/update-helpers.js +0 -505
  59. package/lib/validation.js +0 -460
@@ -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
- }