aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
397 lines (347 loc) • 12.2 kB
JavaScript
/**
* Update Checker
*
* Checks for available updates and prompts the user to update.
* - Stable channel: Checks npm registry for newer version
* - Edge channel: Checks git remote for new commits
*
* @module src/update/checker
* @version 2024.12.0
*/
import fs from 'fs/promises';
import path from 'path';
import https from 'https';
import { execSync } from 'child_process';
import { createInterface } from 'readline';
import { loadConfig, saveConfig, getChannel, getPackageRoot } from '../channel/manager.mjs';
const NPM_REGISTRY = 'https://registry.npmjs.org/aiwg';
/**
* Fetch latest version for a given npm dist-tag
* @param {string} distTag - npm dist-tag to resolve (latest, next, nightly)
* @returns {Promise<string|null>} Resolved version or null on error
*/
async function fetchNpmDistTag(distTag = 'latest') {
return new Promise((resolve) => {
const request = https.get(NPM_REGISTRY, { timeout: 5000 }, (res) => {
if (res.statusCode !== 200) {
resolve(null);
return;
}
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => {
try {
const pkg = JSON.parse(data);
resolve(pkg['dist-tags']?.[distTag] || null);
} catch {
resolve(null);
}
});
});
request.on('error', () => resolve(null));
request.on('timeout', () => {
request.destroy();
resolve(null);
});
});
}
/**
* Fetch latest version from npm registry (stable)
* @returns {Promise<string|null>} Latest version or null on error
*/
async function fetchLatestNpmVersion() {
return fetchNpmDistTag('latest');
}
/**
* Check if there are new commits in the git remote (edge channel)
* @param {string} edgePath - Path to edge installation
* @returns {Promise<boolean>} True if updates available
*/
async function checkGitUpdates(edgePath) {
try {
// Fetch latest without merging
execSync('git fetch --quiet', { cwd: edgePath, timeout: 10000 });
// Check if local is behind remote
const localHash = execSync('git rev-parse HEAD', {
cwd: edgePath,
encoding: 'utf8',
}).trim();
const remoteHash = execSync('git rev-parse origin/main', {
cwd: edgePath,
encoding: 'utf8',
}).trim();
return localHash !== remoteHash;
} catch {
return false;
}
}
/**
* Compare CalVer versions
* @param {string} current - Current version (e.g., "2024.12.0")
* @param {string} latest - Latest version
* @returns {boolean} True if latest is newer
*/
function isNewerVersion(current, latest) {
if (!current || !latest) return false;
const parseVersion = (v) => {
const match = v.match(/^(\d+)\.(\d+)\.(\d+)/);
if (!match) return [0, 0, 0];
return [parseInt(match[1], 10), parseInt(match[2], 10), parseInt(match[3], 10)];
};
const [cy, cm, cp] = parseVersion(current);
const [ly, lm, lp] = parseVersion(latest);
if (ly > cy) return true;
if (ly === cy && lm > cm) return true;
if (ly === cy && lm === cm && lp > cp) return true;
return false;
}
/**
* Prompt user for update confirmation
* @param {string} message - Prompt message
* @returns {Promise<boolean>} True if user confirms
*/
async function promptUpdate(message) {
// Skip prompt if not interactive terminal
if (!process.stdin.isTTY || !process.stdout.isTTY) {
return false;
}
const rl = createInterface({
input: process.stdin,
output: process.stdout,
});
// Hard timeout so the update prompt cannot hang the CLI. The update check
// runs as a background fire-and-forget from bin/aiwg.mjs; if the user isn't
// watching the terminal, we silently decline instead of blocking forever.
// Override via AIWG_PROMPT_TIMEOUT_MS.
const timeoutMs = (() => {
const raw = process.env['AIWG_PROMPT_TIMEOUT_MS'];
const n = raw ? parseInt(raw, 10) : NaN;
return Number.isFinite(n) && n > 0 ? n : 10_000;
})();
return new Promise((resolve) => {
let settled = false;
const timer = setTimeout(() => {
if (settled) return;
settled = true;
rl.close();
resolve(false);
}, timeoutMs);
timer.unref?.();
rl.question(`${message} [y/N]: `, (answer) => {
if (settled) return;
settled = true;
clearTimeout(timer);
rl.close();
const normalized = answer.toLowerCase().trim();
resolve(normalized === 'y' || normalized === 'yes');
});
});
}
/**
* Check for updates (background, non-blocking)
* This is called on every CLI invocation but only checks periodically
*/
export async function checkForUpdates() {
const config = await loadConfig();
// Check if enough time has passed since last check
const now = Date.now();
const lastCheck = config.lastUpdateCheck || 0;
const interval = config.updateCheckInterval || 86400000; // 24 hours
if (now - lastCheck < interval) {
// Too soon to check again
return;
}
// Update last check time
config.lastUpdateCheck = now;
await saveConfig(config);
if (config.channel === 'next') {
await checkNextUpdates(config);
} else if (config.channel === 'nightly') {
await checkNightlyUpdates(config);
} else if (config.channel === 'edge') {
await checkEdgeUpdates(config);
} else {
// stable (or unrecognised — treat as stable)
await checkStableUpdates(config);
}
}
/**
* Check for stable (npm) channel updates
*/
async function checkStableUpdates(config) {
const packageRoot = getPackageRoot();
const packageJsonPath = path.join(packageRoot, 'package.json');
try {
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
const currentVersion = packageJson.version;
const latestVersion = await fetchLatestNpmVersion();
if (latestVersion && isNewerVersion(currentVersion, latestVersion)) {
console.log('');
console.log(`A new version of aiwg is available: ${currentVersion} → ${latestVersion}`);
const shouldUpdate = await promptUpdate('Would you like to update now?');
if (shouldUpdate) {
console.log('');
console.log('Updating aiwg...');
try {
execSync('npm update -g aiwg', { stdio: 'inherit' });
console.log('Update complete! Please restart your terminal.');
} catch (error) {
console.error('Update failed. Run manually: npm update -g aiwg');
}
} else {
console.log('Update skipped. Run `npm update -g aiwg` when ready.');
}
console.log('');
}
} catch {
// Silently ignore errors during update check
}
}
/**
* Check for next (alpha/beta/RC) channel updates
*/
async function checkNextUpdates(config) {
const packageRoot = getPackageRoot();
const packageJsonPath = path.join(packageRoot, 'package.json');
try {
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
const currentVersion = packageJson.version;
const latestVersion = await fetchNpmDistTag('next');
if (!latestVersion) return;
if (currentVersion !== latestVersion) {
console.log('');
console.log(`A new next release is available: ${currentVersion} → ${latestVersion}`);
const shouldUpdate = await promptUpdate('Would you like to update now?');
if (shouldUpdate) {
console.log('');
console.log('Updating aiwg@next...');
try {
execSync('npm install -g aiwg@next', { stdio: 'inherit' });
console.log('Update complete! Please restart your terminal.');
} catch {
console.error('Update failed. Run manually: npm install -g aiwg@next');
}
} else {
console.log('Update skipped. Run `npm install -g aiwg@next` when ready.');
}
console.log('');
}
} catch {
// Silently ignore errors during update check
}
}
/**
* Check for nightly channel updates
*/
async function checkNightlyUpdates(config) {
const packageRoot = getPackageRoot();
const packageJsonPath = path.join(packageRoot, 'package.json');
try {
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
const currentVersion = packageJson.version;
const latestVersion = await fetchNpmDistTag('nightly');
if (!latestVersion) return;
if (currentVersion !== latestVersion) {
console.log('');
console.log(`A new nightly snapshot is available: ${currentVersion} → ${latestVersion}`);
const shouldUpdate = await promptUpdate('Would you like to update now?');
if (shouldUpdate) {
console.log('');
console.log('Updating aiwg@nightly...');
try {
execSync('npm install -g aiwg@nightly', { stdio: 'inherit' });
console.log('Update complete! Please restart your terminal.');
} catch {
console.error('Update failed. Run manually: npm install -g aiwg@nightly');
}
} else {
console.log('Update skipped. Run `npm install -g aiwg@nightly` when ready.');
}
console.log('');
}
} catch {
// Silently ignore errors during update check
}
}
/**
* Check for edge (git) channel updates
*/
async function checkEdgeUpdates(config) {
const hasUpdates = await checkGitUpdates(config.edgePath);
if (hasUpdates) {
console.log('');
console.log('New commits available in the main branch.');
const shouldUpdate = await promptUpdate('Would you like to update now?');
if (shouldUpdate) {
console.log('');
console.log('Updating edge installation...');
try {
execSync('git pull --ff-only', { cwd: config.edgePath, stdio: 'inherit' });
console.log('Update complete!');
} catch (error) {
console.error('Update failed. Run manually:');
console.error(` cd ${config.edgePath} && git pull`);
}
} else {
console.log('Update skipped. Run `aiwg -update` when ready.');
}
console.log('');
}
}
/**
* Force an update check (called by aiwg -update)
*/
export async function forceUpdateCheck() {
const config = await loadConfig();
const packageRoot = getPackageRoot();
const packageJsonPath = path.join(packageRoot, 'package.json');
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
const currentVersion = packageJson.version;
if (config.channel === 'next') {
console.log('Checking for updates on next channel...');
const latestVersion = await fetchNpmDistTag('next');
if (!latestVersion) {
console.log('Could not check npm registry. Try: npm install -g aiwg@next');
return;
}
if (currentVersion !== latestVersion) {
console.log(`Update available: ${currentVersion} → ${latestVersion}`);
console.log('');
console.log('Run: npm install -g aiwg@next');
} else {
console.log(`You are on the latest next release: ${currentVersion}`);
}
} else if (config.channel === 'nightly') {
console.log('Checking for updates on nightly channel...');
const latestVersion = await fetchNpmDistTag('nightly');
if (!latestVersion) {
console.log('Could not check npm registry. Try: npm install -g aiwg@nightly');
return;
}
if (currentVersion !== latestVersion) {
console.log(`Update available: ${currentVersion} → ${latestVersion}`);
console.log('');
console.log('Run: npm install -g aiwg@nightly');
} else {
console.log(`You are on the latest nightly snapshot: ${currentVersion}`);
}
} else if (config.channel === 'edge') {
const { updateEdge } = await import('../channel/manager.mjs');
await updateEdge();
} else {
// stable
console.log('Checking for updates...');
const latestVersion = await fetchLatestNpmVersion();
if (!latestVersion) {
console.log('Could not check npm registry. Try: npm update -g aiwg');
return;
}
if (isNewerVersion(currentVersion, latestVersion)) {
console.log(`Update available: ${currentVersion} → ${latestVersion}`);
console.log('');
console.log('Run: npm update -g aiwg');
} else {
console.log(`You are on the latest version: ${currentVersion}`);
}
}
}