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
5 changes: 5 additions & 0 deletions .changeset/cli-stubs-cast-build-watch-memory-provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bradygaster/squad-cli": patch
---

Implement persisted agent creation for `squad cast --name <name> --role <role>` and `squad hire`, implement `squad build --watch` with initial build and live rebuilds, and report unsupported `squad memory provider --provider copilot` via `SquadError` with aligned help text (#2191, #2192, #2193).
35 changes: 21 additions & 14 deletions packages/squad-cli/src/cli-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,21 +142,21 @@
// "Unknown command: version" reports may be shell-specific (e.g. alias/wrapper
// prepending flags so args[0] is no longer 'version'). No intercepting router found.
if (cmd === '--version' || cmd === '-v' || cmd === 'version') {
console.log(VERSION);

Check warning on line 145 in packages/squad-cli/src/cli-entry.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected console statement. Only these console methods are allowed: warn, error
return;
}

// --help / -h / help
if (cmd === '--help' || cmd === '-h' || cmd === 'help') {
console.log(`\n${BOLD}squad${RESET} v${VERSION} — Add an AI agent team to any project\n`);

Check warning on line 151 in packages/squad-cli/src/cli-entry.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected console statement. Only these console methods are allowed: warn, error
console.log(`Usage: squad [command] [options]\n`);

Check warning on line 152 in packages/squad-cli/src/cli-entry.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected console statement. Only these console methods are allowed: warn, error
console.log(`Commands:`);

Check warning on line 153 in packages/squad-cli/src/cli-entry.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected console statement. Only these console methods are allowed: warn, error
console.log(` ${BOLD}(default)${RESET} Launch interactive shell (no args)`);

Check warning on line 154 in packages/squad-cli/src/cli-entry.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected console statement. Only these console methods are allowed: warn, error
console.log(` Flags: --global (init in personal squad directory)`);

Check warning on line 155 in packages/squad-cli/src/cli-entry.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected console statement. Only these console methods are allowed: warn, error
console.log(` ${BOLD}init${RESET} Initialize Squad (markdown-only, default)`);

Check warning on line 156 in packages/squad-cli/src/cli-entry.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected console statement. Only these console methods are allowed: warn, error
console.log(` Flags: --sdk (SDK builder syntax)`);

Check warning on line 157 in packages/squad-cli/src/cli-entry.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected console statement. Only these console methods are allowed: warn, error
console.log(` --roles (use base roles)`);

Check warning on line 158 in packages/squad-cli/src/cli-entry.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected console statement. Only these console methods are allowed: warn, error
console.log(` --global (personal squad dir)`);

Check warning on line 159 in packages/squad-cli/src/cli-entry.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected console statement. Only these console methods are allowed: warn, error
console.log(` --no-workflows (skip CI setup)`);
console.log(` --no-vscode-default (skip .vscode/settings.json update)`);
console.log(` --preset <name> (apply a preset after init)`);
Expand Down Expand Up @@ -798,24 +798,31 @@

if (cmd === 'cast' || cmd === 'hire') {
const nameIdx = args.indexOf('--name');
const name = (nameIdx !== -1 && args[nameIdx + 1]) ? args[nameIdx + 1] : undefined;
const roleIdx = args.indexOf('--role');
const role = (roleIdx !== -1 && args[roleIdx + 1]) ? args[roleIdx + 1] : undefined;

// `squad cast` with no wizard flags shows the roster; `squad hire` always runs the wizard
if (cmd === 'cast' && !name && !role) {
const { runCast } = await import('./cli/commands/cast.js');
await runCast(getSquadStartDir());
const hasNameFlag = nameIdx !== -1;
const hasRoleFlag = roleIdx !== -1;
const force = args.includes('--force');

const readFlagValue = (idx: number): string | undefined => {
if (idx === -1) return undefined;
const val = args[idx + 1];
return val && !val.startsWith('-') ? val : undefined;
};

if (cmd === 'hire' || hasNameFlag || hasRoleFlag) {
const name = readFlagValue(nameIdx);
const role = readFlagValue(roleIdx);
if (!name || !role) {
fatal('Usage: squad cast --name <name> --role <role> (or: squad hire --name <name> --role <role>)');
}
const { runCastAdd } = await import('./cli/commands/cast.js');
await runCastAdd(getSquadStartDir(), { name, role, force });
return;
}

console.log('🎬 Squad cast — team creation wizard starting... (full implementation pending)');
if (name) {
console.log(` Name: ${name}`);
}
if (role) {
console.log(` Role: ${role}`);
}
// `squad cast` with no wizard flags shows the roster
const { runCast } = await import('./cli/commands/cast.js');
await runCast(getSquadStartDir());
return;
}

Expand Down
124 changes: 114 additions & 10 deletions packages/squad-cli/src/cli/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* @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';
Expand Down Expand Up @@ -66,7 +67,7 @@ interface LoadedConfig {
* 2. squad.config.ts
* 3. squad.config.js
*/
async function loadSquadConfig(cwd: string): Promise<LoadedConfig> {
async function loadSquadConfig(cwd: string, cacheBust = false): Promise<LoadedConfig> {
const storage = new FSStorageProvider();
const candidates = [
path.join(cwd, 'squad', 'index.ts'),
Expand All @@ -77,7 +78,8 @@ async function loadSquadConfig(cwd: string): Promise<LoadedConfig> {
for (const candidate of candidates) {
if (storage.existsSync(candidate)) {
try {
const url = pathToFileURL(candidate).href;
const baseUrl = pathToFileURL(candidate).href;
const url = cacheBust ? `${baseUrl}?update=${Date.now()}-${Math.random()}` : baseUrl;
const mod = await import(url);
const config: SquadSDKConfig = mod.default ?? mod.config ?? mod.squadConfig;
if (!config) {
Expand Down Expand Up @@ -495,6 +497,14 @@ export interface BuildOptions {
check?: boolean;
dryRun?: boolean;
watch?: boolean;
/** Optional AbortSignal to stop `--watch` mode cleanly (used in tests and programmatic callers). */
signal?: AbortSignal;
/** Optional callback invoked when the watcher is active and ready. */
onWatchReady?: (sourcePath: string) => void;
/** Optional callback invoked after each rebuild in `--watch` mode. */
onRebuild?: (result: { written: number; skipped: number; source: string }) => void;
/** Debounce interval in milliseconds for `--watch` rebuilds (default: 75ms). */
debounceMs?: number;
}

/**
Expand All @@ -504,13 +514,6 @@ export interface BuildOptions {
* @param options - CLI flags
*/
export async function runBuild(cwd: string, options: BuildOptions = {}): Promise<void> {
// --watch stub
if (options.watch) {
info(`${YELLOW}⏳${RESET} Watch mode coming soon — rebuilding on file changes is not yet implemented.`);
info(` For now, run ${BOLD}squad build${RESET} manually after changes.`);
return;
}

// Resolve effective state directory (respects externalized state)
let stateDir: string | undefined;
try {
Expand Down Expand Up @@ -564,13 +567,114 @@ export async function runBuild(cwd: string, options: BuildOptions = {}): Promise
return;
}

// Default: write files
// Default: write files (initial build)
const result = writeFiles(cwd, files, stateDir);

success(`squad build complete — generated ${result.written} file(s)`);
if (result.skipped > 0) {
dim(` (${result.skipped} protected file(s) skipped)`);
}

// --watch mode: stay alive and rebuild when the config file changes
if (options.watch) {
const relSource = path.relative(cwd, source) || path.basename(source);
info(`${YELLOW}👀${RESET} Watching ${BOLD}${relSource}${RESET} for changes... (Ctrl+C to stop)`);
await watchSquadConfig(cwd, source, stateDir, options);
}
}

async function watchSquadConfig(
cwd: string,
initialSource: string,
stateDir: string | undefined,
options: BuildOptions,
): Promise<void> {
if (options.signal?.aborted) return;

const watchDir = path.dirname(initialSource);
const targetBasename = path.basename(initialSource);
const debounceMs = options.debounceMs ?? 75;
let lastContent = '';
try {
lastContent = fs.readFileSync(initialSource, 'utf-8');
} catch {
// Ignore initial read failure
}

await new Promise<void>((resolve) => {
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
let rebuilding = false;
let pendingRebuild = false;
let closed = false;

const cleanup = () => {
if (closed) return;
closed = true;
if (debounceTimer) clearTimeout(debounceTimer);
watcher.close();
process.off('SIGINT', onSignal);
process.off('SIGTERM', onSignal);
options.signal?.removeEventListener('abort', onAbort);
resolve();
};

const onSignal = () => cleanup();
const onAbort = () => cleanup();

const triggerRebuild = async () => {
if (closed) return;
if (rebuilding) {
pendingRebuild = true;
return;
}
rebuilding = true;
try {
let currentContent = '';
try {
currentContent = fs.readFileSync(initialSource, 'utf-8');
} catch {
// File may be mid-rename during atomic save; wait for next event
return;
}
if (currentContent === lastContent) {
return;
}
lastContent = currentContent;

const { config: updatedConfig, source: updatedSource } = await loadSquadConfig(cwd, true);
const updatedFiles = buildFilePlan(updatedConfig);
const rebuildResult = writeFiles(cwd, updatedFiles, stateDir);
success(`squad build complete — generated ${rebuildResult.written} file(s)`);
options.onRebuild?.({
written: rebuildResult.written,
skipped: rebuildResult.skipped,
source: updatedSource,
});
} catch (err) {
warn(`Rebuild failed: ${err instanceof Error ? err.message : String(err)}`);
} finally {
rebuilding = false;
if (pendingRebuild && !closed) {
pendingRebuild = false;
void triggerRebuild();
}
}
};

const watcher = fs.watch(watchDir, (_eventType, filename) => {
if (closed) return;
if (filename && filename !== targetBasename) return;
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
void triggerRebuild();
}, debounceMs);
});

process.on('SIGINT', onSignal);
process.on('SIGTERM', onSignal);
options.signal?.addEventListener('abort', onAbort, { once: true });
options.onWatchReady?.(initialSource);
});
}

// ---------------------------------------------------------------------------
Expand Down
Loading
Loading