opencodespaces 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,353 @@
1
+ /**
2
+ * Sync Command
3
+ *
4
+ * Bidirectional file sync between local directory and cloud session.
5
+ * Uses Mutagen for reliable, real-time synchronization.
6
+ *
7
+ * Usage:
8
+ * opencodespaces sync # Interactive session selection
9
+ * opencodespaces sync start <id> [-d .] # Start sync with session
10
+ * opencodespaces sync stop [id] # Stop sync
11
+ * opencodespaces sync status # Show sync status
12
+ */
13
+ import { execSync, spawn } from 'child_process';
14
+ import fs from 'fs';
15
+ import path from 'path';
16
+ import os from 'os';
17
+ import ora from 'ora';
18
+ import inquirer from 'inquirer';
19
+ import { isLoggedIn } from '../lib/auth.js';
20
+ import { api } from '../lib/api.js';
21
+ import { getIgnoreList, saveSessionKey, deleteSessionKey, } from '../lib/config.js';
22
+ import { logger } from '../utils/logger.js';
23
+ // SSH config directory
24
+ const SSH_CONFIG_DIR = path.join(os.homedir(), '.ssh', 'config.d');
25
+ export function syncCommand(program) {
26
+ const sync = program.command('sync').description('Sync local directory with session');
27
+ // Interactive mode (default)
28
+ sync.action(async () => {
29
+ await interactiveSync();
30
+ });
31
+ // Start sync
32
+ sync
33
+ .command('start <sessionId>')
34
+ .description('Start syncing with a session')
35
+ .option('-d, --dir <path>', 'Local directory', '.')
36
+ .action(async (sessionId, options) => {
37
+ await startSync(sessionId, options.dir);
38
+ });
39
+ // Stop sync
40
+ sync
41
+ .command('stop [sessionId]')
42
+ .description('Stop syncing (all or specific session)')
43
+ .action(async (sessionId) => {
44
+ await stopSync(sessionId);
45
+ });
46
+ // Status
47
+ sync
48
+ .command('status')
49
+ .description('Show sync status')
50
+ .action(async () => {
51
+ await showSyncStatus();
52
+ });
53
+ }
54
+ /**
55
+ * Interactive sync setup
56
+ */
57
+ async function interactiveSync() {
58
+ if (!isLoggedIn()) {
59
+ logger.error('Not logged in. Run: opencodespaces login');
60
+ process.exit(1);
61
+ }
62
+ checkDependencies();
63
+ const spinner = ora('Loading sessions...').start();
64
+ try {
65
+ const results = await api.listAllSessions();
66
+ spinner.stop();
67
+ if (results.length === 0) {
68
+ logger.warn('No sessions found');
69
+ logger.dim('Make sure you have running containers with initialized sessions.');
70
+ process.exit(1);
71
+ }
72
+ // Build choices for inquirer
73
+ const choices = results.map(({ workspace, space, session }) => ({
74
+ name: `${session.name} (${workspace.name}/${space.name}) - ${session.sessionStatus}`,
75
+ value: session.id,
76
+ short: session.name,
77
+ }));
78
+ // Session selection
79
+ const { selectedSession } = await inquirer.prompt([
80
+ {
81
+ type: 'list',
82
+ name: 'selectedSession',
83
+ message: 'Select a session to sync:',
84
+ choices,
85
+ },
86
+ ]);
87
+ // Find selected session info
88
+ const selected = results.find((r) => r.session.id === selectedSession);
89
+ const suggestedDir = `./${selected?.session.name || 'project'}`;
90
+ // Local directory selection
91
+ const { localDir } = await inquirer.prompt([
92
+ {
93
+ type: 'input',
94
+ name: 'localDir',
95
+ message: 'Local directory:',
96
+ default: suggestedDir,
97
+ },
98
+ ]);
99
+ // Start sync
100
+ await startSync(selectedSession, localDir);
101
+ }
102
+ catch (error) {
103
+ spinner.fail(`Failed: ${error.message}`);
104
+ process.exit(1);
105
+ }
106
+ }
107
+ /**
108
+ * Start sync with a session
109
+ */
110
+ async function startSync(sessionId, localDir) {
111
+ if (!isLoggedIn()) {
112
+ logger.error('Not logged in. Run: opencodespaces login');
113
+ process.exit(1);
114
+ }
115
+ checkDependencies();
116
+ const spinner = ora('Initializing sync...').start();
117
+ try {
118
+ // Initialize sync on server (get SSH credentials)
119
+ const syncInfo = await api.initSync(sessionId);
120
+ // Save private key
121
+ const keyPath = saveSessionKey(sessionId, syncInfo.privateKey);
122
+ spinner.text = 'Configuring SSH...';
123
+ // Create SSH config
124
+ await createSshConfig(sessionId, keyPath, syncInfo.user);
125
+ // Resolve and create local directory
126
+ const localPath = path.resolve(localDir);
127
+ if (!fs.existsSync(localPath)) {
128
+ fs.mkdirSync(localPath, { recursive: true });
129
+ }
130
+ spinner.text = 'Starting Mutagen sync...';
131
+ // Build Mutagen command
132
+ const hostAlias = `ocs-${sessionId}`;
133
+ const remotePath = syncInfo.remotePath;
134
+ const ignores = getIgnoreList();
135
+ const ignoreArgs = ignores.flatMap((i) => ['--ignore', i]);
136
+ // Create Mutagen sync session
137
+ const mutagenArgs = [
138
+ 'sync',
139
+ 'create',
140
+ localPath,
141
+ `${hostAlias}:${remotePath}`,
142
+ '--name',
143
+ `opencodespaces-${sessionId}`,
144
+ '--sync-mode',
145
+ 'two-way-resolved',
146
+ ...ignoreArgs,
147
+ ];
148
+ execSync(`mutagen ${mutagenArgs.join(' ')}`, { stdio: 'pipe' });
149
+ spinner.succeed(`Syncing ${localPath} ↔ ${hostAlias}:${remotePath}`);
150
+ logger.log('');
151
+ logger.warn('Note: node_modules and other large directories are excluded by default.');
152
+ logger.dim('Run "npm install" in both local and remote if needed.');
153
+ logger.log('');
154
+ logger.info('Watching for changes... (Ctrl+C to stop)');
155
+ logger.log('');
156
+ // Setup cleanup handler
157
+ const cleanup = async () => {
158
+ logger.log('');
159
+ logger.info('Stopping sync...');
160
+ try {
161
+ await stopSyncSession(sessionId);
162
+ logger.success('Disconnected');
163
+ }
164
+ catch {
165
+ // Ignore cleanup errors
166
+ }
167
+ process.exit(0);
168
+ };
169
+ process.on('SIGINT', cleanup);
170
+ process.on('SIGTERM', cleanup);
171
+ // Start Mutagen monitor (shows live sync status)
172
+ const monitor = spawn('mutagen', ['sync', 'monitor', `opencodespaces-${sessionId}`], {
173
+ stdio: 'inherit',
174
+ });
175
+ monitor.on('close', () => {
176
+ // Monitor closed, cleanup
177
+ cleanup();
178
+ });
179
+ }
180
+ catch (error) {
181
+ spinner.fail(`Failed to start sync: ${error.message}`);
182
+ process.exit(1);
183
+ }
184
+ }
185
+ /**
186
+ * Stop sync for a session or all sessions
187
+ */
188
+ async function stopSync(sessionId) {
189
+ if (sessionId) {
190
+ const spinner = ora(`Stopping sync for session ${sessionId}...`).start();
191
+ try {
192
+ await stopSyncSession(sessionId);
193
+ spinner.succeed('Sync stopped');
194
+ }
195
+ catch (error) {
196
+ spinner.fail(`Failed: ${error.message}`);
197
+ process.exit(1);
198
+ }
199
+ }
200
+ else {
201
+ // Stop all OpenCodeSpaces syncs
202
+ const spinner = ora('Stopping all syncs...').start();
203
+ try {
204
+ // List all Mutagen sessions
205
+ const output = execSync('mutagen sync list', { encoding: 'utf-8' });
206
+ // Find OpenCodeSpaces sessions
207
+ const matches = output.match(/opencodespaces-[a-zA-Z0-9-]+/g) || [];
208
+ const uniqueSessions = [...new Set(matches)];
209
+ for (const name of uniqueSessions) {
210
+ try {
211
+ execSync(`mutagen sync terminate ${name}`, { stdio: 'pipe' });
212
+ const sessionId = name.replace('opencodespaces-', '');
213
+ await api.stopSync(sessionId).catch(() => { }); // Ignore API errors
214
+ deleteSessionKey(sessionId);
215
+ deleteSshConfig(sessionId);
216
+ }
217
+ catch {
218
+ // Ignore individual errors
219
+ }
220
+ }
221
+ spinner.succeed(`Stopped ${uniqueSessions.length} sync session(s)`);
222
+ }
223
+ catch (error) {
224
+ spinner.fail(`Failed: ${error.message}`);
225
+ process.exit(1);
226
+ }
227
+ }
228
+ }
229
+ /**
230
+ * Stop a specific sync session
231
+ */
232
+ async function stopSyncSession(sessionId) {
233
+ // Terminate Mutagen session
234
+ try {
235
+ execSync(`mutagen sync terminate opencodespaces-${sessionId}`, { stdio: 'pipe' });
236
+ }
237
+ catch {
238
+ // Session might not exist
239
+ }
240
+ // Notify API (remove SSH key from container)
241
+ try {
242
+ await api.stopSync(sessionId);
243
+ }
244
+ catch {
245
+ // Ignore API errors
246
+ }
247
+ // Cleanup local files
248
+ deleteSessionKey(sessionId);
249
+ deleteSshConfig(sessionId);
250
+ }
251
+ /**
252
+ * Show sync status
253
+ */
254
+ async function showSyncStatus() {
255
+ try {
256
+ const output = execSync('mutagen sync list', { encoding: 'utf-8' });
257
+ // Parse Mutagen output
258
+ const sessions = output
259
+ .split('\n')
260
+ .filter((line) => line.includes('opencodespaces-'));
261
+ if (sessions.length === 0) {
262
+ logger.info('No active sync sessions');
263
+ return;
264
+ }
265
+ logger.log('');
266
+ logger.bold('Active Sync Sessions');
267
+ logger.log('');
268
+ // Show detailed status
269
+ execSync('mutagen sync list', { stdio: 'inherit' });
270
+ }
271
+ catch {
272
+ logger.info('No active sync sessions');
273
+ }
274
+ }
275
+ /**
276
+ * Create SSH config for session
277
+ */
278
+ async function createSshConfig(sessionId, keyPath, user) {
279
+ // Ensure SSH config directory exists
280
+ if (!fs.existsSync(SSH_CONFIG_DIR)) {
281
+ fs.mkdirSync(SSH_CONFIG_DIR, { recursive: true });
282
+ }
283
+ // Get CLI path
284
+ const cliPath = process.argv[1];
285
+ const hostAlias = `ocs-${sessionId}`;
286
+ const sshConfig = `# OpenCodeSpaces session: ${sessionId}
287
+ Host ${hostAlias}
288
+ User ${user}
289
+ IdentityFile ${keyPath}
290
+ IdentitiesOnly yes
291
+ StrictHostKeyChecking no
292
+ UserKnownHostsFile /dev/null
293
+ LogLevel ERROR
294
+ ProxyCommand ${cliPath} ssh ${sessionId} --stdio
295
+ `;
296
+ const configPath = path.join(SSH_CONFIG_DIR, `opencodespaces-${sessionId}`);
297
+ fs.writeFileSync(configPath, sshConfig, { mode: 0o600 });
298
+ // Ensure main SSH config includes our config.d directory
299
+ await ensureSshConfigIncludes();
300
+ }
301
+ /**
302
+ * Delete SSH config for session
303
+ */
304
+ function deleteSshConfig(sessionId) {
305
+ const configPath = path.join(SSH_CONFIG_DIR, `opencodespaces-${sessionId}`);
306
+ if (fs.existsSync(configPath)) {
307
+ fs.unlinkSync(configPath);
308
+ }
309
+ }
310
+ /**
311
+ * Ensure main SSH config includes our config.d directory
312
+ */
313
+ async function ensureSshConfigIncludes() {
314
+ const sshConfigPath = path.join(os.homedir(), '.ssh', 'config');
315
+ const includeDir = SSH_CONFIG_DIR;
316
+ // Create .ssh directory if needed
317
+ const sshDir = path.dirname(sshConfigPath);
318
+ if (!fs.existsSync(sshDir)) {
319
+ fs.mkdirSync(sshDir, { mode: 0o700 });
320
+ }
321
+ // Check if config exists and includes our directory
322
+ if (fs.existsSync(sshConfigPath)) {
323
+ const content = fs.readFileSync(sshConfigPath, 'utf-8');
324
+ if (content.includes(includeDir) || content.includes('config.d/*')) {
325
+ return; // Already configured
326
+ }
327
+ // Prepend include directive
328
+ const newContent = `# Include OpenCodeSpaces configs\nInclude ${includeDir}/*\n\n${content}`;
329
+ fs.writeFileSync(sshConfigPath, newContent, { mode: 0o600 });
330
+ }
331
+ else {
332
+ // Create new config with include
333
+ const newContent = `# Include OpenCodeSpaces configs\nInclude ${includeDir}/*\n`;
334
+ fs.writeFileSync(sshConfigPath, newContent, { mode: 0o600 });
335
+ }
336
+ }
337
+ /**
338
+ * Check if Mutagen is installed
339
+ */
340
+ function checkDependencies() {
341
+ try {
342
+ execSync('mutagen version', { stdio: 'pipe' });
343
+ }
344
+ catch {
345
+ logger.error('Mutagen not found. Please install it:');
346
+ logger.log('');
347
+ logger.log(' macOS: brew install mutagen-io/mutagen/mutagen');
348
+ logger.log(' Linux: Download from https://mutagen.io/documentation/introduction/installation');
349
+ logger.log(' Windows: Download from https://mutagen.io/documentation/introduction/installation');
350
+ logger.log('');
351
+ process.exit(1);
352
+ }
353
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Whoami Command
3
+ *
4
+ * Shows current logged-in user info.
5
+ */
6
+ import { Command } from 'commander';
7
+ export declare function whoamiCommand(program: Command): void;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Whoami Command
3
+ *
4
+ * Shows current logged-in user info.
5
+ */
6
+ import { isLoggedIn, getCredentials } from '../lib/auth.js';
7
+ import { api } from '../lib/api.js';
8
+ import { logger } from '../utils/logger.js';
9
+ import { getServerUrl } from '../lib/config.js';
10
+ export function whoamiCommand(program) {
11
+ program
12
+ .command('whoami')
13
+ .description('Show current user info')
14
+ .action(async () => {
15
+ if (!isLoggedIn()) {
16
+ logger.error('Not logged in. Run: opencodespaces login');
17
+ process.exit(1);
18
+ }
19
+ const creds = getCredentials();
20
+ const serverUrl = getServerUrl();
21
+ try {
22
+ // Verify token is still valid by calling API
23
+ const user = await api.me();
24
+ logger.log('');
25
+ logger.bold('Current User');
26
+ logger.log(` Email: ${user.email}`);
27
+ logger.log(` Name: ${user.name}`);
28
+ logger.log(` Role: ${user.role}`);
29
+ logger.log(` Server: ${serverUrl}`);
30
+ logger.log('');
31
+ }
32
+ catch (error) {
33
+ // Token might be invalid, show cached info with warning
34
+ if (creds?.email) {
35
+ logger.warn('Session may have expired. Run: opencodespaces login');
36
+ logger.log('');
37
+ logger.dim('Cached info:');
38
+ logger.log(` Email: ${creds.email}`);
39
+ if (creds.name)
40
+ logger.log(` Name: ${creds.name}`);
41
+ logger.log(` Server: ${serverUrl}`);
42
+ logger.log('');
43
+ }
44
+ else {
45
+ logger.error('Session expired. Run: opencodespaces login');
46
+ process.exit(1);
47
+ }
48
+ }
49
+ });
50
+ }
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * OpenCodeSpaces CLI
4
+ *
5
+ * Connect your local IDE to cloud sessions for bidirectional file sync.
6
+ *
7
+ * Usage:
8
+ * opencodespaces login [server-url] - Authenticate with browser OAuth
9
+ * opencodespaces logout - Remove stored credentials
10
+ * opencodespaces whoami - Show current user
11
+ * opencodespaces sessions [list] - List available sessions
12
+ * opencodespaces sync - Interactive sync setup
13
+ * opencodespaces sync start <id> - Start syncing with a session
14
+ * opencodespaces sync stop [id] - Stop syncing
15
+ * opencodespaces sync status - Show sync status
16
+ * opencodespaces ssh <id> [--stdio] - SSH tunnel to session
17
+ */
18
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * OpenCodeSpaces CLI
4
+ *
5
+ * Connect your local IDE to cloud sessions for bidirectional file sync.
6
+ *
7
+ * Usage:
8
+ * opencodespaces login [server-url] - Authenticate with browser OAuth
9
+ * opencodespaces logout - Remove stored credentials
10
+ * opencodespaces whoami - Show current user
11
+ * opencodespaces sessions [list] - List available sessions
12
+ * opencodespaces sync - Interactive sync setup
13
+ * opencodespaces sync start <id> - Start syncing with a session
14
+ * opencodespaces sync stop [id] - Stop syncing
15
+ * opencodespaces sync status - Show sync status
16
+ * opencodespaces ssh <id> [--stdio] - SSH tunnel to session
17
+ */
18
+ import { Command } from 'commander';
19
+ import { loginCommand } from './commands/login.js';
20
+ import { logoutCommand } from './commands/logout.js';
21
+ import { whoamiCommand } from './commands/whoami.js';
22
+ import { sessionsCommand } from './commands/sessions.js';
23
+ import { syncCommand } from './commands/sync.js';
24
+ import { sshCommand } from './commands/ssh.js';
25
+ import { version } from './lib/version.js';
26
+ const program = new Command();
27
+ program
28
+ .name('opencodespaces')
29
+ .description('Connect your local IDE to OpenCodeSpaces cloud sessions')
30
+ .version(version);
31
+ // Register commands
32
+ loginCommand(program);
33
+ logoutCommand(program);
34
+ whoamiCommand(program);
35
+ sessionsCommand(program);
36
+ syncCommand(program);
37
+ sshCommand(program);
38
+ program.parse();
@@ -0,0 +1,96 @@
1
+ /**
2
+ * API Client for OpenCodeSpaces
3
+ */
4
+ interface Session {
5
+ id: string;
6
+ name: string;
7
+ branchName: string;
8
+ workDir: string;
9
+ sessionStatus: string;
10
+ spaceId: string;
11
+ createdAt: string;
12
+ lastActive: string;
13
+ }
14
+ interface Workspace {
15
+ id: string;
16
+ name: string;
17
+ description?: string;
18
+ }
19
+ interface Space {
20
+ id: string;
21
+ name: string;
22
+ workspaceId: string;
23
+ container?: {
24
+ id: string;
25
+ status: string;
26
+ mappedPort: number | null;
27
+ };
28
+ }
29
+ interface SyncInitResponse {
30
+ privateKey: string;
31
+ wsUrl: string;
32
+ remotePath: string;
33
+ user: string;
34
+ sshPort: number;
35
+ expiresAt: string;
36
+ }
37
+ interface User {
38
+ id: string;
39
+ email: string;
40
+ name: string;
41
+ role: string;
42
+ }
43
+ declare class ApiError extends Error {
44
+ statusCode: number;
45
+ constructor(statusCode: number, message: string);
46
+ }
47
+ /**
48
+ * API client methods
49
+ */
50
+ export declare const api: {
51
+ /**
52
+ * Get current user info
53
+ */
54
+ me(): Promise<User>;
55
+ /**
56
+ * List workspaces
57
+ */
58
+ listWorkspaces(): Promise<Workspace[]>;
59
+ /**
60
+ * List spaces in a workspace
61
+ */
62
+ listSpaces(workspaceId: string): Promise<Space[]>;
63
+ /**
64
+ * List sessions for a space
65
+ */
66
+ listSessionsForSpace(spaceId: string): Promise<Session[]>;
67
+ /**
68
+ * List all sessions the user has access to
69
+ */
70
+ listAllSessions(): Promise<{
71
+ workspace: Workspace;
72
+ space: Space;
73
+ session: Session;
74
+ }[]>;
75
+ /**
76
+ * Get a specific session
77
+ */
78
+ getSession(sessionId: string): Promise<Session>;
79
+ /**
80
+ * Initialize sync for a session
81
+ */
82
+ initSync(sessionId: string): Promise<SyncInitResponse>;
83
+ /**
84
+ * Stop sync for a session
85
+ */
86
+ stopSync(sessionId: string): Promise<void>;
87
+ /**
88
+ * Get sync status for a session
89
+ */
90
+ getSyncStatus(sessionId: string): Promise<{
91
+ active: boolean;
92
+ expiresAt: string | null;
93
+ }>;
94
+ };
95
+ export { ApiError };
96
+ export type { Session, Workspace, Space, SyncInitResponse, User };
@@ -0,0 +1,130 @@
1
+ /**
2
+ * API Client for OpenCodeSpaces
3
+ */
4
+ import { loadCredentials, getServerUrl } from './config.js';
5
+ class ApiError extends Error {
6
+ statusCode;
7
+ constructor(statusCode, message) {
8
+ super(message);
9
+ this.statusCode = statusCode;
10
+ this.name = 'ApiError';
11
+ }
12
+ }
13
+ /**
14
+ * Make an API request
15
+ */
16
+ async function request(endpoint, options = {}) {
17
+ const { method = 'GET', body, requireAuth = true } = options;
18
+ const creds = loadCredentials();
19
+ const serverUrl = getServerUrl();
20
+ if (requireAuth && !creds?.token) {
21
+ throw new ApiError(401, 'Not logged in. Run: opencodespaces login');
22
+ }
23
+ const url = `${serverUrl}/api${endpoint}`;
24
+ const headers = {
25
+ 'Content-Type': 'application/json',
26
+ };
27
+ if (creds?.token) {
28
+ headers['Authorization'] = `Bearer ${creds.token}`;
29
+ }
30
+ const response = await fetch(url, {
31
+ method,
32
+ headers,
33
+ body: body ? JSON.stringify(body) : undefined,
34
+ });
35
+ const json = (await response.json());
36
+ if (!response.ok || !json.success) {
37
+ throw new ApiError(response.status, json.error || `Request failed with status ${response.status}`);
38
+ }
39
+ return json.data;
40
+ }
41
+ /**
42
+ * API client methods
43
+ */
44
+ export const api = {
45
+ /**
46
+ * Get current user info
47
+ */
48
+ async me() {
49
+ return request('/auth/me');
50
+ },
51
+ /**
52
+ * List workspaces
53
+ */
54
+ async listWorkspaces() {
55
+ return request('/workspaces');
56
+ },
57
+ /**
58
+ * List spaces in a workspace
59
+ */
60
+ async listSpaces(workspaceId) {
61
+ return request(`/workspaces/${workspaceId}/spaces`);
62
+ },
63
+ /**
64
+ * List sessions for a space
65
+ */
66
+ async listSessionsForSpace(spaceId) {
67
+ return request(`/sessions/space/${spaceId}`);
68
+ },
69
+ /**
70
+ * List all sessions the user has access to
71
+ */
72
+ async listAllSessions() {
73
+ const workspaces = await this.listWorkspaces();
74
+ const results = [];
75
+ for (const workspace of workspaces) {
76
+ try {
77
+ const spaces = await this.listSpaces(workspace.id);
78
+ for (const space of spaces) {
79
+ // Check container.status instead of space.status
80
+ if (space.container?.status === 'RUNNING') {
81
+ try {
82
+ const sessions = await this.listSessionsForSpace(space.id);
83
+ for (const session of sessions) {
84
+ if (session.sessionStatus === 'READY') {
85
+ results.push({ workspace, space, session });
86
+ }
87
+ }
88
+ }
89
+ catch {
90
+ // Skip spaces we can't access
91
+ }
92
+ }
93
+ }
94
+ }
95
+ catch {
96
+ // Skip workspaces we can't access
97
+ }
98
+ }
99
+ return results;
100
+ },
101
+ /**
102
+ * Get a specific session
103
+ */
104
+ async getSession(sessionId) {
105
+ return request(`/sessions/${sessionId}`);
106
+ },
107
+ /**
108
+ * Initialize sync for a session
109
+ */
110
+ async initSync(sessionId) {
111
+ return request(`/sessions/${sessionId}/sync/init`, {
112
+ method: 'POST',
113
+ });
114
+ },
115
+ /**
116
+ * Stop sync for a session
117
+ */
118
+ async stopSync(sessionId) {
119
+ await request(`/sessions/${sessionId}/sync`, {
120
+ method: 'DELETE',
121
+ });
122
+ },
123
+ /**
124
+ * Get sync status for a session
125
+ */
126
+ async getSyncStatus(sessionId) {
127
+ return request(`/sessions/${sessionId}/sync`);
128
+ },
129
+ };
130
+ export { ApiError };