create-byan-agent 2.7.8 → 2.8.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.
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Version Comparison Utility
3
+ *
4
+ * Compares semver versions and fetches latest from npm registry.
5
+ *
6
+ * @module utils/version-compare
7
+ */
8
+
9
+ const https = require('https');
10
+
11
+ /**
12
+ * Compare two semver version strings.
13
+ *
14
+ * @param {string} a - First version (e.g., '2.7.9')
15
+ * @param {string} b - Second version (e.g., '2.8.0')
16
+ * @returns {number} -1 if a < b, 0 if equal, 1 if a > b
17
+ */
18
+ function compareVersions(a, b) {
19
+ const partsA = a.replace(/^v/, '').split('.').map(Number);
20
+ const partsB = b.replace(/^v/, '').split('.').map(Number);
21
+ const len = Math.max(partsA.length, partsB.length);
22
+
23
+ for (let i = 0; i < len; i++) {
24
+ const numA = partsA[i] || 0;
25
+ const numB = partsB[i] || 0;
26
+ if (numA < numB) return -1;
27
+ if (numA > numB) return 1;
28
+ }
29
+
30
+ return 0;
31
+ }
32
+
33
+ /**
34
+ * Fetch the latest published version of a package from npm registry.
35
+ *
36
+ * @param {string} packageName - npm package name
37
+ * @returns {Promise<string>} Latest version string
38
+ */
39
+ function getLatestVersion(packageName) {
40
+ const url = `https://registry.npmjs.org/${packageName}/latest`;
41
+
42
+ return new Promise((resolve, reject) => {
43
+ https.get(url, { headers: { 'Accept': 'application/json' } }, (res) => {
44
+ if (res.statusCode !== 200) {
45
+ reject(new Error(`npm registry returned ${res.statusCode} for ${packageName}`));
46
+ res.resume();
47
+ return;
48
+ }
49
+
50
+ let data = '';
51
+ res.on('data', (chunk) => { data += chunk; });
52
+ res.on('end', () => {
53
+ try {
54
+ const parsed = JSON.parse(data);
55
+ resolve(parsed.version);
56
+ } catch (err) {
57
+ reject(new Error(`Failed to parse npm registry response: ${err.message}`));
58
+ }
59
+ });
60
+ }).on('error', (err) => {
61
+ reject(new Error(`Failed to reach npm registry: ${err.message}`));
62
+ });
63
+ });
64
+ }
65
+
66
+ module.exports = {
67
+ compareVersions,
68
+ getLatestVersion
69
+ };
@@ -1,15 +1,15 @@
1
1
  /**
2
2
  * BACKUPER Module
3
- *
4
- * Backs up and restores _bmad/ directory.
5
- *
6
- * Phase 6: 24h development
7
- *
3
+ *
4
+ * Backs up and restores _byan/ directory with timestamp-based snapshots.
5
+ *
8
6
  * @module yanstaller/backuper
9
7
  */
10
8
 
9
+ const fs = require('fs-extra');
11
10
  const path = require('path');
12
- const fileUtils = require('../utils/file-utils');
11
+
12
+ const BACKUP_PREFIX = '_byan.backup-';
13
13
 
14
14
  /**
15
15
  * @typedef {Object} BackupResult
@@ -20,75 +20,130 @@ const fileUtils = require('../utils/file-utils');
20
20
  */
21
21
 
22
22
  /**
23
- * Backup _bmad/ directory
24
- *
25
- * @param {string} bmadPath - Path to _bmad/ directory
23
+ * Recursively count files and total size in a directory.
24
+ *
25
+ * @param {string} dir - Directory to measure
26
+ * @returns {Promise<{count: number, size: number}>}
27
+ */
28
+ async function measureDir(dir) {
29
+ let count = 0;
30
+ let size = 0;
31
+ const entries = await fs.readdir(dir, { withFileTypes: true });
32
+
33
+ for (const entry of entries) {
34
+ const fullPath = path.join(dir, entry.name);
35
+ if (entry.isDirectory()) {
36
+ const sub = await measureDir(fullPath);
37
+ count += sub.count;
38
+ size += sub.size;
39
+ } else if (entry.isFile()) {
40
+ const stat = await fs.stat(fullPath);
41
+ count++;
42
+ size += stat.size;
43
+ }
44
+ }
45
+
46
+ return { count, size };
47
+ }
48
+
49
+ /**
50
+ * Backup _byan/ directory to a timestamped snapshot.
51
+ *
52
+ * @param {string} byanPath - Absolute path to _byan/ directory
26
53
  * @returns {Promise<BackupResult>}
27
54
  */
