diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..b70ad58f --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,52 @@ +name: Build OCcode + +on: + push: + branches: [main, scaffold-mvp] + pull_request: + branches: [main] + +jobs: + build-wrapper: + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + - name: Install dependencies + run: npm ci + working-directory: apps/wrapper + - name: Build Electron app + run: npm run build + working-directory: apps/wrapper + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/upload-artifact@v4 + with: + name: occode-${{ matrix.os }} + path: apps/wrapper/dist/* + + build-extension: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + - name: Install dependencies + run: npm ci + working-directory: apps/extension + - name: Compile extension + run: npm run compile + working-directory: apps/extension + - name: Package VSIX + run: npx @vscode/vsce package + working-directory: apps/extension + - uses: actions/upload-artifact@v4 + with: + name: openclaw-extension + path: apps/extension/*.vsix diff --git a/README.md b/README.md index 29544455..a6ee20b0 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,66 @@ # OCcode -Branded, cross‑platform OpenClaw IDE built on a VS Code portable bundle + custom extension. +**Branded, cross-platform IDE wrapper** that ships a portable VSCodium bundle with the **OpenClaw** VS Code extension pre-installed. ## Structure + ``` apps/ - wrapper/ # Electron app (branding, install flow) - extension/ # VS Code extension (OpenClaw UI + commands) -scripts/ -.github/workflows/ + wrapper/ # Electron app — downloads VSCodium, installs extension, launches editor + extension/ # VS Code extension — Home screen, Setup wizard, Status panel +.github/ + workflows/ # CI: build wrapper (Win/Mac/Linux) + package extension +``` + +## Quick Start + +### Wrapper (Electron) + +```bash +cd apps/wrapper +npm install +npm start +``` + +The wrapper will: +1. Download portable VSCodium (pinned version) to `~/.occode/vscode/` +2. Install any bundled `.vsix` extensions +3. Set default settings (theme, disable welcome) +4. Launch VSCodium with a custom user-data directory + +### Extension (VS Code) + +```bash +cd apps/extension +npm install +npm run compile +``` + +Then press F5 in VS Code to launch the Extension Development Host. + +**Commands:** +- `OpenClaw: Home` — branded home screen with quick links +- `OpenClaw: Setup Local` — wizard to detect OS, check prerequisites (git, node, docker), and guide OpenClaw setup +- `OpenClaw: Status` — shows whether the OpenClaw gateway is running + +## Building + +```bash +# Wrapper — platform-specific installer +cd apps/wrapper && npm run build + +# Extension — .vsix package +cd apps/extension && npx @vscode/vsce package ``` -## Goals -- Ship OCcode.exe / OCcode.dmg / OCcode.AppImage -- Custom home screen + panels via extension -- Install/config OpenClaw locally via extension wizard - -## Plan (MVP) -**Wrapper (Electron)** -1. Download portable VS Code/VSCodium -2. Install OpenClaw extension (.vsix or URL) -3. Write default settings/workspace -4. Launch VS Code - -**Extension (OpenClaw)** -- Custom Home webview -- Install/config local OpenClaw -- Start/stop + logs/status - -## Next -- Scaffold wrapper app -- Scaffold VS Code extension -- Add CI for Windows/Mac/Linux builds +## CI + +GitHub Actions builds the wrapper for Windows, macOS, and Linux on every push to `main`. Extension is compiled and packaged as a `.vsix` artifact. + +## Contributing + +PRs welcome! See [AGENTS.md](AGENTS.md) for the full plan and milestone tracker. + +## License + +MIT diff --git a/apps/extension/package.json b/apps/extension/package.json new file mode 100644 index 00000000..2767f3d9 --- /dev/null +++ b/apps/extension/package.json @@ -0,0 +1,42 @@ +{ + "name": "openclaw-vscode", + "displayName": "OpenClaw", + "description": "OpenClaw home screen, local setup wizard, and status panel for VS Code", + "version": "0.1.0", + "publisher": "openclaw", + "engines": { + "vscode": "^1.85.0" + }, + "categories": ["Other"], + "activationEvents": [ + "onStartupFinished" + ], + "main": "./out/extension.js", + "contributes": { + "commands": [ + { + "command": "openclaw.home", + "title": "OpenClaw: Home" + }, + { + "command": "openclaw.setupLocal", + "title": "OpenClaw: Setup Local" + }, + { + "command": "openclaw.status", + "title": "OpenClaw: Status" + } + ] + }, + "scripts": { + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./", + "package": "vsce package" + }, + "devDependencies": { + "@types/vscode": "^1.85.0", + "@types/node": "^20.0.0", + "typescript": "^5.3.0", + "@vscode/vsce": "^2.22.0" + } +} diff --git a/apps/extension/src/extension.ts b/apps/extension/src/extension.ts new file mode 100644 index 00000000..72679a86 --- /dev/null +++ b/apps/extension/src/extension.ts @@ -0,0 +1,23 @@ +import * as vscode from 'vscode'; +import { HomePanel } from './panels/home'; +import { SetupPanel } from './panels/setup'; +import { StatusPanel } from './panels/status'; + +export function activate(context: vscode.ExtensionContext) { + context.subscriptions.push( + vscode.commands.registerCommand('openclaw.home', () => { + HomePanel.createOrShow(context.extensionUri); + }), + vscode.commands.registerCommand('openclaw.setupLocal', () => { + SetupPanel.createOrShow(context.extensionUri); + }), + vscode.commands.registerCommand('openclaw.status', () => { + StatusPanel.createOrShow(context.extensionUri); + }), + ); + + // Show home panel on first activation + vscode.commands.executeCommand('openclaw.home'); +} + +export function deactivate() {} diff --git a/apps/extension/src/panels/home.ts b/apps/extension/src/panels/home.ts new file mode 100644 index 00000000..b91bdb9f --- /dev/null +++ b/apps/extension/src/panels/home.ts @@ -0,0 +1,82 @@ +import * as vscode from 'vscode'; + +export class HomePanel { + public static currentPanel: HomePanel | undefined; + private readonly _panel: vscode.WebviewPanel; + private _disposables: vscode.Disposable[] = []; + + private constructor(panel: vscode.WebviewPanel) { + this._panel = panel; + this._panel.webview.html = this._getHtml(); + this._panel.onDidDispose(() => this.dispose(), null, this._disposables); + } + + public static createOrShow(extensionUri: vscode.Uri) { + if (HomePanel.currentPanel) { + HomePanel.currentPanel._panel.reveal(); + return; + } + const panel = vscode.window.createWebviewPanel( + 'openclawHome', 'OpenClaw Home', vscode.ViewColumn.One, { enableScripts: true } + ); + HomePanel.currentPanel = new HomePanel(panel); + } + + public dispose() { + HomePanel.currentPanel = undefined; + this._panel.dispose(); + this._disposables.forEach(d => d.dispose()); + } + + private _getHtml(): string { + return ` + +
+ + + + +Your AI-powered development companion
+Detected: ${platform}
+ ++ + ${running ? 'Running' : 'Not Running'} +
+${details}
+
+
+
+`;
+ }
+}
diff --git a/apps/extension/tsconfig.json b/apps/extension/tsconfig.json
new file mode 100644
index 00000000..882d1007
--- /dev/null
+++ b/apps/extension/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "compilerOptions": {
+ "module": "commonjs",
+ "target": "ES2020",
+ "outDir": "out",
+ "lib": ["ES2020"],
+ "sourceMap": true,
+ "rootDir": "src",
+ "strict": true
+ },
+ "exclude": ["node_modules", "out"]
+}
diff --git a/apps/wrapper/assets/.gitkeep b/apps/wrapper/assets/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/apps/wrapper/extensions/.gitkeep b/apps/wrapper/extensions/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/apps/wrapper/package.json b/apps/wrapper/package.json
new file mode 100644
index 00000000..5519b5ac
--- /dev/null
+++ b/apps/wrapper/package.json
@@ -0,0 +1,52 @@
+{
+ "name": "occode-wrapper",
+ "version": "0.1.0",
+ "description": "OCcode Electron wrapper — downloads and launches portable VSCodium",
+ "main": "src/main.js",
+ "scripts": {
+ "start": "electron .",
+ "build": "electron-builder",
+ "build:win": "electron-builder --win",
+ "build:mac": "electron-builder --mac",
+ "build:linux": "electron-builder --linux"
+ },
+ "build": {
+ "appId": "com.openclaw.occode",
+ "productName": "OCcode",
+ "directories": {
+ "output": "dist"
+ },
+ "files": [
+ "src/**/*",
+ "assets/**/*"
+ ],
+ "extraResources": [
+ {
+ "from": "extensions/",
+ "to": "extensions/",
+ "filter": ["**/*.vsix"]
+ }
+ ],
+ "win": {
+ "target": ["nsis", "portable"],
+ "icon": "assets/icon.ico"
+ },
+ "mac": {
+ "target": ["dmg"],
+ "icon": "assets/icon.icns",
+ "category": "public.app-category.developer-tools"
+ },
+ "linux": {
+ "target": ["AppImage", "deb"],
+ "icon": "assets/icon.png",
+ "category": "Development"
+ }
+ },
+ "dependencies": {
+ "electron-log": "^5.0.0"
+ },
+ "devDependencies": {
+ "electron": "^30.0.0",
+ "electron-builder": "^24.0.0"
+ }
+}
diff --git a/apps/wrapper/src/download.js b/apps/wrapper/src/download.js
new file mode 100644
index 00000000..434d6c77
--- /dev/null
+++ b/apps/wrapper/src/download.js
@@ -0,0 +1,81 @@
+const fs = require('fs');
+const path = require('path');
+const os = require('os');
+const { execSync } = require('child_process');
+const https = require('https');
+
+const PLATFORM_MAP = {
+ win32: { os: 'win32', arch: 'x64', ext: 'zip', dir: 'VSCodium-win32-x64' },
+ darwin: { os: 'darwin', arch: 'x64', ext: 'zip', dir: 'VSCodium-darwin-x64' },
+ linux: { os: 'linux', arch: 'x64', ext: 'tar.gz', dir: 'VSCodium-linux-x64' },
+};
+
+function getPlatformInfo() {
+ const p = PLATFORM_MAP[process.platform];
+ if (!p) throw new Error(`Unsupported platform: ${process.platform}`);
+ // Handle Apple Silicon
+ if (process.platform === 'darwin' && process.arch === 'arm64') {
+ return { ...p, arch: 'arm64', dir: 'VSCodium-darwin-arm64' };
+ }
+ return p;
+}
+
+function buildDownloadUrl(version) {
+ const p = getPlatformInfo();
+ const filename = `VSCodium-${p.os}-${p.arch}-${version}.${p.ext}`;
+ return `https://github.com/VSCodium/vscodium/releases/download/${version}/${filename}`;
+}
+
+function downloadFile(url, dest) {
+ return new Promise((resolve, reject) => {
+ const file = fs.createWriteStream(dest);
+ const get = (u) => {
+ https.get(u, { headers: { 'User-Agent': 'OCcode' } }, (res) => {
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
+ return get(res.headers.location);
+ }
+ if (res.statusCode !== 200) {
+ return reject(new Error(`Download failed: HTTP ${res.statusCode}`));
+ }
+ res.pipe(file);
+ file.on('finish', () => { file.close(); resolve(); });
+ }).on('error', reject);
+ };
+ get(url);
+ });
+}
+
+async function downloadVSCodium(version, destDir) {
+ fs.mkdirSync(destDir, { recursive: true });
+ const url = buildDownloadUrl(version);
+ const p = getPlatformInfo();
+ const archivePath = path.join(os.tmpdir(), `vscodium.${p.ext}`);
+
+ await downloadFile(url, archivePath);
+
+ // Extract
+ if (p.ext === 'zip') {
+ if (process.platform === 'win32') {
+ execSync(`powershell -Command "Expand-Archive -Path '${archivePath}' -DestinationPath '${destDir}' -Force"`);
+ } else {
+ execSync(`unzip -o "${archivePath}" -d "${destDir}"`);
+ }
+ } else {
+ execSync(`tar -xzf "${archivePath}" -C "${destDir}"`);
+ }
+
+ fs.unlinkSync(archivePath);
+}
+
+function getVSCodiumBinary(vscodeDir) {
+ switch (process.platform) {
+ case 'win32':
+ return path.join(vscodeDir, 'bin', 'codium.cmd');
+ case 'darwin':
+ return path.join(vscodeDir, 'VSCodium.app', 'Contents', 'Resources', 'app', 'bin', 'codium');
+ default:
+ return path.join(vscodeDir, 'bin', 'codium');
+ }
+}
+
+module.exports = { downloadVSCodium, getVSCodiumBinary, getPlatformInfo };
diff --git a/apps/wrapper/src/main.js b/apps/wrapper/src/main.js
new file mode 100644
index 00000000..62e6b630
--- /dev/null
+++ b/apps/wrapper/src/main.js
@@ -0,0 +1,70 @@
+const { app, BrowserWindow, dialog } = require('electron');
+const path = require('path');
+const { downloadVSCodium, getVSCodiumBinary } = require('./download');
+const { installExtension, setDefaults, launchVSCodium } = require('./setup');
+
+const APP_NAME = 'OCcode';
+const VSCODIUM_VERSION = '1.96.4.25027';
+const OCCODE_DIR = path.join(require('os').homedir(), '.occode');
+const VSCODE_DIR = path.join(OCCODE_DIR, 'vscode');
+
+let mainWindow;
+
+function createWindow() {
+ mainWindow = new BrowserWindow({
+ width: 520,
+ height: 380,
+ title: APP_NAME,
+ resizable: false,
+ webPreferences: {
+ nodeIntegration: true,
+ contextIsolation: false,
+ },
+ });
+ mainWindow.loadFile(path.join(__dirname, 'splash.html'));
+ mainWindow.setMenuBarVisibility(false);
+}
+
+function sendStatus(msg) {
+ if (mainWindow && !mainWindow.isDestroyed()) {
+ mainWindow.webContents.executeJavaScript(
+ `document.getElementById('status').textContent = ${JSON.stringify(msg)}`
+ );
+ }
+}
+
+async function bootstrap() {
+ try {
+ sendStatus('Checking VSCodium installation…');
+ const binary = getVSCodiumBinary(VSCODE_DIR);
+
+ const fs = require('fs');
+ if (!fs.existsSync(binary)) {
+ sendStatus('Downloading VSCodium…');
+ await downloadVSCodium(VSCODIUM_VERSION, VSCODE_DIR);
+ }
+
+ sendStatus('Installing OpenClaw extension…');
+ await installExtension(binary, OCCODE_DIR);
+
+ sendStatus('Setting defaults…');
+ await setDefaults(OCCODE_DIR);
+
+ sendStatus('Launching editor…');
+ await launchVSCodium(binary, OCCODE_DIR);
+
+ // Close wrapper after launching editor
+ setTimeout(() => app.quit(), 2000);
+ } catch (err) {
+ dialog.showErrorBox('OCcode Error', err.message);
+ app.quit();
+ }
+}
+
+app.setName(APP_NAME);
+app.whenReady().then(() => {
+ createWindow();
+ bootstrap();
+});
+
+app.on('window-all-closed', () => app.quit());
diff --git a/apps/wrapper/src/setup.js b/apps/wrapper/src/setup.js
new file mode 100644
index 00000000..314fe276
--- /dev/null
+++ b/apps/wrapper/src/setup.js
@@ -0,0 +1,72 @@
+const fs = require('fs');
+const path = require('path');
+const { execFile, spawn } = require('child_process');
+const { promisify } = require('util');
+
+const execFileAsync = promisify(execFile);
+
+const DEFAULT_SETTINGS = {
+ "workbench.startupEditor": "none",
+ "workbench.colorTheme": "Default Dark Modern",
+ "workbench.tips.enabled": false,
+ "extensions.autoUpdate": true,
+ "telemetry.telemetryLevel": "off",
+};
+
+async function installExtension(codiumBinary, occodeDir) {
+ // Look for bundled .vsix files in the app's resources or local extensions/ dir
+ const searchPaths = [
+ path.join(__dirname, '..', 'extensions'),
+ path.join(process.resourcesPath || '', 'extensions'),
+ ];
+
+ for (const dir of searchPaths) {
+ if (!fs.existsSync(dir)) continue;
+ const vsixFiles = fs.readdirSync(dir).filter(f => f.endsWith('.vsix'));
+ for (const vsix of vsixFiles) {
+ const vsixPath = path.join(dir, vsix);
+ const userDataDir = path.join(occodeDir, 'user-data');
+ try {
+ await execFileAsync(codiumBinary, [
+ '--install-extension', vsixPath,
+ '--user-data-dir', userDataDir,
+ '--force',
+ ], { timeout: 60000 });
+ } catch (err) {
+ console.warn(`Failed to install ${vsix}:`, err.message);
+ }
+ }
+ }
+}
+
+async function setDefaults(occodeDir) {
+ const userDataDir = path.join(occodeDir, 'user-data');
+ const settingsDir = path.join(userDataDir, 'User');
+ const settingsFile = path.join(settingsDir, 'settings.json');
+
+ fs.mkdirSync(settingsDir, { recursive: true });
+
+ // Merge with existing settings if any
+ let existing = {};
+ if (fs.existsSync(settingsFile)) {
+ try {
+ existing = JSON.parse(fs.readFileSync(settingsFile, 'utf8'));
+ } catch {}
+ }
+
+ const merged = { ...DEFAULT_SETTINGS, ...existing };
+ fs.writeFileSync(settingsFile, JSON.stringify(merged, null, 2));
+}
+
+async function launchVSCodium(codiumBinary, occodeDir) {
+ const userDataDir = path.join(occodeDir, 'user-data');
+ const child = spawn(codiumBinary, [
+ '--user-data-dir', userDataDir,
+ ], {
+ detached: true,
+ stdio: 'ignore',
+ });
+ child.unref();
+}
+
+module.exports = { installExtension, setDefaults, launchVSCodium };
diff --git a/apps/wrapper/src/splash.html b/apps/wrapper/src/splash.html
new file mode 100644
index 00000000..e69a2554
--- /dev/null
+++ b/apps/wrapper/src/splash.html
@@ -0,0 +1,43 @@
+
+
+
+
+