Skip to content
Closed
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
Binary file not shown.
8 changes: 5 additions & 3 deletions packages/squad-cli/src/cli-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ function _handleTopLevelSignal(signal: 'SIGINT' | 'SIGTERM'): void {
process.on('SIGINT', () => _handleTopLevelSignal('SIGINT'));
process.on('SIGTERM', () => _handleTopLevelSignal('SIGTERM'));

import fs from 'node:fs';
import { FSStorageProvider } from '@bradygaster/squad-sdk';
import path from 'node:path';
import { fatal, SquadError } from './cli/core/errors.js';
import { BOLD, RESET, DIM, RED, GREEN, YELLOW } from './cli/core/output.js';
Expand Down Expand Up @@ -409,7 +409,8 @@ async function main(): Promise<void> {
const repoSquad = sdk.resolveSquad(process.cwd());
const globalPath = sdk.resolveGlobalSquadPath();
const globalSquadDir = path.join(globalPath, '.squad');
const globalExists = fs.existsSync(globalSquadDir);
const storage = new FSStorageProvider();
const globalExists = await storage.exists(globalSquadDir);

console.log(`\n${BOLD}Squad Status${RESET}\n`);

Expand Down Expand Up @@ -446,9 +447,10 @@ async function main(): Promise<void> {
const localSquad = sdk.resolveSquad(process.cwd());
const globalPath = sdk.resolveGlobalSquadPath();
const globalSquadDir = path.join(globalPath, '.squad');
const storage = new FSStorageProvider();
const teamRoot = localSquad
? path.resolve(localSquad, '..')
: (fs.existsSync(globalSquadDir) ? globalPath : null);
: (await storage.exists(globalSquadDir) ? globalPath : null);

if (!teamRoot) {
fatal('No squad found. Run "squad init" first.');
Expand Down
21 changes: 10 additions & 11 deletions packages/squad-cli/src/cli/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
* @module cli/commands/build
*/

import fs from 'node:fs';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import { FSStorageProvider } from '@bradygaster/squad-sdk';
import { success, warn, info, dim, BOLD, RESET, YELLOW, GREEN, RED } from '../core/output.js';
import { fatal } from '../core/errors.js';

Expand Down Expand Up @@ -66,14 +66,15 @@ interface LoadedConfig {
* 3. squad.config.js
*/
async function loadSquadConfig(cwd: string): Promise<LoadedConfig> {
const storage = new FSStorageProvider();
const candidates = [
path.join(cwd, 'squad', 'index.ts'),
path.join(cwd, 'squad.config.ts'),
path.join(cwd, 'squad.config.js'),
];

for (const candidate of candidates) {
if (fs.existsSync(candidate)) {
if (storage.existsSync(candidate)) {
try {
const url = pathToFileURL(candidate).href;
const mod = await import(url);
Expand Down Expand Up @@ -430,6 +431,7 @@ interface BuildResult {
}

function writeFiles(cwd: string, files: GeneratedFile[]): BuildResult {
const storage = new FSStorageProvider();
let written = 0;
let skipped = 0;
const drifted: string[] = [];
Expand All @@ -441,33 +443,29 @@ function writeFiles(cwd: string, files: GeneratedFile[]): BuildResult {
}

const absPath = path.join(cwd, file.relPath);
const dir = path.dirname(absPath);

if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}

fs.writeFileSync(absPath, file.content, 'utf-8');
storage.writeSync(absPath, file.content);
written++;
}

return { files, written, skipped, drifted };
}

function checkDrift(cwd: string, files: GeneratedFile[]): { drifted: string[]; clean: string[] } {
const storage = new FSStorageProvider();
const drifted: string[] = [];
const clean: string[] = [];

for (const file of files) {
if (isProtected(file.relPath)) continue;

const absPath = path.join(cwd, file.relPath);
if (!fs.existsSync(absPath)) {
const existing = storage.readSync(absPath);
if (existing === undefined) {
drifted.push(file.relPath);
continue;
}

const existing = fs.readFileSync(absPath, 'utf-8');
if (existing !== file.content) {
drifted.push(file.relPath);
} else {
Expand Down Expand Up @@ -532,9 +530,10 @@ export async function runBuild(cwd: string, options: BuildOptions = {}): Promise

// --dry-run mode
if (options.dryRun) {
const dryRunStorage = new FSStorageProvider();
info(`\n${BOLD}Dry run${RESET} — would generate ${files.length} file(s):\n`);
for (const file of files) {
const exists = fs.existsSync(path.join(cwd, file.relPath));
const exists = dryRunStorage.existsSync(path.join(cwd, file.relPath));
const label = exists ? `${YELLOW}overwrite${RESET}` : `${GREEN}create${RESET}`;
console.log(` ${label} ${file.relPath}`);
}
Expand Down
35 changes: 21 additions & 14 deletions packages/squad-cli/src/cli/commands/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
* Exports squad to squad-export.json
*/

import fs from 'node:fs';
import { statSync } from 'node:fs'; // TODO: statSync().isDirectory() not in StorageProvider
import path from 'node:path';
import { FSStorageProvider } from '@bradygaster/squad-sdk';
import { detectSquadDir } from '../core/detect-squad-dir.js';
import { success, warn } from '../core/output.js';
import { fatal } from '../core/errors.js';
Expand All @@ -22,10 +23,11 @@ interface ExportManifest {
* Export squad to JSON
*/
export async function runExport(dest: string, outPath?: string): Promise<void> {
const storage = new FSStorageProvider();
const squadInfo = detectSquadDir(dest);
const teamMd = path.join(squadInfo.path, 'team.md');

if (!fs.existsSync(teamMd)) {
if (!storage.existsSync(teamMd)) {
fatal('No squad found — run init first');
}

Expand All @@ -43,8 +45,9 @@ export async function runExport(dest: string, outPath?: string): Promise<void> {
for (const file of ['registry.json', 'policy.json', 'history.json']) {
const filePath = path.join(castingDir, file);
try {
if (fs.existsSync(filePath)) {
manifest.casting[file.replace('.json', '')] = JSON.parse(fs.readFileSync(filePath, 'utf8'));
const raw = storage.readSync(filePath);
if (raw !== undefined) {
manifest.casting[file.replace('.json', '')] = JSON.parse(raw);
}
} catch (err) {
console.error(`Warning: could not read casting/${file}: ${(err as Error).message}`);
Expand All @@ -54,15 +57,18 @@ export async function runExport(dest: string, outPath?: string): Promise<void> {
// Read agents
const agentsDir = path.join(squadInfo.path, 'agents');
try {
if (fs.existsSync(agentsDir)) {
for (const entry of fs.readdirSync(agentsDir)) {
if (storage.existsSync(agentsDir)) {
for (const entry of storage.listSync(agentsDir)) {
const agentDir = path.join(agentsDir, entry);
if (!fs.statSync(agentDir).isDirectory()) continue;
// TODO: statSync().isDirectory() not in StorageProvider
if (!statSync(agentDir).isDirectory()) continue;
const agent: { charter?: string; history?: string } = {};
const charterPath = path.join(agentDir, 'charter.md');
const historyPath = path.join(agentDir, 'history.md');
if (fs.existsSync(charterPath)) agent.charter = fs.readFileSync(charterPath, 'utf8');
if (fs.existsSync(historyPath)) agent.history = fs.readFileSync(historyPath, 'utf8');
const charterContent = storage.readSync(charterPath);
if (charterContent !== undefined) agent.charter = charterContent;
const historyContent = storage.readSync(historyPath);
if (historyContent !== undefined) agent.history = historyContent;
manifest.agents[entry] = agent;
}
}
Expand All @@ -76,15 +82,16 @@ export async function runExport(dest: string, outPath?: string): Promise<void> {
{ dir: path.join(squadInfo.path, 'skills'), layout: 'nested' as const },
{ dir: path.join(dest, '.ai-team', 'skills'), layout: 'flat' as const },
];
const skillsSource = skillSources.find(({ dir }) => fs.existsSync(dir));
const skillsSource = skillSources.find(({ dir }) => storage.existsSync(dir));
try {
if (skillsSource) {
for (const entry of fs.readdirSync(skillsSource.dir)) {
for (const entry of storage.listSync(skillsSource.dir)) {
const skillFile = skillsSource.layout === 'nested'
? path.join(skillsSource.dir, entry, 'SKILL.md')
: path.join(skillsSource.dir, entry);
if (fs.existsSync(skillFile)) {
manifest.skills.push(fs.readFileSync(skillFile, 'utf8'));
const skillContent = storage.readSync(skillFile);
if (skillContent !== undefined) {
manifest.skills.push(skillContent);
}
}
}
Expand All @@ -96,7 +103,7 @@ export async function runExport(dest: string, outPath?: string): Promise<void> {
const finalOutPath = outPath || path.join(dest, 'squad-export.json');

try {
fs.writeFileSync(finalOutPath, JSON.stringify(manifest, null, 2) + '\n');
storage.writeSync(finalOutPath, JSON.stringify(manifest, null, 2) + '\n');
} catch (err) {
fatal(`Failed to write export file: ${(err as Error).message}`);
}
Expand Down
42 changes: 24 additions & 18 deletions packages/squad-cli/src/cli/commands/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
* Imports squad from squad-export.json
*/

import fs from 'node:fs';
import path from 'node:path';
import { mkdirSync, renameSync } from 'node:fs'; // TODO: mkdirSync (empty dir scaffolding) and renameSync not in StorageProvider
import { FSStorageProvider } from '@bradygaster/squad-sdk';
import { detectSquadDir } from '../core/detect-squad-dir.js';
import { success, warn, info } from '../core/output.js';
import { fatal } from '../core/errors.js';
Expand All @@ -23,15 +24,20 @@ interface ImportManifest {
* Import squad from JSON
*/
export async function runImport(dest: string, importPath: string, force: boolean): Promise<void> {
const storage = new FSStorageProvider();
const resolvedPath = path.resolve(importPath);

if (!fs.existsSync(resolvedPath)) {
if (!storage.existsSync(resolvedPath)) {
fatal(`Import file not found: ${importPath}`);
}

let manifest: ImportManifest;
try {
manifest = JSON.parse(fs.readFileSync(resolvedPath, 'utf8'));
const raw = storage.readSync(resolvedPath);
if (raw === undefined) {
fatal(`Import file not found: ${importPath}`);
}
manifest = JSON.parse(raw);
} catch (err) {
fatal(`Invalid JSON in import file: ${(err as Error).message}`);
}
Expand All @@ -54,31 +60,33 @@ export async function runImport(dest: string, importPath: string, force: boolean
const squadDir = squadInfo.path;

// Conflict detection
if (fs.existsSync(squadDir)) {
if (storage.existsSync(squadDir)) {
if (!force) {
fatal('A squad already exists here. Use --force to replace (current squad will be archived).');
}
// Archive existing squad
const ts = new Date().toISOString().replace(/:/g, '-').replace(/\./g, '-');
const archiveDir = path.join(dest, `${squadInfo.name}-archive-${ts}`);
fs.renameSync(squadDir, archiveDir);
// TODO: renameSync not in StorageProvider — requires filesystem-level move
renameSync(squadDir, archiveDir);
info(`Archived existing squad to ${path.basename(archiveDir)}`);
}

// Create directory structure
fs.mkdirSync(path.join(squadDir, 'casting'), { recursive: true });
fs.mkdirSync(path.join(squadDir, 'decisions', 'inbox'), { recursive: true });
fs.mkdirSync(path.join(squadDir, 'orchestration-log'), { recursive: true });
fs.mkdirSync(path.join(squadDir, 'log'), { recursive: true });
fs.mkdirSync(path.join(dest, '.copilot', 'skills'), { recursive: true });
// TODO: mkdirSync for empty directory scaffolding not in StorageProvider
mkdirSync(path.join(squadDir, 'casting'), { recursive: true });
mkdirSync(path.join(squadDir, 'decisions', 'inbox'), { recursive: true });
mkdirSync(path.join(squadDir, 'orchestration-log'), { recursive: true });
mkdirSync(path.join(squadDir, 'log'), { recursive: true });
mkdirSync(path.join(dest, '.copilot', 'skills'), { recursive: true });

// Write empty project-specific files
fs.writeFileSync(path.join(squadDir, 'decisions.md'), '');
fs.writeFileSync(path.join(squadDir, 'team.md'), '');
storage.writeSync(path.join(squadDir, 'decisions.md'), '');
storage.writeSync(path.join(squadDir, 'team.md'), '');

// Write casting state
for (const [key, value] of Object.entries(manifest.casting)) {
fs.writeFileSync(
storage.writeSync(
path.join(squadDir, 'casting', `${key}.json`),
JSON.stringify(value, null, 2) + '\n'
);
Expand All @@ -93,10 +101,9 @@ export async function runImport(dest: string, importPath: string, force: boolean
for (const name of agentNames) {
const agent = manifest.agents[name]!;
const agentDir = path.join(squadDir, 'agents', name);
fs.mkdirSync(agentDir, { recursive: true });

if (agent.charter) {
fs.writeFileSync(path.join(agentDir, 'charter.md'), agent.charter);
storage.writeSync(path.join(agentDir, 'charter.md'), agent.charter);
}

// History split: separate portable knowledge from project learnings
Expand All @@ -105,7 +112,7 @@ export async function runImport(dest: string, importPath: string, force: boolean
historyContent = splitHistory(agent.history, sourceProject);
}
historyContent = `📌 Imported from ${sourceProject} on ${importDate}. Portable knowledge carried over; project learnings from previous project preserved below.\n\n` + historyContent;
fs.writeFileSync(path.join(agentDir, 'history.md'), historyContent);
storage.writeSync(path.join(agentDir, 'history.md'), historyContent);
}

// Write skills
Expand All @@ -115,8 +122,7 @@ export async function runImport(dest: string, importPath: string, force: boolean
? nameMatch[1]!.trim().toLowerCase().replace(/\s+/g, '-')
: `skill-${manifest.skills.indexOf(skillContent)}`;
const skillDir = path.join(dest, '.copilot', 'skills', skillName);
fs.mkdirSync(skillDir, { recursive: true });
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent);
storage.writeSync(path.join(skillDir, 'SKILL.md'), skillContent);
}

// Determine universe for messaging
Expand Down
Loading
Loading