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
113 changes: 65 additions & 48 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
Expand Down Expand Up @@ -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)`);
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -276,35 +276,35 @@ 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',
'decisions.md',
'routing.md',
'ceremonies.md'
];

// Scrub root-level files
for (const file of filesToScrub) {
const filePath = path.join(dirPath, file);
if (fs.existsSync(filePath)) {
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
Expand All @@ -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));
Expand All @@ -323,7 +323,7 @@ function scrubEmailsFromDirectory(dirPath) {
}
}
}

// Scrub agent history files
const agentsDir = path.join(dirPath, 'agents');
if (fs.existsSync(agentsDir)) {
Expand All @@ -333,27 +333,27 @@ 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;
const after = line.replace(EMAIL_PATTERN, '[email scrubbed]');
if (before !== after) modified = true;
return after;
});

if (modified) {
fs.writeFileSync(historyPath, scrubbed.join('\n'));
scrubbedFiles.push(path.relative(dirPath, historyPath));
Expand All @@ -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)) {
Expand All @@ -385,15 +385,15 @@ function scrubEmailsFromDirectory(dirPath) {
console.error(`${RED}✗${RESET} Failed to scrub log files: ${err.message}`);
}
}

return scrubbedFiles;
}

// Replace legacy .ai-team/ path references inside .md and .json files
function replaceAiTeamReferences(dirPath) {
const updatedFiles = [];
const replacements = [
[/\.ai-team-templates\//g, '.squad-templates/'],
[/\.ai-team-templates\//g, '.squad/templates/'],
[/\.ai-team\//g, '.squad/']
];

Expand Down Expand Up @@ -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';
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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)) {
Expand All @@ -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)) {
Expand All @@ -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);
Expand All @@ -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();
Expand All @@ -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}`);
}
Expand Down Expand Up @@ -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/)`);
}
}
}
];

Expand Down Expand Up @@ -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 };
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
Loading
Loading