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.
- package/LICENSE +22 -0
- package/README.md +536 -0
- package/package.json +1 -1
- package/src/core/agents/adr-writer.md +3 -19
- package/src/core/agents/api.md +9 -43
- package/src/core/agents/ci.md +8 -40
- package/src/core/agents/configuration/archival.md +301 -0
- package/src/core/agents/configuration/attribution.md +318 -0
- package/src/core/agents/configuration/ci.md +1077 -0
- package/src/core/agents/configuration/git-config.md +511 -0
- package/src/core/agents/configuration/hooks.md +507 -0
- package/src/core/agents/configuration/verify.md +540 -0
- package/src/core/agents/devops.md +7 -35
- package/src/core/agents/documentation.md +0 -1
- package/src/core/agents/epic-planner.md +3 -22
- package/src/core/agents/mentor.md +8 -24
- package/src/core/agents/research.md +0 -7
- package/src/core/agents/security.md +0 -5
- package/src/core/agents/ui.md +8 -42
- package/src/core/commands/PATTERNS-AskUserQuestion.md +474 -0
- package/src/core/commands/adr.md +5 -0
- package/src/core/commands/agent.md +4 -0
- package/src/core/commands/assign.md +1 -0
- package/src/core/commands/auto.md +1 -1
- package/src/core/commands/babysit.md +147 -31
- package/src/core/commands/baseline.md +7 -0
- package/src/core/commands/blockers.md +2 -0
- package/src/core/commands/board.md +9 -0
- package/src/core/commands/configure.md +415 -0
- package/src/core/commands/context.md +1 -0
- package/src/core/commands/deps.md +2 -0
- package/src/core/commands/diagnose.md +0 -41
- package/src/core/commands/epic.md +8 -0
- package/src/core/commands/handoff.md +4 -0
- package/src/core/commands/impact.md +1 -1
- package/src/core/commands/metrics.md +10 -0
- package/src/core/commands/research.md +3 -0
- package/src/core/commands/retro.md +11 -1
- package/src/core/commands/sprint.md +2 -1
- package/src/core/commands/status.md +1 -0
- package/src/core/commands/story-validate.md +1 -1
- package/src/core/commands/story.md +29 -2
- package/src/core/commands/template.md +8 -0
- package/src/core/commands/update.md +1 -1
- package/src/core/commands/velocity.md +9 -0
- package/src/core/commands/verify.md +6 -0
- package/src/core/templates/validate-tokens.sh +0 -15
- package/src/core/templates/worktrees-guide.md +0 -4
- package/tools/agileflow-npx.js +21 -9
- package/tools/cli/commands/config.js +284 -0
- package/tools/cli/commands/doctor.js +221 -4
- package/tools/cli/commands/setup.js +4 -1
- package/tools/cli/commands/update.js +59 -15
- package/tools/cli/installers/core/installer.js +369 -37
- package/tools/cli/installers/ide/claude-code.js +1 -1
- package/tools/cli/installers/ide/cursor.js +1 -1
- package/tools/cli/installers/ide/windsurf.js +1 -1
- package/tools/cli/lib/docs-setup.js +52 -28
- package/tools/cli/lib/npm-utils.js +62 -0
- package/tools/cli/lib/ui.js +9 -2
- package/tools/postinstall.js +71 -13
- package/src/core/agents/context7.md +0 -164
- 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
|
|
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
|
|
43
|
+
// Get local CLI version and npm registry version
|
|
42
44
|
const packageJson = require(path.join(__dirname, '..', '..', '..', 'package.json'));
|
|
43
|
-
const
|
|
45
|
+
const localCliVersion = packageJson.version;
|
|
44
46
|
|
|
45
|
-
console.log(chalk.
|
|
46
|
-
|
|
47
|
+
console.log(chalk.dim('Checking npm registry for latest version...'));
|
|
48
|
+
const npmLatestVersion = await getLatestVersion('agileflow');
|
|
47
49
|
|
|
48
|
-
if (
|
|
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${
|
|
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',
|
|
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} → ${
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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 {
|
|
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,
|
|
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
|
-
|
|
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('#
|
|
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);
|