From 01b4ab3c3897e3ddd23c8fe8ddb5309d998900e3 Mon Sep 17 00:00:00 2001 From: htafolla Date: Mon, 30 Mar 2026 16:19:51 -0500 Subject: [PATCH] fix: init.sh fallback for Hermes consumers + prioritize gh in skill:install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Plugin falls back to node_modules/strray-ai/.opencode/init.sh when postinstall skips .opencode/ copy (Hermes consumers) - Reorder cloneRepo: try gh repo clone FIRST when authenticated, then tarball fallback, then git clone. Previously gh was buried after tarball and never fired (tarball always succeeds for public repos) - Applied to both src/ and .opencode/plugin/ (the shipped artifact) Tested: npm pack → clean install → gh fires first, init.sh fallback works --- .opencode/plugin/strray-codex-injection.js | 8 ++++- src/cli/commands/skill-install.ts | 37 ++++++++++++---------- src/plugin/strray-codex-injection.ts | 8 ++++- 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/.opencode/plugin/strray-codex-injection.js b/.opencode/plugin/strray-codex-injection.js index 4ede9ef4e..5c449dee8 100644 --- a/.opencode/plugin/strray-codex-injection.js +++ b/.opencode/plugin/strray-codex-injection.js @@ -791,7 +791,13 @@ export default async function strrayCodexPlugin(input) { const logger = await getOrCreateLogger(directory); logger.log("🔧 Plugin config hook triggered - initializing StrRay integration"); // Initialize StrRay framework - const initScriptPath = path.join(directory, ".opencode", "init.sh"); + // Primary: project .opencode/init.sh (copied by postinstall) + // Fallback: package .opencode/init.sh (works when postinstall skips for Hermes consumers) + let initScriptPath = path.join(directory, ".opencode", "init.sh"); + const pkgInitPath = path.join(directory, "node_modules", "strray-ai", ".opencode", "init.sh"); + if (!fs.existsSync(initScriptPath) && fs.existsSync(pkgInitPath)) { + initScriptPath = pkgInitPath; + } if (fs.existsSync(initScriptPath)) { try { const { stderr } = await spawnPromise("bash", [initScriptPath], directory); diff --git a/src/cli/commands/skill-install.ts b/src/cli/commands/skill-install.ts index ddc1e10bf..e077d93f7 100644 --- a/src/cli/commands/skill-install.ts +++ b/src/cli/commands/skill-install.ts @@ -96,6 +96,25 @@ function cloneRepo(url: string, targetDir: string): string { const errors: string[] = []; + // Prefer gh repo clone when authenticated (faster, handles auth seamlessly) + let hasGh = false; + try { + execSync("gh auth status --hostname github.com", { stdio: "pipe", timeout: 5000 }); + hasGh = true; + } catch { /* gh not installed or not authenticated */ } + + if (hasGh) { + try { + console.log(` Cloning via gh...`); + execSync(`gh repo clone ${slug.owner}/${slug.repo} "${extracted}" -- --depth 1`, { stdio: "pipe", timeout: 60000 }); + return extracted; + } catch (ghErr: unknown) { + const stderr = (ghErr as { stderr?: Buffer })?.stderr?.toString("utf-8") || ""; + errors.push(` gh repo clone: ${stderr.split("\n").filter((l: string) => l.includes("Error:"))[0]?.trim() || (ghErr as Error).message.split("\n")[0]}`); + } + } + + // Fallback: tarball download (public repos) for (const branch of ["main", "master"]) { const archiveUrl = `https://codeload.github.com/${slug.owner}/${slug.repo}/tar.gz/${branch}`; try { @@ -123,26 +142,10 @@ function cloneRepo(url: string, targetDir: string): string { } } + // Last resort: git clone (HTTPS then SSH) const httpsUrl = url; const sshUrl = url.replace(/^https:\/\/github\.com\//, "git@github.com:"); - let hasGh = false; - try { - execSync("gh auth status --hostname github.com", { stdio: "pipe", timeout: 5000 }); - hasGh = true; - } catch { /* gh not installed or not authenticated */ } - - if (hasGh && slug) { - try { - console.log(` Trying gh repo clone...`); - execSync(`gh repo clone ${slug.owner}/${slug.repo} "${extracted}" -- --depth 1`, { stdio: "pipe", timeout: 60000 }); - return extracted; - } catch (ghErr: unknown) { - const stderr = (ghErr as { stderr?: Buffer })?.stderr?.toString("utf-8") || ""; - errors.push(` gh repo clone: ${stderr.split("\n").filter((l: string) => l.includes("Error:"))[0]?.trim() || (ghErr as Error).message.split("\n")[0]}`); - } - } - for (const [proto, gitUrl] of [["HTTPS", httpsUrl], ["SSH", sshUrl]] as const) { try { execSync(`git clone --depth 1 ${gitUrl} "${extracted}"`, { stdio: "pipe", timeout: 30000 }); diff --git a/src/plugin/strray-codex-injection.ts b/src/plugin/strray-codex-injection.ts index 26b81f48e..8909b2452 100644 --- a/src/plugin/strray-codex-injection.ts +++ b/src/plugin/strray-codex-injection.ts @@ -1004,7 +1004,13 @@ export default async function strrayCodexPlugin(input: { ); // Initialize StrRay framework - const initScriptPath = path.join(directory, ".opencode", "init.sh"); + // Primary: project .opencode/init.sh (copied by postinstall) + // Fallback: package .opencode/init.sh (works when postinstall skips for Hermes consumers) + let initScriptPath = path.join(directory, ".opencode", "init.sh"); + const pkgInitPath = path.join(directory, "node_modules", "strray-ai", ".opencode", "init.sh"); + if (!fs.existsSync(initScriptPath) && fs.existsSync(pkgInitPath)) { + initScriptPath = pkgInitPath; + } if (fs.existsSync(initScriptPath)) { try { const { stderr } = await spawnPromise(