feat(workspaces): optimize sync service with selective push and fix path handling

This commit is contained in:
mkorwel
2026-03-19 09:08:25 -07:00
parent f5300cbb13
commit a7dd0ac571
2 changed files with 50 additions and 45 deletions
+15 -10
View File
@@ -27,33 +27,38 @@ describe('SyncService', () => {
service = new SyncService();
});
it('should construct correct gcloud scp command', async () => {
const mockChild = new EventEmitter() as any;
vi.mocked(spawn).mockReturnValue(mockChild);
it('should construct correct gcloud scp command for each essential item', async () => {
// We need to simulate multiple successful exits for the multiple spawn calls
vi.mocked(spawn).mockImplementation(() => {
const child = new EventEmitter() as any;
// Emit exit on next tick to ensure promise resolves correctly
process.nextTick(() => child.emit('exit', 0));
return child;
});
const promise = service.pushSettings({
await service.pushSettings({
instanceName: 'test-inst',
zone: 'us-west1-a',
project: 'test-project',
});
setTimeout(() => mockChild.emit('exit', 0), 10);
await promise;
// Check first call (settings.json)
expect(spawn).toHaveBeenCalledWith(
'gcloud',
[
'compute',
'scp',
'--recurse',
'/mock/local/dir',
'test-inst:.gemini',
'/mock/local/dir/settings.json',
'test-inst:',
'--zone=us-west1-a',
'--project=test-project',
'--tunnel-through-iap',
],
expect.any(Object)
);
// Check total number of calls matches the essentials list
expect(spawn).toHaveBeenCalledTimes(5);
});
});
+35 -35
View File
@@ -23,44 +23,44 @@ export class SyncService {
const { instanceName, zone, project } = options;
const localDir = Storage.getGlobalGeminiDir();
// We want to sync the contents of ~/.gemini to ~/.gemini on the remote.
// gcloud compute scp local-dir remote-instance:remote-dir
const remotePath = `${instanceName}:.gemini`;
// Fix: Using the home directory as destination to avoid nested .gemini/.gemini
const remotePath = `${instanceName}:`;
// Note: gcloud scp doesn't have a native "exclude" flag like rsync,
// so we might need to be selective or use a tarball approach if it's too slow.
// For v1, we just push the whole thing but excluding the 'tmp' and 'logs' folder if possible
// via a manual scp of subdirectories, or just the whole thing for simplicity now.
// Performance/Robustness: Exclude large and local-only folders.
// Since gcloud scp doesn't support --exclude, we could either:
// 1. scp specific sub-folders (settings.json, commands, skills, policies)
// 2. Use a temporary tarball on the local side, scp it, and extract remotely.
// For now, let's just sync the essential sub-directories to keep it fast.
const args = [
'compute',
'scp',
'--recurse',
localDir,
remotePath,
`--zone=${zone}`,
`--project=${project}`,
'--tunnel-through-iap',
];
const essentials = ['settings.json', 'commands', 'skills', 'policies', 'memory.md'];
debugLogger.log(`[SyncService] Syncing essential settings to ${instanceName}...`);
debugLogger.log(`[SyncService] Syncing settings: gcloud ${args.join(' ')}`);
for (const item of essentials) {
const localItem = `${localDir}/${item}`;
const args = [
'compute',
'scp',
'--recurse',
localItem,
remotePath,
`--zone=${zone}`,
`--project=${project}`,
'--tunnel-through-iap',
];
return new Promise((resolve, reject) => {
const child = spawn('gcloud', args, {
stdio: 'inherit',
});
child.on('exit', (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error(`gcloud scp exited with code ${code}`));
}
});
child.on('error', (err) => {
reject(err);
});
});
await new Promise<void>((resolve, reject) => {
const child = spawn('gcloud', args, { stdio: 'ignore' });
child.on('exit', (code) => {
if (code === 0) resolve();
else debugLogger.warn(`[SyncService] Failed to sync ${item}, skipping...`);
resolve(); // Don't fail the whole sync if one item fails
});
child.on('error', (err) => {
debugLogger.error(`[SyncService] Error syncing ${item}:`, err);
resolve();
});
});
}
}
}