diff --git a/main.js b/main.js index b5f917b..aa925aa 100644 --- a/main.js +++ b/main.js @@ -34,7 +34,8 @@ var DEFAULT_SETTINGS = { port: 14096, hostname: "127.0.0.1", autoStart: false, - opencodePath: "opencode" + opencodePath: "opencode", + projectDirectory: "" }; var OPENCODE_VIEW_TYPE = "opencode-view"; @@ -210,9 +211,21 @@ var OpenCodeView = class extends import_obsidian.ItemView { // src/SettingsTab.ts var import_obsidian2 = require("obsidian"); +var import_fs = require("fs"); +var import_os = require("os"); +function expandTilde(path) { + if (path === "~") { + return (0, import_os.homedir)(); + } + if (path.startsWith("~/")) { + return path.replace("~", (0, import_os.homedir)()); + } + return path; +} var OpenCodeSettingTab = class extends import_obsidian2.PluginSettingTab { constructor(app, plugin) { super(app, plugin); + this.validateTimeout = null; this.plugin = plugin; } display() { @@ -243,6 +256,18 @@ var OpenCodeSettingTab = class extends import_obsidian2.PluginSettingTab { await this.plugin.saveSettings(); }) ); + new import_obsidian2.Setting(containerEl).setName("Project directory").setDesc( + "Override the starting directory for OpenCode. Leave empty to use the vault root. Supports ~ for home directory." + ).addText( + (text) => text.setPlaceholder("/path/to/project or ~/project").setValue(this.plugin.settings.projectDirectory).onChange((value) => { + if (this.validateTimeout) { + clearTimeout(this.validateTimeout); + } + this.validateTimeout = setTimeout(async () => { + await this.validateAndSetProjectDirectory(value); + }, 500); + }) + ); containerEl.createEl("h3", { text: "Behavior" }); new import_obsidian2.Setting(containerEl).setName("Auto-start server").setDesc( "Automatically start the OpenCode server when Obsidian opens (not recommended for faster startup)" @@ -256,6 +281,33 @@ var OpenCodeSettingTab = class extends import_obsidian2.PluginSettingTab { const statusContainer = containerEl.createDiv({ cls: "opencode-settings-status" }); this.renderServerStatus(statusContainer); } + async validateAndSetProjectDirectory(value) { + const trimmed = value.trim(); + if (!trimmed) { + await this.plugin.updateProjectDirectory(""); + return; + } + if (!trimmed.startsWith("/") && !trimmed.startsWith("~") && !trimmed.match(/^[A-Za-z]:\\/)) { + new import_obsidian2.Notice("Project directory must be an absolute path (or start with ~)"); + return; + } + const expanded = expandTilde(trimmed); + try { + if (!(0, import_fs.existsSync)(expanded)) { + new import_obsidian2.Notice("Project directory does not exist"); + return; + } + const stat = (0, import_fs.statSync)(expanded); + if (!stat.isDirectory()) { + new import_obsidian2.Notice("Project directory path is not a directory"); + return; + } + } catch (error) { + new import_obsidian2.Notice(`Failed to validate path: ${error.message}`); + return; + } + await this.plugin.updateProjectDirectory(expanded); + } renderServerStatus(container) { container.empty(); const state = this.plugin.getProcessState(); @@ -343,6 +395,9 @@ var ProcessManager = class { updateSettings(settings) { this.settings = settings; } + updateProjectDirectory(directory) { + this.projectDirectory = directory; + } getState() { return this.state; } @@ -500,13 +555,14 @@ var OpenCodePlugin = class extends import_obsidian4.Plugin { console.log("Loading OpenCode plugin"); await this.loadSettings(); const vaultPath = this.getVaultPath(); + const projectDirectory = this.getProjectDirectory(); this.processManager = new ProcessManager( this.settings, vaultPath, - vaultPath, + projectDirectory, (state) => this.notifyStateChange(state) ); - console.log("[OpenCode] Configured with vault directory:", vaultPath); + console.log("[OpenCode] Configured with project directory:", projectDirectory); this.registerView(OPENCODE_VIEW_TYPE, (leaf) => new OpenCodeView(leaf, this)); this.addRibbonIcon("terminal", "OpenCode", () => { this.activateView(); @@ -561,6 +617,18 @@ var OpenCodePlugin = class extends import_obsidian4.Plugin { this.processManager.updateSettings(this.settings); } } + // Update project directory and restart server if running + async updateProjectDirectory(directory) { + this.settings.projectDirectory = directory; + await this.saveData(this.settings); + if (this.processManager) { + this.processManager.updateProjectDirectory(this.getProjectDirectory()); + if (this.getProcessState() === "running") { + this.stopServer(); + await this.startServer(); + } + } + } // Get existing view leaf if any getExistingLeaf() { const leaves = this.app.workspace.getLeavesOfType(OPENCODE_VIEW_TYPE); @@ -636,7 +704,6 @@ var OpenCodePlugin = class extends import_obsidian4.Plugin { } } // Get the vault path - this is the root directory of the Obsidian vault - // which will be passed to OpenCode as the project directory getVaultPath() { const adapter = this.app.vault.adapter; const vaultPath = adapter.basePath || ""; @@ -645,4 +712,11 @@ var OpenCodePlugin = class extends import_obsidian4.Plugin { } return vaultPath; } + // Get the project directory - uses the configured setting if set, otherwise vault path + getProjectDirectory() { + if (this.settings.projectDirectory) { + return this.settings.projectDirectory; + } + return this.getVaultPath(); + } }; diff --git a/src/ProcessManager.ts b/src/ProcessManager.ts index 3c2658f..901a9ea 100644 --- a/src/ProcessManager.ts +++ b/src/ProcessManager.ts @@ -25,10 +25,14 @@ export class ProcessManager { this.onStateChange = onStateChange; } - updateSettings(settings: OpenCodeSettings) { + updateSettings(settings: OpenCodeSettings): void { this.settings = settings; } + updateProjectDirectory(directory: string): void { + this.projectDirectory = directory; + } + getState(): ProcessState { return this.state; } diff --git a/src/SettingsTab.ts b/src/SettingsTab.ts index 7367b75..f0874ef 100644 --- a/src/SettingsTab.ts +++ b/src/SettingsTab.ts @@ -1,8 +1,21 @@ -import { App, PluginSettingTab, Setting } from "obsidian"; +import { App, PluginSettingTab, Setting, Notice } from "obsidian"; +import { existsSync, statSync } from "fs"; +import { homedir } from "os"; import type OpenCodePlugin from "./main"; +function expandTilde(path: string): string { + if (path === "~") { + return homedir(); + } + if (path.startsWith("~/")) { + return path.replace("~", homedir()); + } + return path; +} + export class OpenCodeSettingTab extends PluginSettingTab { plugin: OpenCodePlugin; + private validateTimeout: ReturnType | null = null; constructor(app: App, plugin: OpenCodePlugin) { super(app, plugin); @@ -62,6 +75,26 @@ export class OpenCodeSettingTab extends PluginSettingTab { }) ); + new Setting(containerEl) + .setName("Project directory") + .setDesc( + "Override the starting directory for OpenCode. Leave empty to use the vault root. Supports ~ for home directory." + ) + .addText((text) => + text + .setPlaceholder("/path/to/project or ~/project") + .setValue(this.plugin.settings.projectDirectory) + .onChange((value) => { + // Debounce validation to avoid spamming notices on every keypress + if (this.validateTimeout) { + clearTimeout(this.validateTimeout); + } + this.validateTimeout = setTimeout(async () => { + await this.validateAndSetProjectDirectory(value); + }, 500); + }) + ); + // Behavior settings section containerEl.createEl("h3", { text: "Behavior" }); @@ -86,6 +119,44 @@ export class OpenCodeSettingTab extends PluginSettingTab { this.renderServerStatus(statusContainer); } + private async validateAndSetProjectDirectory(value: string): Promise { + const trimmed = value.trim(); + + // Empty value is valid - means use vault root + if (!trimmed) { + await this.plugin.updateProjectDirectory(""); + return; + } + + // Validate absolute path (supports ~, /, and Windows drive letters) + if (!trimmed.startsWith("/") && !trimmed.startsWith("~") && !trimmed.match(/^[A-Za-z]:\\/)) { + new Notice("Project directory must be an absolute path (or start with ~)"); + return; + } + + // Expand tilde for validation + const expanded = expandTilde(trimmed); + + // Validate path exists and is a directory + try { + if (!existsSync(expanded)) { + new Notice("Project directory does not exist"); + return; + } + const stat = statSync(expanded); + if (!stat.isDirectory()) { + new Notice("Project directory path is not a directory"); + return; + } + } catch (error) { + new Notice(`Failed to validate path: ${(error as Error).message}`); + return; + } + + // Store the expanded path + await this.plugin.updateProjectDirectory(expanded); + } + private renderServerStatus(container: HTMLElement): void { container.empty(); diff --git a/src/main.ts b/src/main.ts index 2b903cb..15d5c7f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -16,16 +16,18 @@ export default class OpenCodePlugin extends Plugin { // Get the vault directory path to pass to OpenCode const vaultPath = this.getVaultPath(); + const projectDirectory = this.getProjectDirectory(); - // Initialize process manager with vault as the project directory + // Initialize process manager with vault as the working directory + // and either the configured project directory or vault as the project this.processManager = new ProcessManager( this.settings, vaultPath, - vaultPath, + projectDirectory, (state) => this.notifyStateChange(state) ); - console.log("[OpenCode] Configured with vault directory:", vaultPath); + console.log("[OpenCode] Configured with project directory:", projectDirectory); // Register the OpenCode view this.registerView(OPENCODE_VIEW_TYPE, (leaf) => new OpenCodeView(leaf, this)); @@ -105,6 +107,22 @@ export default class OpenCodePlugin extends Plugin { } } + // Update project directory and restart server if running + async updateProjectDirectory(directory: string): Promise { + this.settings.projectDirectory = directory; + await this.saveData(this.settings); + + if (this.processManager) { + this.processManager.updateProjectDirectory(this.getProjectDirectory()); + + // Restart server if it's currently running + if (this.getProcessState() === "running") { + this.stopServer(); + await this.startServer(); + } + } + } + // Get existing view leaf if any private getExistingLeaf(): WorkspaceLeaf | null { const leaves = this.app.workspace.getLeavesOfType(OPENCODE_VIEW_TYPE); @@ -193,7 +211,6 @@ export default class OpenCodePlugin extends Plugin { } // Get the vault path - this is the root directory of the Obsidian vault - // which will be passed to OpenCode as the project directory private getVaultPath(): string { const adapter = this.app.vault.adapter as any; const vaultPath = adapter.basePath || ""; @@ -202,4 +219,12 @@ export default class OpenCodePlugin extends Plugin { } return vaultPath; } + + // Get the project directory - uses the configured setting if set, otherwise vault path + getProjectDirectory(): string { + if (this.settings.projectDirectory) { + return this.settings.projectDirectory; + } + return this.getVaultPath(); + } } diff --git a/src/types.ts b/src/types.ts index 71aaf01..89769f5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,6 +3,7 @@ export interface OpenCodeSettings { hostname: string; autoStart: boolean; opencodePath: string; + projectDirectory: string; } export const DEFAULT_SETTINGS: OpenCodeSettings = { @@ -10,6 +11,7 @@ export const DEFAULT_SETTINGS: OpenCodeSettings = { hostname: "127.0.0.1", autoStart: false, opencodePath: "opencode", + projectDirectory: "", }; export const OPENCODE_VIEW_TYPE = "opencode-view";