#!/usr/bin/env bun import { $ } from 'bun'; import { formatReleaseNotes, generateChangelog, getContributors } from './generate-changelog'; const PACKAGE_NAME = 'cc-safety-net'; const bump = process.env.BUMP as 'major' | 'minor' | 'patch' | undefined; const versionOverride = process.env.VERSION; const dryRun = process.argv.includes('--dry-run'); console.log(`=== ${dryRun ? '[DRY-RUN] ' : ''}Publishing cc-safety-net ===\n`); async function fetchPreviousVersion(): Promise { try { const res = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`); if (!res.ok) throw new Error(`Failed to fetch: ${res.statusText}`); const data = (await res.json()) as { version: string }; console.log(`Previous version: ${data.version}`); return data.version; } catch { console.log('No previous version found, starting from 0.0.0'); return '0.0.0'; } } function bumpVersion(version: string, type: 'major' | 'minor' | 'patch'): string { const parts = version.split('.').map((part) => Number(part)); const major = parts[0] ?? 0; const minor = parts[1] ?? 0; const patch = parts[2] ?? 0; switch (type) { case 'major': return `${major + 1}.0.0`; case 'minor': return `${major}.${minor + 1}.0`; case 'patch': return `${major}.${minor}.${patch + 1}`; } } async function updatePackageVersion(newVersion: string): Promise { const pkgPath = new URL('../package.json', import.meta.url).pathname; if (dryRun) { console.log(`Would update: ${pkgPath}`); return; } let pkg = await Bun.file(pkgPath).text(); pkg = pkg.replace(/"version": "[^"]+"/, `"version": "${newVersion}"`); await Bun.write(pkgPath, pkg); console.log(`Updated: ${pkgPath}`); } async function updatePluginVersion(newVersion: string): Promise { const pluginPath = new URL('../.claude-plugin/plugin.json', import.meta.url).pathname; if (dryRun) { console.log(`Would update: ${pluginPath}`); return; } let plugin = await Bun.file(pluginPath).text(); plugin = plugin.replace(/"version": "[^"]+"/, `"version": "${newVersion}"`); await Bun.write(pluginPath, plugin); console.log(`Updated: ${pluginPath}`); } async function buildAndPublish(): Promise { // Build AFTER version files are updated so correct version is injected into bundle console.log('\nBuilding...'); const buildResult = Bun.spawnSync(['bun', 'run', 'build']); if (buildResult.exitCode !== 0) { console.error('Build failed'); console.error(buildResult.stderr.toString()); process.exit(1); } if (dryRun) { console.log('Would publish to npm'); return; } console.log('Publishing to npm...'); if (process.env.CI) { await $`npm publish --access public --provenance --ignore-scripts`; } else { await $`npm publish --access public --ignore-scripts`; } } async function gitTagAndRelease(newVersion: string, notes: string[]): Promise { if (dryRun) { console.log('\nWould commit, tag, push, and create GitHub release (CI only)'); return; } if (!process.env.CI) return; console.log('\nCommitting and tagging...'); await $`git config user.email "github-actions[bot]@users.noreply.github.com"`; await $`git config user.name "github-actions[bot]"`; await $`git add package.json .claude-plugin/plugin.json assets/cc-safety-net.schema.json`; const hasStagedChanges = await $`git diff --cached --quiet`.nothrow(); if (hasStagedChanges.exitCode !== 0) { await $`git commit -m "release: v${newVersion}"`; } else { console.log('No changes to commit (version already updated)'); } const tagExists = await $`git rev-parse v${newVersion}`.nothrow(); if (tagExists.exitCode !== 0) { await $`git tag v${newVersion}`; } else { console.log(`Tag v${newVersion} already exists`); } await $`git push origin HEAD --tags`; console.log('\nCreating GitHub release...'); const releaseNotes = notes.length > 0 ? notes.join('\n') : 'No notable changes'; const releaseExists = await $`gh release view v${newVersion}`.nothrow(); if (releaseExists.exitCode !== 0) { await $`gh release create v${newVersion} --title "v${newVersion}" --notes ${releaseNotes}`; } else { console.log(`Release v${newVersion} already exists`); } } async function checkVersionExists(version: string): Promise { try { const res = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/${version}`); return res.ok; } catch { return false; } } async function main(): Promise { const previous = await fetchPreviousVersion(); const newVersion = versionOverride || (bump ? bumpVersion(previous, bump) : bumpVersion(previous, 'patch')); console.log(`New version: ${newVersion}\n`); if (await checkVersionExists(newVersion)) { console.log(`Version ${newVersion} already exists on npm. Skipping publish.`); process.exit(0); } await updatePackageVersion(newVersion); await updatePluginVersion(newVersion); const changelog = await generateChangelog(`v${previous}`); const contributors = await getContributors(`v${previous}`); const notes = formatReleaseNotes(changelog, contributors); await buildAndPublish(); await gitTagAndRelease(newVersion, notes); if (dryRun) { console.log('\n--- Release Notes ---'); console.log(notes.length > 0 ? notes.join('\n') : 'No notable changes'); console.log(`\n=== [DRY-RUN] Would publish ${PACKAGE_NAME}@${newVersion} ===`); } else { console.log(`\n=== Successfully published ${PACKAGE_NAME}@${newVersion} ===`); } } main();