9 Commits
v0.2.0 ... main

Author SHA1 Message Date
Mateusz Tymek
d56e31a25a v0.2.1 2026-02-23 19:09:20 +01:00
Mateusz Tymek
92af2dd7e8 Fix version number 2026-02-23 19:09:06 +01:00
Mateusz Tymek
e20075db3f Increased OC startup timeout to 45s 2026-02-23 19:01:49 +01:00
Mateusz Tymek
159e7ad7ac Fix issues #28: InvalidCharacterError in getUrl when using non-Latin1 2026-02-23 18:54:47 +01:00
Gerkinfeltser
aaa71df9b2 ci: fix OpenCode CLI installation on Ubuntu
Remove broken caching for OpenCode binary and add explicit PATH
configuration to ensure opencode command is available on Ubuntu runners.
The cache wasn't properly restoring the binary to PATH.
2026-02-23 18:39:32 +01:00
Gerkinfeltser
a7fa8c76a4 fix: use CI env var to detect test environment
Check for CI environment variable (set by GitHub Actions and most CI
systems) instead of VITEST to skip beforeunload handler registration
during automated tests.
2026-02-23 18:39:32 +01:00
Gerkinfeltser
1ee91fcc2f fix: skip beforeunload handler in test environment
The static cleanup handler was interfering with test lifecycle,
causing the server to be killed during database migration.
Skip registration when VITEST environment variable is set.
2026-02-23 18:39:32 +01:00
Gerkinfeltser
427f0d5132 test: fix hardcoded path in PosixProcess test
Changed hardcoded path /etc/profiles/per-user/mat/bin/ls to /bin/ls
which exists on most Unix systems. The previous path was specific to
the original developer's machine and failed on CI runners.
2026-02-23 18:39:32 +01:00
Gerkinfeltser
2a824b6d19 fix: properly cleanup Windows process tree on Obsidian exit
### Fixed
- Replaced unreliable taskkill /T with PowerShell Get-CimInstance for child process detection in WindowsProcess.ts
- Fixed orphaned node.exe processes when Obsidian closes by killing child processes before parent
- Added proper cleanup when shell: true creates cmd.exe -> node.exe process tree

### Added
- Static currentProcess field to track active process for cleanup during window close
- Static cleanupHandlerRegistered flag to prevent duplicate event handlers
- beforeunload event handler for synchronous cleanup when Obsidian window closes
- killProcessSync method for immediate process termination without async delays
- registerCleanupHandler method to set up window close event listener

### Changed
- Updated start method to store process reference and register cleanup handler
- Modified stop method to use PowerShell child lookup before killing parent process
- Enhanced error handling with try/catch blocks for PowerShell and taskkill operations
2026-02-23 18:39:32 +01:00
8 changed files with 148 additions and 22 deletions

View File

@@ -28,24 +28,20 @@ jobs:
restore-keys: |
${{ runner.os }}-bun-
- name: Cache OpenCode binary
id: cache-opencode
uses: actions/cache@v4
with:
path: ~/.bun/bin/opencode
key: ${{ runner.os }}-opencode-${{ hashFiles('.github/workflows/ci.yml') }}
restore-keys: |
${{ runner.os }}-opencode-
- name: Install dependencies
run: bun install
- name: Install OpenCode CLI
if: steps.cache-opencode.outputs.cache-hit != 'true'
run: bun install -g opencode-ai
run: |
bun install -g opencode-ai
echo "$HOME/.bun/bin" >> $GITHUB_PATH
shell: bash
- name: Verify OpenCode installation
run: opencode --version
run: |
export PATH="$HOME/.bun/bin:$PATH"
opencode --version
shell: bash
- name: Type check
run: bun run tsc -noEmit -skipLibCheck

View File

