mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-22 17:26:58 -07:00
feat(workspaces): implement user settings sync (~/.gemini/) for remote workspaces
This commit is contained in:
@@ -135,6 +135,7 @@ export * from './services/keychainService.js';
|
||||
export * from './services/keychainTypes.js';
|
||||
export * from './services/workspaceHubClient.js';
|
||||
export * from './services/sshService.js';
|
||||
export * from './services/syncService.js';
|
||||
export * from './skills/skillManager.js';
|
||||
export * from './skills/skillLoader.js';
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { SyncService } from './syncService.js';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { EventEmitter } from 'node:events';
|
||||
|
||||
vi.mock('node:child_process', () => ({
|
||||
spawn: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../config/storage.js', () => ({
|
||||
Storage: {
|
||||
getGlobalGeminiDir: vi.fn().mockReturnValue('/mock/local/dir'),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('SyncService', () => {
|
||||
let service: SyncService;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
service = new SyncService();
|
||||
});
|
||||
|
||||
it('should construct correct gcloud scp command', async () => {
|
||||
const mockChild = new EventEmitter() as any;
|
||||
vi.mocked(spawn).mockReturnValue(mockChild);
|
||||
|
||||
const promise = service.pushSettings({
|
||||
instanceName: 'test-inst',
|
||||
zone: 'us-west1-a',
|
||||
project: 'test-project',
|
||||
});
|
||||
|
||||
setTimeout(() => mockChild.emit('exit', 0), 10);
|
||||
|
||||
await promise;
|
||||
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
'gcloud',
|
||||
[
|
||||
'compute',
|
||||
'scp',
|
||||
'--recurse',
|
||||
'/mock/local/dir',
|
||||
'test-inst:.gemini',
|
||||
'--zone=us-west1-a',
|
||||
'--project=test-project',
|
||||
'--tunnel-through-iap',
|
||||
],
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { spawn } from 'node:child_process';
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
import { Storage } from '../config/storage.js';
|
||||
|
||||
export interface SyncOptions {
|
||||
instanceName: string;
|
||||
zone: string;
|
||||
project: string;
|
||||
}
|
||||
|
||||
export class SyncService {
|
||||
/**
|
||||
* Push local ~/.gemini directory to the remote workspace.
|
||||
* Currently uses gcloud compute scp.
|
||||
*/
|
||||
async pushSettings(options: SyncOptions): Promise<void> {
|
||||
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`;
|
||||
|
||||
// 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.
|
||||
|
||||
const args = [
|
||||
'compute',
|
||||
'scp',
|
||||
'--recurse',
|
||||
localDir,
|
||||
remotePath,
|
||||
`--zone=${zone}`,
|
||||
`--project=${project}`,
|
||||
'--tunnel-through-iap',
|
||||
];
|
||||
|
||||
debugLogger.log(`[SyncService] Syncing settings: gcloud ${args.join(' ')}`);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user