2 Commits

Author SHA1 Message Date
Mateusz Tymek
8113b8cc0b v0.2.0 2026-02-15 19:53:05 +01:00
Mateusz Tymek
9fd626c2df v0.1.0 2026-02-15 19:52:32 +01:00
8 changed files with 22 additions and 148 deletions

View File

@@ -28,20 +28,24 @@ 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
run: |
bun install -g opencode-ai
echo "$HOME/.bun/bin" >> $GITHUB_PATH
shell: bash
if: steps.cache-opencode.outputs.cache-hit != 'true'
run: bun install -g opencode-ai
- name: Verify OpenCode installation
run: |
export PATH="$HOME/.bun/bin:$PATH"
opencode --version
shell: bash
run: opencode --version
- name: Type check
run: bun run tsc -noEmit -skipLibCheck

View File

@@ -1,7 +1,7 @@
{
"id": "opencode-obsidian",
"name": "OpenCode-Obsidian",
"version": "0.2.1",
"version": "0.2.0",
"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.2.1",
"version": "0.2.0",
"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 = Buffer.from(this.projectDirectory).toString('base64');
const encodedPath = btoa(this.projectDirectory);
return `http://${this.settings.hostname}:${this.settings.port}/${encodedPath}`;
}

View File

@@ -2,130 +2,33 @@ 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 {
const process = spawn(command, args, {
return 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);
// 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;
// Use taskkill with /T flag to kill process tree
await this.execAsync(`taskkill /T /F /PID ${pid}`);
// 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: 45000,
startupTimeout: 15000,
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 = Buffer.from(PROJECT_DIR).toString('base64');
const expectedPath = btoa(PROJECT_DIR);
expect(url).toBe(`${expectedBase}/${expectedPath}`);
});
@@ -285,37 +285,4 @@ 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 most Unix systems
const existingBinary = "/bin/ls";
// Use a binary that exists on this system (found via `which ls`)
const existingBinary = "/etc/profiles/per-user/mat/bin/ls";
const result = await processImpl.verifyCommand(existingBinary);
expect(result).toBeNull();
});