Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion packages/squad-cli/src/cli/core/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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`);
Expand Down
62 changes: 3 additions & 59 deletions packages/squad-cli/src/cli/core/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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(/<!-- version: ([0-9.]+(?:-[a-z]+(?:\.\d+)?)?) -->/);
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 (a<b), 0 (a==b), 1 (a>b)
*/
Expand All @@ -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(/<!-- version: [^>]+ -->/m, `<!-- version: ${version} -->`);

// 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
*/
Expand Down Expand Up @@ -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<UpdateInfo> {
const cliVersion = getCLIVersion();
const cliVersion = getPackageVersion();
const filesUpdated: string[] = [];

// Detect squad directory
Expand All @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions packages/squad-cli/src/cli/core/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(/<!-- version: [^>]+ -->/m, `<!-- version: ${version} -->`);
// 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);
Expand All @@ -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(/<!-- version: ([0-9.]+(?:-[a-z]+)?) -->/);
const commentMatch = content.match(/<!-- version: ([0-9.]+(?:-[a-z]+(?:\.\d+)?)?) -->/);
if (commentMatch) return commentMatch[1]!;
// Fallback: try old frontmatter format for backward compatibility during upgrade
const frontmatterMatch = content.match(/^version:\s*"([^"]+)"/m);
Expand Down
15 changes: 14 additions & 1 deletion packages/squad-sdk/src/config/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: <!-- version: X.Y.Z -->
content = content.replace(
/<!-- version: [^>]* -->/,
`<!-- version: ${version} -->`
);
// 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;
}

/**
Expand Down
17 changes: 17 additions & 0 deletions test/cli/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')}`);

Expand Down Expand Up @@ -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(`<!-- version: ${currentVersion} -->`);
// 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);

Expand Down
3 changes: 3 additions & 0 deletions test/cli/upgrade.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('<!-- version: 0.1.0 -->');
// 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 () => {
Expand Down
Loading