Skip to content
Closed
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-14
164 changes: 164 additions & 0 deletions openspec/changes/improved-opencode-process-management/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
## Context

### Current Implementation
The plugin uses a simple `opencodePath: string` setting that defaults to "opencode". The `ServerManager` spawns this directly with default arguments:
```typescript
this.process = this.processImpl.start(
this.settings.opencodePath,
["serve", "--port", this.settings.port.toString(), ...],
options
);
```

Platform-specific process implementations (`PosixProcess`, `WindowsProcess`) handle spawning and verification. The `verifyCommand` method checks if the executable exists.

### New Requirements
1. One-time autodetect for new users (empty path on first run)
2. Support custom shell commands with full user control
3. Maintain backward compatibility with existing path-based configuration

## Goals / Non-Goals

**Goals:**
- Provide zero-configuration setup for new users via autodetect
- Enable power users to use custom commands with full flexibility
- Maintain backward compatibility for existing users
- Clear UI distinction between path mode and custom command mode
- Platform-aware executable detection (PATH + common locations)

**Non-Goals:**
- Complex command builder UI (simple text input for custom commands)
- Automatic installation of opencode
- Validation of custom commands before execution

## Decisions

### 1. Settings Schema Extension
**Decision:** Extend existing settings rather than replace.

**Rationale:**
- Maintains backward compatibility - existing configs continue working
- Simple migration - just add new fields with sensible defaults
- Minimal code changes to existing path-based logic

**Alternatives considered:**
- Replace with structured command object - rejected due to breaking change
- Separate settings sections - rejected as overkill for this feature

**Implementation:**
```typescript
interface OpenCodeSettings {
// ... existing fields ...
opencodePath: string; // Still used as primary path
customCommand: string; // New: shell command
useCustomCommand: boolean; // New: toggle mode
}
```

### 2. Autodetect Trigger Strategy
**Decision:** Autodetect runs on every plugin startup when path is empty.

**Rationale:**
- Reminds user to configure or disable the plugin if opencode is missing
- Simpler implementation - no state tracking needed
- If user installs opencode later, it will be detected automatically

**Alternatives considered:**
- Run once and remember with flag - rejected as hides the problem
- Run only manually - rejected as adds friction for new users
- Explicit "first setup" wizard - rejected as overkill

**Implementation:**
```typescript
// In main.ts onload()
if (!this.settings.opencodePath && !this.settings.useCustomCommand) {
await this.attemptAutodetect();
}
```

### 3. Custom Command Spawning Strategy
**Decision:** Use `shell: true` for custom commands, user controls ALL arguments.

**Rationale:**
- Maximum flexibility - env vars, pipes, complex invocations all work
- Simple mental model - "what you type is what runs"
- No ambiguity about argument concatenation

**Alternatives considered:**
- Parse and split custom command - rejected as fragile
- Merge plugin args with custom args - rejected as confusing
- Separate args array - rejected as limiting

**Implementation:**
```typescript
// Path mode
this.process = spawn(opencodePath, ["serve", "--port", port, ...], options);

// Custom mode
this.process = spawn(customCommand, [], { ...options, shell: true });
```

### 4. Executable Detection Order
**Decision:** Check PATH first, then platform-specific common locations.

**Rationale:**
- Respects user's environment setup
- Common locations cover most package manager installs (homebrew, cargo, npm -g, etc.)

