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 +1 -1
- package/scripts/agileflow-configure.js +31 -1
- package/scripts/agileflow-welcome.js +172 -9
- package/scripts/check-update.js +325 -0
- package/src/core/commands/configure.md +26 -2
- package/src/core/commands/whats-new.md +94 -0
- package/tools/cli/commands/setup.js +6 -2
- package/tools/cli/commands/update.js +29 -29
package/package.json
CHANGED
|
@@ -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
|
|
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
|
-
|
|
419
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}`);
|