@@ -1,7 +1,7 @@
{
"id": "opencode-obsidian",
"name": "OpenCode-Obsidian",
"version": "0.0.0",
"version": "0.2.1",
"minAppVersion": "1.4.0",
"description": "Embed OpenCode AI assistant in Obsidian for AI-powered note management",
"author": "mtymek",

View File

@@ -1,6 +1,6 @@
{
"name": "obsidian-opencode",
"version": "0.0.0",
"version": "0.2.1",
"description": "Embed OpenCode AI assistant in Obsidian",
"main": "main.js",
"scripts": {

View File

@@ -44,7 +44,7 @@ export class ServerManager extends EventEmitter {
}
getUrl(): string {
const encodedPath = btoa(this.projectDirectory);
const encodedPath = Buffer.from(this.projectDirectory).toString('base64');
return `http://${this.settings.hostname}:${this.settings.port}/${encodedPath}`;
}

View File

@@ -2,33 +2,130 @@ import { ChildProcess, spawn, SpawnOptions } from "child_process";
import { OpenCodeProcess } from "./OpenCodeProcess";
export class WindowsProcess implements OpenCodeProcess {
// Static state to track the current process for cleanup
private static currentProcess: ChildProcess | null = null;
private static cleanupHandlerRegistered = false;
start(
command: string,
args: string[],
options: SpawnOptions
): ChildProcess {
return spawn(command, args, {
const process = spawn(command, args, {
...options,
shell: true,
windowsHide: true,
});
// Store process for cleanup
WindowsProcess.currentProcess = process;
WindowsProcess.registerCleanupHandler();
return process;
}
async stop(process: ChildProcess): Promise<void> {
const pid = process.pid;
if (!pid) {
WindowsProcess.currentProcess = null;
return;
}
console.log("[OpenCode] Stopping server process tree, PID:", pid);
// Use taskkill with /T flag to kill process tree
await this.execAsync(`taskkill /T /F /PID ${pid}`);
// Method 1: Find and kill child processes (actual node.exe) using PowerShell
// This is necessary because shell: true spawns cmd.exe -> node.exe, and
// killing cmd.exe leaves node.exe orphaned
try {
const { execSync } = require("child_process");
const output = execSync(
`powershell -Command "Get-CimInstance Win32_Process -Filter \\"ParentProcessId=${pid}\\" | Select-Object ProcessId"`,
{ encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] }
);
const lines = output.split("\n").slice(3); // Skip headers
for (const line of lines) {
const childPid = line.trim();
if (childPid && !isNaN(parseInt(childPid))) {
try {
execSync(`taskkill /F /PID ${childPid}`, { stdio: "ignore" });
} catch {
// Child may already be gone
}
}
}
} catch {
// PowerShell lookup failed, continue to other methods
}
// Method 2: Kill the parent process (cmd.exe)
try {
await this.execAsync(`taskkill /F /PID ${pid}`);
} catch {
// Parent may already be gone
}
// Clear stored process
WindowsProcess.currentProcess = null;
// Wait for process to exit
await this.waitForExit(process, 5000);
}
private static registerCleanupHandler(): void {
if (WindowsProcess.cleanupHandlerRegistered) {
return;
}
// Register beforeunload handler for window close cleanup
// Skip in CI/test environments to avoid interfering with test lifecycle
if (typeof window !== "undefined" && !process.env.CI) {
window.addEventListener("beforeunload", () => {
if (WindowsProcess.currentProcess?.pid) {
WindowsProcess.killProcessSync(WindowsProcess.currentProcess.pid);
}
});
WindowsProcess.cleanupHandlerRegistered = true;
}
}
private static killProcessSync(pid: number): void {
try {
const { execSync } = require("child_process");
// Method 1: Kill child processes using PowerShell
try {
const output = execSync(
`powershell -Command "Get-CimInstance Win32_Process -Filter \\"ParentProcessId=${pid}\\" | Select-Object ProcessId"`,
{ encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] }
);
const lines = output.split("\n").slice(3);
for (const line of lines) {
const childPid = line.trim();
if (childPid && !isNaN(parseInt(childPid))) {
try {
execSync(`taskkill /F /PID ${childPid}`, { stdio: "ignore" });
} catch {
// Child may already be gone
}
}
}
} catch {
// PowerShell lookup failed
}
// Method 2: Kill parent process
try {
execSync(`taskkill /F /PID ${pid}`, { stdio: "ignore" });
} catch {
// Parent may already be gone
}
} catch {
// Process may already be gone
}
}
async verifyCommand(command: string): Promise<string | null> {
// Use 'where' command to check if executable exists in PATH
try {

View File

@@ -21,7 +21,7 @@ export const DEFAULT_SETTINGS: OpenCodeSettings = {
autoStart: false,
opencodePath: "opencode",
projectDirectory: "",
startupTimeout: 15000,
startupTimeout: 45000,
defaultViewLocation: "sidebar",
injectWorkspaceContext: false,
maxNotesInContext: 20,

View File

@@ -89,7 +89,7 @@ describe("ServerManager", () => {
const url = currentManager.getUrl();
const expectedBase = `http://127.0.0.1:${port}`;
const expectedPath = btoa(PROJECT_DIR);
const expectedPath = Buffer.from(PROJECT_DIR).toString('base64');
expect(url).toBe(`${expectedBase}/${expectedPath}`);
});
@@ -285,4 +285,37 @@ describe("ServerManager", () => {
expect(currentManager.getState()).toBe("stopped");
});
});
describe("Unicode path support", () => {
test("getUrl handles Chinese characters in project directory", () => {
const settings = createTestSettings(getNextPort());
const chinesePath = "C:/用户/Notes";
const manager = new ServerManager(settings, chinesePath);
const url = manager.getUrl();
expect(url).toContain("http://127.0.0.1:");
expect(url).toContain(Buffer.from(chinesePath).toString('base64'));
});
test("getUrl handles Japanese characters in project directory", () => {
const settings = createTestSettings(getNextPort());
const japanesePath = "/home/ユーザー/ノート";
const manager = new ServerManager(settings, japanesePath);
const url = manager.getUrl();
expect(url).toContain(Buffer.from(japanesePath).toString('base64'));
});
test("getUrl handles emoji in project directory", () => {
const settings = createTestSettings(getNextPort());
const emojiPath = "/home/user/📁Notes";
const manager = new ServerManager(settings, emojiPath);
const url = manager.getUrl();
expect(url).toContain(Buffer.from(emojiPath).toString('base64'));
});
});
});

View File

@@ -12,8 +12,8 @@ describe.skipIf(process.platform === "win32")("PosixProcess", () => {
});
test("returns null for existing absolute path", async () => {
// Use a binary that exists on this system (found via `which ls`)
const existingBinary = "/etc/profiles/per-user/mat/bin/ls";
// Use a binary that exists on most Unix systems
const existingBinary = "/bin/ls";
const result = await processImpl.verifyCommand(existingBinary);
expect(result).toBeNull();
});