feat: Add remote sync for project clones

Fetch remote changes before agents start working so they build on
up-to-date code. Adds ProjectSyncManager with git fetch + ff-only
merge of defaultBranch, integrated into phase dispatch to sync
before branch creation.

- Schema: lastFetchedAt column on projects table (migration 0029)
- Events: project:synced, project:sync_failed
- Phase dispatch: sync all linked projects before creating branches
- tRPC: syncProject, syncAllProjects, getProjectSyncStatus
- CLI: cw project sync [name] --all, cw project status [name]
- Frontend: sync button + ahead/behind badge on projects settings
This commit is contained in:
Lukas May
2026-03-05 11:45:09 +01:00
parent 79966cdf20
commit 5e77bf104c
20 changed files with 496 additions and 6 deletions

View File

@@ -1080,6 +1080,76 @@ export function createCli(serverHandler?: (port?: number) => Promise<void>): Com
}
});
// cw project sync [name] --all
projectCommand
.command('sync [name]')
.description('Sync project clone(s) from remote')
.option('--all', 'Sync all projects')
.action(async (name: string | undefined, options: { all?: boolean }) => {
try {
const client = createDefaultTrpcClient();
if (options.all) {
const results = await client.syncAllProjects.mutate();
for (const r of results) {
const status = r.success ? 'ok' : `FAILED: ${r.error}`;
console.log(`${r.projectName}: ${status}`);
}
} else if (name) {
const projects = await client.listProjects.query();
const project = projects.find((p) => p.name === name || p.id === name);
if (!project) {
console.error(`Project not found: ${name}`);
process.exit(1);
}
const result = await client.syncProject.mutate({ id: project.id });
if (result.success) {
console.log(`Synced ${result.projectName}: fetched=${result.fetched}, fast-forwarded=${result.fastForwarded}`);
} else {
console.error(`Sync failed: ${result.error}`);
process.exit(1);
}
} else {
console.error('Specify a project name or use --all');
process.exit(1);
}
} catch (error) {
console.error('Failed to sync:', (error as Error).message);
process.exit(1);
}
});
// cw project status [name]
projectCommand
.command('status [name]')
.description('Show sync status for a project')
.action(async (name: string | undefined) => {
try {
const client = createDefaultTrpcClient();
const projects = await client.listProjects.query();
const targets = name
? projects.filter((p) => p.name === name || p.id === name)
: projects;
if (targets.length === 0) {
console.log(name ? `Project not found: ${name}` : 'No projects registered');
return;
}
for (const project of targets) {
const status = await client.getProjectSyncStatus.query({ id: project.id });
const fetchedStr = status.lastFetchedAt
? new Date(status.lastFetchedAt).toLocaleString()
: 'never';
console.log(`${project.name}:`);
console.log(` Last fetched: ${fetchedStr}`);
console.log(` Ahead: ${status.ahead} Behind: ${status.behind}`);
}
} catch (error) {
console.error('Failed to get status:', (error as Error).message);
process.exit(1);
}
});
// Account command group
const accountCommand = program
.command('account')