Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: CI

on:
push:
pull_request:

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run build
- run: node --check src/server.js
- run: node --check src/server-manager.js
21 changes: 5 additions & 16 deletions src/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@
class Auth {
constructor() {
this.storageKey = 'opencode-auth';
this.password = null;
}

/**
* Store credentials
*/
saveCredentials(username, password) {
this.password = password;
const credentials = {
username,
password: this.encode(password),
timestamp: new Date().toISOString()
};
localStorage.setItem(this.storageKey, JSON.stringify(credentials));
Expand All @@ -27,9 +28,10 @@ class Auth {
try {
const data = JSON.parse(localStorage.getItem(this.storageKey));
if (!data) return null;
if (!this.password) return null;
return {
username: data.username,
password: this.decode(data.password),
password: this.password,
timestamp: data.timestamp
};
} catch (e) {
Expand All @@ -41,6 +43,7 @@ class Auth {
* Clear credentials
*/
clearCredentials() {
this.password = null;
localStorage.removeItem(this.storageKey);
}

Expand All @@ -51,20 +54,6 @@ class Auth {
return this.getCredentials() !== null;
}

/**
* Encode password (obfuscation, not encryption)
*/
encode(text) {
return btoa(encodeURIComponent(text));
}

/**
* Decode password
*/
decode(encoded) {
return decodeURIComponent(atob(encoded));
}

/**
* Verify server connection with credentials
*/
Expand Down
5 changes: 4 additions & 1 deletion src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ class App {
const savedConfig = {
url: localStorage.getItem('opencode-url'),
username: localStorage.getItem('opencode-username') || 'opencode',
password: localStorage.getItem('opencode-password') || ''
password: ''
};

// Remove credentials stored by older versions. Passwords now stay in memory only.
localStorage.removeItem('opencode-password');

// Restore theme
const savedTheme = localStorage.getItem('opencode-theme');
if (savedTheme) {
Expand Down
6 changes: 3 additions & 3 deletions src/server-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
* Run: node src/server-manager.js [start|stop|status]
*/

const { OpenCodeServer } = require('./server.js');
require('dotenv').config();
import { OpenCodeServer } from './server.js';
import 'dotenv/config';

const command = process.argv[2] || 'start';
const port = parseInt(process.env.OPENCODE_PORT || process.argv[3] || '4096', 10);
Expand Down Expand Up @@ -32,7 +32,7 @@ async function main() {
console.log(` http://localhost:${port}`);
console.log(`\n🔐 Credentials:`);
console.log(` Username: ${username}`);
console.log(` Password: ${password}`);
console.log(' Password: configured via OPENCODE_SERVER_PASSWORD');
console.log('\nPress Ctrl+C to stop\n');

// Keep process alive
Expand Down
84 changes: 32 additions & 52 deletions src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
* Starts and manages the OpenCode web server
*/

const { exec, spawn } = require('child_process');
const path = require('path');
const fs = require('fs');
const os = require('os');
import { spawn } from 'node:child_process';
import path from 'node:path';
import fs from 'node:fs';
import os from 'node:os';

class OpenCodeServer {
constructor(options = {}) {
this.options = {
port: options.port || 4096,
hostname: options.hostname || '0.0.0.0',
hostname: options.hostname || '127.0.0.1',
password: options.password || process.env.OPENCODE_SERVER_PASSWORD,
username: options.username || process.env.OPENCODE_SERVER_USERNAME || 'opencode',
cors: options.cors || [],
Expand All @@ -28,9 +28,13 @@ class OpenCodeServer {
*/
checkOpenCodeInstalled() {
return new Promise((resolve) => {
exec('opencode --version', (error) => {
resolve(!error);
const openCodePath = this.getOpenCodePath();
const child = spawn(openCodePath, ['--version'], {
stdio: 'ignore',
shell: process.platform === 'win32' && openCodePath.endsWith('.cmd')
});
child.once('error', () => resolve(false));
child.once('exit', (code) => resolve(code === 0));
});
}

Expand Down Expand Up @@ -79,8 +83,9 @@ class OpenCodeServer {

const installed = await this.checkOpenCodeInstalled();
if (!installed) {
console.log('OpenCode not installed. Installing...');
await this.installOpenCode();
throw new Error(
'OpenCode is not installed. Install it explicitly from the official OpenCode instructions before starting this server.'
);
}

console.log('🚀 Starting OpenCode web server...');
Expand Down Expand Up @@ -133,51 +138,27 @@ class OpenCodeServer {
}

/**
* Install OpenCode
* Wait for server to be ready
*/
installOpenCode() {
return new Promise((resolve, reject) => {
const install = spawn('bash', ['-c', 'curl -fsSL https://opencode.ai/install | bash'], {
stdio: 'inherit',
shell: false
});
async waitForServer() {
const timeout = 10000;
const start = Date.now();
const url = `http://${this.options.hostname}:${this.options.port}/doc`;

install.on('exit', (code) => {
if (code === 0) {
console.log('OpenCode installed successfully');
resolve();
} else {
reject(new Error('Failed to install OpenCode'));
while (Date.now() - start <= timeout) {
try {
const response = await fetch(url, { signal: AbortSignal.timeout(1500) });
if (response.ok) {
console.log(`✅ OpenCode server ready at http://${this.options.hostname}:${this.options.port}`);
return;
}
});
});
}
} catch {
// Server is still starting.
}
await new Promise(resolve => setTimeout(resolve, 1000));
}

/**
* Wait for server to be ready
*/
waitForServer() {
return new Promise((resolve) => {
const timeout = 10000;
const start = Date.now();

const check = setInterval(() => {
const port = this.options.port;
const { exec } = require('child_process');

exec(`curl -s http://${this.options.hostname}:${port}/doc`, (error, stdout) => {
if (!error && stdout) {
clearInterval(check);
console.log(`✅ OpenCode server ready at http://${this.options.hostname}:${port}`);
resolve();
} else if (Date.now() - start > timeout) {
clearInterval(check);
console.log('⚠️ Server not ready within timeout, but may still be starting...');
resolve();
}
});
}, 1000);
});
console.log('⚠️ Server not ready within timeout, but may still be starting...');
}

/**
Expand All @@ -204,5 +185,4 @@ class OpenCodeServer {
}
}

// Export for CommonJS
module.exports = { OpenCodeServer };
export { OpenCodeServer };
4 changes: 2 additions & 2 deletions src/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -731,7 +731,7 @@ class OpenCodeUI {
</div>
<div class="form-group">
<label for="settings-password">Password</label>
<input type="password" id="settings-password" class="form-input" placeholder="••••••••">
<input type="password" id="settings-password" class="form-input" placeholder="Not stored — re-enter after reload" autocomplete="current-password">
</div>
<div class="form-group">
<label>
Expand Down Expand Up @@ -771,7 +771,7 @@ class OpenCodeUI {
// Save to localStorage
localStorage.setItem('opencode-url', url);
localStorage.setItem('opencode-username', username);
localStorage.setItem('opencode-password', password);
localStorage.removeItem('opencode-password');

// Apply theme
document.documentElement.dataset.theme = darkMode ? 'dark' : 'light';
Expand Down
Loading