Bladeren bron

feat: add companion installer release flow

Alvin Unreal 1 maand geleden
bovenliggende
commit
48746c58df

+ 160 - 0
.github/workflows/companion-release.yml

@@ -0,0 +1,160 @@
+name: Companion Release
+
+on:
+  workflow_dispatch:
+    inputs:
+      version:
+        description: 'Companion version, for example 0.1.0'
+        required: true
+        type: string
+      targets:
+        description: >-
+          Comma-separated targets to build: macos-arm64,macos-x64,linux-x64,windows-x64
+        required: true
+        default: macos-arm64
+        type: string
+
+permissions:
+  contents: write
+
+jobs:
+  macos-arm64:
+    name: macOS arm64
+    if: contains(github.event.inputs.targets, 'macos-arm64')
+    runs-on: macos-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: dtolnay/rust-toolchain@stable
+        with:
+          targets: aarch64-apple-darwin
+      - name: Build companion
+        working-directory: companion
+        run: cargo build --release --target aarch64-apple-darwin
+      - name: Package companion
+        run: |
+          set -euo pipefail
+          version='${{ github.event.inputs.version }}'
+          target='aarch64-apple-darwin'
+          name="oh-my-opencode-slim-companion-v${version}-${target}"
+          mkdir -p dist
+          cp "companion/target/${target}/release/oh-my-opencode-slim-companion" dist/oh-my-opencode-slim-companion
+          tar -C dist -czf "${name}.tar.gz" oh-my-opencode-slim-companion
+      - uses: actions/upload-artifact@v4
+        with:
+          name: companion-macos-arm64
+          path: '*.tar.gz'
+
+  macos-x64:
+    name: macOS x64
+    if: contains(github.event.inputs.targets, 'macos-x64')
+    runs-on: macos-13
+    steps:
+      - uses: actions/checkout@v4
+      - uses: dtolnay/rust-toolchain@stable
+        with:
+          targets: x86_64-apple-darwin
+      - name: Build companion
+        working-directory: companion
+        run: cargo build --release --target x86_64-apple-darwin
+      - name: Package companion
+        run: |
+          set -euo pipefail
+          version='${{ github.event.inputs.version }}'
+          target='x86_64-apple-darwin'
+          name="oh-my-opencode-slim-companion-v${version}-${target}"
+          mkdir -p dist
+          cp "companion/target/${target}/release/oh-my-opencode-slim-companion" dist/oh-my-opencode-slim-companion
+          tar -C dist -czf "${name}.tar.gz" oh-my-opencode-slim-companion
+      - uses: actions/upload-artifact@v4
+        with:
+          name: companion-macos-x64
+          path: '*.tar.gz'
+
+  linux-x64:
+    name: Linux x64
+    if: contains(github.event.inputs.targets, 'linux-x64')
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: dtolnay/rust-toolchain@stable
+        with:
+          targets: x86_64-unknown-linux-gnu
+      - name: Install system dependencies
+        run: |
+          sudo apt-get update
+          sudo apt-get install -y libgtk-3-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev
+      - name: Build companion
+        working-directory: companion
+        run: cargo build --release --target x86_64-unknown-linux-gnu
+      - name: Package companion
+        run: |
+          set -euo pipefail
+          version='${{ github.event.inputs.version }}'
+          target='x86_64-unknown-linux-gnu'
+          name="oh-my-opencode-slim-companion-v${version}-${target}"
+          mkdir -p dist
+          cp "companion/target/${target}/release/oh-my-opencode-slim-companion" dist/oh-my-opencode-slim-companion
+          tar -C dist -czf "${name}.tar.gz" oh-my-opencode-slim-companion
+      - uses: actions/upload-artifact@v4
+        with:
+          name: companion-linux-x64
+          path: '*.tar.gz'
+
+  windows-x64:
+    name: Windows x64
+    if: contains(github.event.inputs.targets, 'windows-x64')
+    runs-on: windows-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: dtolnay/rust-toolchain@stable
+        with:
+          targets: x86_64-pc-windows-msvc
+      - name: Build companion
+        working-directory: companion
+        run: cargo build --release --target x86_64-pc-windows-msvc
+      - name: Package companion
+        shell: pwsh
+        run: |
+          $version = '${{ github.event.inputs.version }}'
+          $target = 'x86_64-pc-windows-msvc'
+          $name = "oh-my-opencode-slim-companion-v$version-$target"
+          New-Item -ItemType Directory -Force -Path dist | Out-Null
+          Copy-Item "companion/target/$target/release/oh-my-opencode-slim-companion.exe" "dist/oh-my-opencode-slim-companion.exe"
+          Compress-Archive -Path "dist/oh-my-opencode-slim-companion.exe" -DestinationPath "$name.zip" -Force
+      - uses: actions/upload-artifact@v4
+        with:
+          name: companion-windows-x64
+          path: '*.zip'
+
+  release:
+    name: Publish companion release
+    needs:
+      - macos-arm64
+      - macos-x64
+      - linux-x64
+      - windows-x64
+    if: always() && !cancelled() && !failure()
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/download-artifact@v4
+        with:
+          path: artifacts
+          merge-multiple: true
+      - name: Generate checksums
+        run: |
+          set -euo pipefail
+          cd artifacts
+          sha256sum * > SHA256SUMS
+      - name: Create or update GitHub release
+        env:
+          GH_TOKEN: ${{ github.token }}
+          VERSION: ${{ github.event.inputs.version }}
+        run: |
+          set -euo pipefail
+          tag="companion-v${VERSION}"
+          if ! gh release view "$tag" >/dev/null 2>&1; then
+            gh release create "$tag" \
+              --title "Companion v${VERSION}" \
+              --notes "Manual companion binary release for oh-my-opencode-slim."
+          fi
+          gh release upload "$tag" artifacts/* --clobber

+ 1 - 0
README.md

@@ -526,6 +526,7 @@ Use this section as a map: start with installation, then jump to features, confi
 | **[Clonedeps](docs/clonedeps.md)** | Clone selected dependency source into an ignored local workspace for inspection |
 | **[Interview](docs/interview.md)** | Turn rough ideas into a structured markdown spec through a browser-based Q&A flow |
 | **[Divoom Display](docs/divoom.md)** | Mirror orchestrator and specialist-agent activity to a Divoom MiniToo Bluetooth display |
+| **[Companion](docs/companion.md)** | Floating window companion for parsing, help, and types |
 
 ### ⚙️ Config & Reference
 

+ 189 - 0
docs/companion.md

@@ -0,0 +1,189 @@
+# Desktop Companion App
+
+The desktop companion app provides a floating status overlay showing running and active agents.
+
+## How to Enable in Configuration
+
+You can enable the companion by adding a `companion` section to your setting configuration file (`~/.config/opencode/oh-my-opencode-slim.json` or `.opencode/oh-my-opencode-slim.json`):
+
+```jsonc
+{
+  "companion": {
+    "enabled": true,
+    "position": "bottom-right",
+    "size": "medium"
+  }
+}
+```
+
+### Supported Position & Size Values
+
+- **`companion.position`**:
+  - `bottom-right` (default)
+  - `bottom-left`
+  - `top-right`
+  - `top-left`
+
+- **`companion.size`**:
+  - `small` (80px)
+  - `medium` (120px) (default)
+  - `large` (160px)
+
+---
+
+## Installer Flag
+
+When running the installer, pass `--companion=yes` to download the native
+binary and generate the enabled config block:
+
+```bash
+bunx oh-my-opencode-slim install --companion=yes
+```
+
+Pass `--companion=no` or omit the flag to skip the native binary and omit the
+config block.
+
+---
+
+## Expected Binary Install Path
+
+The runtime looks for the companion binary at:
+
+```text
+$XDG_DATA_HOME/opencode/storage/oh-my-opencode-slim/bin/oh-my-opencode-slim-companion
+```
+
+If `XDG_DATA_HOME` is unset, this resolves to:
+
+```text
+~/.local/share/opencode/storage/oh-my-opencode-slim/bin/oh-my-opencode-slim-companion
+```
+
+If the binary is not located in this directory, the plugin runtime will not start the companion window.
+
+---
+
+## V2 Release Strategy
+
+For the desktop companion app, the release workflow follows the V2 distribution
+plan:
+
+1. **GitHub Release Assets**: companion binaries are uploaded to the
+   `companion-v0.1.0` GitHub release.
+2. **Separate Companion Versioning**: the companion uses its own version so the
+   plugin can ship beta updates without rebuilding native binaries every time.
+3. **Checksums**: the installer downloads `SHA256SUMS` and verifies the selected
+   archive before installing it.
+4. **OS/Arch Detection**: `--companion=yes` selects the archive for the current
+   target.
+5. **No R2 Required**: GitHub Releases are the source of truth. R2 can be added
+   later as a mirror if download volume becomes a problem.
+
+Current release assets are named:
+
+```text
+oh-my-opencode-slim-companion-v0.1.0-aarch64-apple-darwin.tar.gz
+oh-my-opencode-slim-companion-v0.1.0-x86_64-apple-darwin.tar.gz
+oh-my-opencode-slim-companion-v0.1.0-x86_64-unknown-linux-gnu.tar.gz
+oh-my-opencode-slim-companion-v0.1.0-aarch64-unknown-linux-gnu.tar.gz
+oh-my-opencode-slim-companion-v0.1.0-x86_64-pc-windows-msvc.zip
+SHA256SUMS
+```
+
+Supported installer targets:
+
+- macOS arm64: `aarch64-apple-darwin`
+- macOS x64: `x86_64-apple-darwin`
+- Linux x64: `x86_64-unknown-linux-gnu`
+- Linux arm64: `aarch64-unknown-linux-gnu`
+- Windows x64: `x86_64-pc-windows-msvc`
+
+For release bootstrapping only, checksum verification can be bypassed with
+`SKIP_COMPANION_CHECKSUM=true`, but normal user installs should keep checksum
+verification enabled.
+
+## Maintainer Release Process
+
+Companion binaries are **not** built on every plugin beta. The workflow is
+manual-only so GitHub runner usage stays under maintainer control.
+
+Run a companion release only when the Rust companion changes or when the state
+protocol expected by the plugin changes.
+
+### 1. Choose the companion version
+
+The first companion release is:
+
+```text
+0.1.0
+```
+
+The matching GitHub release tag is:
+
+```text
+companion-v0.1.0
+```
+
+The installer currently downloads from that tag.
+
+### 2. Trigger selected target builds manually
+
+Build only the targets you want to pay for. Start with your current platform if
+you are testing the release path:
+
+```bash
+gh workflow run companion-release.yml \
+  -f version=0.1.0 \
+  -f targets=macos-arm64
+```
+
+Build multiple targets by passing a comma-separated list:
+
+```bash
+gh workflow run companion-release.yml \
+  -f version=0.1.0 \
+  -f targets=macos-arm64,macos-x64,linux-x64,windows-x64
+```
+
+Supported workflow target names:
+
+```text
+macos-arm64
+macos-x64
+linux-x64
+windows-x64
+```
+
+The workflow creates or updates the `companion-v<version>` release and uploads
+the selected archives plus `SHA256SUMS`.
+
+### 3. Verify release assets
+
+After the workflow finishes:
+
+```bash
+gh release view companion-v0.1.0
+gh release download companion-v0.1.0 --pattern SHA256SUMS --output -
+```
+
+Confirm the release contains the archive names expected by the installer for the
+targets you built.
+
+### 4. Install with the plugin installer
+
+Once the release assets exist, users can run:
+
+```bash
+bunx oh-my-opencode-slim@beta install --companion=yes
+```
+
+The installer detects the user's OS/architecture, downloads the matching archive
+from `companion-v0.1.0`, verifies it against `SHA256SUMS`, installs it to the
+runtime binary path, and writes the companion config block.
+
+### Cost controls
+
+- The workflow uses `workflow_dispatch` only. It never runs on push, PR, or tag.
+- Build a single target while testing.
+- Reuse one companion release across many plugin betas.
+- Add more targets only when you are ready to support those users.

+ 3 - 1
docs/configuration.md

@@ -303,7 +303,9 @@ Notes:
 
 ### Desktop Companion App
 
-The desktop companion app provides a visual status overlay showing running and active agents. The companion requires the binary installed or downloaded separately until installer support lands. Once installed, configure it in your `oh-my-opencode-slim` settings:
+The desktop companion app provides a visual status overlay showing running and active agents. For quick installation instructions, binary paths, config defaults, and release information, see the full **[Desktop Companion Guide](companion.md)**.
+
+Once installed, configure it in your `oh-my-opencode-slim` settings:
 
 ```jsonc
 {

+ 232 - 0
src/cli/companion.ts

@@ -0,0 +1,232 @@
+import { createHash } from 'node:crypto';
+import {
+  chmodSync,
+  copyFileSync,
+  existsSync,
+  mkdirSync,
+  mkdtempSync,
+  renameSync,
+  rmSync,
+  writeFileSync,
+} from 'node:fs';
+import { homedir, tmpdir } from 'node:os';
+import * as path from 'node:path';
+import type { ConfigMergeResult, InstallConfig } from './types';
+
+const COMPANION_VERSION = '0.1.0';
+const COMPANION_TAG = 'companion-v0.1.0';
+const GITHUB_REPO = 'alvinunreal/oh-my-opencode-slim';
+
+export function getCompanionTarget(): string | null {
+  const p = process.platform;
+  const a = process.arch;
+  if (p === 'darwin') {
+    if (a === 'arm64') return 'aarch64-apple-darwin';
+    if (a === 'x64') return 'x86_64-apple-darwin';
+  } else if (p === 'linux') {
+    if (a === 'x64') return 'x86_64-unknown-linux-gnu';
+    if (a === 'arm64') return 'aarch64-unknown-linux-gnu';
+  } else if (p === 'win32') {
+    if (a === 'x64') return 'x86_64-pc-windows-msvc';
+  }
+  return null;
+}
+
+export function getCompanionBinaryPath(): string {
+  const xdg = process.env.XDG_DATA_HOME?.trim();
+  const base =
+    xdg && path.isAbsolute(xdg) ? xdg : path.join(homedir(), '.local', 'share');
+  return path.join(
+    base,
+    'opencode',
+    'storage',
+    'oh-my-opencode-slim',
+    'bin',
+    process.platform === 'win32'
+      ? 'oh-my-opencode-slim-companion.exe'
+      : 'oh-my-opencode-slim-companion',
+  );
+}
+
+export async function installCompanion(
+  config: InstallConfig,
+): Promise<ConfigMergeResult> {
+  const target = getCompanionTarget();
+  const finalBinaryPath = getCompanionBinaryPath();
+
+  if (!target) {
+    return {
+      success: false,
+      configPath: finalBinaryPath,
+      error: `Unsupported platform/architecture: ${process.platform} ${process.arch}`,
+    };
+  }
+
+  const isWindows = process.platform === 'win32';
+  const ext = isWindows ? 'zip' : 'tar.gz';
+  const archiveName = `oh-my-opencode-slim-companion-v${COMPANION_VERSION}-${target}.${ext}`;
+  const downloadUrl = `https://github.com/${GITHUB_REPO}/releases/download/${COMPANION_TAG}/${archiveName}`;
+  const checksumUrl = `https://github.com/${GITHUB_REPO}/releases/download/${COMPANION_TAG}/SHA256SUMS`;
+
+  if (config.dryRun) {
+    console.log(`  [dry-run] Detected companion target: ${target}`);
+    console.log(`  [dry-run] Would download archive: ${downloadUrl}`);
+    console.log(`  [dry-run] Would download checksum: ${checksumUrl}`);
+    console.log(
+      '  [dry-run] Would verify via SHA256SUMS unless SKIP_COMPANION_CHECKSUM=true',
+    );
+    console.log(`  [dry-run] Would extract and install to: ${finalBinaryPath}`);
+    return {
+      success: true,
+      configPath: finalBinaryPath,
+    };
+  }
+
+  const skipChecksum = process.env.SKIP_COMPANION_CHECKSUM === 'true';
+  let shaSumsText = '';
+
+  if (!skipChecksum) {
+    try {
+      const res = await fetch(checksumUrl);
+      if (!res.ok) {
+        return {
+          success: false,
+          configPath: finalBinaryPath,
+          error: `Failed to fetch checksum manifest (HTTP ${res.status}): ${res.statusText}. For release bootstrap, you can bypass this error by setting the SKIP_COMPANION_CHECKSUM=true environment variable.`,
+        };
+      }
+      shaSumsText = await res.text();
+    } catch (err) {
+      return {
+        success: false,
+        configPath: finalBinaryPath,
+        error: `Failed to fetch SHA256SUMS: ${err instanceof Error ? err.message : String(err)}. For release bootstrap, you can bypass this error by setting the SKIP_COMPANION_CHECKSUM=true environment variable.`,
+      };
+    }
+  }
+
+  let buffer: ArrayBuffer;
+  try {
+    const res = await fetch(downloadUrl);
+    if (!res.ok) {
+      return {
+        success: false,
+        configPath: finalBinaryPath,
+        error: `Failed to download companion binary (HTTP ${res.status}): ${res.statusText}`,
+      };
+    }
+    buffer = await res.arrayBuffer();
+  } catch (err) {
+    return {
+      success: false,
+      configPath: finalBinaryPath,
+      error: `Failed to fetch companion archive: ${err instanceof Error ? err.message : String(err)}`,
+    };
+  }
+
+  if (!skipChecksum) {
+    const lines = shaSumsText.split('\n');
+    let expectedHash: string | undefined;
+    for (const line of lines) {
+      const parts = line.trim().split(/\s+/);
+      if (parts.length >= 2) {
+        const [hashVal, fileVal] = parts;
+        const cleanFilename = fileVal.startsWith('*')
+          ? fileVal.slice(1)
+          : fileVal;
+        if (cleanFilename === archiveName) {
+          expectedHash = hashVal.toLowerCase();
+          break;
+        }
+      }
+    }
+
+    if (!expectedHash) {
+      return {
+        success: false,
+        configPath: finalBinaryPath,
+        error: `No SHA256 checksum entry found for ${archiveName} in SHA256SUMS. For release bootstrap, you can bypass this error by setting the SKIP_COMPANION_CHECKSUM=true environment variable.`,
+      };
+    }
+
+    const computedHash = createHash('sha256')
+      .update(Buffer.from(buffer))
+      .digest('hex');
+    if (computedHash !== expectedHash) {
+      return {
+        success: false,
+        configPath: finalBinaryPath,
+        error: `SHA256 checksum mismatch for ${archiveName}. Expected ${expectedHash}, got ${computedHash}`,
+      };
+    }
+  }
+
+  let tempDir = '';
+  try {
+    tempDir = mkdtempSync(path.join(tmpdir(), 'companion-install-'));
+    const archivePath = path.join(tempDir, archiveName);
+    writeFileSync(archivePath, Buffer.from(buffer));
+
+    const extractedDir = path.join(tempDir, 'extracted');
+    mkdirSync(extractedDir, { recursive: true });
+
+    if (isWindows) {
+      const { extractZip } = await import('../utils/zip-extractor');
+      await extractZip(archivePath, extractedDir);
+    } else {
+      const { crossSpawn } = await import('../utils/compat');
+      const proc = crossSpawn(['tar', '-xzf', archivePath, '-C', extractedDir]);
+      const exitCode = await proc.exited;
+      if (exitCode !== 0) {
+        const stderr = await proc.stderr();
+        return {
+          success: false,
+          configPath: finalBinaryPath,
+          error: `Archive extraction failed (tar exited with ${exitCode}): ${stderr}`,
+        };
+      }
+    }
+
+    const binaryName = isWindows
+      ? 'oh-my-opencode-slim-companion.exe'
+      : 'oh-my-opencode-slim-companion';
+    const extractedBinaryPath = path.join(extractedDir, binaryName);
+
+    if (!existsSync(extractedBinaryPath)) {
+      return {
+        success: false,
+        configPath: finalBinaryPath,
+        error: `Binary ${binaryName} not found in extracted archive`,
+      };
+    }
+
+    const binDir = path.dirname(finalBinaryPath);
+    mkdirSync(binDir, { recursive: true });
+
+    const tmpFinalPath = `${finalBinaryPath}.tmp`;
+    copyFileSync(extractedBinaryPath, tmpFinalPath);
+
+    if (!isWindows) {
+      chmodSync(tmpFinalPath, 0o755);
+    }
+
+    renameSync(tmpFinalPath, finalBinaryPath);
+
+    return {
+      success: true,
+      configPath: finalBinaryPath,
+    };
+  } catch (err) {
+    return {
+      success: false,
+      configPath: finalBinaryPath,
+      error: `Failed to install companion: ${err instanceof Error ? err.message : String(err)}`,
+    };
+  } finally {
+    if (tempDir) {
+      try {
+        rmSync(tempDir, { recursive: true, force: true });
+      } catch {}
+    }
+  }
+}

+ 10 - 0
src/cli/index.ts

@@ -8,6 +8,7 @@ export function parseArgs(args: string[]): InstallArgs {
   const result: InstallArgs = {
     tui: true,
     skills: 'yes',
+    companion: 'no',
   };
 
   for (const arg of args) {
@@ -15,6 +16,13 @@ export function parseArgs(args: string[]): InstallArgs {
       result.tui = false;
     } else if (arg.startsWith('--skills=')) {
       result.skills = arg.split('=')[1] as BooleanArg;
+    } else if (arg.startsWith('--companion=')) {
+      const mode = arg.split('=')[1] as BooleanArg;
+      if (!['yes', 'no'].includes(mode)) {
+        console.error('Unsupported --companion value: use yes or no');
+        process.exit(1);
+      }
+      result.companion = mode;
     } else if (arg.startsWith('--preset=')) {
       const preset = arg.split('=')[1];
       if (!isGeneratedPresetName(preset)) {
@@ -61,6 +69,8 @@ Usage:
 
 Options:
   --skills=yes|no        Install bundled skills (default: yes)
+  --companion=yes|no     Install desktop companion binary and enable config
+                         (default: no)
   --preset=<name>        Active generated config preset (default: openai)
   --background-subagents=ask|yes|no
                          Persist required OpenCode background subagent env

+ 9 - 0
src/cli/install.ts

@@ -8,6 +8,7 @@ import {
   manualBackgroundSubagentsInstructions,
   writeBackgroundSubagentsBlock,
 } from './background-subagents';
+import { installCompanion } from './companion';
 import {
   addPluginToOpenCodeConfig,
   addPluginToOpenCodeTuiConfig,
@@ -246,6 +247,7 @@ async function runInstall(config: InstallConfig): Promise<number> {
 
   let totalSteps = 7;
   if (config.installCustomSkills) totalSteps += 1;
+  if (config.companion === 'yes') totalSteps += 1;
   totalSteps += 1;
 
   let step = 1;
@@ -310,6 +312,12 @@ async function runInstall(config: InstallConfig): Promise<number> {
   printStep(step++, totalSteps, 'Configuring OpenCode background subagents...');
   const backgroundSubagents = await configureBackgroundSubagents(config);
 
+  if (config.companion === 'yes') {
+    printStep(step++, totalSteps, 'Installing desktop companion binary...');
+    const companionResult = await installCompanion(config);
+    if (!handleStepResult(companionResult, 'Companion installed')) return 1;
+  }
+
   printStep(step++, totalSteps, 'Writing oh-my-opencode-slim configuration...');
   if (config.dryRun) {
     const liteConfig = generateLiteConfig(config);
@@ -433,6 +441,7 @@ export async function install(args: InstallArgs): Promise<number> {
     reset: args.reset ?? false,
     backgroundSubagents: args.backgroundSubagents ?? 'no',
     backgroundSubagentsTarget: args.backgroundSubagentsTarget,
+    companion: args.companion,
   };
 
   return runInstall(config);

+ 31 - 0
src/cli/providers.test.ts

@@ -131,6 +131,37 @@ describe('providers', () => {
     expect((config.tmux as any).layout).toBe('main-vertical');
   });
 
+  test('generateLiteConfig companion: yes', () => {
+    const config = generateLiteConfig({
+      hasTmux: false,
+      installCustomSkills: false,
+      reset: false,
+      companion: 'yes',
+    });
+
+    expect(config.companion).toBeDefined();
+    expect((config.companion as any).enabled).toBe(true);
+    expect((config.companion as any).position).toBe('bottom-right');
+    expect((config.companion as any).size).toBe('medium');
+  });
+
+  test('generateLiteConfig companion: no or omitted', () => {
+    const configYes = generateLiteConfig({
+      hasTmux: false,
+      installCustomSkills: false,
+      reset: false,
+      companion: 'no',
+    });
+    expect(configYes.companion).toBeUndefined();
+
+    const configOmitted = generateLiteConfig({
+      hasTmux: false,
+      installCustomSkills: false,
+      reset: false,
+    });
+    expect(configOmitted.companion).toBeUndefined();
+  });
+
   test('generateLiteConfig includes default skills', () => {
     const config = generateLiteConfig({
       hasTmux: false,

+ 8 - 0
src/cli/providers.ts

@@ -145,5 +145,13 @@ export function generateLiteConfig(
     };
   }
 
+  if (installConfig.companion === 'yes') {
+    config.companion = {
+      enabled: true,
+      position: 'bottom-right',
+      size: 'medium',
+    };
+  }
+
   return config;
 }

+ 2 - 0
src/cli/types.ts

@@ -9,6 +9,7 @@ export interface InstallArgs {
   reset?: boolean;
   backgroundSubagents?: BackgroundSubagentsArg;
   backgroundSubagentsTarget?: string;
+  companion?: BooleanArg;
 }
 
 export interface OpenCodeConfig {
@@ -27,6 +28,7 @@ export interface InstallConfig {
   reset: boolean;
   backgroundSubagents: BackgroundSubagentsArg;
   backgroundSubagentsTarget?: string;
+  companion?: BooleanArg;
 }
 
 export interface ConfigMergeResult {

+ 5 - 1
src/companion/manager.ts

@@ -50,13 +50,17 @@ function binaryPath(): string | null {
     xdg && path.isAbsolute(xdg)
       ? xdg
       : path.join(os.homedir(), '.local', 'share');
+  const binaryName =
+    os.platform() === 'win32'
+      ? 'oh-my-opencode-slim-companion.exe'
+      : 'oh-my-opencode-slim-companion';
   const bin = path.join(
     base,
     'opencode',
     'storage',
     'oh-my-opencode-slim',
     'bin',
-    'oh-my-opencode-slim-companion',
+    binaryName,
   );
   return existsSync(bin) ? bin : null;
 }