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 ADDED
@@ -0,0 +1,104 @@
1
+ # OpenCodeSpaces CLI
2
+
3
+ Connect your local IDE (VSCode, JetBrains, Neovim, etc.) to [OpenCodeSpaces](https://github.com/techdivision-rnd/opencodespaces) cloud development sessions.
4
+
5
+ ## Features
6
+
7
+ - **Bidirectional file sync** - Changes sync in real-time between local and cloud
8
+ - **Works with any IDE** - VSCode, JetBrains, Neovim, Sublime, and more
9
+ - **SSH tunneling** - Secure WebSocket connection, no exposed ports required
10
+ - **Interactive session selection** - Easy session picker or direct ID usage
11
+
12
+ ## Prerequisites
13
+
14
+ Install [Mutagen](https://mutagen.io) for file synchronization:
15
+
16
+ ```bash
17
+ # macOS
18
+ brew install mutagen-io/mutagen/mutagen
19
+
20
+ # Linux/Windows
21
+ # Download from https://mutagen.io/documentation/introduction/installation
22
+ ```
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ npm install -g opencodespaces
28
+ ```
29
+
30
+ ## Quick Start
31
+
32
+ ```bash
33
+ # 1. Login to your OpenCodeSpaces server
34
+ opencodespaces login https://your-server.com
35
+
36
+ # 2. Start syncing (interactive mode)
37
+ opencodespaces sync
38
+
39
+ # 3. Open the local directory in your favorite IDE
40
+ ```
41
+
42
+ ## Commands
43
+
44
+ ### Authentication
45
+
46
+ | Command | Description |
47
+ |---------|-------------|
48
+ | `opencodespaces login [server]` | Authenticate via browser OAuth |
49
+ | `opencodespaces login [server] --save` | Login and save server as default |
50
+ | `opencodespaces logout` | Remove stored credentials |
51
+ | `opencodespaces whoami` | Show current user and server |
52
+
53
+ ### Sessions
54
+
55
+ | Command | Description |
56
+ |---------|-------------|
57
+ | `opencodespaces sessions` | List available sessions |
58
+
59
+ ### Sync
60
+
61
+ | Command | Description |
62
+ |---------|-------------|
63
+ | `opencodespaces sync` | Interactive session selection and sync |
64
+ | `opencodespaces sync start <id> [-d path]` | Start syncing with a specific session |
65
+ | `opencodespaces sync stop [id]` | Stop syncing (all or specific session) |
66
+ | `opencodespaces sync status` | Show current sync status |
67
+
68
+ ### SSH
69
+
70
+ | Command | Description |
71
+ |---------|-------------|
72
+ | `opencodespaces ssh <id>` | Open SSH shell to session |
73
+ | `opencodespaces ssh <id> --stdio` | STDIO mode for ProxyCommand |
74
+
75
+ ## Excluded Files
76
+
77
+ By default, these patterns are excluded from sync:
78
+
79
+ ```
80
+ node_modules/ .git/ .DS_Store *.log dist/ build/
81
+ coverage/ .next/ __pycache__/ venv/ .venv/ .turbo/ .cache/
82
+ ```
83
+
84
+ Run `npm install` separately on both local and remote environments.
85
+
86
+ ## Configuration
87
+
88
+ Configuration files are stored in `~/.opencodespaces/`:
89
+
90
+ | File | Purpose |
91
+ |------|---------|
92
+ | `config.json` | Server URL, custom ignores |
93
+ | `credentials.json` | Auth token (auto-managed) |
94
+ | `keys/` | Temporary SSH keys |
95
+
96
+ ## Documentation
97
+
98
+ For full documentation, see:
99
+ - [Local IDE Sync Guide](https://github.com/techdivision-rnd/opencodespaces/blob/master/docs/LOCAL_IDE_SYNC.md)
100
+ - [OpenCodeSpaces Documentation](https://github.com/techdivision-rnd/opencodespaces)
101
+
102
+ ## License
103
+
104
+ MIT
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Login Command
3
+ *
4
+ * Authenticates with OpenCodeSpaces via browser OAuth.
5
+ */
6
+ import { Command } from 'commander';
7
+ export declare function loginCommand(program: Command): void;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Login Command
3
+ *
4
+ * Authenticates with OpenCodeSpaces via browser OAuth.
5
+ */
6
+ import ora from 'ora';
7
+ import { browserLogin } from '../lib/auth.js';
8
+ import { logger } from '../utils/logger.js';
9
+ import { getServerUrl, saveConfig, loadConfig } from '../lib/config.js';
10
+ export function loginCommand(program) {
11
+ program
12
+ .command('login [server]')
13
+ .description('Authenticate with OpenCodeSpaces via browser')
14
+ .option('-s, --save', 'Save server URL as default for future logins')
15
+ .action(async (server, options) => {
16
+ const serverUrl = getServerUrl(server);
17
+ logger.info(`Logging in to ${serverUrl}`);
18
+ logger.dim('Opening browser for authentication...');
19
+ const spinner = ora('Waiting for authentication...').start();
20
+ try {
21
+ const result = await browserLogin(serverUrl);
22
+ if (result.success) {
23
+ spinner.succeed(`Logged in as ${result.email || result.name || 'user'}`);
24
+ // Save server as default if requested
25
+ if (options?.save && server) {
26
+ const config = loadConfig();
27
+ config.server = serverUrl;
28
+ saveConfig(config);
29
+ logger.dim(`Server saved as default: ${serverUrl}`);
30
+ }
31
+ }
32
+ else {
33
+ spinner.fail(`Login failed: ${result.error}`);
34
+ process.exit(1);
35
+ }
36
+ }
37
+ catch (error) {
38
+ spinner.fail(`Login failed: ${error.message}`);
39
+ process.exit(1);
40
+ }
41
+ });
42
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Logout Command
3
+ *
4
+ * Removes stored credentials.
5
+ */
6
+ import { Command } from 'commander';
7
+ export declare function logoutCommand(program: Command): void;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Logout Command
3
+ *
4
+ * Removes stored credentials.
5
+ */
6
+ import { logout, isLoggedIn } from '../lib/auth.js';
7
+ import { logger } from '../utils/logger.js';
8
+ export function logoutCommand(program) {
9
+ program
10
+ .command('logout')
11
+ .description('Remove stored credentials')
12
+ .action(() => {
13
+ if (!isLoggedIn()) {
14
+ logger.warn('Not logged in');
15
+ return;
16
+ }
17
+ logout();
18
+ logger.success('Logged out successfully');
19
+ });
20
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Sessions Command
3
+ *
4
+ * List available sessions with optional interactive selection.
5
+ */
6
+ import { Command } from 'commander';
7
+ export declare function sessionsCommand(program: Command): void;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Sessions Command
3
+ *
4
+ * List available sessions with optional interactive selection.
5
+ */
6
+ import ora from 'ora';
7
+ import chalk from 'chalk';
8
+ import { isLoggedIn } from '../lib/auth.js';
9
+ import { api } from '../lib/api.js';
10
+ import { logger } from '../utils/logger.js';
11
+ export function sessionsCommand(program) {
12
+ const sessions = program
13
+ .command('sessions')
14
+ .description('List and manage sessions');
15
+ sessions
16
+ .command('list')
17
+ .description('List all available sessions')
18
+ .option('-w, --workspace <id>', 'Filter by workspace ID')
19
+ .action(async (options) => {
20
+ if (!isLoggedIn()) {
21
+ logger.error('Not logged in. Run: opencodespaces login');
22
+ process.exit(1);
23
+ }
24
+ const spinner = ora('Loading sessions...').start();
25
+ try {
26
+ const results = await api.listAllSessions();
27
+ // Filter by workspace if specified
28
+ const filtered = options.workspace
29
+ ? results.filter((r) => r.workspace.id === options.workspace)
30
+ : results;
31
+ spinner.stop();
32
+ if (filtered.length === 0) {
33
+ logger.warn('No sessions found');
34
+ logger.dim('Make sure you have running containers with initialized sessions.');
35
+ return;
36
+ }
37
+ logger.log('');
38
+ logger.bold('Available Sessions');
39
+ logger.log('');
40
+ // Group by workspace
41
+ const byWorkspace = new Map();
42
+ for (const item of filtered) {
43
+ const key = item.workspace.id;
44
+ if (!byWorkspace.has(key)) {
45
+ byWorkspace.set(key, []);
46
+ }
47
+ byWorkspace.get(key).push(item);
48
+ }
49
+ for (const [, items] of byWorkspace) {
50
+ const ws = items[0].workspace;
51
+ logger.log(chalk.cyan(` ${ws.name}`));
52
+ for (const { space, session } of items) {
53
+ const statusIcon = session.sessionStatus === 'READY'
54
+ ? chalk.green('●')
55
+ : chalk.yellow('○');
56
+ logger.log(` ${statusIcon} ${session.name} ${chalk.dim(`(${session.id.slice(0, 8)}...)`)}`);
57
+ logger.dim(` Space: ${space.name} | Branch: ${session.branchName || 'none'}`);
58
+ }
59
+ logger.log('');
60
+ }
61
+ logger.dim(`Found ${filtered.length} session(s)`);
62
+ logger.log('');
63
+ }
64
+ catch (error) {
65
+ spinner.fail(`Failed to load sessions: ${error.message}`);
66
+ process.exit(1);
67
+ }
68
+ });
69
+ // Default action (list)
70
+ sessions.action(async () => {
71
+ // Run list command by default
72
+ await sessions.commands
73
+ .find((c) => c.name() === 'list')
74
+ ?.parseAsync([], { from: 'user' });
75
+ });
76
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * SSH Tunnel Command
3
+ *
4
+ * Provides SSH tunnel to sessions via WebSocket.
5
+ * Used as ProxyCommand for SSH/Mutagen.
6
+ *
7
+ * Usage:
8
+ * opencodespaces ssh <sessionId> --stdio # For ProxyCommand
9
+ * opencodespaces ssh <sessionId> # Interactive SSH shell
10
+ */
11
+ import { Command } from 'commander';
12
+ export declare function sshCommand(program: Command): void;
@@ -0,0 +1,141 @@
1
+ /**
2
+ * SSH Tunnel Command
3
+ *
4
+ * Provides SSH tunnel to sessions via WebSocket.
5
+ * Used as ProxyCommand for SSH/Mutagen.
6
+ *
7
+ * Usage:
8
+ * opencodespaces ssh <sessionId> --stdio # For ProxyCommand
9
+ * opencodespaces ssh <sessionId> # Interactive SSH shell
10
+ */
11
+ import { WebSocket } from 'ws';
12
+ import { isLoggedIn, getCredentials } from '../lib/auth.js';
13
+ import { getServerUrl } from '../lib/config.js';
14
+ import { logger } from '../utils/logger.js';
15
+ export function sshCommand(program) {
16
+ program
17
+ .command('ssh <sessionId>')
18
+ .description('SSH tunnel to a session')
19
+ .option('--stdio', 'STDIO mode for SSH ProxyCommand (required for Mutagen)')
20
+ .action(async (sessionId, options) => {
21
+ if (!isLoggedIn()) {
22
+ // Write to stderr in stdio mode to not interfere with SSH protocol
23
+ if (options.stdio) {
24
+ process.stderr.write('Not logged in. Run: opencodespaces login\n');
25
+ }
26
+ else {
27
+ logger.error('Not logged in. Run: opencodespaces login');
28
+ }
29
+ process.exit(1);
30
+ }
31
+ const creds = getCredentials();
32
+ if (!creds?.token) {
33
+ if (options.stdio) {
34
+ process.stderr.write('No credentials found. Run: opencodespaces login\n');
35
+ }
36
+ else {
37
+ logger.error('No credentials found. Run: opencodespaces login');
38
+ }
39
+ process.exit(1);
40
+ }
41
+ const serverUrl = getServerUrl();
42
+ if (options.stdio) {
43
+ // STDIO mode: bidirectional piping for SSH ProxyCommand
44
+ await runStdioTunnel(serverUrl, sessionId, creds.token);
45
+ }
46
+ else {
47
+ // Interactive mode: spawn SSH command using this CLI as ProxyCommand
48
+ await runInteractiveSsh(sessionId);
49
+ }
50
+ });
51
+ }
52
+ /**
53
+ * Run tunnel in STDIO mode (for SSH ProxyCommand)
54
+ *
55
+ * This bridges STDIN/STDOUT to the WebSocket, allowing SSH to use
56
+ * this CLI as a transport layer.
57
+ */
58
+ async function runStdioTunnel(serverUrl, sessionId, token) {
59
+ // Build WebSocket URL
60
+ const wsProtocol = serverUrl.startsWith('https://') ? 'wss://' : 'ws://';
61
+ const wsHost = serverUrl.replace(/^https?:\/\//, '');
62
+ const wsUrl = `${wsProtocol}${wsHost}/api/sessions/${sessionId}/ssh?token=${encodeURIComponent(token)}`;
63
+ return new Promise((resolve, reject) => {
64
+ const ws = new WebSocket(wsUrl);
65
+ // Set binary type for raw SSH data
66
+ ws.binaryType = 'nodebuffer';
67
+ ws.on('open', () => {
68
+ // Process is now in raw mode - pipe everything through
69
+ // STDIN -> WebSocket
70
+ process.stdin.on('data', (data) => {
71
+ if (ws.readyState === WebSocket.OPEN) {
72
+ ws.send(data);
73
+ }
74
+ });
75
+ process.stdin.on('end', () => {
76
+ ws.close();
77
+ });
78
+ // Handle process termination
79
+ process.stdin.resume();
80
+ });
81
+ // WebSocket -> STDOUT
82
+ ws.on('message', (data) => {
83
+ if (Buffer.isBuffer(data)) {
84
+ process.stdout.write(data);
85
+ }
86
+ else {
87
+ process.stdout.write(data);
88
+ }
89
+ });
90
+ ws.on('error', (err) => {
91
+ process.stderr.write(`WebSocket error: ${err.message}\n`);
92
+ reject(err);
93
+ });
94
+ ws.on('close', (code, reason) => {
95
+ if (code !== 1000 && code !== 1001) {
96
+ process.stderr.write(`Connection closed: ${code} ${reason}\n`);
97
+ }
98
+ resolve();
99
+ });
100
+ // Handle process signals
101
+ const cleanup = () => {
102
+ ws.close();
103
+ process.exit(0);
104
+ };
105
+ process.on('SIGINT', cleanup);
106
+ process.on('SIGTERM', cleanup);
107
+ process.on('SIGHUP', cleanup);
108
+ });
109
+ }
110
+ /**
111
+ * Run interactive SSH session
112
+ *
113
+ * Spawns SSH command using this CLI as ProxyCommand.
114
+ */
115
+ async function runInteractiveSsh(sessionId) {
116
+ const { execSync } = await import('child_process');
117
+ // Get the path to this CLI executable
118
+ const cliPath = process.argv[1];
119
+ // Build SSH command with our CLI as ProxyCommand
120
+ const sshArgs = [
121
+ '-o', `ProxyCommand="${cliPath} ssh ${sessionId} --stdio"`,
122
+ '-o', 'StrictHostKeyChecking=no',
123
+ '-o', 'UserKnownHostsFile=/dev/null',
124
+ 'opencode@localhost', // User and host (host is ignored due to ProxyCommand)
125
+ ];
126
+ logger.info(`Connecting to session ${sessionId}...`);
127
+ logger.dim('Use Ctrl+D or type "exit" to disconnect');
128
+ logger.log('');
129
+ try {
130
+ execSync(`ssh ${sshArgs.join(' ')}`, {
131
+ stdio: 'inherit',
132
+ });
133
+ }
134
+ catch (error) {
135
+ // SSH returns non-zero on normal disconnect, ignore
136
+ const err = error;
137
+ if (err.status !== 255) {
138
+ throw error;
139
+ }
140
+ }
141
+ }
@@ -0,0 +1,14 @@
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 { Command } from 'commander';
14
+ export declare function syncCommand(program: Command): void;