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,34 @@
1
+ /**
2
+ * Authentication via Browser OAuth
3
+ *
4
+ * Flow:
5
+ * 1. CLI starts local HTTP server on available port
6
+ * 2. Opens browser to server's /api/auth/cli endpoint with redirect URL
7
+ * 3. User authenticates via Google OAuth
8
+ * 4. Server redirects back to localhost with JWT token
9
+ * 5. CLI stores token in credentials file
10
+ */
11
+ interface AuthResult {
12
+ success: boolean;
13
+ token?: string;
14
+ email?: string;
15
+ name?: string;
16
+ error?: string;
17
+ }
18
+ /**
19
+ * Perform browser-based OAuth login
20
+ */
21
+ export declare function browserLogin(serverUrl?: string): Promise<AuthResult>;
22
+ /**
23
+ * Check if user is logged in
24
+ */
25
+ export declare function isLoggedIn(): boolean;
26
+ /**
27
+ * Get stored credentials
28
+ */
29
+ export declare function getCredentials(): import("./config.js").Credentials | null;
30
+ /**
31
+ * Logout - remove stored credentials
32
+ */
33
+ export declare function logout(): void;
34
+ export {};
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Authentication via Browser OAuth
3
+ *
4
+ * Flow:
5
+ * 1. CLI starts local HTTP server on available port
6
+ * 2. Opens browser to server's /api/auth/cli endpoint with redirect URL
7
+ * 3. User authenticates via Google OAuth
8
+ * 4. Server redirects back to localhost with JWT token
9
+ * 5. CLI stores token in credentials file
10
+ */
11
+ import http from 'http';
12
+ import { URL } from 'url';
13
+ import open from 'open';
14
+ import { saveCredentials, loadCredentials, deleteCredentials, getServerUrl, } from './config.js';
15
+ // Port range to try for local OAuth callback server
16
+ const PORT_RANGE_START = 9876;
17
+ const PORT_RANGE_END = 9900;
18
+ /**
19
+ * Find an available port in the range
20
+ */
21
+ async function findAvailablePort() {
22
+ for (let port = PORT_RANGE_START; port <= PORT_RANGE_END; port++) {
23
+ try {
24
+ await new Promise((resolve, reject) => {
25
+ const server = http.createServer();
26
+ server.listen(port, () => {
27
+ server.close();
28
+ resolve();
29
+ });
30
+ server.on('error', reject);
31
+ });
32
+ return port;
33
+ }
34
+ catch {
35
+ // Port in use, try next
36
+ }
37
+ }
38
+ throw new Error(`No available port in range ${PORT_RANGE_START}-${PORT_RANGE_END}`);
39
+ }
40
+ /**
41
+ * Start local server and wait for OAuth callback
42
+ */
43
+ function startCallbackServer(port) {
44
+ return new Promise((resolve) => {
45
+ let timeoutId;
46
+ let resolved = false;
47
+ const cleanup = (result) => {
48
+ if (resolved)
49
+ return;
50
+ resolved = true;
51
+ clearTimeout(timeoutId);
52
+ server.close();
53
+ resolve(result);
54
+ };
55
+ const server = http.createServer((req, res) => {
56
+ const url = new URL(req.url || '/', `http://localhost:${port}`);
57
+ // Handle callback
58
+ if (url.pathname === '/callback') {
59
+ const token = url.searchParams.get('token');
60
+ const email = url.searchParams.get('email');
61
+ const name = url.searchParams.get('name');
62
+ const error = url.searchParams.get('error');
63
+ // Send response to browser
64
+ res.writeHead(200, { 'Content-Type': 'text/html' });
65
+ if (token) {
66
+ res.end(`
67
+ <!DOCTYPE html>
68
+ <html>
69
+ <head>
70
+ <title>OpenCodeSpaces - Login Successful</title>
71
+ <style>
72
+ body { font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #0f172a; color: #f1f5f9; }
73
+ .container { text-align: center; padding: 2rem; }
74
+ h1 { color: #22c55e; margin-bottom: 0.5rem; }
75
+ p { color: #94a3b8; }
76
+ </style>
77
+ </head>
78
+ <body>
79
+ <div class="container">
80
+ <h1>✓ Login Successful</h1>
81
+ <p>You can close this window and return to your terminal.</p>
82
+ </div>
83
+ </body>
84
+ </html>
85
+ `);
86
+ // Close server after short delay to ensure response is sent
87
+ setTimeout(() => {
88
+ cleanup({ success: true, token, email: email || undefined, name: name || undefined });
89
+ }, 100);
90
+ }
91
+ else {
92
+ res.end(`
93
+ <!DOCTYPE html>
94
+ <html>
95
+ <head>
96
+ <title>OpenCodeSpaces - Login Failed</title>
97
+ <style>
98
+ body { font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #0f172a; color: #f1f5f9; }
99
+ .container { text-align: center; padding: 2rem; }
100
+ h1 { color: #ef4444; margin-bottom: 0.5rem; }
101
+ p { color: #94a3b8; }
102
+ </style>
103
+ </head>
104
+ <body>
105
+ <div class="container">
106
+ <h1>✗ Login Failed</h1>
107
+ <p>${error || 'Unknown error occurred'}</p>
108
+ </div>
109
+ </body>
110
+ </html>
111
+ `);
112
+ setTimeout(() => {
113
+ cleanup({ success: false, error: error || 'Login failed' });
114
+ }, 100);
115
+ }
116
+ }
117
+ else {
118
+ // Unknown path
119
+ res.writeHead(404);
120
+ res.end('Not found');
121
+ }
122
+ });
123
+ server.listen(port);
124
+ // Timeout after 5 minutes
125
+ timeoutId = setTimeout(() => {
126
+ cleanup({ success: false, error: 'Login timed out' });
127
+ }, 5 * 60 * 1000);
128
+ });
129
+ }
130
+ /**
131
+ * Perform browser-based OAuth login
132
+ */
133
+ export async function browserLogin(serverUrl) {
134
+ const server = getServerUrl(serverUrl);
135
+ const port = await findAvailablePort();
136
+ const redirectUrl = `http://localhost:${port}/callback`;
137
+ // Build auth URL
138
+ const authUrl = `${server}/api/auth/cli?redirect=${encodeURIComponent(redirectUrl)}`;
139
+ // Start callback server
140
+ const authPromise = startCallbackServer(port);
141
+ // Open browser
142
+ await open(authUrl);
143
+ // Wait for callback
144
+ const result = await authPromise;
145
+ // Save credentials if successful
146
+ if (result.success && result.token) {
147
+ saveCredentials({
148
+ server,
149
+ token: result.token,
150
+ email: result.email,
151
+ name: result.name,
152
+ });
153
+ }
154
+ return result;
155
+ }
156
+ /**
157
+ * Check if user is logged in
158
+ */
159
+ export function isLoggedIn() {
160
+ const creds = loadCredentials();
161
+ return !!creds?.token;
162
+ }
163
+ /**
164
+ * Get stored credentials
165
+ */
166
+ export function getCredentials() {
167
+ return loadCredentials();
168
+ }
169
+ /**
170
+ * Logout - remove stored credentials
171
+ */
172
+ export function logout() {
173
+ deleteCredentials();
174
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Configuration Management
3
+ *
4
+ * Priority: CLI argument > Environment variable > Config file > Default
5
+ */
6
+ export declare const DEFAULT_IGNORES: string[];
7
+ export interface Config {
8
+ server?: string;
9
+ ignores?: string[];
10
+ }
11
+ export interface Credentials {
12
+ server: string;
13
+ token: string;
14
+ email?: string;
15
+ name?: string;
16
+ expiresAt?: string;
17
+ }
18
+ /**
19
+ * Ensure config directory exists
20
+ */
21
+ export declare function ensureConfigDir(): void;
22
+ /**
23
+ * Get config directory path
24
+ */
25
+ export declare function getConfigDir(): string;
26
+ /**
27
+ * Get keys directory path
28
+ */
29
+ export declare function getKeysDir(): string;
30
+ /**
31
+ * Load configuration
32
+ */
33
+ export declare function loadConfig(): Config;
34
+ /**
35
+ * Save configuration
36
+ */
37
+ export declare function saveConfig(config: Config): void;
38
+ /**
39
+ * Get server URL with priority:
40
+ * 1. CLI argument (serverArg)
41
+ * 2. Environment variable (OPENCODESPACES_SERVER)
42
+ * 3. Config file (server key)
43
+ * 4. Credentials file (server from last login)
44
+ * 5. Default
45
+ */
46
+ export declare function getServerUrl(serverArg?: string): string;
47
+ /**
48
+ * Load stored credentials
49
+ */
50
+ export declare function loadCredentials(): Credentials | null;
51
+ /**
52
+ * Save credentials
53
+ */
54
+ export declare function saveCredentials(credentials: Credentials): void;
55
+ /**
56
+ * Delete credentials
57
+ */
58
+ export declare function deleteCredentials(): void;
59
+ /**
60
+ * Get ignore list from config or defaults
61
+ */
62
+ export declare function getIgnoreList(): string[];
63
+ /**
64
+ * Save SSH key for a session
65
+ */
66
+ export declare function saveSessionKey(sessionId: string, privateKey: string): string;
67
+ /**
68
+ * Delete SSH key for a session
69
+ */
70
+ export declare function deleteSessionKey(sessionId: string): void;
71
+ /**
72
+ * Get SSH key path for a session
73
+ */
74
+ export declare function getSessionKeyPath(sessionId: string): string;
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Configuration Management
3
+ *
4
+ * Priority: CLI argument > Environment variable > Config file > Default
5
+ */
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import os from 'os';
9
+ const CONFIG_DIR = path.join(os.homedir(), '.opencodespaces');
10
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
11
+ const CREDENTIALS_FILE = path.join(CONFIG_DIR, 'credentials.json');
12
+ const KEYS_DIR = path.join(CONFIG_DIR, 'keys');
13
+ // Default server URL
14
+ const DEFAULT_SERVER = 'https://app.opencodespaces.dev';
15
+ // Default ignore patterns for sync
16
+ export const DEFAULT_IGNORES = [
17
+ 'node_modules/',
18
+ '.git/',
19
+ '.DS_Store',
20
+ '*.log',
21
+ 'dist/',
22
+ 'build/',
23
+ 'coverage/',
24
+ '.next/',
25
+ '__pycache__/',
26
+ 'venv/',
27
+ '.venv/',
28
+ '.turbo/',
29
+ '.cache/',
30
+ ];
31
+ /**
32
+ * Ensure config directory exists
33
+ */
34
+ export function ensureConfigDir() {
35
+ if (!fs.existsSync(CONFIG_DIR)) {
36
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
37
+ }
38
+ if (!fs.existsSync(KEYS_DIR)) {
39
+ fs.mkdirSync(KEYS_DIR, { recursive: true });
40
+ }
41
+ }
42
+ /**
43
+ * Get config directory path
44
+ */
45
+ export function getConfigDir() {
46
+ return CONFIG_DIR;
47
+ }
48
+ /**
49
+ * Get keys directory path
50
+ */
51
+ export function getKeysDir() {
52
+ return KEYS_DIR;
53
+ }
54
+ /**
55
+ * Load configuration
56
+ */
57
+ export function loadConfig() {
58
+ try {
59
+ if (fs.existsSync(CONFIG_FILE)) {
60
+ const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
61
+ return JSON.parse(content);
62
+ }
63
+ }
64
+ catch {
65
+ // Ignore parse errors
66
+ }
67
+ return {};
68
+ }
69
+ /**
70
+ * Save configuration
71
+ */
72
+ export function saveConfig(config) {
73
+ ensureConfigDir();
74
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
75
+ }
76
+ /**
77
+ * Get server URL with priority:
78
+ * 1. CLI argument (serverArg)
79
+ * 2. Environment variable (OPENCODESPACES_SERVER)
80
+ * 3. Config file (server key)
81
+ * 4. Credentials file (server from last login)
82
+ * 5. Default
83
+ */
84
+ export function getServerUrl(serverArg) {
85
+ // 1. CLI argument
86
+ if (serverArg) {
87
+ return normalizeServerUrl(serverArg);
88
+ }
89
+ // 2. Environment variable
90
+ const envServer = process.env.OPENCODESPACES_SERVER;
91
+ if (envServer) {
92
+ return normalizeServerUrl(envServer);
93
+ }
94
+ // 3. Config file
95
+ const config = loadConfig();
96
+ if (config.server) {
97
+ return normalizeServerUrl(config.server);
98
+ }
99
+ // 4. Credentials file (use last logged-in server)
100
+ const creds = loadCredentials();
101
+ if (creds?.server) {
102
+ return normalizeServerUrl(creds.server);
103
+ }
104
+ // 5. Default
105
+ return DEFAULT_SERVER;
106
+ }
107
+ /**
108
+ * Normalize server URL (add https:// if missing, remove trailing slash)
109
+ */
110
+ function normalizeServerUrl(url) {
111
+ let normalized = url.trim();
112
+ // Add protocol if missing
113
+ if (!normalized.startsWith('http://') && !normalized.startsWith('https://')) {
114
+ normalized = 'https://' + normalized;
115
+ }
116
+ // Remove trailing slash
117
+ if (normalized.endsWith('/')) {
118
+ normalized = normalized.slice(0, -1);
119
+ }
120
+ return normalized;
121
+ }
122
+ /**
123
+ * Load stored credentials
124
+ */
125
+ export function loadCredentials() {
126
+ try {
127
+ if (fs.existsSync(CREDENTIALS_FILE)) {
128
+ const content = fs.readFileSync(CREDENTIALS_FILE, 'utf-8');
129
+ return JSON.parse(content);
130
+ }
131
+ }
132
+ catch {
133
+ // Ignore parse errors
134
+ }
135
+ return null;
136
+ }
137
+ /**
138
+ * Save credentials
139
+ */
140
+ export function saveCredentials(credentials) {
141
+ ensureConfigDir();
142
+ fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), {
143
+ mode: 0o600, // Restrict to owner only
144
+ });
145
+ }
146
+ /**
147
+ * Delete credentials
148
+ */
149
+ export function deleteCredentials() {
150
+ if (fs.existsSync(CREDENTIALS_FILE)) {
151
+ fs.unlinkSync(CREDENTIALS_FILE);
152
+ }
153
+ }
154
+ /**
155
+ * Get ignore list from config or defaults
156
+ */
157
+ export function getIgnoreList() {
158
+ const config = loadConfig();
159
+ return config.ignores || DEFAULT_IGNORES;
160
+ }
161
+ /**
162
+ * Save SSH key for a session
163
+ */
164
+ export function saveSessionKey(sessionId, privateKey) {
165
+ ensureConfigDir();
166
+ const keyPath = path.join(KEYS_DIR, sessionId);
167
+ fs.writeFileSync(keyPath, privateKey, { mode: 0o600 });
168
+ return keyPath;
169
+ }
170
+ /**
171
+ * Delete SSH key for a session
172
+ */
173
+ export function deleteSessionKey(sessionId) {
174
+ const keyPath = path.join(KEYS_DIR, sessionId);
175
+ if (fs.existsSync(keyPath)) {
176
+ fs.unlinkSync(keyPath);
177
+ }
178
+ }
179
+ /**
180
+ * Get SSH key path for a session
181
+ */
182
+ export function getSessionKeyPath(sessionId) {
183
+ return path.join(KEYS_DIR, sessionId);
184
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * CLI Version
3
+ */
4
+ export declare const version = "0.1.0";
@@ -0,0 +1,4 @@
1
+ /**
2
+ * CLI Version
3
+ */
4
+ export const version = '0.1.0';
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Logger utility with colored output
3
+ */
4
+ export declare const logger: {
5
+ /**
6
+ * Info message (blue)
7
+ */
8
+ info(message: string): void;
9
+ /**
10
+ * Success message (green)
11
+ */
12
+ success(message: string): void;
13
+ /**
14
+ * Warning message (yellow)
15
+ */
16
+ warn(message: string): void;
17
+ /**
18
+ * Error message (red)
19
+ */
20
+ error(message: string): void;
21
+ /**
22
+ * Plain message (no prefix)
23
+ */
24
+ log(message: string): void;
25
+ /**
26
+ * Dimmed message (for secondary info)
27
+ */
28
+ dim(message: string): void;
29
+ /**
30
+ * Bold message
31
+ */
32
+ bold(message: string): void;
33
+ /**
34
+ * Sync status arrow (up = local to remote)
35
+ */
36
+ syncUp(message: string): void;
37
+ /**
38
+ * Sync status arrow (down = remote to local)
39
+ */
40
+ syncDown(message: string): void;
41
+ /**
42
+ * Sync active indicator
43
+ */
44
+ syncActive(message: string): void;
45
+ };
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Logger utility with colored output
3
+ */
4
+ import chalk from 'chalk';
5
+ export const logger = {
6
+ /**
7
+ * Info message (blue)
8
+ */
9
+ info(message) {
10
+ console.log(chalk.blue('ℹ'), message);
11
+ },
12
+ /**
13
+ * Success message (green)
14
+ */
15
+ success(message) {
16
+ console.log(chalk.green('✓'), message);
17
+ },
18
+ /**
19
+ * Warning message (yellow)
20
+ */
21
+ warn(message) {
22
+ console.log(chalk.yellow('⚠'), message);
23
+ },
24
+ /**
25
+ * Error message (red)
26
+ */
27
+ error(message) {
28
+ console.error(chalk.red('✗'), message);
29
+ },
30
+ /**
31
+ * Plain message (no prefix)
32
+ */
33
+ log(message) {
34
+ console.log(message);
35
+ },
36
+ /**
37
+ * Dimmed message (for secondary info)
38
+ */
39
+ dim(message) {
40
+ console.log(chalk.dim(message));
41
+ },
42
+ /**
43
+ * Bold message
44
+ */
45
+ bold(message) {
46
+ console.log(chalk.bold(message));
47
+ },
48
+ /**
49
+ * Sync status arrow (up = local to remote)
50
+ */
51
+ syncUp(message) {
52
+ console.log(chalk.cyan('↑'), message);
53
+ },
54
+ /**
55
+ * Sync status arrow (down = remote to local)
56
+ */
57
+ syncDown(message) {
58
+ console.log(chalk.cyan('↓'), message);
59
+ },
60
+ /**
61
+ * Sync active indicator
62
+ */
63
+ syncActive(message) {
64
+ console.log(chalk.cyan('⟳'), message);
65
+ },
66
+ };
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "opencodespaces",
3
+ "version": "0.1.0",
4
+ "description": "CLI for OpenCodeSpaces - Connect your local IDE to cloud sessions",
5
+ "type": "module",
6
+ "bin": {
7
+ "opencodespaces": "./dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "tsc -w",
12
+ "start": "node dist/index.js",
13
+ "prepublishOnly": "npm run build"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "keywords": [
19
+ "opencodespaces",
20
+ "cli",
21
+ "ide",
22
+ "sync",
23
+ "mutagen",
24
+ "ssh",
25
+ "cloud-development",
26
+ "remote-development",
27
+ "vscode",
28
+ "jetbrains"
29
+ ],
30
+ "author": "Johann Zelger <j.zelger@techdivision.com>",
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/techdivision-rnd/opencodespaces.git",
35
+ "directory": "packages/cli"
36
+ },
37
+ "bugs": {
38
+ "url": "https://github.com/techdivision-rnd/opencodespaces/issues"
39
+ },
40
+ "homepage": "https://github.com/techdivision-rnd/opencodespaces/tree/master/packages/cli#readme",
41
+ "engines": {
42
+ "node": ">=18.0.0"
43
+ },
44
+ "dependencies": {
45
+ "chalk": "^5.3.0",
46
+ "commander": "^12.1.0",
47
+ "inquirer": "^12.4.0",
48
+ "open": "^10.1.0",
49
+ "ora": "^8.1.1",
50
+ "ws": "^8.18.0"
51
+ },
52
+ "devDependencies": {
53
+ "@types/inquirer": "^9.0.7",
54
+ "@types/node": "^22.10.2",
55
+ "@types/ws": "^8.5.13",
56
+ "typescript": "^5.7.2"
57
+ }
58
+ }