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.
- package/README.md +104 -0
- package/dist/commands/login.d.ts +7 -0
- package/dist/commands/login.js +42 -0
- package/dist/commands/logout.d.ts +7 -0
- package/dist/commands/logout.js +20 -0
- package/dist/commands/sessions.d.ts +7 -0
- package/dist/commands/sessions.js +76 -0
- package/dist/commands/ssh.d.ts +12 -0
- package/dist/commands/ssh.js +141 -0
- package/dist/commands/sync.d.ts +14 -0
- package/dist/commands/sync.js +353 -0
- package/dist/commands/whoami.d.ts +7 -0
- package/dist/commands/whoami.js +50 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +38 -0
- package/dist/lib/api.d.ts +96 -0
- package/dist/lib/api.js +130 -0
- package/dist/lib/auth.d.ts +34 -0
- package/dist/lib/auth.js +174 -0
- package/dist/lib/config.d.ts +74 -0
- package/dist/lib/config.js +184 -0
- package/dist/lib/version.d.ts +4 -0
- package/dist/lib/version.js +4 -0
- package/dist/utils/logger.d.ts +45 -0
- package/dist/utils/logger.js +66 -0
- package/package.json +58 -0
|
@@ -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,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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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 };
|
package/dist/lib/api.js
ADDED
|
@@ -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 };
|