agileflow 2.33.1 → 2.35.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 (63) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +536 -0
  3. package/package.json +1 -1
  4. package/src/core/agents/adr-writer.md +3 -19
  5. package/src/core/agents/api.md +9 -43
  6. package/src/core/agents/ci.md +8 -40
  7. package/src/core/agents/configuration/archival.md +301 -0
  8. package/src/core/agents/configuration/attribution.md +318 -0
  9. package/src/core/agents/configuration/ci.md +1077 -0
  10. package/src/core/agents/configuration/git-config.md +511 -0
  11. package/src/core/agents/configuration/hooks.md +507 -0
  12. package/src/core/agents/configuration/verify.md +540 -0
  13. package/src/core/agents/devops.md +7 -35
  14. package/src/core/agents/documentation.md +0 -1
  15. package/src/core/agents/epic-planner.md +3 -22
  16. package/src/core/agents/mentor.md +8 -24
  17. package/src/core/agents/research.md +0 -7
  18. package/src/core/agents/security.md +0 -5
  19. package/src/core/agents/ui.md +8 -42
  20. package/src/core/commands/PATTERNS-AskUserQuestion.md +474 -0
  21. package/src/core/commands/adr.md +5 -0
  22. package/src/core/commands/agent.md +4 -0
  23. package/src/core/commands/assign.md +1 -0
  24. package/src/core/commands/auto.md +1 -1
  25. package/src/core/commands/babysit.md +147 -31
  26. package/src/core/commands/baseline.md +7 -0
  27. package/src/core/commands/blockers.md +2 -0
  28. package/src/core/commands/board.md +9 -0
  29. package/src/core/commands/configure.md +415 -0
  30. package/src/core/commands/context.md +1 -0
  31. package/src/core/commands/deps.md +2 -0
  32. package/src/core/commands/diagnose.md +0 -41
  33. package/src/core/commands/epic.md +8 -0
  34. package/src/core/commands/handoff.md +4 -0
  35. package/src/core/commands/impact.md +1 -1
  36. package/src/core/commands/metrics.md +10 -0
  37. package/src/core/commands/research.md +3 -0
  38. package/src/core/commands/retro.md +11 -1
  39. package/src/core/commands/sprint.md +2 -1
  40. package/src/core/commands/status.md +1 -0
  41. package/src/core/commands/story-validate.md +1 -1
  42. package/src/core/commands/story.md +29 -2
  43. package/src/core/commands/template.md +8 -0
  44. package/src/core/commands/update.md +1 -1
  45. package/src/core/commands/velocity.md +9 -0
  46. package/src/core/commands/verify.md +6 -0
  47. package/src/core/templates/validate-tokens.sh +0 -15
  48. package/src/core/templates/worktrees-guide.md +0 -4
  49. package/tools/agileflow-npx.js +21 -9
  50. package/tools/cli/commands/config.js +284 -0
  51. package/tools/cli/commands/doctor.js +221 -4
  52. package/tools/cli/commands/setup.js +4 -1
  53. package/tools/cli/commands/update.js +59 -15
  54. package/tools/cli/installers/core/installer.js +369 -37
  55. package/tools/cli/installers/ide/claude-code.js +1 -1
  56. package/tools/cli/installers/ide/cursor.js +1 -1
  57. package/tools/cli/installers/ide/windsurf.js +1 -1
  58. package/tools/cli/lib/docs-setup.js +52 -28
  59. package/tools/cli/lib/npm-utils.js +62 -0
  60. package/tools/cli/lib/ui.js +9 -2
  61. package/tools/postinstall.js +71 -13
  62. package/src/core/agents/context7.md +0 -164
  63. package/src/core/commands/setup.md +0 -708
@@ -6,10 +6,12 @@
6
6
 
7
7
  const chalk = require('chalk');
8
8
  const path = require('node:path');
9
+ const semver = require('semver');
9
10
  const { Installer } = require('../installers/core/installer');
10
11
  const { IdeManager } = require('../installers/ide/manager');
11
12
  const { displayLogo, displaySection, success, warning, error, info, confirm } = require('../lib/ui');
12
13
  const { createDocsStructure, getDocsFolderName } = require('../lib/docs-setup');
14
+ const { getLatestVersion } = require('../lib/npm-utils');
13
15
 
14
16
  const installer = new Installer();
