Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions .scratch/web-cli-version/implementation-report.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 4 additions & 0 deletions packages/web/src/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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))
Expand Down
6 changes: 5 additions & 1 deletion packages/web/src/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export async function runCli(
output: CliOutput,
): Promise<number> {
const program = createProgram({ ...dependencies, writeOut: output.stdout });
program.exitOverride();
const fetchCommand = program.commands.find(
(command) => command.name() === "fetch",
);
Expand All @@ -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;
}
Expand Down
3 changes: 3 additions & 0 deletions packages/web/src/version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
declare const __WEB_PACKAGE_VERSION__: string;

export const WEB_PACKAGE_VERSION = __WEB_PACKAGE_VERSION__;
19 changes: 19 additions & 0 deletions packages/web/test/packed-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
39 changes: 39 additions & 0 deletions packages/web/test/program.test.ts
Original file line number Diff line number Diff line change
@@ -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: [
Expand Down Expand Up @@ -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" }),
Expand Down
13 changes: 13 additions & 0 deletions packages/web/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -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"],
Expand All @@ -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",
Expand Down
8 changes: 8 additions & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Loading