diff --git a/.scratch/web-cli-version/implementation-report.md b/.scratch/web-cli-version/implementation-report.md new file mode 100644 index 0000000..1609816 --- /dev/null +++ b/.scratch/web-cli-version/implementation-report.md @@ -0,0 +1,82 @@ +# Embedded Web CLI version implementation report + +## Scope + +- Repository: `guionai/web` +- Branch: `web-cli-version` +- Fixed point: `89cfe70cc33ccb10d403444fcaa2ee0ec083c2fa` +- Implementation commit: `1092788229ff08d25577b77bec8b8000c67cea83` + (`feat(cli): report embedded package version`) +- Delivery boundary: the complete `web-cli-version` spec and ticket 01. + Code review and deployment were excluded. + +## Ticket outcome + +### 01 — Report the embedded Web CLI version + +- The root Commander program now exposes the standard `-V, --version` option. + Both flags write exactly the Web package version and a newline to stdout, + return success, and bypass all research operations and credential loading. +- The Web tsup build reads `packages/web/package.json` while building and + replaces a dedicated compile-time token with that manifest version. The + bundled CLI contains the literal and does not read Git or package metadata at + invocation time. Release checkout synchronization therefore supplies the + tag-derived value, while local builds use the local manifest value. +- The packed Web smoke test invokes both flags, compares stdout with the + packed manifest version, changes the test-owned installed manifest, and + verifies that both flags still report the original packed value. +- The root README documents `web --version` and `web -V` in the CLI guidance. + +## Verification + +All relevant checks passed on Linux: + +- `pnpm format:check` — all repository formatting checks passed. +- `pnpm exec prettier --check vitest.config.ts packages/web/src/version.ts` — + the root config and new source module also match Prettier. +- `git diff --check` — no whitespace errors. +- `pnpm typecheck` — TypeScript completed successfully. +- `pnpm exec vitest run packages/web/test/program.test.ts` — 17 tests passed, + including both Commander version flags and the no-operation/no-credential + contract. +- `pnpm test` — 19 files and 144 tests passed. The existing missing DSH + primitive source-map warning was non-fatal. +- `pnpm build` — all four workspace packages built and Web generated + `packages/web/dist/openapi.yaml`. +- `pnpm test:release` — release version synchronization fixtures passed. +- `pnpm test:pack` — Web, Pi, and DSH packed-installation/host-loading checks + passed; the DSH artifact suite passed its 2 tests. + +The packed smoke uses test-owned temporary package, cache, and browser-fixture +paths. No live credentials, production services, or persistent user state were +used. + +## Changed paths and size + +Against the fixed point, excluding generated `dist` output, lockfiles, and +this report: + +- Product code: 12 additions and 1 deletion across the Commander adapter, + runner, and embedded-version module. +- Build/test configuration: 21 additions in the Web tsup define and Vitest + test define. +- Tests: 58 additions covering both flags, output routing, no credentials or + operations, and packed-manifest mutation. +- Documentation: 1 addition in the root README. +- Total: 92 additions and 1 deletion (93 touched lines). This is 25 lines + above the spec's 28–68-line estimate because the implementation includes + both a direct Commander contract test and a two-stage packed smoke assertion + plus the manifest-derived test define needed for synchronized release + versions. + +## Remaining concerns + +- Code review and deployment were intentionally not run, as excluded by the + request. +- Version commands are scoped to the Web executable; MCP metadata and the Pi, + DSH, and Web Core package versions remain unchanged as required. + +## Acceptance result + +Ticket 01 and the complete `web-cli-version` spec are implemented, committed, +and verified. diff --git a/README.md b/README.md index 96aedda..e625041 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,7 @@ deferred in `.scratch/defered/public-http-service-security.md`. `web` has human-readable output by default. Add `--json` for exactly one JSON document on stdout, which is useful for automation. +Run `web --version` (or `web -V`) to print the installed package version. ```bash web search --provider exa -- "Node AbortSignal" diff --git a/packages/web/src/program.ts b/packages/web/src/program.ts index 9ea2653..423e84c 100644 --- a/packages/web/src/program.ts +++ b/packages/web/src/program.ts @@ -3,6 +3,7 @@ import { Command } from "commander"; import { createMcpCommand } from "./mcp.js"; import { parseHttpPort, startHttpServer } from "./serve.js"; import { DEFAULT_HTTP_HOST, DEFAULT_HTTP_PORT } from "./serve.js"; +import { WEB_PACKAGE_VERSION } from "./version.js"; import { FETCH_MODES, @@ -22,9 +23,12 @@ export type ProgramDependencies = { export function createProgram(dependencies: ProgramDependencies): Command { const program = new Command(); + if (dependencies.writeOut) + program.configureOutput({ writeOut: dependencies.writeOut }); program .name("web") .description("Search the web and fetch web pages") + .version(WEB_PACKAGE_VERSION) .showSuggestionAfterError(false) .showHelpAfterError(false) .addCommand(createSearchCommand(dependencies)) diff --git a/packages/web/src/runner.ts b/packages/web/src/runner.ts index e9a0053..d720082 100644 --- a/packages/web/src/runner.ts +++ b/packages/web/src/runner.ts @@ -22,6 +22,7 @@ export async function runCli( output: CliOutput, ): Promise { const program = createProgram({ ...dependencies, writeOut: output.stdout }); + program.exitOverride(); const fetchCommand = program.commands.find( (command) => command.name() === "fetch", ); @@ -42,7 +43,10 @@ export async function runCli( typeof error.code === "string" && error.code.startsWith("commander.") ) - return error.code === "commander.helpDisplayed" ? 0 : 1; + return error.code === "commander.helpDisplayed" || + error.code === "commander.version" + ? 0 + : 1; output.stderr(formatCliError(error)); return 1; } diff --git a/packages/web/src/version.ts b/packages/web/src/version.ts new file mode 100644 index 0000000..740b4cc --- /dev/null +++ b/packages/web/src/version.ts @@ -0,0 +1,3 @@ +declare const __WEB_PACKAGE_VERSION__: string; + +export const WEB_PACKAGE_VERSION = __WEB_PACKAGE_VERSION__; diff --git a/packages/web/test/packed-smoke.mjs b/packages/web/test/packed-smoke.mjs index 7472a4d..1f44bd3 100644 --- a/packages/web/test/packed-smoke.mjs +++ b/packages/web/test/packed-smoke.mjs @@ -111,6 +111,25 @@ try { ); const binary = join(root, "node_modules", ".bin", "web"); + const packedVersion = installedManifest.version; + if (typeof packedVersion !== "string" || packedVersion.length === 0) + throw new Error("packed web package does not expose a version"); + for (const flag of ["--version", "-V"]) { + const version = await execFileAsync(binary, [flag], { cwd: root }); + if (version.stdout !== `${packedVersion}\n` || version.stderr !== "") + throw new Error(`installed web CLI did not report ${packedVersion}`); + } + await writeFile( + join(root, "node_modules", "@guionai", "web", "package.json"), + JSON.stringify({ ...installedManifest, version: "9.9.9-test-mutated" }), + ); + for (const flag of ["--version", "-V"]) { + const version = await execFileAsync(binary, [flag], { cwd: root }); + if (version.stdout !== `${packedVersion}\n` || version.stderr !== "") + throw new Error( + `installed web CLI consulted its mutated manifest for ${flag}`, + ); + } const help = await execFileAsync(binary, ["--help"], { cwd: root }); if (!help.stdout.includes("Search the web") || !help.stdout.includes("mcp")) throw new Error("installed web CLI did not start with its MCP command"); diff --git a/packages/web/test/program.test.ts b/packages/web/test/program.test.ts index af91005..122061d 100644 --- a/packages/web/test/program.test.ts +++ b/packages/web/test/program.test.ts @@ -1,10 +1,20 @@ import { describe, expect, it, vi } from "vitest"; +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; import { FetchCapabilityError } from "@guionai/web-core"; import { createProgram } from "../src/program.js"; import { runCli } from "../src/runner.js"; import { credentialsFromEnvironment } from "../src/runtime.js"; +const packageManifest = JSON.parse( + readFileSync( + join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), + "utf8", + ), +) as { version: string }; + const result = { provider: "Brave" as const, results: [ @@ -51,6 +61,35 @@ function setup() { } describe("web search Commander adapter", () => { + it.each(["--version", "-V"])( + "reports the embedded package version for %s without invoking operations", + async (flag) => { + const operations = setup().operations; + const credentials = vi.fn(() => ({ braveApiKey: "fixture-key" })); + let stdout = ""; + const program = createProgram({ + operations, + credentials, + writeOut: (text) => { + stdout += text; + }, + }); + program.exitOverride(); + + await expect( + program.parseAsync([flag], { from: "user" }), + ).rejects.toMatchObject({ + code: "commander.version", + exitCode: 0, + }); + + expect(stdout).toBe(`${packageManifest.version}\n`); + expect(credentials).not.toHaveBeenCalled(); + expect(operations.search).not.toHaveBeenCalled(); + expect(operations.fetch).not.toHaveBeenCalled(); + }, + ); + it("loads the DeepSeek key from the host environment without changing selection", () => { expect( credentialsFromEnvironment({ DEEPSEEK_API_KEY: "deepseek-key" }), diff --git a/packages/web/tsup.config.ts b/packages/web/tsup.config.ts index 6fbddd9..88ed5b0 100644 --- a/packages/web/tsup.config.ts +++ b/packages/web/tsup.config.ts @@ -1,5 +1,15 @@ +import { readFileSync } from "node:fs"; import { defineConfig } from "tsup"; +const packageManifest = JSON.parse( + readFileSync(new URL("./package.json", import.meta.url), "utf8"), +) as { version?: unknown }; +if ( + typeof packageManifest.version !== "string" || + packageManifest.version.length === 0 +) + throw new Error("package version is required to build the Web CLI"); + export default defineConfig({ entry: { cli: "src/cli.ts", "generate-openapi": "src/generate-openapi.ts" }, format: ["esm"], @@ -8,6 +18,9 @@ export default defineConfig({ bundle: true, clean: true, dts: true, + define: { + __WEB_PACKAGE_VERSION__: JSON.stringify(packageManifest.version), + }, noExternal: [ "@guionai/web-core", "@modelcontextprotocol/server", diff --git a/vitest.config.ts b/vitest.config.ts index f951647..6aae591 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,9 +1,17 @@ +import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { defineConfig } from "vitest/config"; import { compileCssModule } from "./packages/dsh-web/scripts/css-modules.js"; +const webPackageManifest = JSON.parse( + readFileSync(new URL("./packages/web/package.json", import.meta.url), "utf8"), +) as { version: string }; + export default defineConfig({ + define: { + __WEB_PACKAGE_VERSION__: JSON.stringify(webPackageManifest.version), + }, plugins: [ { name: "guion-dsh-css-modules-test",