From fb706c32b268d89266b73c2789a7a9e2a0fc5521 Mon Sep 17 00:00:00 2001 From: bradygaster Date: Fri, 20 Feb 2026 00:33:09 -0800 Subject: [PATCH] fix: scrub email addresses from .squad/ files during migration (#108) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- index.js | 28 ++++++++ test/email-scrub.test.js | 147 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 test/email-scrub.test.js diff --git a/index.js b/index.js index 6d091eec1..9cacb81c6 100644 --- a/index.js +++ b/index.js @@ -804,6 +804,15 @@ if (isMigrateDirectory) { } } + // Scrub email addresses from migrated files + console.log(`${DIM}Scrubbing email addresses from .squad/ files...${RESET}`); + const scrubbedFiles = scrubEmailsFromDirectory(squadDir); + if (scrubbedFiles.length > 0) { + console.log(`${GREEN}✓${RESET} Scrubbed email addresses from ${scrubbedFiles.length} file(s)`); + } else { + console.log(`${GREEN}✓${RESET} No email addresses found`); + } + console.log(); console.log(`${BOLD}Migration complete.${RESET}`); console.log(`${DIM}Commit the change:${RESET}`); @@ -991,6 +1000,16 @@ if (isUpgrade) { // Non-fatal in early-exit path } + // Scrub email addresses even when already up to date + const squadDir = detectSquadDir(dest); + console.log(`${DIM}Scrubbing email addresses from ${squadDir.name}/ files...${RESET}`); + const scrubbedFiles = scrubEmailsFromDirectory(squadDir.path); + if (scrubbedFiles.length > 0) { + console.log(`${GREEN}✓${RESET} Scrubbed email addresses from ${scrubbedFiles.length} file(s)`); + } else { + console.log(`${GREEN}✓${RESET} No email addresses found`); + } + console.log(`${GREEN}✓${RESET} Already up to date (v${pkg.version})`); process.exit(0); } @@ -1189,6 +1208,15 @@ if (fs.existsSync(workflowsSrc) && fs.statSync(workflowsSrc).isDirectory()) { } if (isUpgrade) { + // Scrub email addresses from existing squad directory + console.log(`${DIM}Scrubbing email addresses from ${squadInfo.name}/ files...${RESET}`); + const scrubbedFiles = scrubEmailsFromDirectory(squadInfo.path); + if (scrubbedFiles.length > 0) { + console.log(`${GREEN}✓${RESET} Scrubbed email addresses from ${scrubbedFiles.length} file(s)`); + } 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/test/email-scrub.test.js b/test/email-scrub.test.js new file mode 100644 index 000000000..9bb7e5997 --- /dev/null +++ b/test/email-scrub.test.js @@ -0,0 +1,147 @@ +const { describe, it, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const path = require('path'); +const { execFileSync } = require('child_process'); +const os = require('os'); + +const CLI = path.join(__dirname, '..', 'index.js'); + +function runSquad(args, cwd) { + try { + const result = execFileSync(process.execPath, [CLI, ...args], { + cwd, + encoding: 'utf8', + timeout: 15000, + env: { ...process.env, NO_COLOR: '1' }, + }); + return { stdout: result, exitCode: 0 }; + } catch (err) { + return { + stdout: (err.stdout || '') + (err.stderr || ''), + exitCode: err.status ?? 1, + }; + } +} + +function makeTempDir() { + return fs.mkdtempSync(path.join(os.tmpdir(), 'squad-email-scrub-test-')); +} + +function cleanDir(dir) { + try { + fs.rmSync(dir, { recursive: true, force: true }); + } catch {} +} + +describe('Email Scrubbing During Migration (#108)', () => { + let tempDir; + + beforeEach(() => { + tempDir = makeTempDir(); + }); + + afterEach(() => { + cleanDir(tempDir); + }); + + it('should scrub email addresses during migration', () => { + // Create a fake .ai-team/ directory with email addresses + const aiTeamDir = path.join(tempDir, '.ai-team'); + fs.mkdirSync(aiTeamDir, { recursive: true }); + + // Create team.md with email addresses in various formats + const teamMd = path.join(aiTeamDir, 'team.md'); + fs.writeFileSync(teamMd, `# Team Roster + +- **Owner:** Brady Gaster (brady@example.com) +- **Member:** Jane Doe (jane.doe@example.org) + +## Notes + +Contact admin. +`); + + // Run migration + const result = runSquad(['upgrade', '--migrate-directory'], tempDir); + assert.equal(result.exitCode, 0, `Migration should succeed: ${result.stdout}`); + + // Check that .squad/ exists + const squadDir = path.join(tempDir, '.squad'); + assert.ok(fs.existsSync(squadDir), '.squad/ should exist after migration'); + assert.ok(!fs.existsSync(aiTeamDir), '.ai-team/ should be renamed'); + + // Verify email addresses are scrubbed from team.md + const scrubbedTeamMd = fs.readFileSync(path.join(squadDir, 'team.md'), 'utf8'); + + // The existing scrubber removes " (email)" pattern for names + assert.ok(scrubbedTeamMd.includes('Brady Gaster'), 'team.md should keep names'); + assert.ok(scrubbedTeamMd.includes('Jane Doe'), 'team.md should keep names'); + assert.ok(!scrubbedTeamMd.includes('brady@example.com'), 'team.md should not have brady email'); + assert.ok(!scrubbedTeamMd.includes('jane.doe@example.org'), 'team.md should not have jane email'); + + // Verify the scrubbing was reported + assert.ok(result.stdout.includes('Scrubbing') || result.stdout.includes('email'), + 'Output should mention scrubbing'); + }); + + it('should scrub email addresses during regular upgrade', () => { + // Initialize squad first + runSquad([], tempDir); + + // Manually add email addresses to .squad/team.md + const squadDir = path.join(tempDir, '.squad'); + fs.mkdirSync(squadDir, { recursive: true }); + + const teamMd = path.join(squadDir, 'team.md'); + fs.writeFileSync(teamMd, `# Team + +- Alice (alice@example.com) +- Bob (bob@test.org) +`); + + // Run upgrade (without --migrate-directory) + const result = runSquad(['upgrade'], tempDir); + assert.equal(result.exitCode, 0, `Upgrade should succeed: ${result.stdout}`); + + // Verify emails are scrubbed + const scrubbedTeamMd = fs.readFileSync(teamMd, 'utf8'); + assert.ok(scrubbedTeamMd.includes('Alice'), 'team.md should keep names'); + assert.ok(scrubbedTeamMd.includes('Bob'), 'team.md should keep names'); + assert.ok(!scrubbedTeamMd.includes('alice@example.com'), 'team.md should not have alice email'); + assert.ok(!scrubbedTeamMd.includes('bob@test.org'), 'team.md should not have bob email'); + + // Verify the scrubbing was reported + assert.ok(result.stdout.includes('Scrubbing') || result.stdout.includes('email'), + 'Output should mention email scrubbing'); + }); + + it('should handle files without email addresses gracefully', () => { + // Initialize squad + runSquad([], tempDir); + + // Create .squad/team.md without emails + const squadDir = path.join(tempDir, '.squad'); + fs.mkdirSync(squadDir, { recursive: true }); + + const teamMd = path.join(squadDir, 'team.md'); + fs.writeFileSync(teamMd, `# Team + +- Alice +- Bob +`); + + // Run upgrade + const result = runSquad(['upgrade'], tempDir); + assert.equal(result.exitCode, 0, `Upgrade should succeed: ${result.stdout}`); + + // Verify file is unchanged + const content = fs.readFileSync(teamMd, 'utf8'); + assert.ok(content.includes('Alice'), 'team.md should be unchanged'); + assert.ok(content.includes('Bob'), 'team.md should be unchanged'); + + // Verify the result mentions scrubbing check + assert.ok(result.stdout.includes('Scrubbing') || result.stdout.includes('email'), + 'Output should mention email scrubbing'); + }); +});