28
- async function backup(bmadPath) {
55
+ async function backup(byanPath) {
56
+ if (!await fs.pathExists(byanPath)) {
57
+ throw new BackupError(`Source directory does not exist: ${byanPath}`);
58
+ }
59
+
29
60
  const timestamp = Date.now();
30
- const backupPath = `${bmadPath}.backup-${timestamp}`;
31
-
61
+ const backupPath = path.join(path.dirname(byanPath), `${BACKUP_PREFIX}${timestamp}`);
62
+
32
63
  try {
33
- // TODO: Copy entire _bmad/ to backup path
34
- // await fileUtils.copy(bmadPath, backupPath);
35
-
64
+ await fs.copy(byanPath, backupPath);
65
+ const { count, size } = await measureDir(backupPath);
66
+
36
67
  return {
37
68
  success: true,
38
69
  backupPath,
39
- filesBackedUp: 0,
40
- size: 0
70
+ filesBackedUp: count,
71
+ size
41
72
  };
42
73
  } catch (error) {
43
- throw new BackupError(`Failed to backup ${bmadPath}`, { cause: error });
74
+ throw new BackupError(`Failed to backup ${byanPath}`, { cause: error });
44
75
  }
45
76
  }
46
77
 
47
78
  /**
48
- * Restore from backup
49
- *
50
- * @param {string} backupPath - Path to backup directory
51
- * @param {string} targetPath - Target restoration path
79
+ * Restore from a backup, replacing the current _byan/ directory.
80
+ *
81
+ * @param {string} backupPath - Absolute path to the backup directory
82
+ * @param {string} targetPath - Absolute path to restore into (e.g., _byan/)
52
83
  * @returns {Promise<void>}
53
84
  */
54
85
  async function restore(backupPath, targetPath) {
55
- // TODO: Remove current _bmad/, copy backup to target
56
- // await fileUtils.remove(targetPath);
57
- // await fileUtils.copy(backupPath, targetPath);
86
+ if (!await fs.pathExists(backupPath)) {
87
+ throw new BackupError(`Backup not found: ${backupPath}`);
88
+ }
89
+
90
+ await fs.remove(targetPath);
91
+ await fs.copy(backupPath, targetPath);
58
92
  }
59
93
 
60
94
  /**
61
- * List available backups
62
- *
95
+ * List available backup directories sorted by timestamp (newest first).
96
+ *
63
97
  * @param {string} projectRoot - Project root directory
64
- * @returns {Promise<string[]>} - Array of backup paths
98
+ * @returns {Promise<string[]>} Absolute paths to backup directories
65
99
  */
66
100
  async function listBackups(projectRoot) {
67
- // TODO: Find all _bmad.backup-* directories
68
- return [];
101
+ if (!await fs.pathExists(projectRoot)) return [];
102
+
103
+ const entries = await fs.readdir(projectRoot, { withFileTypes: true });
104
+ const backups = entries
105
+ .filter(e => e.isDirectory() && e.name.startsWith(BACKUP_PREFIX))
106
+ .map(e => ({
107
+ name: e.name,
108
+ timestamp: parseInt(e.name.slice(BACKUP_PREFIX.length), 10),
109
+ path: path.join(projectRoot, e.name)
110
+ }))
111
+ .filter(b => !isNaN(b.timestamp))
112
+ .sort((a, b) => b.timestamp - a.timestamp);
113
+
114
+ return backups.map(b => b.path);
69
115
  }
70
116
 
71
117
  /**
72
- * Clean old backups (keep last N)
73
- *
118
+ * Prune old backups, keeping only the N most recent.
119
+ *
74
120
  * @param {string} projectRoot - Project root directory
75
- * @param {number} keep - Number of backups to keep
76
- * @returns {Promise<number>} - Number of backups deleted
121
+ * @param {number} [maxBackups=3] - Number of backups to keep
122
+ * @returns {Promise<number>} Number of backups deleted
77
123
  */
78
- async function cleanOldBackups(projectRoot, keep = 3) {
79
- // TODO: Sort by timestamp, delete oldest
80
- return 0;
124
+ async function pruneBackups(projectRoot, maxBackups = 3) {
125
+ const all = await listBackups(projectRoot);
126
+
127
+ if (all.length <= maxBackups) return 0;
128
+
129
+ const toDelete = all.slice(maxBackups);
130
+ for (const backupPath of toDelete) {
131
+ await fs.remove(backupPath);
132
+ }
133
+
134
+ return toDelete.length;
81
135
  }
82
136
 
83
137
  /**
84
- * Get backup size
85
- *
86
- * @param {string} backupPath - Path to backup directory
87
- * @returns {Promise<number>} - Size in bytes
138
+ * Get total size of a backup directory in bytes.
139
+ *
140
+ * @param {string} backupPath - Absolute path to backup directory
141
+ * @returns {Promise<number>} Size in bytes
88
142
  */
89
143
  async function getBackupSize(backupPath) {
90
- // TODO: Recursively calculate directory size
91
- return 0;
144
+ if (!await fs.pathExists(backupPath)) return 0;
145
+ const { size } = await measureDir(backupPath);
146
+ return size;
92
147
  }
93
148
 
94
149
  class BackupError extends Error {
@@ -102,7 +157,8 @@ module.exports = {
102
157
  backup,
103
158
  restore,
104
159
  listBackups,
105
- cleanOldBackups,
160
+ pruneBackups,
106
161
  getBackupSize,
107
- BackupError
162
+ BackupError,
163
+ BACKUP_PREFIX
108
164
  };
@@ -6,6 +6,7 @@
6
6
  * @module yanstaller
7
7
  */
8
8
 
9
+ const path = require('path');
9
10
  const detector = require('./detector');
10
11
  const recommender = require('./recommender');
11
12
  const installer = require('./installer');
@@ -13,6 +14,7 @@ const validator = require('./validator');
13
14
  const troubleshooter = require('./troubleshooter');
14
15
  const interviewer = require('./interviewer');
15
16
  const backuper = require('./backuper');
17
+ const updater = require('./updater');
16
18
  const wizard = require('./wizard');
17
19
  const platformSelector = require('./platform-selector');
18
20
  const logger = require('../utils/logger');
@@ -122,18 +124,53 @@ async function uninstall() {
122
124
  /**
123
125
  * Update existing BYAN installation
124
126
  *
125
- * @param {string} version - Target version
127
+ * @param {string} projectRoot - Project root directory
128
+ * @param {Object} [options={}] - Update options
129
+ * @param {boolean} [options.force] - Force update even if same version
130
+ * @param {boolean} [options.preview] - Show diff without applying
131
+ * @returns {Promise<import('./updater').UpdateResult>}
132
+ */
133
+ async function update(projectRoot, options = {}) {
134
+ return updater.update(projectRoot, options);
135
+ }
136
+
137
+ /**
138
+ * Rollback to the most recent backup.
139
+ *
140
+ * @param {string} projectRoot - Project root directory
126
141
  * @returns {Promise<void>}
127
142
  */
128
- async function update(version) {
129
- // TODO: Backup Update agents → Merge configs
143
+ async function rollback(projectRoot) {
144
+ const backups = await backuper.listBackups(projectRoot);
145
+ if (backups.length === 0) {
146
+ throw new Error('No backups found to restore from.');
147
+ }
148
+
149
+ const latestBackup = backups[0];
150
+ const targetPath = path.join(projectRoot, '_byan');
151
+ logger.info(`Restoring from ${path.basename(latestBackup)}...`);
152
+ await backuper.restore(latestBackup, targetPath);
153
+ }
154
+
155
+ /**
156
+ * List all available BYAN backups.
157
+ *
158
+ * @param {string} projectRoot - Project root directory
159
+ * @returns {Promise<string[]>} Absolute paths, newest first
160
+ */
161
+ async function listBackups(projectRoot) {
162
+ return backuper.listBackups(projectRoot);
130
163
  }
131
164
 
132
165
  module.exports = {
133
166
  install,
134
167
  uninstall,
135
168
  update,
169
+ rollback,
170
+ listBackups,
136
171
  // Expose for testing
137
172
  detector,
138
- platformSelector
173
+ platformSelector,
174
+ updater,
175
+ backuper
139
176
  };
@@ -0,0 +1,271 @@
1
+ /**
2
+ * UPDATER Module
3
+ *
4
+ * Orchestrates the BYAN update lifecycle:
5
+ * version check -> diff -> backup -> apply -> manifest -> validate.
6
+ *
7
+ * @module yanstaller/updater
8
+ */
9
+
10
+ const fs = require('fs-extra');
11
+ const path = require('path');
12
+ const { compareVersions, getLatestVersion } = require('../utils/version-compare');
13
+ const { diffFiles } = require('../utils/file-differ');
14
+ const { readManifest, writeManifest, generateManifest, detectUserModifications } = require('../utils/manifest');
15
+ const backuper = require('./backuper');
16
+ const logger = require('../utils/logger');
17
+
18
+ /**
19
+ * Resolve the template directory containing the canonical _byan/ files.
20
+ * Same logic as getTemplateDir() in create-byan-agent-v2.js.
21
+ *
22
+ * @returns {string|null} Absolute path to templates/ or null
23
+ */
24
+ function getTemplateDir() {
25
+ const npmPackagePath = path.join(__dirname, '..', '..', 'templates');
26
+ if (fs.existsSync(npmPackagePath)) return npmPackagePath;
27
+
28
+ const devPath = path.join(__dirname, '..', '..', '..');
29
+ if (fs.existsSync(devPath)) return devPath;
30
+
31
+ return null;
32
+ }
33
+
34
+ /**
35
+ * @typedef {Object} UpdateCheck
36
+ * @property {boolean} updateAvailable
37
+ * @property {string} installed - Currently installed version
38
+ * @property {string} latest - Latest version on npm
39
+ * @property {string[]} changes - List of files that would change
40
+ */
41
+
42
+ /**
43
+ * Check whether an update is available.
44
+ *
45
+ * @param {string} projectRoot - Project root directory
46
+ * @returns {Promise<UpdateCheck>}
47
+ */
48
+ async function checkForUpdate(projectRoot) {
49
+ const installedVersion = await getInstalledVersion(projectRoot);
50
+ const latest = await getLatestVersion('create-byan-agent');
51
+ const cmp = compareVersions(installedVersion, latest);
52
+
53
+ let changes = [];
54
+ if (cmp < 0) {
55
+ const templateDir = getTemplateDir();
56
+ if (templateDir) {
57
+ const templateByan = path.join(templateDir, '_byan');
58
+ const installedByan = path.join(projectRoot, '_byan');
59
+ if (await fs.pathExists(templateByan) && await fs.pathExists(installedByan)) {
60
+ const diff = await diffFiles(installedByan, templateByan);
61
+ changes = [...diff.toUpdate, ...diff.toAdd];
62
+ }
63
+ }
64
+ }
65
+
66
+ return {
67
+ updateAvailable: cmp < 0,
68
+ installed: installedVersion,
69
+ latest,
70
+ changes
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Preview what an update would do without applying changes.
76
+ *
77
+ * @param {string} projectRoot - Project root directory
78
+ * @returns {Promise<{diff: import('../utils/file-differ').DiffResult, userModified: string[], installed: string, latest: string}>}
79
+ */
80
+ async function preview(projectRoot) {
81
+ const installedVersion = await getInstalledVersion(projectRoot);
82
+ const latest = await getLatestVersion('create-byan-agent');
83
+
84
+ const templateDir = getTemplateDir();
85
+ if (!templateDir) {
86
+ throw new Error('Template directory not found. Is the package installed correctly?');
87
+ }
88
+
89
+ const templateByan = path.join(templateDir, '_byan');
90
+ const installedByan = path.join(projectRoot, '_byan');
91
+
92
+ if (!await fs.pathExists(installedByan)) {
93
+ throw new Error('No _byan/ directory found. Run install first.');
94
+ }
95
+
96
+ const diff = await diffFiles(installedByan, templateByan);
97
+ const userModified = await detectUserModifications(projectRoot);
98
+
99
+ return { diff, userModified, installed: installedVersion, latest };
100
+ }
101
+
102
+ /**
103
+ * @typedef {Object} UpdateOptions
104
+ * @property {boolean} [force=false] - Force update even if same version
105
+ * @property {boolean} [preview=false] - Only show diff, don't apply
106
+ */
107
+
108
+ /**
109
+ * @typedef {Object} UpdateResult
110
+ * @property {boolean} success
111
+ * @property {string} previousVersion
112
+ * @property {string} newVersion
113
+ * @property {string} backupPath
114
+ * @property {number} filesUpdated
115
+ * @property {number} filesAdded
116
+ * @property {number} filesSkipped - User-modified files left untouched
117
+ */
118
+
119
+ /**
120
+ * Execute the full update flow.
121
+ *
122
+ * @param {string} projectRoot - Project root directory
123
+ * @param {UpdateOptions} [options={}]
124
+ * @returns {Promise<UpdateResult>}
125
+ */
126
+ async function update(projectRoot, options = {}) {
127
+ const installedVersion = await getInstalledVersion(projectRoot);
128
+ const latest = await getLatestVersion('create-byan-agent');
129
+ const cmp = compareVersions(installedVersion, latest);
130
+
131
+ if (cmp >= 0 && !options.force) {
132
+ logger.info(`Already up to date (${installedVersion})`);
133
+ return {
134
+ success: true,
135
+ previousVersion: installedVersion,
136
+ newVersion: installedVersion,
137
+ backupPath: null,
138
+ filesUpdated: 0,
139
+ filesAdded: 0,
140
+ filesSkipped: 0
141
+ };
142
+ }
143
+
144
+ const templateDir = getTemplateDir();
145
+ if (!templateDir) {
146
+ throw new Error('Template directory not found. Is the package installed correctly?');
147
+ }
148
+
149
+ const templateByan = path.join(templateDir, '_byan');
150
+ const installedByan = path.join(projectRoot, '_byan');
151
+
152
+ if (!await fs.pathExists(templateByan)) {
153
+ throw new Error('Template _byan/ directory not found.');
154
+ }
155
+ if (!await fs.pathExists(installedByan)) {
156
+ throw new Error('No _byan/ directory found. Run install first.');
157
+ }
158
+
159
+ const diff = await diffFiles(installedByan, templateByan);
160
+ const userModified = new Set(await detectUserModifications(projectRoot));
161
+
162
+ if (options.preview) {
163
+ return formatPreviewResult(diff, userModified, installedVersion, latest);
164
+ }
165
+
166
+ // Backup before applying changes
167
+ const backupResult = await backuper.backup(installedByan);
168
+ logger.debug(`Backup created: ${backupResult.backupPath} (${backupResult.filesBackedUp} files)`);
169
+
170
+ let filesUpdated = 0;
171
+ let filesAdded = 0;
172
+ let filesSkipped = 0;
173
+
174
+ try {
175
+ // Apply updated files (skip user-modified unless forced)
176
+ for (const file of diff.toUpdate) {
177
+ if (userModified.has(file) && !options.force) {
178
+ filesSkipped++;
179
+ logger.debug(`Skipped (user-modified): ${file}`);
180
+ continue;
181
+ }
182
+ const src = path.join(templateByan, file);
183
+ const dest = path.join(installedByan, file);
184
+ await fs.ensureDir(path.dirname(dest));
185
+ await fs.copy(src, dest);
186
+ filesUpdated++;
187
+ }
188
+
189
+ // Add new files
190
+ for (const file of diff.toAdd) {
191
+ const src = path.join(templateByan, file);
192
+ const dest = path.join(installedByan, file);
193
+ await fs.ensureDir(path.dirname(dest));
194
+ await fs.copy(src, dest);
195
+ filesAdded++;
196
+ }
197
+
198
+ // Regenerate manifest with new state
199
+ const newManifest = await generateManifest(installedByan, latest);
200
+ await writeManifest(projectRoot, newManifest);
201
+
202
+ // Prune old backups
203
+ await backuper.pruneBackups(projectRoot);
204
+
205
+ } catch (error) {
206
+ // Rollback on failure
207
+ logger.error(`Update failed, restoring backup: ${error.message}`);
208
+ await backuper.restore(backupResult.backupPath, installedByan);
209
+ throw error;
210
+ }
211
+
212
+ return {
213
+ success: true,
214
+ previousVersion: installedVersion,
215
+ newVersion: latest,
216
+ backupPath: backupResult.backupPath,
217
+ filesUpdated,
218
+ filesAdded,
219
+ filesSkipped
220
+ };
221
+ }
222
+
223
+ /**
224
+ * Read the installed BYAN version from the manifest or package.json fallback.
225
+ *
226
+ * @param {string} projectRoot - Project root
227
+ * @returns {Promise<string>}
228
+ */
229
+ async function getInstalledVersion(projectRoot) {
230
+ const manifest = await readManifest(projectRoot);
231
+ if (manifest && manifest.version) return manifest.version;
232
+
233
+ // Fallback: read from the package that shipped this code
234
+ const pkgPath = path.join(__dirname, '..', '..', 'package.json');
235
+ if (await fs.pathExists(pkgPath)) {
236
+ const pkg = await fs.readJSON(pkgPath);
237
+ return pkg.version;
238
+ }
239
+
240
+ return '0.0.0';
241
+ }
242
+
243
+ /**
244
+ * Format a preview-only result for display.
245
+ *
246
+ * @param {import('../utils/file-differ').DiffResult} diff
247
+ * @param {Set<string>} userModified
248
+ * @param {string} installedVersion
249
+ * @param {string} latest
250
+ * @returns {UpdateResult}
251
+ */
252
+ function formatPreviewResult(diff, userModified, installedVersion, latest) {
253
+ const skippable = diff.toUpdate.filter(f => userModified.has(f));
254
+ return {
255
+ success: true,
256
+ previousVersion: installedVersion,
257
+ newVersion: latest,
258
+ backupPath: null,
259
+ filesUpdated: diff.toUpdate.length - skippable.length,
260
+ filesAdded: diff.toAdd.length,
261
+ filesSkipped: skippable.length
262
+ };
263
+ }
264
+
265
+ module.exports = {
266
+ checkForUpdate,
267
+ preview,
268
+ update,
269
+ getInstalledVersion,
270
+ getTemplateDir
271
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-byan-agent",
3
- "version": "2.7.8",
3
+ "version": "2.8.0",
4
4
  "description": "BYAN v2.2.2 - Intelligent AI agent installer with multi-platform native support (GitHub Copilot CLI, Claude Code, Codex/OpenCode)",
5
5
  "bin": {
6
6
  "create-byan-agent": "bin/create-byan-agent-v2.js"
@@ -44,7 +44,8 @@
44
44
  "fs-extra": "^11.2.0",
45
45
  "inquirer": "^8.2.5",
46
46
  "js-yaml": "^4.1.0",
47
- "ora": "^5.4.1"
47
+ "ora": "^5.4.1",
48
+ "ws": "^8.20.0"
48
49
  },
49
50
  "engines": {
50
51
  "node": ">=18.0.0"
@@ -55,6 +56,8 @@
55
56
  "src/",
56
57
  "templates/",
57
58
  "setup-turbo-whisper.js",
59
+ "setup-parakeet.js",
60
+ "docker-compose.parakeet.yml",
58
61
  "README.md",
59
62
  "CHANGELOG.md",
60
63
  "LICENSE"