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
28 changes: 28 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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
Expand Down
147 changes: 147 additions & 0 deletions test/email-scrub.test.js
Original file line number Diff line number Diff line change
@@ -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');
});
});
Loading