fix: force launcher relink during update (#1594)

* fix: force launcher relink during update (#1591)

* fix: address launcher refresh review feedback (#1591)

* fix: improve launcher refresh diagnostics (#1591)
This commit is contained in:
Ashish Huddar 2026-05-01 17:09:58 +05:30 committed by GitHub
parent b2cdf7adab
commit 9ca3c1fcd7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 177 additions and 26 deletions

View File

@ -6,6 +6,7 @@ import {
mkdirSync,
readFileSync,
rmSync,
symlinkSync,
utimesSync,
writeFileSync,
} from "node:fs";
@ -181,7 +182,7 @@ describe("ao-doctor.sh", () => {
expect(result.status).toBe(0);
expect(result.stdout).toContain("FIXED");
expect(npmCommands).toContain("link");
expect(npmCommands).toContain("link --force");
expect(result.stdout).toContain("launcher");
expect(result.stdout).toContain("stale temp files");
expect(staleStillExists).toBe(false);
@ -191,6 +192,63 @@ describe("ao-doctor.sh", () => {
expect(commentedWorktreeDirExists).toBe(false);
});
it("repairs a dangling ao launcher shim in fix mode", () => {
const tempRoot = mkdtempSync(join(tmpdir(), "ao-doctor-dangling-launcher-"));
const fakeRepo = createHealthyRepo(tempRoot);
const binDir = join(tempRoot, "bin");
mkdirSync(binDir, { recursive: true });
createHealthyPath(binDir);
const aoPath = join(binDir, "ao");
rmSync(aoPath, { force: true });
symlinkSync(join(tempRoot, "deleted-checkout", "dist", "index.js"), aoPath);
const npmLog = join(tempRoot, "npm.log");
createFakeBinary(
binDir,
"npm",
`printf '%s\n' "$*" >> ${JSON.stringify(npmLog)}
if [ "$1" = "link" ]; then
rm -f ${JSON.stringify(aoPath)}
printf '#!/bin/bash\nexit 0\n' > ${JSON.stringify(aoPath)}
chmod +x ${JSON.stringify(aoPath)}
fi
if [ "$1" = "bin" ]; then
printf "/tmp/npm-bin\n"
fi
exit 0`,
);
const configPath = join(tempRoot, "agent-orchestrator.yaml");
const dataDir = join(tempRoot, "data");
const worktreeDir = join(tempRoot, "worktrees");
mkdirSync(dataDir, { recursive: true });
mkdirSync(worktreeDir, { recursive: true });
writeFileSync(
configPath,
[`dataDir: ${dataDir}`, `worktreeDir: ${worktreeDir}`, "projects: {}"].join("\n"),
);
const result = spawnSync("bash", [scriptPath, "--fix"], {
env: {
...process.env,
PATH: `${binDir}:/bin:/usr/bin`,
AO_REPO_ROOT: fakeRepo,
AO_CONFIG_PATH: configPath,
},
encoding: "utf8",
});
const npmCommands = existsSync(npmLog) ? readFileSync(npmLog, "utf8") : "";
const repairedLauncherIsExecutable = existsSync(aoPath);
rmSync(tempRoot, { recursive: true, force: true });
expect(result.status).toBe(0);
expect(result.stdout).toContain("FIXED");
expect(repairedLauncherIsExecutable).toBe(true);
expect(npmCommands).toContain("link --force");
});
it("reports a healthy packaged install without source-checkout failures", () => {
const tempRoot = mkdtempSync(join(tmpdir(), "ao-doctor-package-"));
const fakeInstall = createHealthyPackageInstall(tempRoot);

View File

@ -1,5 +1,13 @@
import { describe, it, expect } from "vitest";
import { chmodSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import {
chmodSync,
existsSync,
mkdtempSync,
mkdirSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs";
import { dirname, join, resolve } from "node:path";
import { tmpdir } from "node:os";
import { spawnSync } from "node:child_process";
@ -76,7 +84,7 @@ esac\nexit 0`,
expect(commands).toContain("pnpm install");
expect(commands).toContain("pnpm --filter @aoagents/ao-core clean");
expect(commands).toContain("pnpm --filter @aoagents/ao-cli build");
expect(commands).toContain("npm link");
expect(commands).toContain("npm link --force");
});
it("syncs the fork with upstream via gh and fast-forwards the local checkout from upstream", () => {
@ -146,14 +154,77 @@ esac\nexit 0`,
expect(commands).not.toContain("git fetch origin main");
});
it("uses forced npm link so stale global ao shims are overwritten", () => {
const tempRoot = mkdtempSync(join(tmpdir(), "ao-update-stale-shim-"));
const fakeRepo = join(tempRoot, "repo");
mkdirSync(join(fakeRepo, "packages", "cli"), { recursive: true });
mkdirSync(join(fakeRepo, "packages", "ao"), { recursive: true });
const binDir = join(tempRoot, "bin");
mkdirSync(binDir, { recursive: true });
const commandLog = join(tempRoot, "commands.log");
createFakeBinary(
binDir,
"git",
`case "$*" in
"remote get-url upstream") exit 1 ;;
"rev-parse --is-inside-work-tree") printf 'true\n' ;;
"status --porcelain") ;;
"branch --show-current") printf 'main\n' ;;
"fetch origin main") ;;
"rev-parse HEAD") printf 'oldsha000\n' ;;
"rev-parse origin/main") printf 'newsha111\n' ;;
"pull --ff-only origin main") ;;
esac
exit 0`,
);
createFakeBinary(
binDir,
"pnpm",
`if [ "$1" = "--version" ]; then printf '9.15.4\n'; fi
exit 0`,
);
createFakeBinary(
binDir,
"npm",
`printf 'npm %s\n' "$*" >> ${JSON.stringify(commandLog)}
if [ "$*" = "link" ]; then
printf 'npm error code EEXIST\n' >&2
exit 1
fi
exit 0`,
);
createFakeBinary(
binDir,
"node",
`if [ "$1" = "--version" ]; then printf 'v20.11.1\n'; fi
exit 0`,
);
const result = spawnSync("bash", [scriptPath, "--skip-smoke"], {
env: {
...process.env,
PATH: `${binDir}:${process.env.PATH || ""}`,
AO_REPO_ROOT: fakeRepo,
},
encoding: "utf8",
});
const commands = existsSync(commandLog) ? readFileSync(commandLog, "utf8") : "";
rmSync(tempRoot, { recursive: true, force: true });
expect(result.status).toBe(0);
expect(commands).toContain("npm link --force");
expect(commands).not.toContain("npm link\n");
expect(result.stdout).not.toContain("Permission denied");
});
it("runs the built-in smoke commands in smoke-only mode", () => {
const tempRoot = mkdtempSync(join(tmpdir(), "ao-update-smoke-"));
const fakeRepo = join(tempRoot, "repo");
mkdirSync(join(fakeRepo, "packages", "ao", "bin"), { recursive: true });
writeFileSync(
join(fakeRepo, "packages", "ao", "bin", "ao.js"),
"#!/usr/bin/env node\n",
);
writeFileSync(join(fakeRepo, "packages", "ao", "bin", "ao.js"), "#!/usr/bin/env node\n");
const binDir = join(tempRoot, "bin");
mkdirSync(binDir, { recursive: true });
@ -270,7 +341,11 @@ exit 0`,
"pnpm",
`printf 'pnpm %s\\n' "$*" >> ${JSON.stringify(commandLog)}\nif [ "$1" = "--version" ]; then\n printf '9.15.4\\n'\nfi\nexit 0`,
);
createFakeBinary(binDir, "npm", `printf 'npm %s\\n' "$*" >> ${JSON.stringify(commandLog)}\nexit 0`);
createFakeBinary(
binDir,
"npm",
`printf 'npm %s\\n' "$*" >> ${JSON.stringify(commandLog)}\nexit 0`,
);
createFakeBinary(
binDir,
"node",

View File

@ -215,23 +215,27 @@ check_launcher() {
local ao_path
ao_path="$(command -v ao 2>/dev/null || true)"
if [ -n "$ao_path" ]; then
pass "ao launcher resolves to $ao_path"
return
if [ -x "$ao_path" ]; then
pass "ao launcher resolves to $ao_path"
return
fi
warn "ao launcher resolves to $ao_path, but its target is missing or not executable"
fi
if [ "$SCRIPT_LAYOUT" = "source-checkout" ] && [ "$FIX_MODE" = true ] && command -v npm >/dev/null 2>&1 && [ -d "$REPO_ROOT/packages/ao" ]; then
if (cd "$REPO_ROOT/packages/ao" && npm link >/dev/null 2>&1) && command -v ao >/dev/null 2>&1; then
fixed "ao launcher refreshed with npm link"
if (cd "$REPO_ROOT/packages/ao" && npm link --force >/dev/null 2>&1) && command -v ao >/dev/null 2>&1; then
fixed "ao launcher refreshed with npm link --force"
return
fi
if [ -t 0 ]; then
printf ' Permission denied. Retrying with sudo...\n'
if (cd "$REPO_ROOT/packages/ao" && sudo npm link >/dev/null 2>&1) && command -v ao >/dev/null 2>&1; then
fixed "ao launcher refreshed with sudo npm link"
printf ' Launcher refresh failed. Retrying with sudo...\n'
if (cd "$REPO_ROOT/packages/ao" && sudo npm link --force >/dev/null 2>&1) && command -v ao >/dev/null 2>&1; then
fixed "ao launcher refreshed with sudo npm link --force"
return
fi
printf 'ERROR: sudo npm link --force failed. Inspect npm output above.\n' >&2
fi
warn "ao launcher refresh failed. Fix: cd $REPO_ROOT/packages/ao && sudo npm link"
warn "ao launcher refresh failed. Fix: cd $REPO_ROOT/packages/ao && sudo npm link --force"
return
fi

View File

@ -197,13 +197,20 @@ if [ "$SMOKE_ONLY" = false ]; then
printf '\nRefreshing ao launcher...\n'
(
cd "$REPO_ROOT/packages/ao"
if npm link 2>/dev/null; then
:
npm_link_error="$(mktemp)"
if npm link --force 2>"$npm_link_error"; then
rm -f "$npm_link_error"
elif [ -t 0 ]; then
printf ' Permission denied. Retrying with sudo...\n'
sudo npm link
rm -f "$npm_link_error"
printf ' Launcher refresh failed. Retrying with sudo...\n'
if ! sudo npm link --force; then
printf 'ERROR: sudo npm link --force failed. Inspect npm output above.\n' >&2
exit 1
fi
else
printf 'ERROR: Permission denied. Run manually: cd %s/packages/ao && sudo npm link\n' "$REPO_ROOT"
cat "$npm_link_error" >&2
rm -f "$npm_link_error"
printf 'ERROR: Launcher refresh failed. Run manually: cd %s/packages/ao && sudo npm link --force\n' "$REPO_ROOT"
exit 1
fi
)

View File

@ -137,13 +137,20 @@ pnpm build
echo ""
echo "Linking CLI globally..."
cd packages/ao
if npm link 2>/dev/null; then
:
npm_link_error="$(mktemp)"
if npm link --force 2>"$npm_link_error"; then
rm -f "$npm_link_error"
elif [ "$INTERACTIVE" = true ]; then
echo " Permission denied. Retrying with sudo..."
sudo npm link
rm -f "$npm_link_error"
echo " Launcher refresh failed. Retrying with sudo..."
if ! sudo npm link --force; then
echo "ERROR: sudo npm link --force failed. Inspect npm output above." >&2
exit 1
fi
else
echo "ERROR: Permission denied. Run manually: cd packages/ao && sudo npm link"
cat "$npm_link_error" >&2
rm -f "$npm_link_error"
echo "ERROR: Launcher refresh failed. Run manually: cd packages/ao && sudo npm link --force"
exit 1
fi
cd "$REPO_ROOT"