From 20c92c71dc523d3dee5c05a59bf6e4afb8093a1e Mon Sep 17 00:00:00 2001 From: ethan Date: Thu, 23 Apr 2026 19:31:14 +0800 Subject: [PATCH] fix(install-wizard): tolerate slow emulators + ANSI in skills ls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two independent bugs made `npx @larksuite/cli install` unreliable on slow emulators (iSH, QEMU, Wine) and underpowered CI runners: 1. **Hardcoded 120s timeouts**. `npm install -g` and `npx skills add` child processes take >120s on slow runtimes (measured 140s on iSH ARM64 for `skills add https://open.feishu.cn -y -g` which fetches 30+ skill manifests). The wizard always SIGTERM'd them mid-flight and reported "Failed to install skills", even though the underlying install was succeeding. New `LARK_CLI_INSTALL_TIMEOUT_MS` env var overrides, defaulting to 600000 (10 min) — safe for both fast and slow environments. 2. **`skillsAlreadyInstalled` regex never matches**. `skills ls -g` prints ANSI colour codes before each name (`\x1b[36mlark-approval`). The existing `/^lark-/m.test(out)` consequently returned false every time, so subsequent re-runs always triggered a fresh `skills add` (with the 120s timeout that then failed). Added a `stripAnsi` helper and apply it before matching; now re-runs correctly short-circuit with `step2Skip` ("Already installed. Skipped"). Verified on iSH ARM64 under macOS: - Scenario A (fresh install, skills not present): `skills add` runs for ~140s, succeeds, `step2Done` reached. Step 3 (QR code + app config) reached in ~5 min total. - Scenario B (re-run with skills already in ~/.agents/skills/): `skillsAlreadyInstalled` matches → `step2Skip` fires immediately → Step 3 reached in ~30s. Both scenarios exit 0 end-to-end. No regressions on fast Docker Alpine ARM64 (times are unchanged; the higher timeout ceiling only matters when the child is genuinely slow). --- scripts/install-wizard.js | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/scripts/install-wizard.js b/scripts/install-wizard.js index b5b197d87..874ddebcd 100644 --- a/scripts/install-wizard.js +++ b/scripts/install-wizard.js @@ -79,6 +79,27 @@ const messages = { // Helpers // --------------------------------------------------------------------------- +// Timeout for npm install / skills install child processes. npm install of +// @larksuite/cli fetches a ~16MB Go binary via its postinstall script, and +// `skills add` fetches and materialises 30+ skill manifests — both can take +// over 120s on slow emulators (iSH, QEMU, Wine), underpowered CI runners, or +// cold-cache runs on high-latency networks. 120s hardcoded timeouts were too +// tight for those environments; override via env var with a safer default. +const INSTALL_TIMEOUT_MS = (() => { + const n = parseInt(process.env.LARK_CLI_INSTALL_TIMEOUT_MS || "", 10); + return Number.isFinite(n) && n > 0 ? n : 600000; +})(); + +// Strip ANSI escape sequences (colour/cursor codes) from a string. `skills ls` +// emits cyan/gray codes around every skill name, so matching `/^lark-/m` +// directly against its output fails because each line starts with `\x1b[36m`, +// not `l`. Strip codes before pattern-matching content. +function stripAnsi(s) { + // Covers CSI (colour, cursor, etc.) and OSC (title). Good enough for + // tool output — we never need to preserve formatting in regex checks. + return String(s).replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\x1b\][^\x07]*\x07/g, ""); +} + function handleCancel(value, msg) { if (p.isCancel(value)) { p.cancel(msg.cancelled); @@ -244,7 +265,7 @@ async function stepInstallGlobally(msg) { s.start(fmt(msg.step1, PKG)); } try { - await runSilentAsync("npm", ["install", "-g", PKG], { timeout: 120000 }); + await runSilentAsync("npm", ["install", "-g", PKG], { timeout: INSTALL_TIMEOUT_MS }); s.stop(needsUpgrade ? fmt(msg.step1Upgraded, latestVer) : msg.step1Done); return needsUpgrade; } catch (_) { @@ -256,9 +277,13 @@ async function stepInstallGlobally(msg) { async function skillsAlreadyInstalled() { try { const out = await runSilentAsync("npx", ["-y", "skills", "ls", "-g"], { - timeout: 120000, + timeout: INSTALL_TIMEOUT_MS, }); - return /^lark-/m.test(out.toString()); + // `skills ls` prints ANSI colour codes before each skill name (e.g. + // `\x1b[36mlark-approval\x1b[0m`), so the previous `/^lark-/m` regex + // never matched even when lark skills were clearly listed. Strip + // escapes first so the content can be matched directly. + return /^lark-/m.test(stripAnsi(out.toString())); } catch (_) { return false; } @@ -274,11 +299,11 @@ async function stepInstallSkills(msg) { } try { await runSilentAsync("npx", ["-y", "skills", "add", SKILLS_REPO, "-y", "-g"], { - timeout: 120000, + timeout: INSTALL_TIMEOUT_MS, }); } catch (_) { await runSilentAsync("npx", ["-y", "skills", "add", SKILLS_REPO_FALLBACK, "-y", "-g"], { - timeout: 120000, + timeout: INSTALL_TIMEOUT_MS, }); } s.stop(msg.step2Done);