diff --git a/.gitmodules b/.gitmodules index 3a1a0b6..44a19ab 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "vendor/pages"] path = vendor/pages url = https://github.com/agentlab-in/pages.git +[submodule "vendor/review"] + path = vendor/review + url = https://github.com/agentlab-in/review.git diff --git a/README.md b/README.md index 308e0e9..f264155 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Alab CLI -Alab is one self-contained executable for AgentLab tools. The current release integrates [AgentLab Pages](https://github.com/agentlab-in/pages), a static site publisher backed by your own Cloudflare account. +Alab is one self-contained executable for AgentLab tools. The current release integrates [AgentLab Pages](https://github.com/agentlab-in/pages), a static site publisher backed by your own Cloudflare account, and [AgentLab Review](https://github.com/agentlab-in/review), a local click-to-comment review server for static HTML folders. No separate Node.js, npm, Bun, Python, Go, Wrangler, or helper CLI installation is required to run a downloaded binary. @@ -22,10 +22,14 @@ alab --version alab pages --help alab pages setup alab pages put ./site +alab review --help +alab review start ./site ``` `alab pages setup` guides Cloudflare API token validation, account selection, and project creation or reuse. Pages maintains authoritative state on one machine. A repeated `put` of the same directory retains its ID and URL. Cloudflare's default URL is used unless configuration explicitly supplies another base URL. +`alab review start` serves a local folder on loopback with a click-to-comment overlay, prints the project id and URL, and blocks until interrupted. Comments persist to `/.alab/review.json` for the agent to read. Review is local only and never deploys. + See [USAGE.md](./USAGE.md) for setup, command behavior, and limitations. See [docs/integration.md](./docs/integration.md) for the internal tool interface and update procedure. ## Build diff --git a/USAGE.md b/USAGE.md index 263c472..3fb20a4 100644 --- a/USAGE.md +++ b/USAGE.md @@ -36,6 +36,17 @@ alab pages info `delete` aliases `remove`, and `ls` aliases `list`. +### Review commands + +```text +alab review start [dir] [--port ] [--project ] [--json] +alab review list [--dir ] [--project ] [--rematch] [--json] +alab review remove [--dir ] [--project ] --remove-comments [--clean] [--cleanup] [--json] +alab review cleanup [--dir ] [--project ] [--json] +``` + +`review start` takes a directory only and serves it on 127.0.0.1 with the comment overlay injected in memory. Served HTML on disk is never rewritten. It prints the project id and URL and blocks until interrupted. Human comments persist to `/.alab/review.json` with sequential c1, c2 ids that are never reused; the agent reads that file and edits the HTML. `--project ` resumes a known folder. `--clean` clears all comments but keeps the project; `cleanup` deletes `/.alab/` entirely. + `put` copies a static directory into the local full snapshot and deploys it. Repeating it for the same directory uses `.alab/pages.json` to retain the same ID and URL. `remove` deletes the locally known page and redeploys the remaining snapshot. A deployment failure does not roll back the preceding local change, so retry the same command after fixing access. `--dry-run` skips Cloudflare deployment but still changes local state. `--skip-deploy` changes only local state. Use isolated `ALAB_HOME` and `ALAB_PAGES_CONTENT` directories for experiments. diff --git a/docs/integration.md b/docs/integration.md index 1a2097e..6e8e3c0 100644 --- a/docs/integration.md +++ b/docs/integration.md @@ -2,15 +2,15 @@ ## Current interface -The router mounts `createPagesCommand()` from the exact Pages `src/program.ts` revision with `addCommand` and parses the original `process.argv` array in the same process when the first argument is `pages`. +The router mounts `createPagesCommand()` from the exact Pages `src/program.ts` revision and `createReviewCommand()` from the exact Review `src/program.ts` revision with `addCommand`, then parses the original `process.argv` array in the same process when the first argument is `pages` or `review`. This preserves every remaining argument and option, standard input for hidden interactive prompts, standard output including JSON, standard error and subprocess progress, the working directory and environment, and success or failure through the process exit status. -No extraction is used. Bun embeds the router, Pages source, and JavaScript dependencies in the executable. Pages v3.0.0 deploys through the Cloudflare HTTP API and does not invoke Wrangler. Pages publishes no bins; the `pages` command surface exists only inside this binary. +No extraction is used. Bun embeds the router, Pages source, Review source, and JavaScript dependencies in the executable. Pages v3.0.0 deploys through the Cloudflare HTTP API and does not invoke Wrangler. Pages publishes no bins; the `pages` command surface exists only inside this binary. Review v0.1.0 serves a local folder on loopback with an in-memory comment overlay; the `review` command surface exists only inside this binary. ## Pin and integrity policy -`vendor/pages` is a Git submodule pinned by the parent commit. `pins.json` repeats its version, repository, commit SHA, and expected `package.json` SHA-256. `scripts/verify-pins.ts` rejects any mismatch before build or release. +`vendor/pages` and `vendor/review` are Git submodules pinned by the parent commit. `pins.json` repeats each tool version, repository, commit SHA, and expected `package.json` SHA-256. `scripts/verify-pins.ts` rejects any mismatch before build or release. JavaScript packages use exact versions and a committed Bun lockfile. The release workflow uses a pinned Bun version and emits `SHA256SUMS` for downloadable archives. @@ -24,4 +24,14 @@ JavaScript packages use exact versions and a committed Bun lockfile. The release 6. Update public docs and the Alab version when behavior changes. 7. Commit the submodule pointer, pins, lockfile changes, code, and docs together. +## Updating Review + +1. Fetch branches inside `vendor/review`. +2. Check out the intended immutable commit exposing `createReviewCommand`. +3. Update the Review version, commit, and package hash in `pins.json`. +4. Review the Review CLI entry point and dependencies for interface or runtime changes. +5. Run every command in `AGENTS.md`, including the compiled binary smoke test. +6. Update public docs and the Alab version when behavior changes. +7. Commit the submodule pointer, pins, lockfile changes, code, and docs together. + Never pin a moving branch or fetch tool code at runtime. diff --git a/pins.json b/pins.json index a5bcd2f..c55665a 100644 --- a/pins.json +++ b/pins.json @@ -4,5 +4,11 @@ "repository": "https://github.com/agentlab-in/pages.git", "commit": "887e73f189c192423fd9f897730b06417fa82362", "packageSha256": "0b4296a0d930002ed0a3cc36c7316501d272d1400576dc7a8e25719714e9dd0b" + }, + "review": { + "version": "0.1.0", + "repository": "https://github.com/agentlab-in/review.git", + "commit": "94650db26a44d21f5b3d9cbf1714a11607c21405", + "packageSha256": "3b4d5de4dfa55d739fec11f96270b3be5f4bd996c4334e26e9ac25fa863e59fb" } } diff --git a/scripts/smoke-binary.sh b/scripts/smoke-binary.sh index 4ba98a4..6fc2c8a 100644 --- a/scripts/smoke-binary.sh +++ b/scripts/smoke-binary.sh @@ -36,7 +36,7 @@ assert_json() { capture --version assert_success -[[ "$stdout" == $'alab 0.1.0\npages 3.0.0 (887e73f189c192423fd9f897730b06417fa82362)' ]] +[[ "$stdout" == $'alab 0.1.0\npages 3.0.0 (887e73f189c192423fd9f897730b06417fa82362)\nreview 0.1.0 (94650db26a44d21f5b3d9cbf1714a11607c21405)' ]] capture pages --help assert_success @@ -44,6 +44,17 @@ assert_success [[ "$stdout" == *'setup [options]'* ]] [[ "$stdout" == *'put [options] [dir]'* ]] +capture review --help +assert_success +[[ "$stdout" == *'Usage: alab review [options] [command]'* ]] +[[ "$stdout" == *'start [options] [dir]'* ]] +[[ "$stdout" == *'list [options]'* ]] + +capture review list --dir "$scratch/site" +[[ "$status" -eq 1 ]] +[[ -z "$stdout" ]] +[[ "$stderr" == "Error: No review project in $scratch/site. Run review start on that folder first." ]] + capture pages info assert_success [[ "$stdout" == *'CF token missing'* ]] @@ -90,4 +101,9 @@ capture put [[ -z "$stdout" ]] [[ "$stderr" == "Error: unknown tool 'put'. Run 'alab --help'." ]] +capture start +[[ "$status" -eq 1 ]] +[[ -z "$stdout" ]] +[[ "$stderr" == "Error: unknown tool 'start'. Run 'alab --help'." ]] + echo "compiled binary smoke test passed" diff --git a/scripts/verify-pins.ts b/scripts/verify-pins.ts index 6eb556d..543eabb 100644 --- a/scripts/verify-pins.ts +++ b/scripts/verify-pins.ts @@ -4,16 +4,25 @@ import { resolve } from "node:path"; import { spawnSync } from "node:child_process"; import pins from "../pins.json"; import pagesPackage from "../vendor/pages/package.json"; +import reviewPackage from "../vendor/review/package.json"; const root = resolve(import.meta.dir, ".."); -const packagePath = resolve(root, "vendor/pages/package.json"); -const revision = spawnSync("git", ["-C", resolve(root, "vendor/pages"), "rev-parse", "HEAD"], { encoding: "utf8" }); -if (revision.status !== 0) throw new Error(revision.stderr.trim() || "Could not read Pages revision"); -if (revision.stdout.trim() !== pins.pages.commit) throw new Error(`Pages revision mismatch: ${revision.stdout.trim()}`); -if (pagesPackage.version !== pins.pages.version) throw new Error(`Pages version mismatch: ${pagesPackage.version}`); +const tools = [ + { name: "pages", pin: pins.pages, version: pagesPackage.version }, + { name: "review", pin: pins.review, version: reviewPackage.version }, +] as const; -const packageHash = createHash("sha256").update(readFileSync(packagePath)).digest("hex"); -if (packageHash !== pins.pages.packageSha256) throw new Error(`Pages package hash mismatch: ${packageHash}`); +for (const tool of tools) { + const packagePath = resolve(root, `vendor/${tool.name}/package.json`); + const revision = spawnSync("git", ["-C", resolve(root, `vendor/${tool.name}`), "rev-parse", "HEAD"], { encoding: "utf8" }); -console.log(`pages ${pins.pages.version} ${pins.pages.commit} sha256:${packageHash}`); + if (revision.status !== 0) throw new Error(revision.stderr.trim() || `Could not read ${tool.name} revision`); + if (revision.stdout.trim() !== tool.pin.commit) throw new Error(`${tool.name} revision mismatch: ${revision.stdout.trim()}`); + if (tool.version !== tool.pin.version) throw new Error(`${tool.name} version mismatch: ${tool.version}`); + + const packageHash = createHash("sha256").update(readFileSync(packagePath)).digest("hex"); + if (packageHash !== tool.pin.packageSha256) throw new Error(`${tool.name} package hash mismatch: ${packageHash}`); + + console.log(`${tool.name} ${tool.pin.version} ${tool.pin.commit} sha256:${packageHash}`); +} diff --git a/src/cli.ts b/src/cli.ts index f5adbda..4ef7bd1 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -3,7 +3,9 @@ import { Command } from "commander"; import alabPackage from "../package.json"; import pins from "../pins.json"; import pagesPackage from "../vendor/pages/package.json"; +import reviewPackage from "../vendor/review/package.json"; import { createPagesCommand } from "../vendor/pages/src/program.ts"; +import { createReviewCommand } from "../vendor/review/src/program.ts"; const HELP = `Alab ${alabPackage.version} @@ -11,15 +13,17 @@ Usage: alab [command] [options] Tools: pages Publish static sites with AgentLab Pages + review Review static HTML folders with click-to-comment Commands: alab pages --help Show Pages commands and first-use guidance + alab review --help Show Review commands and first-use guidance alab --version Show Alab and integrated tool versions alab help Show this help `; export function versionText(): string { - return `alab ${alabPackage.version}\npages ${pagesPackage.version} (${pins.pages.commit})`; + return `alab ${alabPackage.version}\npages ${pagesPackage.version} (${pins.pages.commit})\nreview ${reviewPackage.version} (${pins.review.commit})`; } export function createAlabProgram(): Command { @@ -27,6 +31,7 @@ export function createAlabProgram(): Command { program.exitOverride(); program.name("alab").description("AgentLab tools").version(versionText()); program.addCommand(createPagesCommand()); + program.addCommand(createReviewCommand()); return program; } @@ -40,7 +45,7 @@ export async function main(argv = process.argv): Promise { console.log(versionText()); return 0; } - if (args[0] !== "pages") { + if (args[0] !== "pages" && args[0] !== "review") { console.error(`Error: unknown tool '${args[0]}'. Run 'alab --help'.`); return 1; } diff --git a/tests/router.test.ts b/tests/router.test.ts index f4166c9..d784132 100644 --- a/tests/router.test.ts +++ b/tests/router.test.ts @@ -12,8 +12,8 @@ function run(args: string[]) { } describe("Alab router", () => { - test("reports combined and pinned Pages versions", () => { - expect(versionText()).toBe(`alab 0.1.0\npages 3.0.0 (${pins.pages.commit})`); + test("reports combined and pinned tool versions", () => { + expect(versionText()).toBe(`alab 0.1.0\npages 3.0.0 (${pins.pages.commit})\nreview 0.1.0 (${pins.review.commit})`); }); test("routes Pages help with exact successful streams", () => { @@ -27,6 +27,17 @@ describe("Alab router", () => { expect(stdout).toContain("put [options] [dir]"); }); + test("routes Review help with exact successful streams", () => { + const result = run(["review", "--help"]); + const stdout = result.stdout.toString(); + + expect(result.exitCode).toBe(0); + expect(result.stderr.toString()).toBe(""); + expect(stdout.match(/^Usage:/gm)).toHaveLength(1); + expect(stdout).toContain("Usage: alab review [options] [command]"); + expect(stdout).toContain("start [options] [dir]"); + }); + test("returns an exact error status without polluting stdout", () => { const result = run(["unknown"]); @@ -42,4 +53,12 @@ describe("Alab router", () => { expect(result.stdout.toString()).toBe(""); expect(result.stderr.toString()).toBe("Error: unknown tool 'put'. Run 'alab --help'.\n"); }); + + test("rejects top-level Review commands outside the review tool", () => { + const result = run(["start"]); + + expect(result.exitCode).toBe(1); + expect(result.stdout.toString()).toBe(""); + expect(result.stderr.toString()).toBe("Error: unknown tool 'start'. Run 'alab --help'.\n"); + }); }); diff --git a/vendor/review b/vendor/review new file mode 160000 index 0000000..94650db --- /dev/null +++ b/vendor/review @@ -0,0 +1 @@ +Subproject commit 94650db26a44d21f5b3d9cbf1714a11607c21405