From 7b5e2fd64397922a507ed3e0f4aa6dfcfc3f5101 Mon Sep 17 00:00:00 2001 From: williamhallatt Date: Wed, 4 Mar 2026 12:20:51 +1000 Subject: [PATCH] fix(templates): install to .squad/templates/ not .squad-templates/ squad.agent.md references .squad/templates/ throughout, but index.js was installing template files to .squad-templates/ at the repo root. This mismatch meant the coordinator prompt could never find its templates on a fresh install. Root cause: PR #113 migrated all path references in squad.agent.md to .squad/templates/ but PR #111's code change (which renamed the dir to .squad-templates/) was never reconciled with it. Both landed in v0.5.0. Fix: - Change install destination from .squad-templates to .squad/templates - Update migrate-directory rename to match the new path - Update help text and replace-regex to reference .squad/templates - Add a v0.5.5 migration entry that moves .squad-templates/ to .squad/templates/ for existing users upgrading from v0.5.4 or earlier Bumps version to 0.5.5 so the migration runs on upgrade. --- index.js | 113 +++++++++++++++++++-------------- package.json | 2 +- test/migrate-directory.test.js | 24 +++---- 3 files changed, 78 insertions(+), 61 deletions(-) diff --git a/index.js b/index.js index 8766668a4..2d6ad129f 100644 --- a/index.js +++ b/index.js @@ -19,7 +19,7 @@ function fatal(msg) { function detectSquadDir(dest) { const squadDir = path.join(dest, '.squad'); const aiTeamDir = path.join(dest, '.ai-team'); - + if (fs.existsSync(squadDir)) { return { path: squadDir, name: '.squad', isLegacy: false }; } @@ -67,7 +67,7 @@ if (cmd === '--help' || cmd === '-h' || cmd === 'help') { console.log(`Commands:`); console.log(` ${BOLD}(default)${RESET} Initialize Squad (skip files that already exist)`); console.log(` ${BOLD}upgrade${RESET} Update Squad-owned files to latest version`); - console.log(` Overwrites: squad.agent.md, templates dir (.squad-templates/ or .ai-team-templates/)`); + console.log(` Overwrites: squad.agent.md, templates dir (.squad/templates/ or .ai-team-templates/)`); console.log(` Never touches: .squad/ or .ai-team/ (your team state)`); console.log(` Flags: --migrate-directory (rename .ai-team/ → .squad/)`); console.log(` ${BOLD}copilot${RESET} Add/remove the Copilot coding agent (@copilot)`); @@ -210,15 +210,15 @@ if (cmd === 'watch') { for (const member of members) { const role = member.role.toLowerCase(); if ((role.includes('frontend') || role.includes('ui')) && - (issueText.includes('ui') || issueText.includes('frontend') || issueText.includes('css'))) { + (issueText.includes('ui') || issueText.includes('frontend') || issueText.includes('css'))) { assignedMember = member; reason = 'frontend/UI domain'; break; } if ((role.includes('backend') || role.includes('api') || role.includes('server')) && - (issueText.includes('api') || issueText.includes('backend') || issueText.includes('database'))) { + (issueText.includes('api') || issueText.includes('backend') || issueText.includes('database'))) { assignedMember = member; reason = 'backend/API domain'; break; } if ((role.includes('test') || role.includes('qa')) && - (issueText.includes('test') || issueText.includes('bug') || issueText.includes('fix'))) { + (issueText.includes('test') || issueText.includes('bug') || issueText.includes('fix'))) { assignedMember = member; reason = 'testing/QA domain'; break; } } @@ -276,7 +276,7 @@ if (cmd === 'watch') { function scrubEmailsFromDirectory(dirPath) { const EMAIL_PATTERN = /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g; const NAME_WITH_EMAIL_PATTERN = /([a-zA-Z0-9_-]+)\s*\(([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})\)/g; - + const scrubbedFiles = []; const filesToScrub = [ 'team.md', @@ -284,7 +284,7 @@ function scrubEmailsFromDirectory(dirPath) { 'routing.md', 'ceremonies.md' ]; - + // Scrub root-level files for (const file of filesToScrub) { const filePath = path.join(dirPath, file); @@ -292,19 +292,19 @@ function scrubEmailsFromDirectory(dirPath) { try { let content = fs.readFileSync(filePath, 'utf8'); let modified = false; - + // Replace "name (email)" → "name" const beforeNameEmail = content; content = content.replace(NAME_WITH_EMAIL_PATTERN, '$1'); if (content !== beforeNameEmail) modified = true; - + // Replace bare emails in identity contexts (but preserve in URLs and code examples) const lines = content.split('\n'); const scrubbed = lines.map(line => { // Skip lines that look like URLs, code blocks, or examples - if (line.includes('http://') || line.includes('https://') || - line.includes('```') || line.includes('example.com') || - line.trim().startsWith('//') || line.trim().startsWith('#')) { + if (line.includes('http://') || line.includes('https://') || + line.includes('```') || line.includes('example.com') || + line.trim().startsWith('//') || line.trim().startsWith('#')) { return line; } // Scrub emails from identity/attribution contexts @@ -313,7 +313,7 @@ function scrubEmailsFromDirectory(dirPath) { if (before !== after) modified = true; return after; }); - + if (modified) { fs.writeFileSync(filePath, scrubbed.join('\n')); scrubbedFiles.push(path.relative(dirPath, filePath)); @@ -323,7 +323,7 @@ function scrubEmailsFromDirectory(dirPath) { } } } - + // Scrub agent history files const agentsDir = path.join(dirPath, 'agents'); if (fs.existsSync(agentsDir)) { @@ -333,19 +333,19 @@ function scrubEmailsFromDirectory(dirPath) { if (fs.existsSync(historyPath)) { let content = fs.readFileSync(historyPath, 'utf8'); let modified = false; - + // Replace "name (email)" → "name" const beforeNameEmail = content; content = content.replace(NAME_WITH_EMAIL_PATTERN, '$1'); if (content !== beforeNameEmail) modified = true; - + // Scrub bare emails carefully const lines = content.split('\n'); const scrubbed = lines.map(line => { // Skip URLs, code, examples - if (line.includes('http://') || line.includes('https://') || - line.includes('```') || line.includes('example.com') || - line.trim().startsWith('//') || line.trim().startsWith('#')) { + if (line.includes('http://') || line.includes('https://') || + line.includes('```') || line.includes('example.com') || + line.trim().startsWith('//') || line.trim().startsWith('#')) { return line; } const before = line; @@ -353,7 +353,7 @@ function scrubEmailsFromDirectory(dirPath) { if (before !== after) modified = true; return after; }); - + if (modified) { fs.writeFileSync(historyPath, scrubbed.join('\n')); scrubbedFiles.push(path.relative(dirPath, historyPath)); @@ -364,7 +364,7 @@ function scrubEmailsFromDirectory(dirPath) { console.error(`${RED}✗${RESET} Failed to scrub agent histories: ${err.message}`); } } - + // Scrub log files const logDir = path.join(dirPath, 'log'); if (fs.existsSync(logDir)) { @@ -385,7 +385,7 @@ function scrubEmailsFromDirectory(dirPath) { console.error(`${RED}✗${RESET} Failed to scrub log files: ${err.message}`); } } - + return scrubbedFiles; } @@ -393,7 +393,7 @@ function scrubEmailsFromDirectory(dirPath) { function replaceAiTeamReferences(dirPath) { const updatedFiles = []; const replacements = [ - [/\.ai-team-templates\//g, '.squad-templates/'], + [/\.ai-team-templates\//g, '.squad/templates/'], [/\.ai-team\//g, '.squad/'] ]; @@ -429,14 +429,14 @@ function detectProjectType(dir) { if (fs.existsSync(path.join(dir, 'package.json'))) return 'npm'; if (fs.existsSync(path.join(dir, 'go.mod'))) return 'go'; if (fs.existsSync(path.join(dir, 'requirements.txt')) || - fs.existsSync(path.join(dir, 'pyproject.toml'))) return 'python'; + fs.existsSync(path.join(dir, 'pyproject.toml'))) return 'python'; if (fs.existsSync(path.join(dir, 'pom.xml')) || - fs.existsSync(path.join(dir, 'build.gradle')) || - fs.existsSync(path.join(dir, 'build.gradle.kts'))) return 'java'; + fs.existsSync(path.join(dir, 'build.gradle')) || + fs.existsSync(path.join(dir, 'build.gradle.kts'))) return 'java'; try { const entries = fs.readdirSync(dir); if (entries.some(e => e.endsWith('.csproj') || e.endsWith('.sln') || e.endsWith('.slnx') || e.endsWith('.fsproj') || e.endsWith('.vbproj'))) return 'dotnet'; - } catch {} + } catch { } return 'unknown'; } @@ -612,14 +612,14 @@ function writeWorkflowFile(file, srcPath, destPath, projectType) { // --- Email scrubbing subcommand --- if (cmd === 'scrub-emails') { const targetDir = process.argv[3] || path.join(dest, '.ai-team'); - + if (!fs.existsSync(targetDir)) { fatal(`Directory not found: ${targetDir}`); } - + console.log(`${DIM}Scanning ${path.relative(dest, targetDir)} for email addresses...${RESET}`); const scrubbedFiles = scrubEmailsFromDirectory(targetDir); - + if (scrubbedFiles.length === 0) { console.log(`${GREEN}✓${RESET} No email addresses found — all clean`); } else { @@ -1163,15 +1163,15 @@ const isMigrateDirectory = isUpgrade && process.argv.includes('--migrate-directo if (isMigrateDirectory) { const aiTeamDir = path.join(dest, '.ai-team'); const squadDir = path.join(dest, '.squad'); - + if (!fs.existsSync(aiTeamDir)) { fatal('No .ai-team/ directory found — nothing to migrate.'); } - + if (fs.existsSync(squadDir)) { fatal('.squad/ directory already exists — migration appears to be complete.'); } - + // Safe rename that falls back to copy+delete on Windows EPERM/EACCES function safeRename(source, target) { try { @@ -1187,12 +1187,12 @@ if (isMigrateDirectory) { } console.log(`${DIM}Migrating .ai-team/ → .squad/...${RESET}`); - + try { // Rename directory safeRename(aiTeamDir, squadDir); console.log(`${GREEN}✓${RESET} Renamed .ai-team/ → .squad/`); - + // Update .gitattributes const gitattributes = path.join(dest, '.gitattributes'); if (fs.existsSync(gitattributes)) { @@ -1203,7 +1203,7 @@ if (isMigrateDirectory) { console.log(`${GREEN}✓${RESET} Updated .gitattributes`); } } - + // Update .gitignore if it exists const gitignore = path.join(dest, '.gitignore'); if (fs.existsSync(gitignore)) { @@ -1214,7 +1214,7 @@ if (isMigrateDirectory) { console.log(`${GREEN}✓${RESET} Updated .gitignore`); } } - + // Scrub email addresses from migrated files console.log(`${DIM}Scrubbing email addresses from .squad/ files...${RESET}`); const scrubbedFiles = scrubEmailsFromDirectory(squadDir); @@ -1233,12 +1233,12 @@ if (isMigrateDirectory) { console.log(`${GREEN}✓${RESET} No .ai-team/ references found`); } - // Rename .ai-team-templates/ → .squad-templates/ if it exists + // Rename .ai-team-templates/ → .squad/templates/ if it exists const aiTeamTemplatesDir = path.join(dest, '.ai-team-templates'); - const squadTemplatesDir = path.join(dest, '.squad-templates'); + const squadTemplatesDir = path.join(dest, '.squad', 'templates'); if (fs.existsSync(aiTeamTemplatesDir)) { safeRename(aiTeamTemplatesDir, squadTemplatesDir); - console.log(`${GREEN}✓${RESET} Renamed .ai-team-templates/ → .squad-templates/`); + console.log(`${GREEN}✓${RESET} Renamed .ai-team-templates/ → .squad/templates/`); } console.log(); @@ -1247,7 +1247,7 @@ if (isMigrateDirectory) { console.log(` git add -A`); console.log(` git commit -m "chore: migrate .ai-team/ → .squad/"`); console.log(); - + } catch (err) { fatal(`Migration failed: ${err.message}`); } @@ -1342,6 +1342,21 @@ const migrations = [ console.log(`${GREEN}✓${RESET} Removed squad-main-guard.yml — .squad/ files can now flow freely to all branches`); } } + }, + { + version: '0.5.5', + description: 'Move .squad-templates/ into .squad/templates/', + run(dest, squadDir) { + const oldPath = path.join(dest, '.squad-templates'); + const newPath = path.join(squadDir || path.join(dest, '.squad'), 'templates'); + if (fs.existsSync(oldPath) && !fs.existsSync(newPath)) { + safeRename(oldPath, newPath); + console.log(`${GREEN}✓${RESET} Moved .squad-templates/ → .squad/templates/`); + } else if (fs.existsSync(oldPath) && fs.existsSync(newPath)) { + fs.rmSync(oldPath, { recursive: true }); + console.log(`${GREEN}✓${RESET} Removed redundant .squad-templates/ (already have .squad/templates/)`); + } + } } ]; @@ -1486,7 +1501,7 @@ if (isUpgrade) { const squadInfo = (() => { const squadDir = path.join(dest, '.squad'); const aiTeamDir = path.join(dest, '.ai-team'); - + if (fs.existsSync(squadDir)) { return { path: squadDir, name: '.squad', isLegacy: false }; } @@ -1651,20 +1666,22 @@ if (missing.length) { // Copy templates (Squad-owned — overwrite on upgrade) const templatesSrc = path.join(root, 'templates'); -const templatesDestName = squadInfo.isLegacy ? '.ai-team-templates' : '.squad-templates'; -const templatesDest = path.join(dest, templatesDestName); +const templatesDest = squadInfo.isLegacy + ? path.join(dest, '.ai-team-templates') + : path.join(dest, '.squad', 'templates'); +const templatesDestDisplay = squadInfo.isLegacy ? '.ai-team-templates/' : '.squad/templates/'; if (isUpgrade) { copyRecursive(templatesSrc, templatesDest); - console.log(`${GREEN}✓${RESET} ${BOLD}upgraded${RESET} ${templatesDestName}/`); + console.log(`${GREEN}✓${RESET} ${BOLD}upgraded${RESET} ${templatesDestDisplay}`); // Run migrations applicable for this version jump runMigrations(dest, oldVersion || '0.0.0', squadInfo.path); } else if (fs.existsSync(templatesDest)) { - console.log(`${DIM}${templatesDestName}/ already exists — skipping (run 'upgrade' to update)${RESET}`); + console.log(`${DIM}${templatesDestDisplay} already exists — skipping (run 'upgrade' to update)${RESET}`); } else { copyRecursive(templatesSrc, templatesDest); - console.log(`${GREEN}✓${RESET} ${templatesDestName}/`); + console.log(`${GREEN}✓${RESET} ${templatesDestDisplay}`); } // Copy workflow templates (Squad-owned — overwrite on upgrade) @@ -1709,7 +1726,7 @@ if (isUpgrade) { } else { console.log(`${GREEN}✓${RESET} No email addresses found`); } - + console.log(`\n${DIM}${squadInfo.name}/ untouched — your team state is safe${RESET}`); // Hint about new features available after upgrade diff --git a/package.json b/package.json index 3c7d37224..6ec9f21e6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bradygaster/create-squad", - "version": "0.5.4", + "version": "0.5.5", "description": "Add an AI agent team to any project", "bin": { "create-squad": "./index.js" diff --git a/test/migrate-directory.test.js b/test/migrate-directory.test.js index fa51d83b8..3e28352c6 100644 --- a/test/migrate-directory.test.js +++ b/test/migrate-directory.test.js @@ -136,11 +136,11 @@ describe('upgrade --migrate-directory: full upgrade runs (no early exit)', () => assert.ok(files.length > 0, 'workflow files should be written after migrate+upgrade'); }); - it('.squad-templates/ is created/updated after migration (not .ai-team-templates/)', () => { + it('.squad/templates/ is created/updated after migration (not .ai-team-templates/)', () => { runSquad(['upgrade', '--migrate-directory'], tmpDir); assert.ok( - fs.existsSync(path.join(tmpDir, '.squad-templates')), - '.squad-templates/ should exist after migrate+upgrade' + fs.existsSync(path.join(tmpDir, '.squad', 'templates')), + '.squad/templates/ should exist after migrate+upgrade' ); assert.ok( !fs.existsSync(path.join(tmpDir, '.ai-team-templates')), @@ -374,7 +374,7 @@ describe('templates directory migration', () => { let tmpDir; afterEach(() => cleanDir(tmpDir)); - it('.ai-team-templates/ is renamed to .squad-templates/ during --migrate-directory', () => { + it('.ai-team-templates/ is renamed to .squad/templates/ during --migrate-directory', () => { tmpDir = makeTempDir(); makeOldSquadRepo(tmpDir); // Create .ai-team-templates/ with a dummy file @@ -385,8 +385,8 @@ describe('templates directory migration', () => { const result = runSquad(['upgrade', '--migrate-directory'], tmpDir); assert.equal(result.exitCode, 0, `expected exit 0: ${result.stdout}`); assert.ok( - fs.existsSync(path.join(tmpDir, '.squad-templates')), - '.squad-templates/ should exist after migration' + fs.existsSync(path.join(tmpDir, '.squad', 'templates')), + '.squad/templates/ should exist after migration' ); assert.ok( !fs.existsSync(path.join(tmpDir, '.ai-team-templates')), @@ -394,7 +394,7 @@ describe('templates directory migration', () => { ); }); - it('.squad-templates/ contents are preserved after rename', () => { + it('.squad/templates/ contents are preserved after rename', () => { tmpDir = makeTempDir(); makeOldSquadRepo(tmpDir); const aiTeamTemplates = path.join(tmpDir, '.ai-team-templates'); @@ -402,10 +402,10 @@ describe('templates directory migration', () => { fs.writeFileSync(path.join(aiTeamTemplates, 'my-template.md'), '# preserved\n'); runSquad(['upgrade', '--migrate-directory'], tmpDir); - const preservedFile = path.join(tmpDir, '.squad-templates', 'my-template.md'); + const preservedFile = path.join(tmpDir, '.squad', 'templates', 'my-template.md'); assert.ok( fs.existsSync(preservedFile), - 'my-template.md should be in .squad-templates/ after rename' + 'my-template.md should be in .squad/templates/ after rename' ); const content = fs.readFileSync(preservedFile, 'utf8'); assert.equal(content, '# preserved\n', 'file contents should be unchanged after rename'); @@ -424,7 +424,7 @@ describe('templates directory migration', () => { ); }); - it('after migration, upgrade writes templates to .squad-templates/ not .ai-team-templates/', () => { + it('after migration, upgrade writes templates to .squad/templates/ not .ai-team-templates/', () => { tmpDir = makeTempDir(); // Set up a fully migrated state: .squad/ exists, no .ai-team/ const squadDir = path.join(tmpDir, '.squad'); @@ -447,8 +447,8 @@ describe('templates directory migration', () => { const result = runSquad(['upgrade'], tmpDir); assert.equal(result.exitCode, 0, `upgrade from migrated state should succeed: ${result.stdout}`); assert.ok( - fs.existsSync(path.join(tmpDir, '.squad-templates')), - '.squad-templates/ should be created/updated by upgrade on migrated repo' + fs.existsSync(path.join(tmpDir, '.squad', 'templates')), + '.squad/templates/ should be created/updated by upgrade on migrated repo' ); assert.ok( !fs.existsSync(path.join(tmpDir, '.ai-team-templates')),