From 07d66628790ca9c1ce7e4f25c2a5872beca1fd75 Mon Sep 17 00:00:00 2001 From: bradygaster Date: Sun, 15 Mar 2026 05:29:00 -0700 Subject: [PATCH] fix: stamp CLI version in squad.agent.md during init/upgrade (#321) Both squad init and squad upgrade now update the version comment in .github/agents/squad.agent.md to match the installed CLI version. Changes: - SDK stampVersionInContent() now replaces all 3 version locations (HTML comment, Identity Version line, {version} placeholder) - CLI init.ts calls stampVersion() after SDK creates the file - CLI upgrade.ts uses shared version utilities from version.ts - version.ts regex updated to handle pre-release with dot suffix Previously the SDK only stamped the HTML comment, and the init command had no post-creation stamping, leaving stale version identity. Closes #321 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/squad-cli/src/cli/core/init.ts | 9 +++- packages/squad-cli/src/cli/core/upgrade.ts | 62 ++-------------------- packages/squad-cli/src/cli/core/version.ts | 4 +- packages/squad-sdk/src/config/init.ts | 15 +++++- test/cli/init.test.ts | 17 ++++++ test/cli/upgrade.test.ts | 3 ++ 6 files changed, 47 insertions(+), 63 deletions(-) diff --git a/packages/squad-cli/src/cli/core/init.ts b/packages/squad-cli/src/cli/core/init.ts index 63f4d1e5d..d61b77447 100644 --- a/packages/squad-cli/src/cli/core/init.ts +++ b/packages/squad-cli/src/cli/core/init.ts @@ -3,13 +3,14 @@ * Scaffolds a new Squad project with templates, workflows, and directory structure */ +import fs from 'node:fs'; import path from 'node:path'; import { execFileSync } from 'node:child_process'; import { detectSquadDir } from './detect-squad-dir.js'; import { success, BOLD, RESET, YELLOW, GREEN, DIM } from './output.js'; import { fatal } from './errors.js'; import { detectProjectType } from './project-type.js'; -import { getPackageVersion } from './version.js'; +import { getPackageVersion, stampVersion } from './version.js'; import { initSquad as sdkInitSquad, cleanupOrphanInitPrompt, type InitOptions } from '@bradygaster/squad-sdk'; const CYAN = '\x1b[36m'; @@ -196,6 +197,12 @@ export async function runInit(dest: string, options: RunInitOptions = {}): Promi process.off('SIGINT', sigintHandler); + // Ensure version is fully stamped in squad.agent.md + const agentPath = path.join(dest, '.github', 'agents', 'squad.agent.md'); + if (fs.existsSync(agentPath)) { + stampVersion(agentPath, version); + } + // Report .init-prompt storage if (options.prompt) { success(`.init-prompt stored — team will be cast when you start squad`); diff --git a/packages/squad-cli/src/cli/core/upgrade.ts b/packages/squad-cli/src/cli/core/upgrade.ts index 494aea1d1..c2f8b4b4e 100644 --- a/packages/squad-cli/src/cli/core/upgrade.ts +++ b/packages/squad-cli/src/cli/core/upgrade.ts @@ -6,17 +6,13 @@ import fs from 'node:fs'; import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { dirname } from 'node:path'; import { success, warn, info, dim, bold } from './output.js'; import { fatal } from './errors.js'; import { detectSquadDir } from './detect-squad-dir.js'; import { TEMPLATE_MANIFEST, getTemplatesDir } from './templates.js'; import { runMigrations } from './migrations.js'; import { scrubEmails } from './email-scrub.js'; - -const currentFile = fileURLToPath(import.meta.url); -const currentDir = dirname(currentFile); +import { getPackageVersion, stampVersion, readInstalledVersion } from './version.js'; export interface UpgradeOptions { migrateDirectory?: boolean; @@ -30,26 +26,6 @@ export interface UpdateInfo { migrationsRun: string[]; } -/** - * Read version from squad.agent.md HTML comment - */ -function readInstalledVersion(agentPath: string): string { - try { - if (!fs.existsSync(agentPath)) return '0.0.0'; - const content = fs.readFileSync(agentPath, 'utf8'); - - // Try HTML comment first (new format) - const commentMatch = content.match(//); - if (commentMatch) return commentMatch[1]!; - - // Fallback to frontmatter (old format) - const frontmatterMatch = content.match(/^version:\s*"([^"]+)"/m); - return frontmatterMatch ? frontmatterMatch[1]! : '0.0.0'; - } catch { - return '0.0.0'; - } -} - /** * Compare semver strings: -1 (ab) */ @@ -72,24 +48,6 @@ function compareSemver(a: string, b: string): number { return 0; } -/** - * Stamp version into squad.agent.md after copying - */ -function stampVersion(filePath: string, version: string): void { - let content = fs.readFileSync(filePath, 'utf8'); - - // Replace version in HTML comment - content = content.replace(//m, ``); - - // Replace version in Identity section's Version line - content = content.replace(/- \*\*Version:\*\* [0-9.]+(?:-[a-z]+(?:\.\d+)?)?/m, `- **Version:** ${version}`); - - // Replace {version} placeholder - content = content.replace(/`Squad v\{version\}`/g, `\`Squad v${version}\``); - - fs.writeFileSync(filePath, content); -} - /** * Detect project type by checking marker files */ @@ -287,25 +245,11 @@ function writeWorkflowFile(file: string, srcPath: string, destPath: string, proj fs.copyFileSync(srcPath, destPath); } -/** - * Get CLI version from package.json - */ -function getCLIVersion(): string { - try { - // From src/cli/core/upgrade.ts, go up to package root - const pkgPath = path.join(currentDir, '..', '..', '..', 'package.json'); - const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); - return pkg.version || '0.0.0'; - } catch { - return '0.0.0'; - } -} - /** * Run the upgrade command */ export async function runUpgrade(dest: string, options: UpgradeOptions = {}): Promise { - const cliVersion = getCLIVersion(); + const cliVersion = getPackageVersion(); const filesUpdated: string[] = []; // Detect squad directory @@ -323,7 +267,7 @@ export async function runUpgrade(dest: string, options: UpgradeOptions = {}): Pr } const agentDest = path.join(dest, '.github', 'agents', 'squad.agent.md'); - const oldVersion = readInstalledVersion(agentDest); + const oldVersion = readInstalledVersion(agentDest) ?? '0.0.0'; // Check if already current const isAlreadyCurrent = oldVersion && oldVersion !== '0.0.0' && compareSemver(oldVersion, cliVersion) === 0; diff --git a/packages/squad-cli/src/cli/core/version.ts b/packages/squad-cli/src/cli/core/version.ts index 93c87b113..3e2e261c1 100644 --- a/packages/squad-cli/src/cli/core/version.ts +++ b/packages/squad-cli/src/cli/core/version.ts @@ -35,7 +35,7 @@ export function stampVersion(filePath: string, version: string): void { // Replace version in HTML comment (must come immediately after frontmatter closing ---) content = content.replace(//m, ``); // Replace version in the Identity section's Version line - content = content.replace(/- \*\*Version:\*\* [0-9.]+(?:-[a-z]+)?/m, `- **Version:** ${version}`); + content = content.replace(/- \*\*Version:\*\* [0-9.]+(?:-[a-z]+(?:\.\d+)?)?/m, `- **Version:** ${version}`); // Replace {version} placeholder in the greeting instruction so it's unambiguous content = content.replace(/`Squad v\{version\}`/g, `\`Squad v${version}\``); fs.writeFileSync(filePath, content); @@ -49,7 +49,7 @@ export function readInstalledVersion(filePath: string): string | null { if (!fs.existsSync(filePath)) return null; const content = fs.readFileSync(filePath, 'utf8'); // Try to read from HTML comment first (new format) - const commentMatch = content.match(//); + const commentMatch = content.match(//); if (commentMatch) return commentMatch[1]!; // Fallback: try old frontmatter format for backward compatibility during upgrade const frontmatterMatch = content.match(/^version:\s*"([^"]+)"/m); diff --git a/packages/squad-sdk/src/config/init.ts b/packages/squad-sdk/src/config/init.ts index 6f32d058b..74d750656 100644 --- a/packages/squad-sdk/src/config/init.ts +++ b/packages/squad-sdk/src/config/init.ts @@ -457,12 +457,25 @@ function titleCase(str: string): string { /** * Stamp version into squad.agent.md content. + * Replaces three locations: HTML comment, Identity Version line, and {version} placeholder. */ function stampVersionInContent(content: string, version: string): string { - return content.replace( + // HTML comment: + content = content.replace( //, `` ); + // Identity section: - **Version:** X.Y.Z + content = content.replace( + /- \*\*Version:\*\* [0-9.]+(?:-[a-z]+(?:\.\d+)?)?/m, + `- **Version:** ${version}` + ); + // Greeting placeholder: `Squad v{version}` + content = content.replace( + /`Squad v\{version\}`/g, + `\`Squad v${version}\`` + ); + return content; } /** diff --git a/test/cli/init.test.ts b/test/cli/init.test.ts index 8d27d21da..87a697a4d 100644 --- a/test/cli/init.test.ts +++ b/test/cli/init.test.ts @@ -9,6 +9,7 @@ import { join } from 'path'; import { existsSync } from 'fs'; import { randomBytes } from 'crypto'; import { runInit } from '@bradygaster/squad-cli/core/init'; +import { getPackageVersion } from '@bradygaster/squad-cli/core/version'; const TEST_ROOT = join(process.cwd(), `.test-cli-init-${randomBytes(4).toString('hex')}`); @@ -37,6 +38,22 @@ describe('CLI: init command', () => { expect(content).toContain('version:'); }); + it('should stamp CLI version in squad.agent.md during init (#321)', async () => { + await runInit(TEST_ROOT); + + const agentPath = join(TEST_ROOT, '.github', 'agents', 'squad.agent.md'); + const content = await readFile(agentPath, 'utf-8'); + const currentVersion = getPackageVersion(); + + // HTML comment must contain the current CLI version + expect(content).toContain(``); + // Identity section must contain the current CLI version + expect(content).toContain(`- **Version:** ${currentVersion}`); + // {version} placeholder must be replaced + expect(content).not.toContain('`Squad v{version}`'); + expect(content).toContain(`Squad v${currentVersion}`); + }); + it('should create .squad/ directory structure', async () => { await runInit(TEST_ROOT); diff --git a/test/cli/upgrade.test.ts b/test/cli/upgrade.test.ts index 286a92aa8..7c63b3f41 100644 --- a/test/cli/upgrade.test.ts +++ b/test/cli/upgrade.test.ts @@ -55,6 +55,9 @@ describe('CLI: upgrade command', () => { // Verify the file was updated (version should be different from 0.1.0) const upgraded = await readFile(agentPath, 'utf-8'); expect(upgraded).not.toContain(''); + // Identity section and greeting placeholder must also be stamped + expect(upgraded).toContain(`- **Version:** ${getPackageVersion()}`); + expect(upgraded).not.toContain('`Squad v{version}`'); }); it('should return upgrade info with updated files', async () => {