From 76120f9bad73387455a6aa3a75a35f3a992b0b45 Mon Sep 17 00:00:00 2001 From: Onur <2453968+osolmaz@users.noreply.github.com> Date: Thu, 26 Feb 2026 09:36:08 +0100 Subject: [PATCH] cli: add dynamic --version resolution --- src/cli.ts | 2 ++ src/client.ts | 3 +- src/version.ts | 66 ++++++++++++++++++++++++++++++++++++++++++++ test/cli.test.ts | 32 +++++++++++++++++++++ test/version.test.ts | 41 +++++++++++++++++++++++++++ 5 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 src/version.ts create mode 100644 test/version.test.ts diff --git a/src/cli.ts b/src/cli.ts index 3bee7a7..c4640ce 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -46,6 +46,7 @@ import { sendSession, } from "./session.js"; import { normalizeAgentSessionId } from "./agent-session-id.js"; +import { getAcpxVersion } from "./version.js"; import { AUTH_POLICIES, EXIT_CODES, @@ -1858,6 +1859,7 @@ export async function main(argv: string[] = process.argv): Promise { program .name("acpx") .description("Headless CLI client for the Agent Client Protocol") + .version(getAcpxVersion()) .enablePositionalOptions() .showHelpAfterError(); diff --git a/src/client.ts b/src/client.ts index f6d28ab..4e7d47d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -36,6 +36,7 @@ import { import { FileSystemHandlers } from "./filesystem.js"; import { classifyPermissionDecision, resolvePermissionRequest } from "./permissions.js"; import { extractAgentSessionId } from "./agent-session-id.js"; +import { getAcpxVersion } from "./version.js"; import { TerminalManager } from "./terminal.js"; import type { AcpClientOptions, PermissionStats } from "./types.js"; @@ -511,7 +512,7 @@ export class AcpClient { }, clientInfo: { name: "acpx", - version: "0.1.0", + version: getAcpxVersion(), }, }); diff --git a/src/version.ts b/src/version.ts new file mode 100644 index 0000000..ea35745 --- /dev/null +++ b/src/version.ts @@ -0,0 +1,66 @@ +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const UNKNOWN_VERSION = "0.0.0-unknown"; +const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url)); + +let cachedVersion: string | null = null; + +function parseVersion(value: unknown): string | null { + if (typeof value !== "string") { + return null; + } + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : null; +} + +function readPackageVersion(packageJsonPath: string): string | null { + try { + const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8")) as { + version?: unknown; + }; + return parseVersion(parsed.version); + } catch { + return null; + } +} + +function resolveVersionFromAncestors(startDir: string): string | null { + let current = startDir; + while (true) { + const packageVersion = readPackageVersion(path.join(current, "package.json")); + if (packageVersion) { + return packageVersion; + } + const parent = path.dirname(current); + if (parent === current) { + return null; + } + current = parent; + } +} + +export function resolveAcpxVersion(params?: { + env?: NodeJS.ProcessEnv; + packageJsonPath?: string; +}): string { + const envVersion = parseVersion((params?.env ?? process.env).npm_package_version); + if (envVersion) { + return envVersion; + } + + if (params?.packageJsonPath) { + return readPackageVersion(params.packageJsonPath) ?? UNKNOWN_VERSION; + } + + return resolveVersionFromAncestors(MODULE_DIR) ?? UNKNOWN_VERSION; +} + +export function getAcpxVersion(): string { + if (cachedVersion) { + return cachedVersion; + } + cachedVersion = resolveAcpxVersion(); + return cachedVersion; +} diff --git a/test/cli.test.ts b/test/cli.test.ts index c71a932..6fd47ea 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -1,5 +1,6 @@ import assert from "node:assert/strict"; import { spawn } from "node:child_process"; +import { readFileSync } from "node:fs"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; @@ -11,6 +12,28 @@ import type { SessionRecord } from "../src/types.js"; const CLI_PATH = fileURLToPath(new URL("../src/cli.js", import.meta.url)); const MOCK_AGENT_PATH = fileURLToPath(new URL("./mock-agent.js", import.meta.url)); +function readPackageVersionForTest(): string { + const candidates = [ + fileURLToPath(new URL("../package.json", import.meta.url)), + fileURLToPath(new URL("../../package.json", import.meta.url)), + path.join(process.cwd(), "package.json"), + ]; + for (const candidate of candidates) { + try { + const parsed = JSON.parse(readFileSync(candidate, "utf8")) as { + version?: unknown; + }; + if (typeof parsed.version === "string" && parsed.version.trim().length > 0) { + return parsed.version; + } + } catch { + // continue searching + } + } + throw new Error("package.json version is missing"); +} + +const PACKAGE_VERSION = readPackageVersionForTest(); const MOCK_AGENT_COMMAND = `node ${JSON.stringify(MOCK_AGENT_PATH)}`; const MOCK_AGENT_IGNORING_SIGTERM = `${MOCK_AGENT_COMMAND} --ignore-sigterm`; const MOCK_CODEX_AGENT_WITH_AGENT_SESSION_ID = `${MOCK_AGENT_COMMAND} --agent-session-id codex-runtime-session`; @@ -25,6 +48,15 @@ type CliRunResult = { stderr: string; }; +test("CLI --version prints package version", async () => { + await withTempHome(async (homeDir) => { + const result = await runCli(["--version"], homeDir); + assert.equal(result.code, 0, result.stderr); + assert.equal(result.stderr.trim(), ""); + assert.equal(result.stdout.trim(), PACKAGE_VERSION); + }); +}); + test("parseTtlSeconds parses and rounds valid numeric values", () => { assert.equal(parseTtlSeconds("30"), 30_000); assert.equal(parseTtlSeconds("0"), 0); diff --git a/test/version.test.ts b/test/version.test.ts new file mode 100644 index 0000000..0cc3de5 --- /dev/null +++ b/test/version.test.ts @@ -0,0 +1,41 @@ +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { resolveAcpxVersion } from "../src/version.js"; + +test("resolveAcpxVersion prefers npm_package_version from env", () => { + const version = resolveAcpxVersion({ + env: { npm_package_version: "9.9.9-ci" }, + packageJsonPath: "/definitely/missing/package.json", + }); + assert.equal(version, "9.9.9-ci"); +}); + +test("resolveAcpxVersion reads version from package.json when env is unset", async () => { + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "acpx-version-test-")); + try { + const packagePath = path.join(tmpDir, "package.json"); + await fs.writeFile( + packagePath, + `${JSON.stringify({ name: "acpx", version: "1.2.3" }, null, 2)}\n`, + "utf8", + ); + const version = resolveAcpxVersion({ + env: {}, + packageJsonPath: packagePath, + }); + assert.equal(version, "1.2.3"); + } finally { + await fs.rm(tmpDir, { recursive: true, force: true }); + } +}); + +test("resolveAcpxVersion falls back to unknown when version cannot be resolved", () => { + const version = resolveAcpxVersion({ + env: {}, + packageJsonPath: "/definitely/missing/package.json", + }); + assert.equal(version, "0.0.0-unknown"); +});