**Search algorithm:**
1. If configured path is absolute and exists, return it directly
2. Extract basename from configured path (e.g., "opencode" from "/path/to/opencode" or just "opencode")
3. Search platform-specific locations for that basename:
- **Linux:** `~/.local/bin/`, `~/.opencode/bin/`, `~/.bun/bin/`, `~/.npm-global/bin/`, `~/.nvm/versions/node/*/bin/`, `/usr/local/bin/`, `/usr/bin/`
- **macOS:** `~/.local/bin/`, `/opt/homebrew/bin/`, `/usr/local/bin/`
- **Windows:** `%LOCALAPPDATA%\opencode\bin\`, `%USERPROFILE%\.bun\bin\`, `%USERPROFILE%\.local\bin\`
4. If found, return full path; if not found, return configured path as fallback

**nvm wildcard handling:** For `~/.nvm/versions/node/*/bin/`, expand the wildcard to find actual Node version directories.

### 5. UI Layout
**Decision:** Toggle switch to select mode, conditional display of relevant input.

**Rationale:**
- Clear mental model - one or the other, not both
- Reduces visual clutter
- Toggle state directly maps to `useCustomCommand` boolean

**Layout:**
```
Command Mode:
[ Use custom command ●─────○ ]

[Path Mode - shown when toggle off]
OpenCode path: [____________] [Autodetect]

[Custom Mode - shown when toggle on]
Custom command:
[______________________________]
(Full shell command with all arguments)
```

## Risks / Trade-offs

**[Risk]** Custom command mode is powerful but dangerous - users can break their setup.
→ **Mitigation:** This is intentional flexibility. Users opting into "custom command" are advanced users. No validation performed - natural failure on spawn.

**[Risk]** Autodetect could find wrong executable (different binary with same name).
→ **Mitigation:** Low probability - "opencode" is unique. Could add version check in future if needed.

**[Risk]** Users may not understand difference between path and custom command modes.
→ **Mitigation:** Clear UI labels and descriptions. Path mode is default, custom mode opt-in.

**[Risk]** Toast notification on every startup might be annoying if user intentionally leaves path empty.
→ **Mitigation:** User can switch to custom command mode (even with empty command) to suppress autodetect, or configure a path.

## Migration Plan

1. **No breaking changes** - existing configs with `opencodePath` set continue working
2. **New fields** default to empty/false
3. **Autodetect** triggers on every startup when path is empty and custom command mode is disabled
4. **Settings UI** adapts to existing data - if `opencodePath` is set, starts in path mode

## Open Questions

None - design is complete based on user requirements.
64 changes: 64 additions & 0 deletions openspec/changes/improved-opencode-process-management/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
## Why

Currently, the plugin requires users to manually specify an OpenCode path in settings, defaulting to just "opencode" (expecting it in PATH). This creates friction for new users who haven't configured anything yet. We need:
1. **Autodetect on first run** - Seamless setup for new users
2. **Custom command support** - Power users need flexibility for custom flags, env vars, wrapper scripts
3. **Backward compatibility** - Existing `opencodePath` settings must continue working

## What Changes

### New Capabilities
- **Startup autodetect**: On every plugin startup with empty `opencodePath`, automatically search for opencode executable
- Check PATH first
- Check platform-specific common locations (homebrew, ~/.local/bin, etc.)
- Save found path to settings if successful
- Show toast notification if not found, prompting user to check Settings
- **Manual autodetect button**: "Autodetect" button in Settings to trigger search on demand
- **Custom command mode**: Toggle between "Path" and "Custom command"
- Path mode: Use `opencodePath` directly, append default args (`--serve --port X`)
- Custom mode: Full shell command, user controls all arguments
- Custom mode uses `shell: true` for maximum flexibility

### Settings Schema Changes
```typescript
interface OpenCodeSettings {
// ... existing fields ...
opencodePath: string; // Path to executable (or empty)
customCommand: string; // Full shell command
useCustomCommand: boolean; // Toggle: false=path, true=custom
}
```

### UI Changes
- Toggle: "Use custom command" (default: off)
- When off: Show path input + "Autodetect" button
- When on: Show custom command textarea

### Validation
- Path mode: Verify with `opencode --version` (existing behavior)
- Custom mode: Trust user, let it fail naturally

## Capabilities

### New Capabilities
- `executable-autodetect`: Cross-platform executable detection on startup when path is empty
- `custom-command-launch`: Shell-based command execution with full user control

### Modified Capabilities
- `process-launch`: Extended to support both direct path execution and shell-based custom commands

## Impact

- **Settings (`src/types.ts`)**: Add new fields to `OpenCodeSettings` interface
- **Settings UI (`src/settings/SettingsTab.ts`)**: Add toggle, autodetect button, conditional inputs
- **Process spawning (`src/server/ServerManager.ts`)**: Route to appropriate spawn method based on mode
- **New module**: `src/server/ExecutableResolver.ts` for cross-platform autodetect logic

## Success Criteria

- [ ] Plugin attempts autodetect on every startup when path is empty
- [ ] If autodetect fails, user sees clear toast notification with action to check Settings
- [ ] Existing `opencodePath` values continue working (backward compatibility)
- [ ] Users can switch to custom command mode for full control
- [ ] Settings UI clearly distinguishes path vs custom command modes
- [ ] Manual "Autodetect" button works in Settings
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
## ADDED Requirements

### Requirement: Custom shell command execution
The system SHALL support executing user-defined shell commands with full control over arguments and environment.

#### Scenario: Execute custom command with shell
- **GIVEN** useCustomCommand setting is true
- **AND** customCommand setting contains a shell command string
- **WHEN** the server is started
- **THEN** the system SHALL spawn the process with shell: true option
- **AND** execute the exact command string as provided by the user

#### Scenario: Custom command with environment variables
- **GIVEN** customCommand is "FOO=bar opencode serve --port 14096"
- **WHEN** the server is started
- **THEN** the system SHALL execute the command with shell: true
- **AND** the environment variable FOO SHALL be set to "bar"

#### Scenario: Custom command with custom arguments
- **GIVEN** customCommand is "opencode serve --port 9999 --verbose"
- **WHEN** the server is started
- **THEN** the system SHALL execute the command with shell: true
- **AND** pass exactly "--port 9999 --verbose" as arguments
- **AND** NOT append any default arguments (port, hostname, etc.)

#### Scenario: Custom command with wrapper script
- **GIVEN** customCommand is "/path/to/my-wrapper.sh"
- **WHEN** the server is started
- **THEN** the system SHALL execute the wrapper script via shell
- **AND** the wrapper script SHALL have full control of opencode invocation

### Requirement: No validation for custom commands
The system SHALL NOT validate custom commands before execution.

#### Scenario: Invalid custom command fails naturally
- **GIVEN** customCommand is "invalid-command-that-does-not-exist"
- **WHEN** the server is started
- **THEN** the system SHALL attempt to execute it
- **AND** let the spawn fail naturally with ENOENT error
- **AND** NOT perform pre-flight validation

### Requirement: User controls all arguments in custom mode
The system SHALL NOT append any default arguments when using custom command mode.

#### Scenario: User provides complete command
- **GIVEN** useCustomCommand is true
- **AND** customCommand is "opencode serve"
- **WHEN** the server is started
- **THEN** the system SHALL execute exactly "opencode serve"
- **AND** NOT append --port, --hostname, or --cors arguments
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
## ADDED Requirements

### Requirement: Cross-platform executable autodetection
The system SHALL provide a mechanism to automatically detect the opencode executable location across macOS, Linux, and Windows platforms.

#### Scenario: Autodetect finds executable in PATH
- **GIVEN** the platform PATH contains an executable named "opencode"
- **WHEN** autodetection is triggered
- **THEN** the system SHALL return the full path to the executable

#### Scenario: Configured absolute path takes precedence
- **GIVEN** opencodePath is set to an absolute path like "/opt/opencode/bin/opencode"
- **AND** the file exists at that location
- **WHEN** autodetection is triggered
- **THEN** the system SHALL return the configured absolute path directly
- **AND** skip searching platform-specific locations

#### Scenario: Autodetect finds executable in platform-specific location
- **GIVEN** opencodePath is set to "opencode" (not an absolute path)
- **OR** the configured absolute path does not exist
- **AND** opencode is installed at a platform-specific common location
- Linux: ~/.local/bin/opencode, ~/.opencode/bin/opencode, ~/.bun/bin/opencode, ~/.npm-global/bin/opencode, ~/.nvm/versions/node/*/bin/opencode, /usr/local/bin/opencode, /usr/bin/opencode
- macOS: ~/.local/bin/opencode, /opt/homebrew/bin/opencode, /usr/local/bin/opencode
- Windows: %LOCALAPPDATA%\opencode\bin\opencode.exe, %USERPROFILE%\.bun\bin\opencode.exe
- **WHEN** autodetection is triggered
- **THEN** the system SHALL extract the basename from configured path (e.g., "opencode")
- **AND** search platform-specific locations for that basename
- **AND** return the full path if found