15
17
  const ideManager = new IdeManager();
@@ -19,7 +21,7 @@ module.exports = {
19
21
  description: 'Update existing AgileFlow installation',
20
22
  options: [
21
23
  ['-d, --directory <path>', 'Project directory (default: current directory)'],
22
- ['--force', 'Force update, overwriting modified files'],
24
+ ['--force', 'Force reinstall (skip prompts; overwrites local changes)'],
23
25
  ],
24
26
  action: async (options) => {
25
27
  try {
@@ -38,21 +40,49 @@ module.exports = {
38
40
 
39
41
  displaySection('Updating AgileFlow', `Current version: ${status.version}`);
40
42
 
41
- // Get package version
43
+ // Get local CLI version and npm registry version
42
44
  const packageJson = require(path.join(__dirname, '..', '..', '..', 'package.json'));
43
- const newVersion = packageJson.version;
45
+ const localCliVersion = packageJson.version;
44
46
 
45
- console.log(chalk.bold('Current: '), status.version);
46
- console.log(chalk.bold('Latest: '), newVersion);
47
+ console.log(chalk.dim('Checking npm registry for latest version...'));
48
+ const npmLatestVersion = await getLatestVersion('agileflow');
47
49
 
48
- if (status.version === newVersion && !options.force) {
50
+ if (!npmLatestVersion) {
51
+ warning('Could not check npm registry for latest version');
52
+ console.log(chalk.dim('Continuing with local CLI version...\n'));
53
+ }
54
+
55
+ const latestVersion = npmLatestVersion || localCliVersion;
56
+
57
+ console.log(chalk.bold('Installed: '), status.version);
58
+ console.log(chalk.bold('CLI version: '), localCliVersion);
59
+ if (npmLatestVersion) {
60
+ console.log(chalk.bold('Latest (npm):'), npmLatestVersion);
61
+ }
62
+
63
+ // Check if CLI itself is outdated
64
+ if (npmLatestVersion && semver.lt(localCliVersion, npmLatestVersion)) {
65
+ console.log();
66
+ warning('Your CLI is outdated!');
67
+ console.log(chalk.dim(` To update your installation, run:\n`));
68
+ console.log(chalk.cyan(` npx agileflow@latest update\n`));
69
+
70
+ const useOutdated = options.force ? true : await confirm('Continue with outdated CLI anyway?');
71
+ if (!useOutdated) {
72
+ console.log(chalk.dim('\nUpdate cancelled\n'));
73
+ process.exit(0);
74
+ }
75
+ }
76
+
77
+ // Check if project installation is up to date
78
+ if (status.version === latestVersion && !options.force) {
49
79
  success('Already on the latest version');
50
80
  process.exit(0);
51
81
  }
52
82
 
53
83
  // Confirm update
54
84
  if (!options.force) {
55
- const proceed = await confirm(`Update to v${newVersion}?`);
85
+ const proceed = await confirm(`Update to v${latestVersion}?`);
56
86
  if (!proceed) {
57
87
  console.log(chalk.dim('\nUpdate cancelled\n'));
58
88
  process.exit(0);
@@ -64,17 +94,17 @@ module.exports = {
64
94
  // Get docs folder name from metadata (or default to 'docs')
65
95
  const docsFolder = await getDocsFolderName(directory);
66
96
 
67
- // Re-run installation with existing config
97
+ // Re-run installation with existing config from manifest
68
98
  const config = {
69
99
  directory,
70
100
  ides: status.ides || ['claude-code'],
71
- userName: 'Developer', // Could read from existing config
72
- agileflowFolder: path.basename(status.path),
73
- docsFolder,
101
+ userName: status.userName || 'Developer',
102
+ agileflowFolder: status.agileflowFolder || path.basename(status.path),
103
+ docsFolder: status.docsFolder || docsFolder,
74
104
  };
75
105
 
76
106
  // Run core installation
77
- const coreResult = await installer.install(config);
107
+ const coreResult = await installer.install(config, { force: options.force });
78
108
 
79
109
  if (!coreResult.success) {
80
110
  error('Update failed');
@@ -82,6 +112,20 @@ module.exports = {
82
112
  }
83
113
 
84
114
  success('Updated core content');
115
+ if (coreResult.fileOps) {
116
+ info(
117
+ `Files: ${coreResult.fileOps.created} added, ${coreResult.fileOps.updated} updated, ${coreResult.fileOps.preserved} preserved`
118
+ );
119
+ if (coreResult.fileOps.updatesPath) {
120
+ info(`Preserved-file updates saved to: ${coreResult.fileOps.updatesPath}`);
121
+ }
122
+ if (coreResult.fileOps.backupPath) {
123
+ info(`Backup saved to: ${coreResult.fileOps.backupPath}`);
124
+ }
125
+ if (coreResult.fileOps.preserved > 0 && !options.force) {
126
+ warning('Some local changes were preserved; use --force to overwrite them.');
127
+ }
128
+ }
85
129
 
86
130
  // Re-setup IDEs
87
131
  ideManager.setAgileflowFolder(config.agileflowFolder);
@@ -92,8 +136,8 @@ module.exports = {
92
136
  }
93
137
 
94
138
  // Create/update docs structure (idempotent - only creates missing files)
95
- displaySection('Updating Documentation Structure', `Folder: ${docsFolder}/`);
96
- const docsResult = await createDocsStructure(directory, docsFolder);
139
+ displaySection('Updating Documentation Structure', `Folder: ${config.docsFolder}/`);
140
+ const docsResult = await createDocsStructure(directory, config.docsFolder, { updateGitignore: false });
97
141
 
98
142
  if (!docsResult.success) {
99
143
  warning('Failed to update docs structure');
@@ -102,7 +146,7 @@ module.exports = {
102
146
  }
103
147
  }
104
148
 
105
- console.log(chalk.green(`\n✨ Update complete! (${status.version} → ${newVersion})\n`));
149
+ console.log(chalk.green(`\n✨ Update complete! (${status.version} → ${latestVersion})\n`));
106
150
 
107
151
  process.exit(0);
108
152
  } catch (err) {
@@ -5,11 +5,26 @@
5
5
  */
6
6
 
7
7
  const path = require('node:path');
8
+ const crypto = require('node:crypto');
8
9
  const fs = require('fs-extra');
9
10
  const chalk = require('chalk');
10
11
  const ora = require('ora');
11
12
  const yaml = require('js-yaml');
12
13
 
14
+ const TEXT_EXTENSIONS = new Set(['.md', '.yaml', '.yml', '.txt', '.json']);
15
+
16
+ function sha256Hex(data) {
17
+ return crypto.createHash('sha256').update(data).digest('hex');
18
+ }
19
+
20
+ function toPosixPath(filePath) {
21
+ return filePath.split(path.sep).join('/');
22
+ }
23
+
24
+ function safeTimestampForPath(date = new Date()) {
25
+ return date.toISOString().replace(/[:.]/g, '-');
26
+ }
27
+
13
28
  /**
14
29
  * Get the source path for AgileFlow content
15
30
  * @returns {string} Path to src directory
@@ -38,10 +53,13 @@ class Installer {
38
53
  /**
39
54
  * Install AgileFlow to a project
40
55
  * @param {Object} config - Installation configuration
56
+ * @param {Object} options - Installation options
57
+ * @param {boolean} options.force - Overwrite local changes
41
58
  * @returns {Promise<Object>} Installation result
42
59
  */
43
- async install(config) {
44
- const { directory, ides, userName, agileflowFolder } = config;
60
+ async install(config, options = {}) {
61
+ const { directory, ides, userName, agileflowFolder, docsFolder } = config;
62
+ const requestedForce = Boolean(options.force);
45
63
 
46
64
  const agileflowDir = path.join(directory, agileflowFolder);
47
65
  const spinner = ora('Installing AgileFlow...').start();
@@ -55,24 +73,76 @@ class Installer {
55
73
  const cfgDir = path.join(agileflowDir, '_cfg');
56
74
  await fs.ensureDir(cfgDir);
57
75
 
76
+ const manifestPath = path.join(cfgDir, 'manifest.yaml');
77
+ const fileIndexPath = path.join(cfgDir, 'files.json');
78
+
79
+ const hasManifest = await fs.pathExists(manifestPath);
80
+ const existingFileIndex = await this.readFileIndex(fileIndexPath);
81
+ const hasValidFileIndex = Boolean(existingFileIndex);
82
+
83
+ // Migration: installation predates file indexing
84
+ const isMigration = hasManifest && !hasValidFileIndex;
85
+ const effectiveForce = requestedForce || isMigration;
86
+
87
+ const timestamp = safeTimestampForPath();
88
+ let backupPath = null;
89
+ if (isMigration) {
90
+ spinner.text = 'Backing up existing installation...';
91
+ backupPath = await this.createBackup(agileflowDir, cfgDir, timestamp);
92
+ }
93
+
94
+ const fileIndex = existingFileIndex || { schema: 1, files: {} };
95
+ if (!fileIndex.files || typeof fileIndex.files !== 'object') {
96
+ fileIndex.files = {};
97
+ }
98
+
99
+ const fileOps = {
100
+ created: 0,
101
+ updated: 0,
102
+ preserved: 0,
103
+ stashed: 0,
104
+ backupPath,
105
+ updatesPath: null,
106
+ isMigration,
107
+ force: effectiveForce,
108
+ };
109
+
58
110
  // Copy core content
59
111
  spinner.text = 'Installing core content...';
60
112
  const coreSourcePath = path.join(this.sourcePath, 'core');
61
113
 
62
114
  if (await fs.pathExists(coreSourcePath)) {
63
- await this.copyContent(coreSourcePath, agileflowDir, agileflowFolder);
115
+ await this.copyContent(coreSourcePath, agileflowDir, agileflowFolder, {
116
+ agileflowDir,
117
+ cfgDir,
118
+ fileIndex,
119
+ fileOps,
120
+ force: effectiveForce,
121
+ timestamp,
122
+ });
64
123
  } else {
65
124
  // Fallback: copy from old structure (commands, agents, skills at root)
66
- await this.copyLegacyContent(directory, agileflowDir, agileflowFolder);
125
+ await this.copyLegacyContent(directory, agileflowDir, agileflowFolder, {
126
+ agileflowDir,
127
+ cfgDir,
128
+ fileIndex,
129
+ fileOps,
130
+ force: effectiveForce,
131
+ timestamp,
132
+ });
67
133
  }
68
134
 
69
135
  // Create config.yaml
70
136
  spinner.text = 'Creating configuration...';
71
- await this.createConfig(agileflowDir, userName, agileflowFolder);
137
+ await this.createConfig(agileflowDir, userName, agileflowFolder, { force: effectiveForce });
72
138
 
73
139
  // Create manifest
74
140
  spinner.text = 'Creating manifest...';
75
- await this.createManifest(cfgDir, ides);
141
+ await this.createManifest(cfgDir, { ides, userName, agileflowFolder, docsFolder }, { force: effectiveForce });
142
+
143
+ // Persist file index (used for safe future updates)
144
+ spinner.text = 'Writing file index...';
145
+ await this.writeFileIndex(fileIndexPath, fileIndex);
76
146
 
77
147
  // Count installed items
78
148
  const counts = await this.countInstalledItems(agileflowDir);
@@ -84,6 +154,7 @@ class Installer {
84
154
  path: agileflowDir,
85
155
  projectDir: directory,
86
156
  counts,
157
+ fileOps,
87
158
  };
88
159
  } catch (error) {
89
160
  spinner.fail('Installation failed');
@@ -96,8 +167,9 @@ class Installer {
96
167
  * @param {string} source - Source directory
97
168
  * @param {string} dest - Destination directory
98
169
  * @param {string} agileflowFolder - AgileFlow folder name
170
+ * @param {Object|null} policy - Copy policy (safe update behavior)
99
171
  */
100
- async copyContent(source, dest, agileflowFolder) {
172
+ async copyContent(source, dest, agileflowFolder, policy = null) {
101
173
  const entries = await fs.readdir(source, { withFileTypes: true });
102
174
 
103
175
  for (const entry of entries) {
@@ -106,9 +178,13 @@ class Installer {
106
178
 
107
179
  if (entry.isDirectory()) {
108
180
  await fs.ensureDir(destPath);
109
- await this.copyContent(srcPath, destPath, agileflowFolder);
181
+ await this.copyContent(srcPath, destPath, agileflowFolder, policy);
110
182
  } else {
111
- await this.copyFileWithReplacements(srcPath, destPath, agileflowFolder);
183
+ if (policy) {
184
+ await this.copyFileWithPolicy(srcPath, destPath, agileflowFolder, policy);
185
+ } else {
186
+ await this.copyFileWithReplacements(srcPath, destPath, agileflowFolder);
187
+ }
112
188
  }
113
189
  }
114
190
  }
@@ -118,8 +194,9 @@ class Installer {
118
194
  * @param {string} projectDir - Project directory
119
195
  * @param {string} agileflowDir - AgileFlow installation directory
120
196
  * @param {string} agileflowFolder - AgileFlow folder name
197
+ * @param {Object|null} policy - Copy policy (safe update behavior)
121
198
  */
122
- async copyLegacyContent(projectDir, agileflowDir, agileflowFolder) {
199
+ async copyLegacyContent(projectDir, agileflowDir, agileflowFolder, policy = null) {
123
200
  const packageRoot = this.packageRoot;
124
201
 
125
202
  // Copy commands
@@ -127,7 +204,7 @@ class Installer {
127
204
  const commandsDest = path.join(agileflowDir, 'commands');
128
205
  if (await fs.pathExists(commandsSource)) {
129
206
  await fs.ensureDir(commandsDest);
130
- await this.copyContent(commandsSource, commandsDest, agileflowFolder);
207
+ await this.copyContent(commandsSource, commandsDest, agileflowFolder, policy);
131
208
  }
132
209
 
133
210
  // Copy agents
@@ -135,7 +212,7 @@ class Installer {
135
212
  const agentsDest = path.join(agileflowDir, 'agents');
136
213
  if (await fs.pathExists(agentsSource)) {
137
214
  await fs.ensureDir(agentsDest);
138
- await this.copyContent(agentsSource, agentsDest, agileflowFolder);
215
+ await this.copyContent(agentsSource, agentsDest, agileflowFolder, policy);
139
216
  }
140
217
 
141
218
  // Copy skills
@@ -143,7 +220,7 @@ class Installer {
143
220
  const skillsDest = path.join(agileflowDir, 'skills');
144
221
  if (await fs.pathExists(skillsSource)) {
145
222
  await fs.ensureDir(skillsDest);
146
- await this.copyContent(skillsSource, skillsDest, agileflowFolder);
223
+ await this.copyContent(skillsSource, skillsDest, agileflowFolder, policy);
147
224
  }
148
225
 
149
226
  // Copy templates
@@ -151,7 +228,7 @@ class Installer {
151
228
  const templatesDest = path.join(agileflowDir, 'templates');
152
229
  if (await fs.pathExists(templatesSource)) {
153
230
  await fs.ensureDir(templatesDest);
154
- await this.copyContent(templatesSource, templatesDest, agileflowFolder);
231
+ await this.copyContent(templatesSource, templatesDest, agileflowFolder, policy);
155
232
  }
156
233
  }
157
234
 
@@ -162,10 +239,9 @@ class Installer {
162
239
  * @param {string} agileflowFolder - AgileFlow folder name
163
240
  */
164
241
  async copyFileWithReplacements(source, dest, agileflowFolder) {
165
- const textExtensions = ['.md', '.yaml', '.yml', '.txt', '.json'];
166
242
  const ext = path.extname(source).toLowerCase();
167
243
 
168
- if (textExtensions.includes(ext)) {
244
+ if (TEXT_EXTENSIONS.has(ext)) {
169
245
  let content = await fs.readFile(source, 'utf8');
170
246
 
171
247
  // Replace placeholders
@@ -178,43 +254,289 @@ class Installer {
178
254
  }
179
255
  }
180
256
 
257
+ /**
258
+ * Copy a file with safe update behavior.
259
+ * - If the destination file is unchanged since the last install, overwrite.
260
+ * - If it has local changes, preserve and write the new version to _cfg/updates/<timestamp>/...
261
+ * - If --force is set, overwrite all.
262
+ * @param {string} source - Source file path
263
+ * @param {string} dest - Destination file path
264
+ * @param {string} agileflowFolder - AgileFlow folder name
265
+ * @param {Object} policy - Copy policy
266
+ */
267
+ async copyFileWithPolicy(source, dest, agileflowFolder, policy) {
268
+ const { agileflowDir, cfgDir, fileIndex, fileOps, force, timestamp } = policy;
269
+
270
+ const relativePath = toPosixPath(path.relative(agileflowDir, dest));
271
+ const maybeRecord = fileIndex.files[relativePath];
272
+ const existingRecord = maybeRecord && typeof maybeRecord === 'object' ? maybeRecord : null;
273
+
274
+ const ext = path.extname(source).toLowerCase();
275
+ const isText = TEXT_EXTENSIONS.has(ext);
276
+
277
+ let newContent;
278
+ if (isText) {
279
+ let content = await fs.readFile(source, 'utf8');
280
+ content = content.replace(/\{agileflow_folder\}/g, agileflowFolder);
281
+ content = content.replace(/\{project-root\}/g, '{project-root}');
282
+ newContent = content;
283
+ } else {
284
+ newContent = await fs.readFile(source);
285
+ }
286
+
287
+ const newHash = sha256Hex(newContent);
288
+ const destExists = await fs.pathExists(dest);
289
+
290
+ if (!destExists) {
291
+ await fs.ensureDir(path.dirname(dest));
292
+ if (isText) {
293
+ await fs.writeFile(dest, newContent, 'utf8');
294
+ } else {
295
+ await fs.copy(source, dest);
296
+ }
297
+
298
+ fileIndex.files[relativePath] = { sha256: newHash, protected: false };
299
+ fileOps.created++;
300
+ return;
301
+ }
302
+
303
+ if (force) {
304
+ if (isText) {
305
+ await fs.writeFile(dest, newContent, 'utf8');
306
+ } else {
307
+ await fs.copy(source, dest);
308
+ }
309
+
310
+ fileIndex.files[relativePath] = { sha256: newHash, protected: false };
311
+ fileOps.updated++;
312
+ return;
313
+ }
314
+
315
+ const currentHash = await this.hashFile(dest);
316
+
317
+ // Respect previously protected files unless they now match upstream.
318
+ if (existingRecord && existingRecord.protected) {
319
+ if (currentHash === newHash) {
320
+ fileIndex.files[relativePath] = { sha256: newHash, protected: false };
321
+ return;
322
+ }
323
+
324
+ await this.writeStash(cfgDir, timestamp, relativePath, isText, newContent, source, fileOps);
325
+ fileIndex.files[relativePath] = { sha256: currentHash, protected: true };
326
+ fileOps.preserved++;
327
+ return;
328
+ }
329
+
330
+ // If we have a baseline hash and the file is unchanged since last install, we can update in place.
331
+ if (existingRecord && existingRecord.sha256 === currentHash) {
332
+ if (currentHash !== newHash) {
333
+ if (isText) {
334
+ await fs.writeFile(dest, newContent, 'utf8');
335
+ } else {
336
+ await fs.copy(source, dest);
337
+ }
338
+ fileOps.updated++;
339
+ }
340
+
341
+ fileIndex.files[relativePath] = { sha256: newHash, protected: false };
342
+ return;
343
+ }
344
+
345
+ // Unknown or locally modified file: preserve and stash the new version for manual merge.
346
+ if (currentHash === newHash) {
347
+ fileIndex.files[relativePath] = { sha256: newHash, protected: false };
348
+ return;
349
+ }
350
+
351
+ await this.writeStash(cfgDir, timestamp, relativePath, isText, newContent, source, fileOps);
352
+ fileIndex.files[relativePath] = { sha256: currentHash, protected: true };
353
+ fileOps.preserved++;
354
+ }
355
+
356
+ async writeStash(cfgDir, timestamp, relativePath, isText, newContent, source, fileOps) {
357
+ const updatesRoot = path.join(cfgDir, 'updates', timestamp);
358
+ const stashPath = path.join(updatesRoot, relativePath);
359
+
360
+ await fs.ensureDir(path.dirname(stashPath));
361
+ if (isText) {
362
+ await fs.writeFile(stashPath, newContent, 'utf8');
363
+ } else {
364
+ await fs.copy(source, stashPath);
365
+ }
366
+
367
+ fileOps.stashed++;
368
+ fileOps.updatesPath = updatesRoot;
369
+ }
370
+
371
+ async hashFile(filePath) {
372
+ const data = await fs.readFile(filePath);
373
+ return sha256Hex(data);
374
+ }
375
+
376
+ async readFileIndex(fileIndexPath) {
377
+ if (!(await fs.pathExists(fileIndexPath))) return null;
378
+ try {
379
+ const index = await fs.readJson(fileIndexPath);
380
+ if (!index || typeof index !== 'object') return null;
381
+ if (index.schema !== 1) return null;
382
+ if (!index.files || typeof index.files !== 'object') return null;
383
+ return index;
384
+ } catch {
385
+ return null;
386
+ }
387
+ }
388
+
389
+ async writeFileIndex(fileIndexPath, fileIndex) {
390
+ const packageJson = require(path.join(this.packageRoot, 'package.json'));
391
+ const nextIndex = {
392
+ schema: 1,
393
+ generated_at: new Date().toISOString(),
394
+ version: packageJson.version,
395
+ files: fileIndex.files || {},
396
+ };
397
+
398
+ await fs.writeJson(fileIndexPath, nextIndex, { spaces: 2 });
399
+ }
400
+
401
+ async createBackup(agileflowDir, cfgDir, timestamp) {
402
+ const backupRoot = path.join(cfgDir, 'backups', timestamp);
403
+ await fs.ensureDir(backupRoot);
404
+
405
+ const candidates = ['agents', 'commands', 'skills', 'templates', 'config.yaml'];
406
+ for (const name of candidates) {
407
+ const srcPath = path.join(agileflowDir, name);
408
+ if (await fs.pathExists(srcPath)) {
409
+ await fs.copy(srcPath, path.join(backupRoot, name));
410
+ }
411
+ }
412
+
413
+ const manifestPath = path.join(cfgDir, 'manifest.yaml');
414
+ if (await fs.pathExists(manifestPath)) {
415
+ await fs.copy(manifestPath, path.join(backupRoot, 'manifest.yaml'));
416
+ }
417
+
418
+ return backupRoot;
419
+ }
420
+
181
421
  /**
182
422
  * Create configuration file
183
423
  * @param {string} agileflowDir - AgileFlow directory
184
424
  * @param {string} userName - User name
185
425
  * @param {string} agileflowFolder - AgileFlow folder name
426
+ * @param {Object} options - Options
427
+ * @param {boolean} options.force - Overwrite existing file
186
428
  */
187
- async createConfig(agileflowDir, userName, agileflowFolder) {
188
- const config = {
189
- version: require(path.join(this.packageRoot, 'package.json')).version,
190
- user_name: userName,
191
- agileflow_folder: agileflowFolder,
192
- communication_language: 'English',
193
- created_at: new Date().toISOString(),
194
- };
195
-
429
+ async createConfig(agileflowDir, userName, agileflowFolder, options = {}) {
196
430
  const configPath = path.join(agileflowDir, 'config.yaml');
197
- await fs.writeFile(configPath, yaml.dump(config), 'utf8');
431
+ const packageJson = require(path.join(this.packageRoot, 'package.json'));
432
+
433
+ if (!(await fs.pathExists(configPath))) {
434
+ const config = {
435
+ version: packageJson.version,
436
+ user_name: userName,
437
+ agileflow_folder: agileflowFolder,
438
+ communication_language: 'English',
439
+ created_at: new Date().toISOString(),
440
+ };
441
+
442
+ await fs.writeFile(configPath, yaml.dump(config), 'utf8');
443
+ return;
444
+ }
445
+
446
+ try {
447
+ const existingContent = await fs.readFile(configPath, 'utf8');
448
+ const loaded = yaml.load(existingContent);
449
+ const existing = loaded && typeof loaded === 'object' && !Array.isArray(loaded) ? loaded : {};
450
+
451
+ const next = {
452
+ ...existing,
453
+ version: packageJson.version,
454
+ user_name: userName,
455
+ agileflow_folder: agileflowFolder,
456
+ created_at: existing.created_at || new Date().toISOString(),
457
+ updated_at: new Date().toISOString(),
458
+ };
459
+
460
+ await fs.writeFile(configPath, yaml.dump(next), 'utf8');
461
+ } catch {
462
+ if (options.force) {
463
+ const config = {
464
+ version: packageJson.version,
465
+ user_name: userName,
466
+ agileflow_folder: agileflowFolder,
467
+ communication_language: 'English',
468
+ created_at: new Date().toISOString(),
469
+ };
470
+
471
+ await fs.writeFile(configPath, yaml.dump(config), 'utf8');
472
+ }
473
+ }
198
474
  }
199
475
 
200
476
  /**
201
477
  * Create manifest file
202
478
  * @param {string} cfgDir - Config directory
203
- * @param {string[]} ides - Selected IDEs
479
+ * @param {Object} config - Manifest configuration
480
+ * @param {Object} options - Options
481
+ * @param {boolean} options.force - Overwrite existing file
204
482
  */
205
- async createManifest(cfgDir, ides) {
483
+ async createManifest(cfgDir, config, options = {}) {
484
+ const { ides, userName, agileflowFolder, docsFolder } = config;
206
485
  const packageJson = require(path.join(this.packageRoot, 'package.json'));
207
486
 
208
- const manifest = {
209
- version: packageJson.version,
210
- installed_at: new Date().toISOString(),
211
- updated_at: new Date().toISOString(),
212
- ides: ides,
213
- modules: ['core'],
214
- };
215
-
216
487
  const manifestPath = path.join(cfgDir, 'manifest.yaml');
217
- await fs.writeFile(manifestPath, yaml.dump(manifest), 'utf8');
488
+ const now = new Date().toISOString();
489
+
490
+ if (!(await fs.pathExists(manifestPath))) {
491
+ const manifest = {
492
+ version: packageJson.version,
493
+ installed_at: now,
494
+ updated_at: now,
495
+ ides: ides,
496
+ modules: ['core'],
497
+ user_name: userName,
498
+ agileflow_folder: agileflowFolder || '.agileflow',
499
+ docs_folder: docsFolder || 'docs',
500
+ };
501
+
502
+ await fs.writeFile(manifestPath, yaml.dump(manifest), 'utf8');
503
+ return;
504
+ }
505
+
506
+ try {
507
+ const existingContent = await fs.readFile(manifestPath, 'utf8');
508
+ const loaded = yaml.load(existingContent);
509
+ const existing = loaded && typeof loaded === 'object' && !Array.isArray(loaded) ? loaded : {};
510
+
511
+ const manifest = {
512
+ ...existing,
513
+ version: packageJson.version,
514
+ installed_at: existing.installed_at || now,
515
+ updated_at: now,
516
+ ides: ides,
517
+ modules: existing.modules || ['core'],
518
+ user_name: userName,
519
+ agileflow_folder: agileflowFolder || existing.agileflow_folder || '.agileflow',
520
+ docs_folder: docsFolder || existing.docs_folder || 'docs',
521
+ };
522
+
523
+ await fs.writeFile(manifestPath, yaml.dump(manifest), 'utf8');
524
+ } catch {
525
+ if (options.force) {
526
+ const manifest = {
527
+ version: packageJson.version,
528
+ installed_at: now,
529
+ updated_at: now,
530
+ ides: ides,
531
+ modules: ['core'],
532
+ user_name: userName,
533
+ agileflow_folder: agileflowFolder || '.agileflow',
534
+ docs_folder: docsFolder || 'docs',
535
+ };
536
+
537
+ await fs.writeFile(manifestPath, yaml.dump(manifest), 'utf8');
538
+ }
539
+ }
218
540
  }
219
541
 
220
542
  /**
@@ -265,6 +587,11 @@ class Installer {
265
587
  version: null,
266
588
  ides: [],
267
589
  modules: [],
590
+ userName: null,
591
+ agileflowFolder: null,
592
+ docsFolder: null,
593
+ installedAt: null,
594
+ updatedAt: null,
268
595
  };
269
596
 
270
597
  // Look for AgileFlow installation
@@ -284,6 +611,11 @@ class Installer {
284
611
  status.version = manifest.version;
285
612
  status.ides = manifest.ides || [];
286
613
  status.modules = manifest.modules || [];
614
+ status.userName = manifest.user_name || 'Developer';
615
+ status.agileflowFolder = manifest.agileflow_folder || folder;
616
+ status.docsFolder = manifest.docs_folder || 'docs';
617
+ status.installedAt = manifest.installed_at;
618
+ status.updatedAt = manifest.updated_at;
287
619
 
288
620
  break;
289
621
  }
@@ -26,7 +26,7 @@ class ClaudeCodeSetup extends BaseIdeSetup {
26
26
  * @param {Object} options - Setup options
27
27
  */
28
28
  async setup(projectDir, agileflowDir, options = {}) {
29
- console.log(chalk.hex('#C15F3C')(` Setting up ${this.displayName}...`));
29
+ console.log(chalk.hex('#e8683a')(` Setting up ${this.displayName}...`));
30
30
 
31
31
  // Clean up old installation first
32
32
  await this.cleanup(projectDir);