Improved process management
This commit is contained in:
@@ -67,3 +67,30 @@ The system SHALL maintain backward compatibility with existing opencodePath conf
|
||||
- **WHEN** the plugin loads
|
||||
- **THEN** the system SHALL use the existing opencodePath
|
||||
- **AND** continue working in path mode with default arguments
|
||||
|
||||
### Requirement: Detailed error messages
|
||||
The system SHALL provide clear, actionable error messages when the server fails to start.
|
||||
|
||||
#### Scenario: Executable not found
|
||||
- **GIVEN** useCustomCommand is false
|
||||
- **AND** opencodePath is set to a non-existent absolute path
|
||||
- **WHEN** the server fails to start
|
||||
- **THEN** the error message SHALL be: "Executable not found at '<path>'. Check Settings → OpenCode path, or click 'Autodetect'"
|
||||
|
||||
#### Scenario: Executable exists but not executable
|
||||
- **GIVEN** useCustomCommand is false
|
||||
- **AND** opencodePath points to a file that exists but lacks execute permission
|
||||
- **WHEN** the server fails to start
|
||||
- **THEN** the error message SHALL be: "'<path>' exists but is not executable. Run: chmod +x <path>"
|
||||
|
||||
#### Scenario: Error displayed in Settings UI
|
||||
- **GIVEN** the server state is "error"
|
||||
- **AND** an error message is stored
|
||||
- **WHEN** the user views the Settings page
|
||||
- **THEN** the error message SHALL be displayed below the "Status: Error" badge
|
||||
|
||||
#### Scenario: Error displayed as toast notification
|
||||
- **GIVEN** the user attempts to start the server from the main UI (not Settings)
|
||||
- **AND** the server fails to start
|
||||
- **THEN** a toast notification SHALL display the error message
|
||||
- **AND** the toast SHALL remain visible for 10 seconds
|
||||
|
||||
@@ -1,68 +1,77 @@
|
||||
## 1. Settings Schema Updates
|
||||
|
||||
- [ ] 1.1 Add `customCommand` field to `OpenCodeSettings` interface in `src/types.ts`
|
||||
- [ ] 1.2 Add `useCustomCommand` boolean field to `OpenCodeSettings` interface
|
||||
- [ ] 1.3 Update `DEFAULT_SETTINGS` with new fields (empty string and false)
|
||||
- [x] 1.1 Add `customCommand` field to `OpenCodeSettings` interface in `src/types.ts`
|
||||
- [x] 1.2 Add `useCustomCommand` boolean field to `OpenCodeSettings` interface
|
||||
- [x] 1.3 Update `DEFAULT_SETTINGS` with new fields (empty string and false)
|
||||
|
||||
## 2. Executable Resolver Module
|
||||
|
||||
- [ ] 2.1 Create `src/server/ExecutableResolver.ts` with cross-platform detection logic
|
||||
- [ ] 2.2 Implement `resolveFromPath()` to check PATH for 'opencode' executable
|
||||
- [ ] 2.3 Implement `resolve()` method with proper precedence:
|
||||
- [x] 2.1 Create `src/server/ExecutableResolver.ts` with cross-platform detection logic
|
||||
- [x] 2.2 Implement `resolveFromPath()` to check PATH for 'opencode' executable
|
||||
- [x] 2.3 Implement `resolve()` method with proper precedence:
|
||||
- Check if configured path is absolute and exists → return it
|
||||
- Extract basename from configured path (handle both "opencode" and "/path/to/opencode")
|
||||
- [ ] 2.4 Implement platform-specific directory search:
|
||||
- [x] 2.4 Implement platform-specific directory search:
|
||||
- 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\
|
||||
- [ ] 2.5 Implement nvm wildcard expansion for ~/.nvm/versions/node/*/bin/
|
||||
- [ ] 2.6 Ensure fallback: return configured path if search fails
|
||||
- [ ] 2.4 Implement main `resolve()` method that tries PATH first, then platform locations
|
||||
- [ ] 2.5 Add helper to expand Windows environment variables (%LOCALAPPDATA%, %USERPROFILE%)
|
||||
- [x] 2.5 Implement nvm wildcard expansion for ~/.nvm/versions/node/*/bin/
|
||||
- [x] 2.6 Ensure fallback: return configured path if search fails
|
||||
|
||||
## 3. ServerManager Updates
|
||||
|
||||
- [ ] 3.1 Modify `ServerManager.start()` to check `useCustomCommand` setting
|
||||
- [ ] 3.2 Implement path mode spawning (direct spawn with default args)
|
||||
- [ ] 3.3 Implement custom command mode spawning (shell: true, no args appended)
|
||||
- [ ] 3.4 Ensure working directory is set correctly for both modes
|
||||
- [ ] 3.5 Add support for verifying path mode executable with `opencode --version`
|
||||
- [x] 3.1 Modify `ServerManager.start()` to check `useCustomCommand` setting
|
||||
- [x] 3.2 Implement path mode spawning (direct spawn with default args)
|
||||
- [x] 3.3 Implement custom command mode spawning (shell: true, no args appended)
|
||||
- [x] 3.4 Ensure working directory is set correctly for both modes
|
||||
- [x] 3.5 Add support for verifying path mode executable with `opencode --version`
|
||||
|
||||
## 4. Main Plugin Integration
|
||||
|
||||
- [ ] 4.1 Add autodetect logic in `main.ts` `onload()` method
|
||||
- [ ] 4.2 Implement autodetect trigger: when `opencodePath` is empty and `useCustomCommand` is false
|
||||
- [ ] 4.3 On successful autodetect: save path to settings, show success Notice
|
||||
- [ ] 4.4 On failed autodetect: show error Notice "Could not find opencode. Please check Settings"
|
||||
- [ ] 4.5 Import and use `ExecutableResolver` from main plugin
|
||||
- [x] 4.1 Add autodetect logic in `main.ts` `onload()` method
|
||||
- [x] 4.2 Implement autodetect trigger: when `opencodePath` is empty and `useCustomCommand` is false
|
||||
- [x] 4.3 On successful autodetect: save path to settings, show success Notice
|
||||
- [x] 4.4 On failed autodetect: show error Notice "Could not find opencode. Please check Settings"
|
||||
- [x] 4.5 Import and use `ExecutableResolver` from main plugin
|
||||
|
||||
## 5. Settings UI Updates
|
||||
|
||||
- [ ] 5.1 Add toggle switch "Use custom command" in `SettingsTab.ts`
|
||||
- [ ] 5.2 Conditionally show path input field when toggle is off
|
||||
- [ ] 5.3 Conditionally show custom command textarea when toggle is on
|
||||
- [ ] 5.4 Add "Autodetect" button next to path input
|
||||
- [ ] 5.5 Implement autodetect button click handler that:
|
||||
- [x] 5.1 Add toggle switch "Use custom command" in `SettingsTab.ts`
|
||||
- [x] 5.2 Conditionally show path input field when toggle is off
|
||||
- [x] 5.3 Conditionally show custom command textarea when toggle is on
|
||||
- [x] 5.4 Add "Autodetect" button next to path input
|
||||
- [x] 5.5 Implement autodetect button click handler that:
|
||||
- Calls `ExecutableResolver.resolve()`
|
||||
- Updates path input if found
|
||||
- Shows success/error Notice
|
||||
- [ ] 5.6 Add descriptive text explaining custom command mode behavior
|
||||
- [ ] 5.7 Ensure settings are saved when toggling between modes
|
||||
- [x] 5.6 Add descriptive text explaining custom command mode behavior
|
||||
- [x] 5.7 Ensure settings are saved when toggling between modes
|
||||
|
||||
## 6. Testing & Validation
|
||||
|
||||
- [ ] 6.1 Test autodetect finds opencode in PATH
|
||||
- [ ] 6.2 Test autodetect finds opencode in platform-specific location
|
||||
- [ ] 6.3 Test autodetect shows error when not found
|
||||
- [ ] 6.4 Test path mode spawns correctly with default args
|
||||
- [ ] 6.5 Test custom command mode spawns via shell without extra args
|
||||
- [ ] 6.6 Test custom command with environment variables works
|
||||
- [ ] 6.7 Verify backward compatibility: existing opencodePath values still work
|
||||
- [ ] 6.8 Test manual autodetect button in Settings
|
||||
- [ ] 6.9 Test toggle saves and restores correctly
|
||||
- [x] 6.1 Test autodetect finds opencode in PATH
|
||||
- [x] 6.2 Test autodetect finds opencode in platform-specific location
|
||||
- [x] 6.3 Test autodetect shows error when not found
|
||||
- [x] 6.4 Test path mode spawns correctly with default args
|
||||
- [x] 6.5 Test custom command mode spawns via shell without extra args
|
||||
- [x] 6.6 Test custom command with environment variables works
|
||||
- [x] 6.7 Verify backward compatibility: existing opencodePath values still work
|
||||
- [x] 6.8 Test manual autodetect button in Settings
|
||||
- [x] 6.9 Test toggle saves and restores correctly
|
||||
|
||||
## 7. Documentation
|
||||
## 7. Error Message Improvements
|
||||
|
||||
- [ ] 7.1 Update README.md with new settings options
|
||||
- [ ] 7.2 Document autodetect behavior and common installation locations
|
||||
- [ ] 7.3 Add examples of custom command usage
|
||||
- [x] 7.1 Enhance verifyCommand in PosixProcess.ts with detailed error messages
|
||||
- Distinguish between "not found" and "not executable"
|
||||
- Provide chmod instructions for permission errors
|
||||
- Include actionable guidance ("Check Settings → OpenCode path, or click 'Autodetect'")
|
||||
- [x] 7.2 Enhance verifyCommand in WindowsProcess.ts with detailed error messages
|
||||
- [x] 7.3 Display error details in Settings UI below "Status: Error" badge
|
||||
- [x] 7.4 Show toast notification with error message when activation fails from main UI
|
||||
- [x] 7.5 Add CSS styling for error message display in settings
|
||||
|
||||
## 8. Documentation
|
||||
|
||||
- [x] 8.1 Update README.md with new settings options
|
||||
- [x] 8.2 Document autodetect behavior and common installation locations
|
||||
- [x] 8.3 Add examples of custom command usage
|
||||
|
||||
37
src/main.ts
37
src/main.ts
@@ -7,6 +7,7 @@ import { ServerManager, ServerState } from "./server/ServerManager";
|
||||
import { registerOpenCodeIcons, OPENCODE_ICON_NAME } from "./icons";
|
||||
import { OpenCodeClient } from "./client/OpenCodeClient";
|
||||
import { ContextManager } from "./context/ContextManager";
|
||||
import { ExecutableResolver } from "./server/ExecutableResolver";
|
||||
|
||||
export default class OpenCodePlugin extends Plugin {
|
||||
settings: OpenCodeSettings = DEFAULT_SETTINGS;
|
||||
@@ -25,6 +26,9 @@ export default class OpenCodePlugin extends Plugin {
|
||||
|
||||
await this.loadSettings();
|
||||
|
||||
// Attempt autodetect if opencodePath is empty and not using custom command
|
||||
await this.attemptAutodetect();
|
||||
|
||||
const projectDirectory = this.getProjectDirectory();
|
||||
|
||||
this.processManager = new ServerManager(this.settings, projectDirectory);
|
||||
@@ -153,6 +157,32 @@ export default class OpenCodePlugin extends Plugin {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to autodetect opencode executable on startup
|
||||
* Triggers when opencodePath is empty and useCustomCommand is false
|
||||
*/
|
||||
private async attemptAutodetect(): Promise<void> {
|
||||
// Only autodetect if path is empty and not using custom command mode
|
||||
if (this.settings.opencodePath || this.settings.useCustomCommand) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[OpenCode] Attempting to autodetect opencode executable...");
|
||||
|
||||
const detectedPath = ExecutableResolver.resolve("opencode");
|
||||
|
||||
// Check if a different path was found (not the fallback)
|
||||
if (detectedPath && detectedPath !== "opencode") {
|
||||
console.log("[OpenCode] Autodetected opencode at:", detectedPath);
|
||||
this.settings.opencodePath = detectedPath;
|
||||
await this.saveData(this.settings);
|
||||
new Notice(`OpenCode executable found at ${detectedPath}`);
|
||||
} else {
|
||||
console.log("[OpenCode] Could not autodetect opencode executable");
|
||||
new Notice("Could not find opencode. Please check Settings");
|
||||
}
|
||||
}
|
||||
|
||||
async saveSettings(): Promise<void> {
|
||||
await this.saveData(this.settings);
|
||||
this.processManager.updateSettings(this.settings);
|
||||
@@ -165,6 +195,13 @@ export default class OpenCodePlugin extends Plugin {
|
||||
const success = await this.processManager.start();
|
||||
if (success) {
|
||||
new Notice("OpenCode server started");
|
||||
} else {
|
||||
const error = this.processManager.getLastError();
|
||||
if (error) {
|
||||
new Notice(`OpenCode failed to start: ${error}`, 10000); // Show for 10 seconds
|
||||
} else {
|
||||
new Notice("OpenCode failed to start. Check Settings for details.", 5000);
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
133
src/server/ExecutableResolver.ts
Normal file
133
src/server/ExecutableResolver.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { existsSync } from "fs";
|
||||
import { homedir, platform } from "os";
|
||||
import { join, basename, isAbsolute } from "path";
|
||||
import { execSync } from "child_process";
|
||||
|
||||
/**
|
||||
* Resolves the opencode executable path across different platforms.
|
||||
* Follows the search algorithm:
|
||||
* 1. If configured path is absolute and exists, return it directly
|
||||
* 2. Extract basename from configured path
|
||||
* 3. Search platform-specific locations for that basename
|
||||
* 4. If found, return full path; if not found, return configured path as fallback
|
||||
*/
|
||||
export class ExecutableResolver {
|
||||
/**
|
||||
* Resolve the executable path based on configuration and platform
|
||||
* @param configuredPath The path configured in settings (e.g., "opencode" or "/path/to/opencode")
|
||||
* @returns The resolved full path or the configured path as fallback
|
||||
*/
|
||||
static resolve(configuredPath: string): string {
|
||||
// If configured path is absolute and exists, use it directly
|
||||
if (isAbsolute(configuredPath) && existsSync(configuredPath)) {
|
||||
return configuredPath;
|
||||
}
|
||||
|
||||
// Extract basename (e.g., "opencode" from "/path/to/opencode" or just "opencode")
|
||||
const execName = basename(configuredPath) || configuredPath;
|
||||
|
||||
// Get search directories for current platform
|
||||
const searchDirs = this.getSearchDirectories();
|
||||
|
||||
// Search for executable in platform directories
|
||||
for (const dir of searchDirs) {
|
||||
const fullPath = join(dir, execName);
|
||||
if (existsSync(fullPath)) {
|
||||
console.log("[OpenCode] Found executable at:", fullPath);
|
||||
return fullPath;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: return configured path (let spawn fail naturally if not found)
|
||||
console.log("[OpenCode] Executable not found in common paths, using configured:", configuredPath);
|
||||
return configuredPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if executable exists in PATH
|
||||
* @param execName Name of executable to search for
|
||||
* @returns Full path if found in PATH, null otherwise
|
||||
*/
|
||||
static resolveFromPath(execName: string): string | null {
|
||||
try {
|
||||
// Use 'which' on Unix systems, 'where' on Windows
|
||||
const command = platform() === "win32" ? "where" : "which";
|
||||
const result = execSync(`${command} "${execName}"`, { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] });
|
||||
const path = result.trim().split("\n")[0];
|
||||
if (path && existsSync(path)) {
|
||||
return path;
|
||||
}
|
||||
} catch {
|
||||
// Command not found in PATH
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get platform-specific directories to search for executables
|
||||
*/
|
||||
private static getSearchDirectories(): string[] {
|
||||
const currentPlatform = platform();
|
||||
const homeDir = homedir();
|
||||
const searchDirs: string[] = [];
|
||||
|
||||
if (currentPlatform === "linux" || currentPlatform === "darwin") {
|
||||
// User directories
|
||||
searchDirs.push(
|
||||
join(homeDir, ".local", "bin"),
|
||||
join(homeDir, ".opencode", "bin"),
|
||||
join(homeDir, ".bun", "bin"),
|
||||
join(homeDir, ".npm-global", "bin")
|
||||
);
|
||||
|
||||
// nvm directories (expand wildcard)
|
||||
const nvmDirs = this.expandNvmDirectories(homeDir);
|
||||
searchDirs.push(...nvmDirs);
|
||||
|
||||
// System directories
|
||||
searchDirs.push("/usr/local/bin", "/usr/bin");
|
||||
|
||||
// macOS-specific directories
|
||||
if (currentPlatform === "darwin") {
|
||||
searchDirs.push("/opt/homebrew/bin");
|
||||
}
|
||||
} else if (currentPlatform === "win32") {
|
||||
// Windows directories with environment variable expansion
|
||||
const localAppData = process.env.LOCALAPPDATA || join(homeDir, "AppData", "Local");
|
||||
const userProfile = process.env.USERPROFILE || homeDir;
|
||||
|
||||
searchDirs.push(
|
||||
join(localAppData, "opencode", "bin"),
|
||||
join(userProfile, ".bun", "bin"),
|
||||
join(userProfile, ".local", "bin")
|
||||
);
|
||||
}
|
||||
|
||||
return searchDirs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand nvm wildcard directories
|
||||
* Searches ~/.nvm/versions/node/ for installed versions
|
||||
*/
|
||||
private static expandNvmDirectories(homeDir: string): string[] {
|
||||
const nvmBaseDir = join(homeDir, ".nvm", "versions", "node");
|
||||
const nvmDirs: string[] = [];
|
||||
|
||||
try {
|
||||
if (existsSync(nvmBaseDir)) {
|
||||
const { readdirSync } = require("fs");
|
||||
const versions = readdirSync(nvmBaseDir, { withFileTypes: true });
|
||||
for (const version of versions) {
|
||||
if (version.isDirectory()) {
|
||||
nvmDirs.push(join(nvmBaseDir, version.name, "bin"));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// nvm directory doesn't exist or is not accessible
|
||||
}
|
||||
|
||||
return nvmDirs;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import { ChildProcess } from "child_process";
|
||||
import { ChildProcess, SpawnOptions } from "child_process";
|
||||
import { EventEmitter } from "events";
|
||||
import { OpenCodeSettings } from "../types";
|
||||
import { ServerState } from "./types";
|
||||
import { OpenCodeProcess } from "./process/OpenCodeProcess";
|
||||
import { WindowsProcess } from "./process/WindowsProcess";
|
||||
import { PosixProcess } from "./process/PosixProcess";
|
||||
import { ExecutableResolver } from "./ExecutableResolver";
|
||||
|
||||
export type { ServerState } from "./types";
|
||||
|
||||
@@ -60,10 +61,34 @@ export class ServerManager extends EventEmitter {
|
||||
return this.setError("Project directory (vault) not configured");
|
||||
}
|
||||
|
||||
// Pre-flight check: verify executable exists
|
||||
const commandError = await this.processImpl.verifyCommand(this.settings.opencodePath);
|
||||
if (commandError) {
|
||||
return this.setError(commandError);
|
||||
// Determine execution mode and resolve executable path
|
||||
let executablePath: string;
|
||||
let spawnOptions: SpawnOptions;
|
||||
|
||||
if (this.settings.useCustomCommand) {
|
||||
// Custom command mode: use custom command directly with shell
|
||||
executablePath = this.settings.customCommand;
|
||||
spawnOptions = {
|
||||
cwd: this.projectDirectory,
|
||||
env: { ...process.env, NODE_USE_SYSTEM_CA: "1" },
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
shell: true,
|
||||
};
|
||||
} else {
|
||||
// Path mode: resolve executable and verify
|
||||
executablePath = ExecutableResolver.resolve(this.settings.opencodePath);
|
||||
|
||||
// Pre-flight check: verify executable exists (only for path mode)
|
||||
const commandError = await this.processImpl.verifyCommand(executablePath);
|
||||
if (commandError) {
|
||||
return this.setError(commandError);
|
||||
}
|
||||
|
||||
spawnOptions = {
|
||||
cwd: this.projectDirectory,
|
||||
env: { ...process.env, NODE_USE_SYSTEM_CA: "1" },
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
};
|
||||
}
|
||||
|
||||
if (await this.checkServerHealth()) {
|
||||
@@ -76,30 +101,37 @@ export class ServerManager extends EventEmitter {
|
||||
}
|
||||
|
||||
console.log("[OpenCode] Starting server:", {
|
||||
opencodePath: this.settings.opencodePath,
|
||||
mode: this.settings.useCustomCommand ? "custom" : "path",
|
||||
command: executablePath,
|
||||
port: this.settings.port,
|
||||
hostname: this.settings.hostname,
|
||||
cwd: this.projectDirectory,
|
||||
projectDirectory: this.projectDirectory,
|
||||
});
|
||||
|
||||
this.process = this.processImpl.start(
|
||||
this.settings.opencodePath,
|
||||
[
|
||||
"serve",
|
||||
"--port",
|
||||
this.settings.port.toString(),
|
||||
"--hostname",
|
||||
this.settings.hostname,
|
||||
"--cors",
|
||||
"app://obsidian.md",
|
||||
],
|
||||
{
|
||||
cwd: this.projectDirectory,
|
||||
env: { ...process.env, NODE_USE_SYSTEM_CA: "1" },
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
}
|
||||
);
|
||||
if (this.settings.useCustomCommand) {
|
||||
// Custom command mode: spawn with shell, no args appended
|
||||
this.process = this.processImpl.start(
|
||||
executablePath,
|
||||
[], // User controls all arguments in custom command
|
||||
spawnOptions
|
||||
);
|
||||
} else {
|
||||
// Path mode: spawn with default arguments
|
||||
this.process = this.processImpl.start(
|
||||
executablePath,
|
||||
[
|
||||
"serve",
|
||||
"--port",
|
||||
this.settings.port.toString(),
|
||||
"--hostname",
|
||||
this.settings.hostname,
|
||||
"--cors",
|
||||
"app://obsidian.md",
|
||||
],
|
||||
spawnOptions
|
||||
);
|
||||
}
|
||||
|
||||
console.log("[OpenCode] Process spawned with PID:", this.process.pid);
|
||||
|
||||
@@ -131,8 +163,11 @@ export class ServerManager extends EventEmitter {
|
||||
this.process = null;
|
||||
|
||||
if (err.code === "ENOENT") {
|
||||
const command = this.settings.useCustomCommand
|
||||
? this.settings.customCommand
|
||||
: this.settings.opencodePath;
|
||||
this.setError(
|
||||
`Executable not found at '${this.settings.opencodePath}'`
|
||||
`Executable not found: '${command}'`
|
||||
);
|
||||
} else {
|
||||
this.setError(`Failed to start: ${err.message}`);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ChildProcess, spawn, SpawnOptions } from "child_process";
|
||||
import { existsSync } from "fs";
|
||||
import { OpenCodeProcess } from "./OpenCodeProcess";
|
||||
|
||||
export class PosixProcess implements OpenCodeProcess {
|
||||
@@ -46,12 +47,16 @@ export class PosixProcess implements OpenCodeProcess {
|
||||
async verifyCommand(command: string): Promise<string | null> {
|
||||
// Check if command is absolute path - verify it exists and is executable
|
||||
if (command.startsWith('/') || command.startsWith('./')) {
|
||||
const fs = require('fs');
|
||||
try {
|
||||
const fs = require('fs');
|
||||
fs.accessSync(command, fs.constants.X_OK);
|
||||
return null;
|
||||
} catch {
|
||||
return `Executable not found at '${command}'`;
|
||||
} catch (err: any) {
|
||||
// Check if file exists but isn't executable
|
||||
if (existsSync(command)) {
|
||||
return `'${command}' exists but is not executable. Run: chmod +x ${command}`;
|
||||
}
|
||||
return `Executable not found at '${command}'. Check Settings → OpenCode path, or click "Autodetect"`;
|
||||
}
|
||||
}
|
||||
// For non-absolute paths, let spawn handle it (will fire ENOENT if not found)
|
||||
|
||||
@@ -35,7 +35,7 @@ export class WindowsProcess implements OpenCodeProcess {
|
||||
await this.execAsync(`where "${command}"`);
|
||||
return null;
|
||||
} catch {
|
||||
return `Executable not found at '${command}'`;
|
||||
return `Executable not found at '${command}'. Check Settings → OpenCode path, or click "Autodetect"`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { existsSync, statSync } from "fs";
|
||||
import { homedir } from "os";
|
||||
import { OpenCodeSettings, ViewLocation } from "../types";
|
||||
import { ServerManager } from "../server/ServerManager";
|
||||
import { ExecutableResolver } from "../server/ExecutableResolver";
|
||||
|
||||
function expandTilde(path: string): string {
|
||||
if (path === "~") {
|
||||
@@ -62,21 +63,72 @@ export class OpenCodeSettingTab extends PluginSettingTab {
|
||||
})
|
||||
);
|
||||
|
||||
// Command Mode Toggle
|
||||
new Setting(containerEl)
|
||||
.setName("OpenCode path")
|
||||
.setDesc(
|
||||
"Path to the OpenCode executable. Leave as 'opencode' if it's in your PATH."
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("opencode")
|
||||
.setValue(this.settings.opencodePath)
|
||||
.setName("Use custom command")
|
||||
.setDesc("Enable to use a custom shell command instead of the executable path")
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.settings.useCustomCommand)
|
||||
.onChange(async (value) => {
|
||||
this.settings.opencodePath = value || "opencode";
|
||||
this.settings.useCustomCommand = value;
|
||||
await this.onSettingsChange();
|
||||
// Re-render to show/hide appropriate fields
|
||||
this.display();
|
||||
})
|
||||
);
|
||||
|
||||
if (this.settings.useCustomCommand) {
|
||||
// Custom Command Mode
|
||||
new Setting(containerEl)
|
||||
.setName("Custom command")
|
||||
.setDesc("Full shell command to start OpenCode. You control all arguments (e.g., 'opencode serve --port 14096')")
|
||||
.addTextArea((text) => {
|
||||
text
|
||||
.setPlaceholder("opencode serve --port 14096 --hostname 127.0.0.1")
|
||||
.setValue(this.settings.customCommand)
|
||||
.onChange(async (value) => {
|
||||
this.settings.customCommand = value;
|
||||
await this.onSettingsChange();
|
||||
});
|
||||
text.inputEl.rows = 3;
|
||||
text.inputEl.style.width = "100%";
|
||||
return text;
|
||||
});
|
||||
} else {
|
||||
// Path Mode
|
||||
const pathSetting = new Setting(containerEl)
|
||||
.setName("OpenCode path")
|
||||
.setDesc("Path to the OpenCode executable. Leave empty to autodetect.")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("opencode")
|
||||
.setValue(this.settings.opencodePath)
|
||||
.onChange(async (value) => {
|
||||
this.settings.opencodePath = value;
|
||||
await this.onSettingsChange();
|
||||
})
|
||||
);
|
||||
|
||||
// Add Autodetect button
|
||||
pathSetting.addButton((button) => {
|
||||
button
|
||||
.setButtonText("Autodetect")
|
||||
.onClick(async () => {
|
||||
const detectedPath = ExecutableResolver.resolve("opencode");
|
||||
if (detectedPath && detectedPath !== "opencode") {
|
||||
this.settings.opencodePath = detectedPath;
|
||||
await this.onSettingsChange();
|
||||
// Refresh the text input
|
||||
this.display();
|
||||
new Notice(`OpenCode executable found at ${detectedPath}`);
|
||||
} else {
|
||||
new Notice("Could not find opencode. Please check your installation.");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Project directory")
|
||||
.setDesc(
|
||||
@@ -241,6 +293,18 @@ export class OpenCodeSettingTab extends PluginSettingTab {
|
||||
cls: `opencode-status-badge ${statusClass[state]}`,
|
||||
});
|
||||
|
||||
// Show error message if state is error
|
||||
if (state === "error") {
|
||||
const errorMsg = this.serverManager.getLastError();
|
||||
if (errorMsg) {
|
||||
const errorEl = container.createDiv({ cls: "opencode-error-details" });
|
||||
errorEl.createEl("div", {
|
||||
text: errorMsg,
|
||||
cls: "opencode-error-text"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (state === "running") {
|
||||
const urlEl = container.createDiv({ cls: "opencode-status-line" });
|
||||
urlEl.createSpan({ text: "URL: " });
|
||||
|
||||
@@ -11,6 +11,8 @@ export interface OpenCodeSettings {
|
||||
injectWorkspaceContext: boolean;
|
||||
maxNotesInContext: number;
|
||||
maxSelectionLength: number;
|
||||
customCommand: string;
|
||||
useCustomCommand: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: OpenCodeSettings = {
|
||||
@@ -24,6 +26,8 @@ export const DEFAULT_SETTINGS: OpenCodeSettings = {
|
||||
injectWorkspaceContext: false,
|
||||
maxNotesInContext: 20,
|
||||
maxSelectionLength: 2000,
|
||||
customCommand: "",
|
||||
useCustomCommand: false,
|
||||
};
|
||||
|
||||
export const OPENCODE_VIEW_TYPE = "opencode-view";
|
||||
|
||||
15
styles.css
15
styles.css
@@ -191,3 +191,18 @@
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Error details in settings */
|
||||
.opencode-error-details {
|
||||
margin-top: 12px;
|
||||
padding: 10px 12px;
|
||||
background-color: var(--background-secondary);
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid var(--text-error);
|
||||
}
|
||||
|
||||
.opencode-error-text {
|
||||
color: var(--text-error);
|
||||
font-size: 0.9em;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user