fix(10): centralize semver comparison policy across hooks and changeset

This commit is contained in:
Tom Boucher
2026-05-24 18:14:56 -04:00
parent 7170120325
commit 6913dbcdb1
7 changed files with 143 additions and 88 deletions

View File

@@ -12,23 +12,12 @@
const fs = require('fs');
const path = require('path');
const { execFileSync } = require('child_process');
const { isSemverNewer } = require('../get-shit-done/bin/lib/semver-compare.cjs');
const cacheFile = process.env.GSD_CACHE_FILE;
const projectVersionFile = process.env.GSD_PROJECT_VERSION_FILE;
const globalVersionFile = process.env.GSD_GLOBAL_VERSION_FILE;
// Compare semver: true if a > b (a is strictly newer than b)
// Strips pre-release suffixes (e.g. '3-beta.1' → '3') to avoid NaN from Number()
function isNewer(a, b) {
const pa = (a || '').split('.').map(s => Number(s.replace(/-.*/, '')) || 0);
const pb = (b || '').split('.').map(s => Number(s.replace(/-.*/, '')) || 0);
for (let i = 0; i < 3; i++) {
if (pa[i] > pb[i]) return true;
if (pa[i] < pb[i]) return false;
}
return false;
}
// Check project directory first (local install), then global
let installed = '0.0.0';
let configDir = '';
@@ -75,7 +64,7 @@ if (configDir) {
const versionMatch = content.match(/(?:\/\/|#) gsd-hook-version:\s*(.+)/);
if (versionMatch) {
const hookVersion = versionMatch[1].trim();
if (isNewer(installed, hookVersion) && !hookVersion.includes('{{')) {
if (isSemverNewer(installed, hookVersion) && !hookVersion.includes('{{')) {
staleHooks.push({ file: hookFile, hookVersion, installedVersion: installed });
}
} else {
@@ -105,7 +94,7 @@ try {
} catch (e) {}
const result = {
update_available: latest && isNewer(latest, installed),
update_available: latest && isSemverNewer(latest, installed),
installed,
latest: latest || 'unknown',
checked: Math.floor(Date.now() / 1000),

View File

@@ -6,6 +6,7 @@
const fs = require('fs');
const path = require('path');
const os = require('os');
const { isSemverNewer } = require('../get-shit-done/bin/lib/semver-compare.cjs');
// --- Config + last-command readers ------------------------------------------
@@ -403,13 +404,12 @@ function runStatusline() {
if (cache.stale_hooks && cache.stale_hooks.length > 0) {
// If installed version is ahead of npm latest, this is a dev install.
// Running /gsd:update would downgrade — show a contextual warning instead.
const isDevInstall = (() => {
if (!cache.installed || !cache.latest || cache.latest === 'unknown') return false;
const parseV = v => v.replace(/^v/, '').split('.').map(Number);
const [ai, bi, ci] = parseV(cache.installed);
const [an, bn, cn] = parseV(cache.latest);
return ai > an || (ai === an && bi > bn) || (ai === an && bi === bn && ci > cn);
})();
const isDevInstall = (
cache.installed &&
cache.latest &&
cache.latest !== 'unknown' &&
isInstalledAheadOfLatest(cache.installed, cache.latest)
);
if (isDevInstall) {
gsdUpdate += '\x1b[33m⚠ dev install — re-run installer to sync hooks\x1b[0m │ ';
} else {
@@ -497,11 +497,16 @@ function composeStatusline({
return `${gsdUpdate}${modelSeg} │ ${dirSeg}${ctx}${lastCmdSuffix}`;
}
function isInstalledAheadOfLatest(installed, latest) {
return isSemverNewer(installed, latest);
}
// Export helpers for unit tests. Harmless when run as a script.
module.exports = {
readGsdState, parseStateMd, formatGsdState,
readGsdConfig, getConfigValue, readLastSlashCommand,
composeStatusline,
isInstalledAheadOfLatest,
};
/**