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
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -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 `<dir>/.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
Expand Down
11 changes: 11 additions & 0 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ alab pages info

`delete` aliases `remove`, and `ls` aliases `list`.

### Review commands

```text
alab review start [dir] [--port <number>] [--project <id>] [--json]
alab review list [--dir <path>] [--project <id>] [--rematch] [--json]
alab review remove [--dir <path>] [--project <id>] --remove-comments <cN|all> [--clean] [--cleanup] [--json]
alab review cleanup [--dir <path>] [--project <id>] [--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 `<dir>/.alab/review.json` with sequential c1, c2 ids that are never reused; the agent reads that file and edits the HTML. `--project <id>` resumes a known folder. `--clean` clears all comments but keeps the project; `cleanup` deletes `<dir>/.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.
Expand Down
16 changes: 13 additions & 3 deletions docs/integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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.
6 changes: 6 additions & 0 deletions pins.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
18 changes: 17 additions & 1 deletion scripts/smoke-binary.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,25 @@ 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
[[ "$stdout" == *'Usage: alab pages [options] [command]'* ]]
[[ "$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'* ]]
Expand Down Expand Up @@ -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"
25 changes: 17 additions & 8 deletions scripts/verify-pins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
}
9 changes: 7 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,35 @@ 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}

Usage: alab <tool> [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 {
const program = new Command();
program.exitOverride();
program.name("alab").description("AgentLab tools").version(versionText());
program.addCommand(createPagesCommand());
program.addCommand(createReviewCommand());
return program;
}

Expand All @@ -40,7 +45,7 @@ export async function main(argv = process.argv): Promise<number> {
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;
}
Expand Down
23 changes: 21 additions & 2 deletions tests/router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand All @@ -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"]);

Expand All @@ -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");
});
});
1 change: 1 addition & 0 deletions vendor/review
Submodule review added at 94650d
Loading