agileflow 2.56.0 → 2.58.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agileflow",
3
- "version": "2.56.0",
3
+ "version": "2.58.0",
4
4
  "description": "AI-driven agile development system for Claude Code, Cursor, Windsurf, and more",
5
5
  "keywords": [
6
6
  "agile",
@@ -23,7 +23,7 @@
23
23
  * --detect Show current status
24
24
  * --help Show help
25
25
  *
26
- * Features: sessionstart, precompact, stop, archival, statusline
26
+ * Features: sessionstart, precompact, stop, archival, statusline, autoupdate
27
27
  */
28
28
 
29
29
  const fs = require('fs');
@@ -42,6 +42,7 @@ const FEATURES = {
42
42
  stop: { hook: 'Stop', script: 'agileflow-stop.sh', type: 'bash' },
43
43
  archival: { script: 'archive-completed-stories.sh', requiresHook: 'sessionstart' },
44
44
  statusline: { script: 'agileflow-statusline.sh' },
45
+ autoupdate: { metadataOnly: true }, // Stored in metadata.updates.autoUpdate
45
46
  };
46
47
 
47
48
  // Statusline component names
@@ -559,6 +560,21 @@ echo "[$MODEL] AgileFlow"
559
560
  success('Status line enabled');
560
561
  }
561
562
 
563
+ // Handle autoupdate (metadata only, no hooks needed)
564
+ if (feature === 'autoupdate') {
565
+ const frequency = options.checkFrequency || 'daily';
566
+ updateMetadata({
567
+ updates: {
568
+ autoUpdate: true,
569
+ checkFrequency: frequency,
570
+ showChangelog: true,
571
+ },
572
+ });
573
+ success(`Auto-update enabled (check frequency: ${frequency})`);
574
+ info('AgileFlow will automatically update on session start');
575
+ return true; // Skip settings.json write for this feature
576
+ }
577
+
562
578
  writeJSON('.claude/settings.json', settings);
