diff --git a/packages/commands/src/commands/update.ts b/packages/commands/src/commands/update.ts index ee6e3e5..fbca980 100644 --- a/packages/commands/src/commands/update.ts +++ b/packages/commands/src/commands/update.ts @@ -142,6 +142,7 @@ export default defineCommand({ `\n${color.green(`\u2713 Update complete: ${currentVersion} \u2192 ${newVer}`)}\n`, ); writeUpdateState(newVer); + updateAgentSkill(color); } catch (error) { const message = error instanceof Error ? error.message : String(error); const reinstall = diff --git a/packages/commands/tests/update-binary-skill-sync.test.ts b/packages/commands/tests/update-binary-skill-sync.test.ts new file mode 100644 index 0000000..6a55a8b --- /dev/null +++ b/packages/commands/tests/update-binary-skill-sync.test.ts @@ -0,0 +1,62 @@ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, expect, test, vi } from "vite-plus/test"; + +const runtimeMocks = vi.hoisted(() => ({ + performBinaryUpdate: vi.fn(), +})); + +const childProcessMocks = vi.hoisted(() => ({ + execSync: vi.fn(), +})); + +vi.mock("bailian-cli-runtime", async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, performBinaryUpdate: runtimeMocks.performBinaryUpdate }; +}); + +vi.mock("child_process", async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, execSync: childProcessMocks.execSync }; +}); + +import updateCommand from "../src/commands/update.ts"; + +let configDir: string; +let previousConfigDir: string | undefined; +let previousInstallMethod: string | undefined; + +beforeEach(() => { + configDir = mkdtempSync(join(tmpdir(), "bl-update-binary-")); + previousConfigDir = process.env.BAILIAN_CONFIG_DIR; + previousInstallMethod = process.env.BAILIAN_INSTALL_METHOD; + process.env.BAILIAN_CONFIG_DIR = configDir; + process.env.BAILIAN_INSTALL_METHOD = "binary"; + runtimeMocks.performBinaryUpdate.mockResolvedValue("1.15.0"); +}); + +afterEach(() => { + if (previousConfigDir === undefined) delete process.env.BAILIAN_CONFIG_DIR; + else process.env.BAILIAN_CONFIG_DIR = previousConfigDir; + if (previousInstallMethod === undefined) delete process.env.BAILIAN_INSTALL_METHOD; + else process.env.BAILIAN_INSTALL_METHOD = previousInstallMethod; + rmSync(configDir, { recursive: true, force: true }); + vi.clearAllMocks(); +}); + +test("binary bl update syncs bailian skills after the CLI update succeeds", async () => { + await updateCommand.run({ + identity: { + binName: "bl", + clientName: "bailian-cli", + npmPackage: "bailian-cli", + version: "1.14.3", + }, + flags: { to: "1.15.0" }, + settings: {}, + } as never); + + expect(runtimeMocks.performBinaryUpdate).toHaveBeenCalledWith("1.15.0"); + expect(childProcessMocks.execSync).toHaveBeenCalledWith("bl skill init", { stdio: "inherit" }); +}); diff --git a/packages/runtime/src/utils/update-checker.ts b/packages/runtime/src/utils/update-checker.ts index 1a79a0a..65999c0 100644 --- a/packages/runtime/src/utils/update-checker.ts +++ b/packages/runtime/src/utils/update-checker.ts @@ -209,6 +209,25 @@ function errorMessage(err: unknown): string { return String(err); } +async function syncAgentSkillsAfterUpdate( + dim: string, + green: string, + yellow: string, + reset: string, +): Promise { + try { + process.stderr.write(` ${dim}Syncing agent skill...${reset}\n`); + const { execSync } = await import("child_process"); + execSync("bl skill init", { stdio: "inherit" }); + process.stderr.write(` ${green}\u2713 Agent skill updated.${reset}\n\n`); + } catch (error) { + process.stderr.write( + ` ${yellow}\u26a0 Agent skill sync failed: ${errorMessage(error)}${reset}\n`, + ); + process.stderr.write(` ${yellow} Run manually: bl skill init${reset}\n\n`); + } +} + /** * Perform auto-update for npm or binary installs. * Returns true if update succeeded, false otherwise. @@ -256,6 +275,7 @@ export async function performAutoUpdate( writeState({ lastChecked: Date.now(), latestVersion: newVer }); process.stderr.write(` ${green}✓ Update complete: ${currentVersion} → ${newVer}${reset}\n`); process.stderr.write(` ${dim}Run ${cyan}bl --version${reset}${dim} to verify.${reset}\n\n`); + await syncAgentSkillsAfterUpdate(dim, green, yellow, reset); pendingNotification = null; return true; } catch (err) { @@ -297,14 +317,7 @@ export async function performAutoUpdate( ); process.stderr.write(` ${dim}Run ${cyan}bl --version${reset}${dim} to verify.${reset}\n\n`); - try { - process.stderr.write(` ${dim}Syncing agent skill...${reset}\n`); - execSync(`bl skill init`, { stdio: "inherit" }); - process.stderr.write(` ${green}✓ Agent skill updated.${reset}\n\n`); - } catch (err) { - process.stderr.write(` ${yellow}⚠ Agent skill sync failed: ${errorMessage(err)}${reset}\n`); - process.stderr.write(` ${yellow} Run manually: bl skill init${reset}\n\n`); - } + await syncAgentSkillsAfterUpdate(dim, green, yellow, reset); pendingNotification = null; return true; diff --git a/packages/runtime/tests/update-checker.test.ts b/packages/runtime/tests/update-checker.test.ts index 94886ea..7c1dc0b 100644 --- a/packages/runtime/tests/update-checker.test.ts +++ b/packages/runtime/tests/update-checker.test.ts @@ -1,13 +1,58 @@ -import { expect, test } from "vite-plus/test"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, expect, test, vi } from "vite-plus/test"; + +const binaryUpdateMocks = vi.hoisted(() => ({ + performBinaryUpdate: vi.fn(), +})); + +const childProcessMocks = vi.hoisted(() => ({ + execSync: vi.fn(), +})); + +vi.mock("../src/utils/binary-update.ts", async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, performBinaryUpdate: binaryUpdateMocks.performBinaryUpdate }; +}); + +vi.mock("child_process", async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, execSync: childProcessMocks.execSync }; +}); + import { compareVersion, isMajorUpgrade, isNewerVersion, isPrerelease, parseVersion, + performAutoUpdate, shouldAutoUpdate, } from "../src/utils/update-checker.ts"; +let configDir: string; +let previousConfigDir: string | undefined; +let previousInstallMethod: string | undefined; + +beforeEach(() => { + configDir = mkdtempSync(join(tmpdir(), "bl-auto-update-binary-")); + previousConfigDir = process.env.BAILIAN_CONFIG_DIR; + previousInstallMethod = process.env.BAILIAN_INSTALL_METHOD; + process.env.BAILIAN_CONFIG_DIR = configDir; + process.env.BAILIAN_INSTALL_METHOD = "binary"; + binaryUpdateMocks.performBinaryUpdate.mockResolvedValue("2.0.0"); +}); + +afterEach(() => { + if (previousConfigDir === undefined) delete process.env.BAILIAN_CONFIG_DIR; + else process.env.BAILIAN_CONFIG_DIR = previousConfigDir; + if (previousInstallMethod === undefined) delete process.env.BAILIAN_INSTALL_METHOD; + else process.env.BAILIAN_INSTALL_METHOD = previousInstallMethod; + rmSync(configDir, { recursive: true, force: true }); + vi.clearAllMocks(); +}); + test("parseVersion strips pre-release and build metadata", () => { expect(parseVersion("1.4.2")).toEqual([1, 4, 2]); expect(parseVersion("2.0.0-beta.1")).toEqual([2, 0, 0]); @@ -124,3 +169,11 @@ test("shouldAutoUpdate only targets stable releases with a significant gap", () // Same core, release over its pre-release: notify only (no major gap). expect(shouldAutoUpdate("1.4.2", "1.4.2-beta.1")).toBe(false); }); + +test("binary auto-update syncs bailian skills after the CLI update succeeds", async () => { + const updated = await performAutoUpdate("1.14.3", "2.0.0"); + + expect(updated).toBe(true); + expect(binaryUpdateMocks.performBinaryUpdate).toHaveBeenCalledWith("2.0.0"); + expect(childProcessMocks.execSync).toHaveBeenCalledWith("bl skill init", { stdio: "inherit" }); +});