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
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,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,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,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;
|