563
579
  updateMetadata({
564
580
  features: { [feature]: { enabled: true, version: VERSION, at: new Date().toISOString() } },
@@ -607,6 +623,17 @@ function disableFeature(feature) {
607
623
  success('Status line disabled');
608
624
  }
609
625
 
626
+ // Disable autoupdate
627
+ if (feature === 'autoupdate') {
628
+ updateMetadata({
629
+ updates: {
630
+ autoUpdate: false,
631
+ },
632
+ });
633
+ success('Auto-update disabled');
634
+ return true; // Skip settings.json write for this feature
635
+ }
636
+
610
637
  writeJSON('.claude/settings.json', settings);
611
638
  updateMetadata({
612
639
  features: { [feature]: { enabled: false, version: VERSION, at: new Date().toISOString() } },
@@ -639,6 +666,9 @@ function updateMetadata(updates) {
639
666
  meta.features[key] = { ...meta.features[key], ...value };
640
667
  });
641
668
  }
669
+ if (updates.updates) {
670
+ meta.updates = { ...meta.updates, ...updates.updates };
671
+ }
642
672
 
643
673
  meta.version = VERSION;
644
674
  meta.updated = new Date().toISOString();
@@ -18,6 +18,14 @@ const { execSync, spawnSync } = require('child_process');
18
18
  // Session manager path (relative to script location)
19
19
  const SESSION_MANAGER_PATH = path.join(__dirname, 'session-manager.js');
20
20
 
21
+ // Update checker module
22
+ let updateChecker;
23
+ try {
24
+ updateChecker = require('./check-update.js');
25
+ } catch (e) {
26
+ // Update checker not available
27
+ }
28
+
21
29
  // ANSI color codes
22
30
  const c = {
23
31
  reset: '\x1b[0m',
@@ -322,6 +330,98 @@ function compareVersions(a, b) {
322
330
  return 0;
323
331
  }
324
332
 
333
+ // Check for updates (async but we'll use sync approach for welcome)
334
+ async function checkUpdates() {
335
+ const result = {
336
+ available: false,
337
+ installed: null,
338
+ latest: null,
339
+ justUpdated: false,
340
+ previousVersion: null,
341
+ autoUpdate: false,
342
+ changelog: [],
343
+ };
344
+
345
+ if (!updateChecker) return result;
346
+
347
+ try {
348
+ const updateInfo = await updateChecker.checkForUpdates();
349
+ result.installed = updateInfo.installed;
350
+ result.latest = updateInfo.latest;
351
+ result.available = updateInfo.updateAvailable;
352
+ result.justUpdated = updateInfo.justUpdated;
353
+ result.previousVersion = updateInfo.previousVersion;
354
+ result.autoUpdate = updateInfo.autoUpdate;
355
+
356
+ // If just updated, try to get changelog entries
357
+ if (result.justUpdated && result.installed) {
358
+ result.changelog = getChangelogEntries(result.installed);
359
+ }
360
+ } catch (e) {
361
+ // Silently fail - update check is non-critical
362
+ }
363
+
364
+ return result;
365
+ }
366
+
367
+ // Parse CHANGELOG.md for entries of a specific version
368
+ function getChangelogEntries(version) {
369
+ const entries = [];
370
+
371
+ try {
372
+ // Look for CHANGELOG.md in .agileflow or package location
373
+ const possiblePaths = [
374
+ path.join(__dirname, '..', 'CHANGELOG.md'),
375
+ path.join(__dirname, 'CHANGELOG.md'),
376
+ ];
377
+
378
+ let changelogContent = null;
379
+ for (const p of possiblePaths) {
380
+ if (fs.existsSync(p)) {
381
+ changelogContent = fs.readFileSync(p, 'utf8');
382
+ break;
383
+ }
384
+ }
385
+
386
+ if (!changelogContent) return entries;
387
+
388
+ // Find the section for this version
389
+ const versionPattern = new RegExp(`## \\[${version}\\].*?\\n([\\s\\S]*?)(?=## \\[|$)`);
390
+ const match = changelogContent.match(versionPattern);
391
+
392
+ if (match) {
393
+ // Extract bullet points from Added/Changed/Fixed sections
394
+ const lines = match[1].split('\n');
395
+ for (const line of lines) {
396
+ const bulletMatch = line.match(/^- (.+)$/);
397
+ if (bulletMatch && entries.length < 3) {
398
+ entries.push(bulletMatch[1]);
399
+ }
400
+ }
401
+ }
402
+ } catch (e) {
403
+ // Silently fail
404
+ }
405
+
406
+ return entries;
407
+ }
408
+
409
+ // Run auto-update if enabled
410
+ async function runAutoUpdate(rootDir) {
411
+ try {
412
+ console.log(`${c.cyan}Updating AgileFlow...${c.reset}`);
413
+ execSync('npx agileflow update', {
414
+ cwd: rootDir,
415
+ encoding: 'utf8',
416
+ stdio: 'inherit',
417
+ });
418
+ return true;
419
+ } catch (e) {
420
+ console.log(`${c.yellow}Auto-update failed. Run manually: npx agileflow update${c.reset}`);
421
+ return false;
422
+ }
423
+ }
424
+
325
425
  function getFeatureVersions(rootDir) {
326
426
  const result = {
327
427
  hooks: { version: null, outdated: false },
@@ -394,7 +494,7 @@ function truncate(str, maxLen, suffix = '..') {
394
494
  return str.substring(0, cutIndex) + suffix;
395
495
  }
396
496
 
397
- function formatTable(info, archival, session, precompact, parallelSessions) {
497
+ function formatTable(info, archival, session, precompact, parallelSessions, updateInfo = {}) {
398
498
  const W = 58; // inner width
399
499
  const R = W - 24; // right column width (34 chars)
400
500
  const lines = [];
@@ -407,25 +507,66 @@ function formatTable(info, archival, session, precompact, parallelSessions) {
407
507
  return `${c.dim}${box.v}${c.reset} ${pad(leftStr, 20)} ${c.dim}${box.v}${c.reset} ${pad(rightStr, R)} ${c.dim}${box.v}${c.reset}`;
408
508
  };
409
509
 
510
+ // Helper for full-width row (spans both columns)
511
+ const fullRow = (content, color = '') => {
512
+ const contentStr = `${color}${content}${color ? c.reset : ''}`;
513
+ return `${c.dim}${box.v}${c.reset} ${pad(contentStr, W - 1)} ${c.dim}${box.v}${c.reset}`;
514
+ };
515
+
410
516
  const divider = () =>
411
517
  `${c.dim}${box.lT}${box.h.repeat(22)}${box.cross}${box.h.repeat(W - 22)}${box.rT}${c.reset}`;
518
+ const fullDivider = () =>
519
+ `${c.dim}${box.lT}${box.h.repeat(W)}${box.rT}${c.reset}`;
412
520
  const topBorder = `${c.dim}${box.tl}${box.h.repeat(22)}${box.tT}${box.h.repeat(W - 22)}${box.tr}${c.reset}`;
413
521
  const bottomBorder = `${c.dim}${box.bl}${box.h.repeat(22)}${box.bT}${box.h.repeat(W - 22)}${box.br}${c.reset}`;
414
522
 
415
- // Header (truncate branch name if too long)
523
+ // Header with version and optional update indicator
416
524
  const branchColor =
417
525
  info.branch === 'main' ? c.green : info.branch.startsWith('fix') ? c.red : c.cyan;
418
- // Fixed parts: "agileflow " (10) + "v" (1) + version + " " (2) + " (" (2) + commit (7) + ")" (1) = 23 + version.length
419
- const maxBranchLen = W - 1 - 23 - info.version.length;
526
+
527
+ // Build version string with update status
528
+ let versionStr = `v${info.version}`;
529
+ if (updateInfo.justUpdated && updateInfo.previousVersion) {
530
+ versionStr = `v${info.version} ${c.green}✓${c.reset}${c.dim} (was v${updateInfo.previousVersion})`;
531
+ } else if (updateInfo.available && updateInfo.latest) {
532
+ versionStr = `v${info.version} ${c.yellow}↑${updateInfo.latest}${c.reset}`;
533
+ }
534
+
535
+ // Calculate remaining space for branch
536
+ const versionVisibleLen = updateInfo.justUpdated
537
+ ? info.version.length + 20 + (updateInfo.previousVersion?.length || 0)
538
+ : updateInfo.available
539
+ ? info.version.length + 3 + (updateInfo.latest?.length || 0)
540
+ : info.version.length;
541
+ const maxBranchLen = W - 1 - 15 - versionVisibleLen;
420
542
  const branchDisplay =
421
543
  info.branch.length > maxBranchLen
422
- ? info.branch.substring(0, maxBranchLen - 2) + '..'
544
+ ? info.branch.substring(0, Math.max(5, maxBranchLen - 2)) + '..'
423
545
  : info.branch;
424
- const header = `${c.brand}${c.bold}agileflow${c.reset} ${c.dim}v${info.version}${c.reset} ${branchColor}${branchDisplay}${c.reset} ${c.dim}(${info.commit})${c.reset}`;
546
+
547
+ const header = `${c.brand}${c.bold}agileflow${c.reset} ${c.dim}${versionStr}${c.reset} ${branchColor}${branchDisplay}${c.reset} ${c.dim}(${info.commit})${c.reset}`;
425
548
  const headerLine = `${c.dim}${box.v}${c.reset} ${pad(header, W - 1)} ${c.dim}${box.v}${c.reset}`;
426
549
 
427
550
  lines.push(topBorder);
428
551
  lines.push(headerLine);
552
+
553
+ // Show update available notification
554
+ if (updateInfo.available && updateInfo.latest && !updateInfo.justUpdated) {
555
+ lines.push(fullDivider());
556
+ lines.push(fullRow(`↑ Update available: v${updateInfo.latest}`, c.yellow));
557
+ lines.push(fullRow(` Run: npx agileflow update`, c.dim));
558
+ }
559
+
560
+ // Show "just updated" changelog
561
+ if (updateInfo.justUpdated && updateInfo.changelog && updateInfo.changelog.length > 0) {
562
+ lines.push(fullDivider());
563
+ lines.push(fullRow(`What's new in v${info.version}:`, c.green));
564
+ for (const entry of updateInfo.changelog.slice(0, 2)) {
565
+ lines.push(fullRow(`• ${truncate(entry, W - 4)}`, c.dim));
566
+ }
567
+ lines.push(fullRow(`Run /agileflow:whats-new for full changelog`, c.dim));
568
+ }
569
+
429
570
  lines.push(divider());
430
571
 
431
572
  // Stories section
@@ -535,7 +676,7 @@ function formatTable(info, archival, session, precompact, parallelSessions) {
535
676
  }
536
677
 
537
678
  // Main
538
- function main() {
679
+ async function main() {
539
680
  const rootDir = getProjectRoot();
540
681
  const info = getProjectInfo(rootDir);
541
682
  const archival = runArchival(rootDir);
@@ -543,7 +684,29 @@ function main() {
543
684
  const precompact = checkPreCompact(rootDir);
544
685
  const parallelSessions = checkParallelSessions(rootDir);
545
686
 
546
- console.log(formatTable(info, archival, session, precompact, parallelSessions));
687
+ // Check for updates (async, cached)
688
+ let updateInfo = {};
689
+ try {
690
+ updateInfo = await checkUpdates();
691
+
692
+ // If auto-update is enabled and update available, run it
693
+ if (updateInfo.available && updateInfo.autoUpdate) {
694
+ const updated = await runAutoUpdate(rootDir);
695
+ if (updated) {
696
+ // Re-run welcome after update (the new version will show changelog)
697
+ return;
698
+ }
699
+ }
700
+
701
+ // Mark current version as seen to track for next update
702
+ if (updateInfo.justUpdated && updateChecker) {
703
+ updateChecker.markVersionSeen(info.version);
704
+ }
705
+ } catch (e) {
706
+ // Update check failed - continue without it
707
+ }
708
+
709
+ console.log(formatTable(info, archival, session, precompact, parallelSessions, updateInfo));
547
710
 
548
711
  // Show warning and tip if other sessions are active
549
712
  if (parallelSessions.otherActive > 0) {
@@ -554,4 +717,4 @@ function main() {
554
717
  }
555
718
  }
556
719
 
557
- main();
720
+ main().catch(console.error);
@@ -0,0 +1,325 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * check-update.js - Check for AgileFlow updates with caching
5
+ *
6
+ * Features:
7
+ * - Checks npm registry for latest version
8
+ * - Caches results to avoid excessive requests
9
+ * - Configurable check frequency (hourly, daily, weekly)
10
+ * - Returns structured JSON for easy parsing
11
+ *
12
+ * Usage:
13
+ * node check-update.js [--force] [--json]
14
+ *
15
+ * Options:
16
+ * --force Bypass cache and check npm directly
17
+ * --json Output as JSON (default is human-readable)
18
+ *
19
+ * Environment:
20
+ * DEBUG_UPDATE=1 Enable debug logging
21
+ */
22
+
23
+ const fs = require('fs');
24
+ const path = require('path');
25
+ const https = require('https');
26
+
27
+ // Debug mode
28
+ const DEBUG = process.env.DEBUG_UPDATE === '1';
29
+
30
+ function debugLog(message, data = null) {
31
+ if (DEBUG) {
32
+ console.error(`[check-update] ${message}`, data ? JSON.stringify(data) : '');
33
+ }
34
+ }
35
+
36
+ // Find project root (has .agileflow directory)
37
+ function getProjectRoot() {
38
+ let dir = process.cwd();
39
+ while (!fs.existsSync(path.join(dir, '.agileflow')) && dir !== '/') {
40
+ dir = path.dirname(dir);
41
+ }
42
+ return dir !== '/' ? dir : process.cwd();
43
+ }
44
+
45
+ // Get installed AgileFlow version
46
+ function getInstalledVersion(rootDir) {
47
+ // First check .agileflow/package.json (installed version)
48
+ const agileflowPkg = path.join(rootDir, '.agileflow', 'package.json');
49
+ if (fs.existsSync(agileflowPkg)) {
50
+ try {
51
+ const pkg = JSON.parse(fs.readFileSync(agileflowPkg, 'utf8'));
52
+ if (pkg.version) return pkg.version;
53
+ } catch (e) {
54
+ debugLog('Error reading .agileflow/package.json', e.message);
55
+ }
56
+ }
57
+
58
+ // Fallback: check if this is the AgileFlow dev repo
59
+ const cliPkg = path.join(rootDir, 'packages/cli/package.json');
60
+ if (fs.existsSync(cliPkg)) {
61
+ try {
62
+ const pkg = JSON.parse(fs.readFileSync(cliPkg, 'utf8'));
63
+ if (pkg.name === 'agileflow' && pkg.version) return pkg.version;
64
+ } catch (e) {}
65
+ }
66
+
67
+ return null;
68
+ }
69
+
70
+ // Get update configuration from metadata
71
+ function getUpdateConfig(rootDir) {
72
+ const defaults = {
73
+ autoUpdate: false,
74
+ checkFrequency: 'daily', // hourly, daily, weekly, never
75
+ showChangelog: true,
76
+ lastCheck: null,
77
+ lastSeenVersion: null,
78
+ latestVersion: null,
79
+ };
80
+
81
+ try {
82
+ const metadataPath = path.join(rootDir, 'docs/00-meta/agileflow-metadata.json');
83
+ if (fs.existsSync(metadataPath)) {
84
+ const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
85
+ if (metadata.updates) {
86
+ return { ...defaults, ...metadata.updates };
87
+ }
88
+ }
89
+ } catch (e) {
90
+ debugLog('Error reading update config', e.message);
91
+ }
92
+
93
+ return defaults;
94
+ }
95
+
96
+ // Save update configuration
97
+ function saveUpdateConfig(rootDir, config) {
98
+ try {
99
+ const metadataPath = path.join(rootDir, 'docs/00-meta/agileflow-metadata.json');
100
+ let metadata = {};
101
+
102
+ if (fs.existsSync(metadataPath)) {
103
+ metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
104
+ }
105
+
106
+ metadata.updates = config;
107
+ fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2) + '\n');
108
+ return true;
109
+ } catch (e) {
110
+ debugLog('Error saving update config', e.message);
111
+ return false;
112
+ }
113
+ }
114
+
115
+ // Check if cache is still valid
116
+ function isCacheValid(config) {
117
+ if (!config.lastCheck) return false;
118
+
119
+ const now = Date.now();
120
+ const lastCheck = new Date(config.lastCheck).getTime();
121
+ const age = now - lastCheck;
122
+
123
+ // Convert frequency to milliseconds
124
+ const frequencies = {
125
+ hourly: 60 * 60 * 1000, // 1 hour
126
+ daily: 24 * 60 * 60 * 1000, // 24 hours
127
+ weekly: 7 * 24 * 60 * 60 * 1000, // 7 days
128
+ never: Infinity,
129
+ };
130
+
131
+ const maxAge = frequencies[config.checkFrequency] || frequencies.daily;
132
+
133
+ debugLog('Cache check', {
134
+ lastCheck: config.lastCheck,
135
+ ageMs: age,
136
+ maxAgeMs: maxAge,
137
+ valid: age < maxAge,
138
+ });
139
+
140
+ return age < maxAge;
141
+ }
142
+
143
+ // Fetch latest version from npm
144
+ async function fetchLatestVersion() {
145
+ return new Promise(resolve => {
146
+ const options = {
147
+ hostname: 'registry.npmjs.org',
148
+ port: 443,
149
+ path: '/agileflow/latest',
150
+ method: 'GET',
151
+ headers: {
152
+ 'User-Agent': 'agileflow-cli',
153
+ },
154
+ };
155
+
156
+ debugLog('Fetching from npm registry');
157
+
158
+ const req = https.request(options, res => {
159
+ let data = '';
160
+
161
+ res.on('data', chunk => {
162
+ data += chunk;
163
+ });
164
+
165
+ res.on('end', () => {
166
+ if (res.statusCode !== 200) {
167
+ debugLog('Non-200 status', { statusCode: res.statusCode });
168
+ return resolve(null);
169
+ }
170
+
171
+ try {
172
+ const json = JSON.parse(data);
173
+ debugLog('Version found', { version: json.version });
174
+ resolve(json.version || null);
175
+ } catch (err) {
176
+ debugLog('JSON parse error', err.message);
177
+ resolve(null);
178
+ }
179
+ });
180
+ });
181
+
182
+ req.on('error', err => {
183
+ debugLog('Network error', err.message);
184
+ resolve(null);
185
+ });
186
+
187
+ req.setTimeout(5000, () => {
188
+ debugLog('Request timeout');
189
+ req.destroy();
190
+ resolve(null);
191
+ });
192
+
193
+ req.end();
194
+ });
195
+ }
196
+
197
+ // Compare semantic versions: returns -1 if a < b, 0 if equal, 1 if a > b
198
+ function compareVersions(a, b) {
199
+ if (!a || !b) return 0;
200
+ const partsA = a.split('.').map(Number);
201
+ const partsB = b.split('.').map(Number);
202
+ for (let i = 0; i < 3; i++) {
203
+ const numA = partsA[i] || 0;
204
+ const numB = partsB[i] || 0;
205
+ if (numA < numB) return -1;
206
+ if (numA > numB) return 1;
207
+ }
208
+ return 0;
209
+ }
210
+
211
+ // Main check function
212
+ async function checkForUpdates(options = {}) {
213
+ const rootDir = getProjectRoot();
214
+ const installedVersion = getInstalledVersion(rootDir);
215
+ const config = getUpdateConfig(rootDir);
216
+
217
+ const result = {
218
+ installed: installedVersion,
219
+ latest: null,
220
+ updateAvailable: false,
221
+ autoUpdate: config.autoUpdate,
222
+ justUpdated: false,
223
+ previousVersion: config.lastSeenVersion,
224
+ fromCache: false,
225
+ error: null,
226
+ };
227
+
228
+ // No installed version found
229
+ if (!installedVersion) {
230
+ result.error = 'Could not determine installed version';
231
+ return result;
232
+ }
233
+
234
+ // Check if we just updated (lastSeenVersion < installed)
235
+ if (
236
+ config.lastSeenVersion &&
237
+ compareVersions(config.lastSeenVersion, installedVersion) < 0
238
+ ) {
239
+ result.justUpdated = true;
240
+ result.previousVersion = config.lastSeenVersion;
241
+ }
242
+
243
+ // Use cache if valid and not forced
244
+ if (!options.force && isCacheValid(config) && config.latestVersion) {
245
+ result.latest = config.latestVersion;
246
+ result.fromCache = true;
247
+ } else if (config.checkFrequency !== 'never') {
248
+ // Fetch from npm
249
+ result.latest = await fetchLatestVersion();
250
+
251
+ // Update cache
252
+ if (result.latest) {
253
+ config.lastCheck = new Date().toISOString();
254
+ config.latestVersion = result.latest;
255
+ saveUpdateConfig(rootDir, config);
256
+ }
257
+ }
258
+
259
+ // Compare versions
260
+ if (result.latest && compareVersions(installedVersion, result.latest) < 0) {
261
+ result.updateAvailable = true;
262
+ }
263
+
264
+ return result;
265
+ }
266
+
267
+ // Mark version as seen (after update or dismissal)
268
+ function markVersionSeen(version) {
269
+ const rootDir = getProjectRoot();
270
+ const config = getUpdateConfig(rootDir);
271
+ config.lastSeenVersion = version;
272
+ saveUpdateConfig(rootDir, config);
273
+ }
274
+
275
+ // CLI interface
276
+ async function main() {
277
+ const args = process.argv.slice(2);
278
+ const force = args.includes('--force');
279
+ const jsonOutput = args.includes('--json');
280
+ const markSeen = args.includes('--mark-seen');
281
+
282
+ if (markSeen) {
283
+ const version = args[args.indexOf('--mark-seen') + 1];
284
+ if (version) {
285
+ markVersionSeen(version);
286
+ if (jsonOutput) {
287
+ console.log(JSON.stringify({ success: true, version }));
288
+ } else {
289
+ console.log(`Marked version ${version} as seen`);
290
+ }
291
+ }
292
+ return;
293
+ }
294
+
295
+ const result = await checkForUpdates({ force });
296
+
297
+ if (jsonOutput) {
298
+ console.log(JSON.stringify(result, null, 2));
299
+ } else {
300
+ if (result.error) {
301
+ console.log(`Error: ${result.error}`);
302
+ } else if (result.justUpdated) {
303
+ console.log(`Updated from v${result.previousVersion} to v${result.installed}`);
304
+ } else if (result.updateAvailable) {
305
+ console.log(`Update available: v${result.installed} -> v${result.latest}`);
306
+ console.log(`Run: npx agileflow update`);
307
+ } else {
308
+ console.log(`AgileFlow v${result.installed} is up to date`);
309
+ }
310
+ }
311
+ }
312
+
313
+ // Export for use as module
314
+ module.exports = {
315
+ checkForUpdates,
316
+ markVersionSeen,
317
+ getUpdateConfig,
318
+ saveUpdateConfig,
319
+ compareVersions,
320
+ };
321
+
322
+ // Run CLI if executed directly
323
+ if (require.main === module) {
324
+ main().catch(console.error);
325
+ }
@@ -28,7 +28,7 @@ node .agileflow/scripts/agileflow-configure.js --disable=archival # Disable spec
28
28
 
29
29
  ### Features
30
30
 
31
- `sessionstart`, `precompact`, `stop`, `archival`, `statusline`
31
+ `sessionstart`, `precompact`, `stop`, `archival`, `statusline`, `autoupdate`
32
32
 
33
33
  ### Critical Rules
34
34
 
@@ -178,7 +178,8 @@ Based on selection, run appropriate command.
178
178
  {"label": "PreCompact Hook", "description": "Context preservation on compact"},
179
179
  {"label": "Stop Hook", "description": "Warns about uncommitted git changes"},
180
180
  {"label": "Archival", "description": "Auto-archive old completed stories"},
181
- {"label": "Status Line", "description": "Custom status bar"}
181
+ {"label": "Status Line", "description": "Custom status bar"},
182
+ {"label": "Auto-Update", "description": "Automatically update AgileFlow on session start"}
182
183
  ]
183
184
  }]</parameter>
184
185
  </invoke>
@@ -190,6 +191,29 @@ Map selections:
190
191
  - "Stop Hook" → `stop`
191
192
  - "Archival" → `archival`
192
193
  - "Status Line" → `statusline`
194
+ - "Auto-Update" → `autoupdate`
195
+
196
+ ## Auto-Update Configuration
197
+
198
+ Enable auto-update to automatically update AgileFlow when a new version is available:
199
+
200
+ ```bash
201
+ # Enable auto-update
202
+ node .agileflow/scripts/agileflow-configure.js --enable=autoupdate
203
+
204
+ # Or manually edit docs/00-meta/agileflow-metadata.json:
205
+ ```
206
+
207
+ ```json
208
+ {
209
+ "updates": {
210
+ "autoUpdate": true,
211
+ "checkFrequency": "daily"
212
+ }
213
+ }
214
+ ```
215
+
216
+ **Check frequencies:** `hourly`, `daily`, `weekly`, `never`
193
217
 
194
218
  ## Format Migration Details
195
219
 
@@ -0,0 +1,94 @@
1
+ ---
2
+ description: Show what's new in AgileFlow
3
+ ---
4
+
5
+ # /agileflow:whats-new
6
+
7
+ Display recent AgileFlow updates and version history.
8
+
9
+ ---
10
+
11
+ ## Prompt
12
+
13
+ ROLE: AgileFlow Version Reporter
14
+
15
+ **STEP 1: Get current version and changelog**
16
+
17
+ Read the AgileFlow CHANGELOG.md:
18
+
19
+ ```bash
20
+ cat .agileflow/CHANGELOG.md 2>/dev/null || echo "Changelog not found"
21
+ ```
22
+
23
+ Also check the installed version:
24
+ ```bash
25
+ cat .agileflow/package.json 2>/dev/null | grep '"version"' || echo "Version unknown"
26
+ ```
27
+
28
+ **STEP 2: Parse and display**
29
+
30
+ Parse the CHANGELOG.md and display:
31
+
32
+ 1. **Current installed version** at the top
33
+ 2. **Last 3-5 versions** with their changes
34
+ 3. Use clear formatting with headers and bullets
35
+
36
+ **FORMAT:**
37
+
38
+ ```
39
+ ╭─────────────────────────────────────────────────────╮
40
+ │ AgileFlow Changelog │
41
+ │ Current: v2.57.0 │
42
+ ╰─────────────────────────────────────────────────────╯
43
+
44
+ ## v2.57.0 (2025-12-27)
45
+
46
+ ### Added
47
+ • Auto-update system with configurable check frequency
48
+ • Update notifications in welcome message and status line
49
+ • Changelog display after updates
50
+
51
+ ---
52
+
53
+ ## v2.56.0 (2025-12-27)
54
+
55
+ ### Added
56
+ • Dynamic IDE discovery - Codex CLI now in setup
57
+
58
+ ### Changed
59
+ • Replaced hardcoded IDE list with dynamic loading
60
+
61
+ ---
62
+
63
+ ## v2.55.0 (2025-12-26)
64
+
65
+ ### Changed
66
+ • Consolidated code improvements and debugging
67
+
68
+ ---
69
+
70
+ 📖 Full changelog: https://github.com/projectquestorg/AgileFlow/blob/main/packages/cli/CHANGELOG.md
71
+ 🔄 Check for updates: npx agileflow update
72
+ ⚙️ Configure auto-update: /agileflow:configure --auto-update
73
+ ```
74
+
75
+ **STEP 3: Check for updates**
76
+
77
+ After displaying, check if an update is available:
78
+
79
+ ```bash
80
+ npm view agileflow version 2>/dev/null
81
+ ```
82
+
83
+ If a newer version exists, show:
84
+ ```
85
+ ⚡ Update available: v2.57.0 → v2.58.0
86
+ Run: npx agileflow update
87
+ ```
88
+
89
+ **RULES:**
90
+ - Show max 5 versions (most recent first)
91
+ - Use clear section headers
92
+ - Include dates if available
93
+ - Always show the GitHub link for full history
94
+ - Mention update availability if applicable
@@ -46,7 +46,8 @@ module.exports = {
46
46
  const npmLatestVersion = await getLatestVersion('agileflow');
47
47
 
48
48
  if (npmLatestVersion && semver.lt(localCliVersion, npmLatestVersion)) {
49
- displayLogo();
49
+ // Don't show logo here - it will be shown by promptInstall() or after self-update
50
+ console.log(chalk.hex('#e8683a').bold('\n AgileFlow Update Available\n'));
50
51
  info(`Newer version available: v${localCliVersion} → v${npmLatestVersion}`);
51
52
  console.log(chalk.dim(' Fetching latest version from npm...\n'));
52
53
 
@@ -69,7 +70,10 @@ module.exports = {
69
70
  // If we self-updated, show confirmation
70
71
  if (options.selfUpdated) {
71
72
  const packageJson = require(path.join(__dirname, '..', '..', '..', 'package.json'));
72
- displayLogo();
73
+ // Only show logo here if using -y flag (since promptInstall won't be called)
74
+ if (options.yes) {
75
+ displayLogo();
76
+ }
73
77
  success(`Using latest CLI v${packageJson.version}`);
74
78
  console.log();
75
79
  }
@@ -39,6 +39,35 @@ module.exports = {
39
39
  try {
40
40
  const directory = path.resolve(options.directory || '.');
41
41
 
42
+ // Get local CLI version and npm registry version early to decide on self-update
43
+ const packageJson = require(path.join(__dirname, '..', '..', '..', 'package.json'));
44
+ const localCliVersion = packageJson.version;
45
+ const npmLatestVersion = await getLatestVersion('agileflow');
46
+
47
+ // Self-update check: if CLI is outdated and we haven't already self-updated, re-run with latest
48
+ const shouldSelfUpdate = options.selfUpdate !== false && !options.selfUpdated;
49
+ if (npmLatestVersion && semver.lt(localCliVersion, npmLatestVersion) && shouldSelfUpdate) {
50
+ // Don't show logo - the self-updated process will show it
51
+ console.log(chalk.hex('#e8683a').bold('\n AgileFlow CLI Update\n'));
52
+ info(`Updating CLI from v${localCliVersion} to v${npmLatestVersion}...`);
53
+ console.log(chalk.dim(' Fetching latest version from npm...\n'));
54
+
55
+ // Build the command with all current options forwarded
56
+ const args = ['agileflow@latest', 'update', '--self-updated'];
57
+ if (options.directory) args.push('-d', options.directory);
58
+ if (options.force) args.push('--force');
59
+
60
+ const result = spawnSync('npx', args, {
61
+ stdio: 'inherit',
62
+ cwd: process.cwd(),
63
+ shell: process.platform === 'win32',
64
+ });
65
+
66
+ // Exit with the same code as the spawned process
67
+ process.exit(result.status ?? 0);
68
+ }
69
+
70
+ // Now show the logo (either first run without update, or after self-update)
42
71
  displayLogo();
43
72
 
44
73
  // Check for existing installation
@@ -52,13 +81,6 @@ module.exports = {
52
81
 
53
82
  displaySection('Updating AgileFlow', `Current version: ${status.version}`);
54
83
 
55
- // Get local CLI version and npm registry version
56
- const packageJson = require(path.join(__dirname, '..', '..', '..', 'package.json'));
57
- const localCliVersion = packageJson.version;
58
-
59
- console.log(chalk.dim('Checking npm registry for latest version...'));
60
- const npmLatestVersion = await getLatestVersion('agileflow');
61
-
62
84
  if (!npmLatestVersion) {
63
85
  warning('Could not check npm registry for latest version');
64
86
  console.log(chalk.dim('Continuing with local CLI version...\n'));
@@ -72,28 +94,6 @@ module.exports = {
72
94
  console.log(chalk.bold('Latest (npm):'), npmLatestVersion);
73
95
  }
74
96
 
75
- // Self-update: if CLI is outdated and we haven't already self-updated, re-run with latest
76
- const shouldSelfUpdate = options.selfUpdate !== false && !options.selfUpdated;
77
- if (npmLatestVersion && semver.lt(localCliVersion, npmLatestVersion) && shouldSelfUpdate) {
78
- console.log();
79
- info(`Updating CLI from v${localCliVersion} to v${npmLatestVersion}...`);
80
- console.log(chalk.dim(' Fetching latest version from npm...\n'));
81
-
82
- // Build the command with all current options forwarded
83
- const args = ['agileflow@latest', 'update', '--self-updated'];
84
- if (options.directory) args.push('-d', options.directory);
85
- if (options.force) args.push('--force');
86
-
87
- const result = spawnSync('npx', args, {
88
- stdio: 'inherit',
89
- cwd: process.cwd(),
90
- shell: process.platform === 'win32',
91
- });
92
-
93
- // Exit with the same code as the spawned process
94
- process.exit(result.status ?? 0);
95
- }
96
-
97
97
  // If we self-updated, show confirmation
98
98
  if (options.selfUpdated) {
99
99
  success(`CLI updated to v${localCliVersion}`);