Skip to content

Commit 8c5c3e3

Browse files
committed
cli: add dynamic --version resolution
1 parent a31fdae commit 8c5c3e3

5 files changed

Lines changed: 143 additions & 1 deletion

File tree

src/cli.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
sendSession,
4747
} from "./session.js";
4848
import { normalizeAgentSessionId } from "./agent-session-id.js";
49+
import { getAcpxVersion } from "./version.js";
4950
import {
5051
AUTH_POLICIES,
5152
EXIT_CODES,
@@ -1858,6 +1859,7 @@ export async function main(argv: string[] = process.argv): Promise<void> {
18581859
program
18591860
.name("acpx")
18601861
.description("Headless CLI client for the Agent Client Protocol")
1862+
.version(getAcpxVersion())
18611863
.enablePositionalOptions()
18621864
.showHelpAfterError();
18631865

src/client.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
import { FileSystemHandlers } from "./filesystem.js";
3737
import { classifyPermissionDecision, resolvePermissionRequest } from "./permissions.js";
3838
import { extractAgentSessionId } from "./agent-session-id.js";
39+
import { getAcpxVersion } from "./version.js";
3940
import { TerminalManager } from "./terminal.js";
4041
import type { AcpClientOptions, PermissionStats } from "./types.js";
4142

@@ -511,7 +512,7 @@ export class AcpClient {
511512
},
512513
clientInfo: {
513514
name: "acpx",
514-
version: "0.1.0",
515+
version: getAcpxVersion(),
515516
},
516517
});
517518

src/version.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { readFileSync } from "node:fs";
2+
import path from "node:path";
3+
import { fileURLToPath } from "node:url";
4+
5+
const UNKNOWN_VERSION = "0.0.0-unknown";
6+
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
7+
8+
let cachedVersion: string | null = null;
9+
10+
function parseVersion(value: unknown): string | null {
11+
if (typeof value !== "string") {
12+
return null;
13+
}
14+
const trimmed = value.trim();
15+
return trimmed.length > 0 ? trimmed : null;
16+
}
17+
18+
function readPackageVersion(packageJsonPath: string): string | null {
19+
try {
20+
const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8")) as {
21+
version?: unknown;
22+
};
23+
return parseVersion(parsed.version);
24+
} catch {
25+
return null;
26+
}
27+
}
28+
29+
function resolveVersionFromAncestors(startDir: string): string | null {
30+
let current = startDir;
31+
while (true) {
32+
const packageVersion = readPackageVersion(path.join(current, "package.json"));
33+
if (packageVersion) {
34+
return packageVersion;
35+
}
36+
const parent = path.dirname(current);
37+
if (parent === current) {
38+
return null;
39+
}
40+
current = parent;
41+
}
42+
}
43+
44+
export function resolveAcpxVersion(params?: {
45+
env?: NodeJS.ProcessEnv;
46+
packageJsonPath?: string;
47+
}): string {
48+
const envVersion = parseVersion((params?.env ?? process.env).npm_package_version);
49+
if (envVersion) {
50+
return envVersion;
51+
}
52+
53+
if (params?.packageJsonPath) {
54+
return readPackageVersion(params.packageJsonPath) ?? UNKNOWN_VERSION;
55+
}
56+
57+
return resolveVersionFromAncestors(MODULE_DIR) ?? UNKNOWN_VERSION;
58+
}
59+
60+
export function getAcpxVersion(): string {
61+
if (cachedVersion) {
62+
return cachedVersion;
63+
}
64+
cachedVersion = resolveAcpxVersion();
65+
return cachedVersion;
66+
}

test/cli.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import assert from "node:assert/strict";
22
import { spawn } from "node:child_process";
3+
import { readFileSync } from "node:fs";
34
import fs from "node:fs/promises";
45
import os from "node:os";
56
import path from "node:path";
@@ -11,6 +12,28 @@ import type { SessionRecord } from "../src/types.js";
1112

1213
const CLI_PATH = fileURLToPath(new URL("../src/cli.js", import.meta.url));
1314
const MOCK_AGENT_PATH = fileURLToPath(new URL("./mock-agent.js", import.meta.url));
15+
function readPackageVersionForTest(): string {
16+
const candidates = [
17+
fileURLToPath(new URL("../package.json", import.meta.url)),
18+
fileURLToPath(new URL("../../package.json", import.meta.url)),
19+
path.join(process.cwd(), "package.json"),
20+
];
21+
for (const candidate of candidates) {
22+
try {
23+
const parsed = JSON.parse(readFileSync(candidate, "utf8")) as {
24+
version?: unknown;
25+
};
26+
if (typeof parsed.version === "string" && parsed.version.trim().length > 0) {
27+
return parsed.version;
28+
}
29+
} catch {
30+
// continue searching
31+
}
32+
}
33+
throw new Error("package.json version is missing");
34+
}
35+
36+
const PACKAGE_VERSION = readPackageVersionForTest();
1437
const MOCK_AGENT_COMMAND = `node ${JSON.stringify(MOCK_AGENT_PATH)}`;
1538
const MOCK_AGENT_IGNORING_SIGTERM = `${MOCK_AGENT_COMMAND} --ignore-sigterm`;
1639
const MOCK_CODEX_AGENT_WITH_AGENT_SESSION_ID = `${MOCK_AGENT_COMMAND} --agent-session-id codex-runtime-session`;
@@ -25,6 +48,15 @@ type CliRunResult = {
2548
stderr: string;
2649
};
2750

51+
test("CLI --version prints package version", async () => {
52+
await withTempHome(async (homeDir) => {
53+
const result = await runCli(["--version"], homeDir);
54+
assert.equal(result.code, 0, result.stderr);
55+
assert.equal(result.stderr.trim(), "");
56+
assert.equal(result.stdout.trim(), PACKAGE_VERSION);
57+
});
58+
});
59+
2860
test("parseTtlSeconds parses and rounds valid numeric values", () => {
2961
assert.equal(parseTtlSeconds("30"), 30_000);
3062
assert.equal(parseTtlSeconds("0"), 0);

test/version.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import assert from "node:assert/strict";
2+
import fs from "node:fs/promises";
3+
import os from "node:os";
4+
import path from "node:path";
5+
import test from "node:test";
6+
import { resolveAcpxVersion } from "../src/version.js";
7+
8+
test("resolveAcpxVersion prefers npm_package_version from env", () => {
9+
const version = resolveAcpxVersion({
10+
env: { npm_package_version: "9.9.9-ci" },
11+
packageJsonPath: "/definitely/missing/package.json",
12+
});
13+
assert.equal(version, "9.9.9-ci");
14+
});
15+
16+
test("resolveAcpxVersion reads version from package.json when env is unset", async () => {
17+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "acpx-version-test-"));
18+
try {
19+
const packagePath = path.join(tmpDir, "package.json");
20+
await fs.writeFile(
21+
packagePath,
22+
`${JSON.stringify({ name: "acpx", version: "1.2.3" }, null, 2)}\n`,
23+
"utf8",
24+
);
25+
const version = resolveAcpxVersion({
26+
env: {},
27+
packageJsonPath: packagePath,
28+
});
29+
assert.equal(version, "1.2.3");
30+
} finally {
31+
await fs.rm(tmpDir, { recursive: true, force: true });
32+
}
33+
});
34+
35+
test("resolveAcpxVersion falls back to unknown when version cannot be resolved", () => {
36+
const version = resolveAcpxVersion({
37+
env: {},
38+
packageJsonPath: "/definitely/missing/package.json",
39+
});
40+
assert.equal(version, "0.0.0-unknown");
41+
});

0 commit comments

Comments
 (0)