diff --git a/scripts/lib/cli-startup-root-help-bundle.ts b/scripts/lib/cli-startup-root-help-bundle.ts new file mode 100644 index 000000000000..14b2cb2c3121 --- /dev/null +++ b/scripts/lib/cli-startup-root-help-bundle.ts @@ -0,0 +1,42 @@ +// Resolves the generated root-help bundle identity for CLI startup metadata caching. +import { createHash } from "node:crypto"; +import { readdirSync, readFileSync } from "node:fs"; +import path from "node:path"; + +export function resolveCliStartupRootHelpBundleIdentity( + distDir: string, +): { bundleName: string; signature: string } | null { + const bundleName = readdirSync(distDir).find( + (entry) => + entry.startsWith("root-help-") && + !entry.startsWith("root-help-metadata-") && + entry.endsWith(".js"), + ); + if (!bundleName) { + return null; + } + const bundleContents = readFileSync(path.join(distDir, bundleName), "utf8"); + const buildInfo = readBuildIdentity(distDir); + return { + bundleName, + signature: createHash("sha1") + .update(bundleContents) + .update(JSON.stringify(buildInfo)) + .digest("hex"), + }; +} + +function readBuildIdentity(distDir: string): { version: string | null; commit: string | null } { + try { + const parsed = JSON.parse(readFileSync(path.join(distDir, "build-info.json"), "utf8")) as { + commit?: unknown; + version?: unknown; + }; + return { + version: typeof parsed.version === "string" ? parsed.version : null, + commit: typeof parsed.commit === "string" ? parsed.commit : null, + }; + } catch { + return { version: null, commit: null }; + } +} diff --git a/scripts/write-cli-startup-metadata.ts b/scripts/write-cli-startup-metadata.ts index 4bb2d7b7d920..2a010bc92eba 100644 --- a/scripts/write-cli-startup-metadata.ts +++ b/scripts/write-cli-startup-metadata.ts @@ -7,6 +7,7 @@ import { fileURLToPath, pathToFileURL } from "node:url"; import pMap from "p-map"; import type { RootHelpRenderOptions } from "../src/cli/program/root-help.js"; import type { OpenClawConfig } from "../src/config/config.js"; +import { resolveCliStartupRootHelpBundleIdentity } from "./lib/cli-startup-root-help-bundle.js"; import { resolveWindowsTaskkillPath } from "./lib/windows-taskkill.mjs"; function dedupe(values: string[]): string[] { @@ -155,26 +156,6 @@ function signalCliStartupMetadataProcessTree( child.kill(signal); } -function resolveRootHelpBundleIdentity( - distDirOverride: string = distDir, -): { bundleName: string; signature: string } | null { - const bundleName = readdirSync(distDirOverride).find( - (entry) => - entry.startsWith("root-help-") && - !entry.startsWith("root-help-metadata-") && - entry.endsWith(".js"), - ); - if (!bundleName) { - return null; - } - const bundlePath = path.join(distDirOverride, bundleName); - const raw = readFileSync(bundlePath, "utf8"); - return { - bundleName, - signature: createHash("sha1").update(raw).digest("hex"), - }; -} - function updateHashFromFiles( hash: ReturnType, files: string[], @@ -591,7 +572,7 @@ export async function renderBundledRootHelpText( : extensionsDir, ), ): Promise { - const bundleIdentity = resolveRootHelpBundleIdentity(_distDirOverride); + const bundleIdentity = resolveCliStartupRootHelpBundleIdentity(_distDirOverride); if (!bundleIdentity) { throw new Error("No root-help bundle found in dist; cannot write CLI startup metadata."); } @@ -778,7 +759,7 @@ export async function writeCliStartupMetadata(options?: { const resolvedExtensionsDir = options?.extensionsDir ?? extensionsDir; const resolvedSourceRootDir = options?.sourceRootDir ?? rootDir; const channelCatalog = readBundledChannelCatalog(resolvedExtensionsDir); - const bundleIdentity = resolveRootHelpBundleIdentity(resolvedDistDir); + const bundleIdentity = resolveCliStartupRootHelpBundleIdentity(resolvedDistDir); const browserHelpSourceSignature = resolveBrowserHelpSourceSignature(resolvedSourceRootDir); const secretsHelpSourceSignature = resolveSecretsHelpSourceSignature(resolvedSourceRootDir); const nodesHelpSourceSignature = resolveNodesHelpSourceSignature(resolvedSourceRootDir); diff --git a/test/scripts/write-cli-startup-metadata.test.ts b/test/scripts/write-cli-startup-metadata.test.ts index 513ab8dcb4d4..bc3d89d94367 100644 --- a/test/scripts/write-cli-startup-metadata.test.ts +++ b/test/scripts/write-cli-startup-metadata.test.ts @@ -592,4 +592,64 @@ describe("write-cli-startup-metadata", () => { expect(nodesRenderCount).toBe(3); expect(written.nodesHelpText).toContain("openclaw nodes 3"); }); + + it("regenerates help when build version or commit changes", async () => { + const tempRoot = createTempDir("openclaw-startup-metadata-build-identity-"); + const distDir = path.join(tempRoot, "dist"); + const extensionsDir = path.join(tempRoot, "extensions"); + const outputPath = path.join(distDir, "cli-startup-metadata.json"); + let renderCount = 0; + + writeStartupMetadataSourceSignatureFixture(tempRoot); + writeFixtureFile(distDir, "root-help-fixture.js", "export function outputRootHelp() {}\n"); + + const writeMetadata = async (): Promise => { + await writeCliStartupMetadata({ + distDir, + outputPath, + extensionsDir, + sourceRootDir: tempRoot, + renderBundledRootHelpText: async () => { + renderCount += 1; + return `Usage: openclaw ${renderCount}\n`; + }, + renderSourceBrowserHelpText: () => "Usage: openclaw browser\n", + renderSourceSecretsHelpText: () => "Usage: openclaw secrets\n", + renderSourceNodesHelpText: () => "Usage: openclaw nodes\n", + renderSourceSubcommandHelpTextRecord: () => ({ + doctor: "Usage: openclaw doctor\n", + gateway: "Usage: openclaw gateway\n", + models: "Usage: openclaw models\n", + plugins: "Usage: openclaw plugins\n", + sessions: "Usage: openclaw sessions\n", + tasks: "Usage: openclaw tasks\n", + }), + }); + }; + + writeFixtureFile( + distDir, + "build-info.json", + JSON.stringify({ version: "2026.7.2", commit: "a".repeat(40) }), + ); + await writeMetadata(); + await writeMetadata(); + expect(renderCount).toBe(1); + + writeFixtureFile( + distDir, + "build-info.json", + JSON.stringify({ version: "2026.7.2", commit: "b".repeat(40) }), + ); + await writeMetadata(); + expect(renderCount).toBe(2); + + writeFixtureFile( + distDir, + "build-info.json", + JSON.stringify({ version: "2026.7.3", commit: "b".repeat(40) }), + ); + await writeMetadata(); + expect(renderCount).toBe(3); + }); });