Browse Source

fix(windows): detect opencode installed via .cmd shims and add Windows install paths

On Windows, the installer's isOpenCodeInstalled check always failed even
when opencode was running, because:

1. where opencode returns multiple shims (opencode, opencode.cmd,
   opencode.ps1). The code picked the first line via .find(Boolean),
   which is the extensionless npm wrapper that Node cannot spawn.
2. Since Node's CVE-2024-27980 patch, spawnSync on .cmd/.bat without
   shell: true throws Error: spawn EINVAL, swallowed by the try/catch.
3. The fallback getOpenCodePaths list had zero Windows entries, so the
   check never recovered.

This made unx oh-my-opencode-slim install exit with code 1 right
after the companion prompt, claiming OpenCode was not installed.

Fix:
- Pass shell: true to spawnSync on win32 in 
esolvePathCommand
  and canExecute.
- Prefer executable shims (.cmd/.exe/.ps1/.bat) over the extensionless
  npm wrapper when parsing where output.
- Add Windows install paths (npm %APPDATA%, pnpm, yarn, opencode.ai
  installer, Scoop, Chocolatey) to getOpenCodePaths.
- Print Windows-specific install hints (PowerShell install, winget)
  in checkOpenCodeInstalled instead of the Unix-only curl/export.
GiuseppeBellamacina 2 tuần trước cách đây
mục cha
commit
b5f59e3cea
2 tập tin đã thay đổi với 73 bổ sung11 xóa
  1. 22 7
      src/cli/install.ts
  2. 51 4
      src/cli/system.ts

+ 22 - 7
src/cli/install.ts

@@ -126,15 +126,30 @@ async function checkOpenCodeInstalled(): Promise<{
 }> {
   const installed = await isOpenCodeInstalled();
   if (!installed) {
+    const isWindows = process.platform === 'win32';
     printError('OpenCode is not installed on this system.');
     printInfo('Install it with:');
-    console.log(
-      `     ${BLUE}curl -fsSL https://opencode.ai/install | bash${RESET}`,
-    );
-    console.log();
-    printInfo('Or if already installed, add it to your PATH:');
-    console.log(`     ${BLUE}export PATH="$HOME/.local/bin:$PATH"${RESET}`);
-    console.log(`     ${BLUE}export PATH="$HOME/.opencode/bin:$PATH"${RESET}`);
+    if (isWindows) {
+      console.log(
+        `     ${BLUE}powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://opencode.ai/install.ps1 | iex"${RESET}`,
+      );
+      console.log();
+      printInfo('Or with winget:');
+      console.log(`     ${BLUE}winget install opencode${RESET}`);
+      console.log();
+      printInfo('Or if already installed, add it to your PATH:');
+      console.log(
+        `     ${BLUE}setx PATH "%PATH%;%LOCALAPPDATA%\\Programs\\opencode"${RESET}`,
+      );
+    } else {
+      console.log(
+        `     ${BLUE}curl -fsSL https://opencode.ai/install | bash${RESET}`,
+      );
+      console.log();
+      printInfo('Or if already installed, add it to your PATH:');
+      console.log(`     ${BLUE}export PATH="$HOME/.local/bin:$PATH"${RESET}`);
+      console.log(`     ${BLUE}export PATH="$HOME/.opencode/bin:$PATH"${RESET}`);
+    }
     return { ok: false };
   }
   const version = await getOpenCodeVersion();

+ 51 - 4
src/cli/system.ts

@@ -9,23 +9,41 @@ function resolvePathCommand(
   environment: NodeJS.ProcessEnv = process.env,
 ): string | null {
   try {
-    const resolver = process.platform === 'win32' ? 'where' : 'which';
+    const isWindows = process.platform === 'win32';
+    const resolver = isWindows ? 'where' : 'which';
     const result = spawnSync(resolver, [command], {
       encoding: 'utf-8',
       stdio: ['ignore', 'pipe', 'ignore'],
       env: environment,
+      // On Windows, `where opencode` returns multiple shims (opencode, opencode.cmd,
+      // opencode.ps1). Node's spawnSync cannot execute an extensionless npm shim,
+      // and since Node's CVE-2024-27980 patch .cmd/.bat files also require a shell.
+      shell: isWindows,
     });
 
     if (result.status !== 0) {
       return null;
     }
 
-    const resolved = result.stdout
+    const lines = result.stdout
       .split(/\r?\n/)
       .map((line) => line.trim())
-      .find(Boolean);
+      .filter(Boolean);
 
-    return resolved ?? null;
+    if (lines.length === 0) {
+      return null;
+    }
+
+    // On Windows, prefer executable shims (.cmd, .exe, .ps1) over the
+    // extensionless npm wrapper that Node cannot spawn directly.
+    if (isWindows) {
+      const executable = lines.find((line) =>
+        /\.(cmd|exe|ps1|bat)$/i.test(line),
+      );
+      return executable ?? lines[0];
+    }
+
+    return lines[0];
   } catch {
     return null;
   }
@@ -37,9 +55,13 @@ function canExecute(
   environment: NodeJS.ProcessEnv = process.env,
 ): boolean {
   try {
+    const isWindows = process.platform === 'win32';
     const result = spawnSync(command, args, {
       stdio: 'ignore',
       env: environment,
+      // Required on Windows to execute .cmd/.bat shims produced by npm/pnpm/yarn
+      // (Node's CVE-2024-27980 patch blocks them without a shell).
+      shell: isWindows,
     });
     return result.status === 0;
   } catch {
@@ -51,10 +73,35 @@ function getOpenCodePaths(
   environment: NodeJS.ProcessEnv = process.env,
 ): string[] {
   const home = environment.HOME || environment.USERPROFILE || '';
+  const isWindows = process.platform === 'win32';
+  const appData =
+    environment.APPDATA || `${home}\\AppData\\Roaming`;
+  const localAppData =
+    environment.LOCALAPPDATA || `${home}\\AppData\\Local`;
+
+  const windowsPaths = isWindows
+    ? [
+        // npm global shims (created as .cmd on Windows)
+        `${appData}\\npm\\opencode.cmd`,
+        `${appData}\\npm\\opencode.ps1`,
+        // pnpm global
+        `${localAppData}\\pnpm\\opencode.cmd`,
+        // Yarn global
+        `${localAppData}\\Yarn\\bin\\opencode.cmd`,
+        // opencode.ai/install .exe location
+        `${localAppData}\\Programs\\opencode\\opencode.exe`,
+        // Scoop
+        `${home}\\scoop\\shims\\opencode.exe`,
+        `${home}\\scoop\\apps\\opencode\\current\\bin\\opencode.exe`,
+        // Chocolatey
+        `C:\\ProgramData\\chocolatey\\bin\\opencode.exe`,
+      ]
+    : [];
 
   return [
     // PATH (try this first)
     'opencode',
+    ...windowsPaths,
     // User local installations (Linux & macOS)
     `${home}/.local/bin/opencode`,
     `${home}/.opencode/bin/opencode`,