#### Scenario: Basename extraction for custom executable names
- **GIVEN** opencodePath is set to "my-custom-opencode"
- **WHEN** autodetection is triggered
- **THEN** the system SHALL search for "my-custom-opencode" in platform-specific locations
- **AND** return the full path if found

#### Scenario: Autodetect fails to find executable - fallback to configured path
- **GIVEN** opencode is not in PATH or any platform-specific location
- **AND** opencodePath is set to "opencode"
- **WHEN** autodetection is triggered
- **THEN** the system SHALL return the configured path "opencode" as fallback
- **AND** display a toast notification to the user: "Could not find opencode. Please check Settings"

### Requirement: Startup autodetection
The system SHALL attempt to autodetect the opencode executable on every plugin startup when the path is not configured.

#### Scenario: Startup autodetect succeeds
- **GIVEN** the plugin loads
- **AND** opencodePath setting is empty
- **WHEN** the plugin initializes
- **THEN** the system SHALL trigger autodetection
- **AND** if found, save the path to opencodePath setting
- **AND** display a success notification: "OpenCode executable found at <path>"

#### Scenario: Startup autodetect fails
- **GIVEN** the plugin loads
- **AND** opencodePath setting is empty
- **WHEN** the plugin initializes
- **THEN** the system SHALL trigger autodetection
- **AND** if not found, display a toast notification: "Could not find opencode. Please check Settings"

### Requirement: Manual autodetect trigger
The system SHALL provide a UI control to manually trigger autodetection at any time.

#### Scenario: User clicks Autodetect button
- **GIVEN** the user is on the Settings page
- **AND** Path mode is selected
- **WHEN** the user clicks the "Autodetect" button
- **THEN** the system SHALL trigger autodetection
- **AND** if found, update the opencodePath field with the detected path
- **AND** display a success notification

#### Scenario: Manual autodetect fails
- **GIVEN** the user clicks the "Autodetect" button
- **WHEN** autodetection fails to find the executable
- **THEN** the system SHALL display a toast notification: "Could not find opencode. Please check your installation."
Loading
Loading