diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..791066f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,41 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + verify: + strategy: + matrix: + include: + - os: ubuntu-latest + target: bun-linux-x64 + - os: ubuntu-24.04-arm + target: bun-linux-arm64 + - os: macos-14 + target: bun-darwin-arm64 + - os: macos-15-intel + target: bun-darwin-x64 + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + submodules: true + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + bun-version: 1.3.10 + - run: bun install --frozen-lockfile + - run: bun run verify:pins + - run: bun run test + - run: bun build --compile --minify --target=${{ matrix.target }} --outfile=dist/alab src/cli.ts + - run: bun run test:binary + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: alab-${{ matrix.target }} + path: dist/alab + if-no-files-found: error diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..f1dee68 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,65 @@ +name: Release + +on: + push: + tags: ["v*"] + +permissions: + contents: read + +jobs: + build: + strategy: + matrix: + include: + - os: ubuntu-latest + target: bun-linux-x64 + - os: ubuntu-24.04-arm + target: bun-linux-arm64 + - os: macos-14 + target: bun-darwin-arm64 + - os: macos-15-intel + target: bun-darwin-x64 + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + submodules: true + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + bun-version: 1.3.10 + - run: bun install --frozen-lockfile + - run: bun run verify:pins + - run: bun run test + - run: bun build --compile --minify --target=${{ matrix.target }} --outfile=dist/alab src/cli.ts + - run: bun run test:binary + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: alab-${{ matrix.target }} + path: dist/alab + if-no-files-found: error + + release: + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + path: artifacts + - name: Package exact tested binaries + run: | + mkdir -p dist staging + for artifact_dir in artifacts/alab-bun-*; do + target="${artifact_dir##*/alab-bun-}" + install -m 0755 "$artifact_dir/alab" staging/alab + touch -d '@0' staging/alab + tar --sort=name --format=ustar --owner=0 --group=0 --numeric-owner --mtime='@0' -C staging -cf - alab | gzip -n > "dist/alab-$target.tar.gz" + done + LC_ALL=C sha256sum dist/*.tar.gz > dist/SHA256SUMS + - uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 + with: + files: | + dist/*.tar.gz + dist/SHA256SUMS diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba34fb6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +dist/ +node_modules/ +.DS_Store diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..3a1a0b6 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "vendor/pages"] + path = vendor/pages + url = https://github.com/agentlab-in/pages.git diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..cacc5f6 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,26 @@ +# Alab CLI + +This repository owns command routing, exact tool pins, combined executable builds, and release automation for the `alab` CLI. + +## Boundaries + +- Tool repositories own their implementations. Do not fork tool behavior here. +- Integrations must use an exact version and commit SHA. +- A released artifact must not download a runtime or helper executable when used. +- Preserve child command arguments, standard input, standard output, standard error, and exit status. +- Add only platforms exercised by CI with a real compiled executable. +- Do not publish releases or tags without explicit authorization. + +## Workflow + +Use Bun 1.3.10 for dependency installation, tests, and compilation. After changing a pin or router, run: + +```bash +bun install --frozen-lockfile +bun run verify:pins +bun run test +bun run build +bun run test:binary +``` + +Run Git commands from this repository. Preserve unrelated work. Never commit credentials, generated `dist/` files, or local Alab state. diff --git a/README.md b/README.md index c3c3301..308e0e9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,53 @@ # Alab CLI -Alab is the self-contained command line entry point for AgentLab tools. +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. -The first integrated tool is [AgentLab Pages](https://github.com/agentlab-in/pages). Downloadable binaries and usage documentation are being prepared. +No separate Node.js, npm, Bun, Python, Go, Wrangler, or helper CLI installation is required to run a downloaded binary. + +## Install + +Download the archive matching your system from the repository's Releases page, verify it with `SHA256SUMS`, extract `alab`, and put it on your `PATH`. + +Supported release targets: + +| Operating system | CPU | +| --- | --- | +| macOS | Apple silicon, Intel | +| Linux | x86-64, ARM64 | + +Windows is not currently built or tested. + +```bash +alab --version +alab pages --help +alab pages setup +alab pages put ./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. + +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 + +The repository pins Bun and every tool revision. Bun is a build dependency only. + +```bash +bun install --frozen-lockfile +bun run verify:pins +bun run test +bun run build +bun run test:binary +``` + +`bun run build:all` cross-compiles the four declared release targets into `dist/`. + +Every CI run retains its four verified platform binaries as downloadable workflow artifacts. Tagged releases package the same target set into archives with a `SHA256SUMS` manifest. + +## Security + +Every published page is public. The optional Pages root password only obscures the root listing and does not protect direct page URLs. Never publish credentials or private content. + +## License + +MIT diff --git a/USAGE.md b/USAGE.md new file mode 100644 index 0000000..263c472 --- /dev/null +++ b/USAGE.md @@ -0,0 +1,56 @@ +# Usage + +## Consumer usage + +### Prerequisites + +Downloaded Alab binaries have no language runtime or helper CLI prerequisite. Publishing requires a Cloudflare account and an API token with `Account`, `Cloudflare Pages`, `Edit` permission. + +### Installation + +Download the archive for your operating system and CPU from Releases. Verify the archive against `SHA256SUMS`, extract it, and place `alab` in a directory on `PATH`. + +### Configuration + +Run: + +```bash +alab pages setup +``` + +The command links to Cloudflare's token creation page, accepts token input without echoing it, validates access, selects an account, and creates or reuses the configured Pages project. On macOS, the token is stored in Keychain. Other settings and Linux token storage use files under `~/.alab/` with restricted permissions. + +For noninteractive use, provide `CLOUDFLARE_API_TOKEN` and the required setup options. Missing input fails instead of waiting for a prompt. + +### Commands + +```text +alab pages setup [options] +alab pages put [dir] [--id ] [--dry-run] [--skip-deploy] [--json] +alab pages remove [id] [--dry-run] [--skip-deploy] [--json] +alab pages read [id] [--dir ] [--json] +alab pages list [--json] +alab pages open [id] +alab pages info +``` + +`delete` aliases `remove`, and `ls` aliases `list`. + +`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. + +### Availability and limitations + +Release automation builds macOS binaries for Apple silicon and Intel, plus Linux binaries for x86-64 and ARM64. CI runs the compiled command smoke test natively on all four operating system and CPU combinations. + +Pages accepts ready-made static files only. It does not build frameworks, run servers, recover state across machines, download deployed pages, or manage custom domains. The local machine is authoritative. Every page URL is public. + +## Enterprise usage + +Alab can be integrated into internal agent workflows that need a stable, inspectable command surface for publishing static artifacts through organization-owned Cloudflare infrastructure. + +If you want to deploy this internally, integrate it with your existing systems, +adapt it to your environment, or discuss adjacent engineering work, book a call: + +https://cal.com/harshitsinghbhandari/30min diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..51e23ba --- /dev/null +++ b/bun.lock @@ -0,0 +1,29 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "@agentlab/alab-cli", + "dependencies": { + "@noble/hashes": "2.4.0", + "commander": "13.1.0", + }, + "devDependencies": { + "@types/bun": "1.3.10", + }, + }, + }, + "packages": { + "@noble/hashes": ["@noble/hashes@2.4.0", "", {}, "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA=="], + + "@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], + + "@types/node": ["@types/node@26.5.1", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-CzNm2FezW4VR/LjG6yUdiEgLE/rAQ9Slj5gCu/C2VrdcW7I0ahNZ8DRbHT7zOZ6r3ONgd/bsQIeSaoDGrd1C6g=="], + + "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], + + "commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="], + + "undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="], + } +} diff --git a/docs/integration.md b/docs/integration.md new file mode 100644 index 0000000..b378ed8 --- /dev/null +++ b/docs/integration.md @@ -0,0 +1,27 @@ +# Tool integration interface + +## Current interface + +The router imports `runCli(argv)` from the exact Pages `src/program.ts` revision and calls it in the same process when the first argument is `pages`. Pages receives the original `process.argv` array. + +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 v2.0.0 deploys through the Cloudflare HTTP API and does not invoke Wrangler. + +## 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. + +JavaScript packages use exact versions and a committed Bun lockfile. The release workflow uses a pinned Bun version and emits `SHA256SUMS` for downloadable archives. + +## Updating Pages + +1. Fetch tags inside `vendor/pages`. +2. Check out the intended immutable release commit. +3. Update the Pages version, commit, and package hash in `pins.json`. +4. Review the Pages 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/docs/spec.md b/docs/spec.md new file mode 100644 index 0000000..83ffaca --- /dev/null +++ b/docs/spec.md @@ -0,0 +1,28 @@ +# Alab Pages integration specification + +## Outcome + +Distribute one `alab` executable per supported operating system and CPU. The executable routes `alab pages ...` to the pinned Pages implementation without requiring or downloading a runtime or helper CLI. + +## Decisions + +- Bun compile produces native executables and is used only in development and CI. +- Pages v2.0.0 plus its side-effect-free integration entry point is pinned as a Git submodule at merged commit `c6a71e826bfecb9f7c65c88c1f1d28ba23f72bd8`. +- Runtime dependencies are exact versions in `package.json` and `bun.lock`. +- Pages executes in-process. Arguments and process streams remain unchanged. Its rejection reaches one top-level error handler and sets exit code 1. +- `alab --version` reports the router version and every integrated tool version and SHA. +- Releases contain compressed binaries and a `SHA256SUMS` integrity manifest. +- CI runs native binary smoke tests on macOS ARM64, macOS Intel, Linux x86-64, and Linux ARM64. + +## Excluded + +Review integration, a plugin system, Pages recovery or pull behavior, framework builds, and custom-domain management are not part of this integration. + +## Completion checks + +1. Verify the Pages submodule SHA and package version. +2. Run router tests. +3. Compile the host executable. +4. Run Pages help, info, put, read, list, update, and remove against isolated temporary state with development tools absent from `PATH`. +5. Build all release targets and produce deterministic artifact names and checksums. +6. Retain each natively tested binary as a downloadable CI artifact. diff --git a/package.json b/package.json new file mode 100644 index 0000000..f0edcd5 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "@agentlab/alab-cli", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "bun build --compile --minify --outfile dist/alab src/cli.ts", + "build:all": "bun scripts/build-all.ts", + "test": "bun test tests/router.test.ts", + "test:binary": "bash scripts/smoke-binary.sh dist/alab", + "verify:pins": "bun scripts/verify-pins.ts" + }, + "dependencies": { + "@noble/hashes": "2.4.0", + "commander": "13.1.0" + }, + "devDependencies": { + "@types/bun": "1.3.10" + }, + "engines": { + "bun": "1.3.10" + } +} diff --git a/pins.json b/pins.json new file mode 100644 index 0000000..dcfcd37 --- /dev/null +++ b/pins.json @@ -0,0 +1,8 @@ +{ + "pages": { + "version": "2.0.0", + "repository": "https://github.com/agentlab-in/pages.git", + "commit": "c6a71e826bfecb9f7c65c88c1f1d28ba23f72bd8", + "packageSha256": "33359d14b47a323d4447890b4e5edf8700b25956e1a46f3843c6d0ab52287125" + } +} diff --git a/scripts/build-all.ts b/scripts/build-all.ts new file mode 100644 index 0000000..41cf14f --- /dev/null +++ b/scripts/build-all.ts @@ -0,0 +1,16 @@ +import { mkdirSync, rmSync } from "node:fs"; +import { resolve } from "node:path"; +import { spawnSync } from "node:child_process"; + +const root = resolve(import.meta.dir, ".."); +const dist = resolve(root, "dist"); +const targets = ["bun-darwin-arm64", "bun-darwin-x64", "bun-linux-x64", "bun-linux-arm64"]; + +rmSync(dist, { recursive: true, force: true }); +mkdirSync(dist, { recursive: true }); + +for (const target of targets) { + const name = target.replace(/^bun-/, ""); + const result = spawnSync("bun", ["build", "--compile", "--minify", `--target=${target}`, `--outfile=${resolve(dist, `alab-${name}`)}`, resolve(root, "src/cli.ts")], { stdio: "inherit" }); + if (result.status !== 0) process.exit(result.status ?? 1); +} diff --git a/scripts/smoke-binary.sh b/scripts/smoke-binary.sh new file mode 100644 index 0000000..591d1ac --- /dev/null +++ b/scripts/smoke-binary.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +set -euo pipefail +trap 'echo "smoke assertion failed at line $LINENO: status=$status stdout=$stdout stderr=$stderr" >&2' ERR + +binary="$(cd "$(dirname "$1")" && pwd)/$(basename "$1")" +status=0 +stdout="" +stderr="" +scratch="$(mktemp -d)" +trap 'rm -rf "$scratch"' EXIT + +mkdir -p "$scratch/site" "$scratch/empty-path" +printf '

first

\n' > "$scratch/site/index.html" + +run_clean() { + env -i PATH="$scratch/empty-path" ALAB_HOME="$scratch/home" ALAB_PAGES_CONTENT="$scratch/content" "$binary" "$@" +} + +capture() { + set +e + run_clean "$@" >"$scratch/stdout" 2>"$scratch/stderr" + status=$? + set -e + stdout="$(<"$scratch/stdout")" + stderr="$(<"$scratch/stderr")" +} + +assert_success() { + [[ "$status" -eq 0 ]] + [[ -z "$stderr" ]] +} + +assert_json() { + [[ "$stdout" == \{*\} || "$stdout" == \[*\] ]] +} + +capture --version +assert_success +[[ "$stdout" == $'alab 0.1.0\npages 2.0.0 (c6a71e826bfecb9f7c65c88c1f1d28ba23f72bd8)' ]] + +capture pages --help +assert_success +[[ "$stdout" == *'Usage: alab pages [options] [command]'* ]] +[[ "$stdout" == *'setup [options]'* ]] +[[ "$stdout" == *'put [options] [dir]'* ]] + +capture pages info +assert_success +[[ "$stdout" == *'CF token missing'* ]] + +capture pages put "$scratch/site" --id Alpha9 --skip-deploy --json +assert_success +assert_json +[[ "$stdout" =~ \"id\"[[:space:]]*:[[:space:]]*\"alpha9\" ]] +[[ "${stdout#*\"id\"}" != *'"id"'* ]] + +capture pages read --dir "$scratch/site" --json +assert_success +assert_json +[[ "$stdout" =~ \"id\"[[:space:]]*:[[:space:]]*\"alpha9\" ]] + +capture pages list --json +assert_success +assert_json +[[ "$stdout" =~ \"id\"[[:space:]]*:[[:space:]]*\"alpha9\" ]] + +printf '

updated

\n' > "$scratch/site/index.html" +capture pages put "$scratch/site" --skip-deploy --json +assert_success +assert_json +[[ "$stdout" =~ \"id\"[[:space:]]*:[[:space:]]*\"alpha9\" ]] + +capture pages remove alpha9 --skip-deploy --json +assert_success +assert_json +[[ "$stdout" =~ \"id\"[[:space:]]*:[[:space:]]*\"alpha9\" ]] + +capture pages read missing --json +[[ "$status" -eq 1 ]] +[[ -z "$stdout" ]] +[[ "$stderr" == 'Error: Unknown page id "missing". Run agentlab-pages list.' ]] + +capture unknown +[[ "$status" -eq 1 ]] +[[ -z "$stdout" ]] +[[ "$stderr" == "Error: unknown tool 'unknown'. Run 'alab --help'." ]] + +echo "compiled binary smoke test passed" diff --git a/scripts/verify-pins.ts b/scripts/verify-pins.ts new file mode 100644 index 0000000..6eb556d --- /dev/null +++ b/scripts/verify-pins.ts @@ -0,0 +1,19 @@ +import { createHash } from "node:crypto"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { spawnSync } from "node:child_process"; +import pins from "../pins.json"; +import pagesPackage from "../vendor/pages/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 packageHash = createHash("sha256").update(readFileSync(packagePath)).digest("hex"); +if (packageHash !== pins.pages.packageSha256) throw new Error(`Pages package hash mismatch: ${packageHash}`); + +console.log(`pages ${pins.pages.version} ${pins.pages.commit} sha256:${packageHash}`); diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..2b637dc --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,50 @@ +#!/usr/bin/env bun +import alabPackage from "../package.json"; +import pins from "../pins.json"; +import pagesPackage from "../vendor/pages/package.json"; +import { runCli as runPagesCli } from "../vendor/pages/src/program.ts"; + +const HELP = `Alab ${alabPackage.version} + +Usage: alab [command] [options] + +Tools: + pages Publish static sites with AgentLab Pages + +Commands: + alab pages --help Show Pages 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})`; +} + +export async function main(argv = process.argv): Promise { + const args = argv.slice(2); + if (args.length === 0 || args[0] === "help" || args[0] === "--help" || args[0] === "-h") { + process.stdout.write(HELP); + return 0; + } + if (args[0] === "--version" || args[0] === "-V") { + console.log(versionText()); + return 0; + } + if (args[0] !== "pages") { + console.error(`Error: unknown tool '${args[0]}'. Run 'alab --help'.`); + return 1; + } + + await runPagesCli(argv); + return process.exitCode ?? 0; +} + +if (import.meta.main) { + main().then((code) => { + process.exitCode = code; + }).catch((error: unknown) => { + console.error(`Error: ${error instanceof Error ? error.message : String(error)}`); + process.exitCode = 1; + }); +} diff --git a/tests/router.test.ts b/tests/router.test.ts new file mode 100644 index 0000000..aef9753 --- /dev/null +++ b/tests/router.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, test } from "bun:test"; +import { versionText } from "../src/cli.ts"; +import pins from "../pins.json"; + +function run(args: string[]) { + return Bun.spawnSync([process.execPath, "src/cli.ts", ...args], { + cwd: import.meta.dir + "/..", + env: {}, + stdout: "pipe", + stderr: "pipe", + }); +} + +describe("Alab router", () => { + test("reports combined and pinned Pages versions", () => { + expect(versionText()).toBe(`alab 0.1.0\npages 2.0.0 (${pins.pages.commit})`); + }); + + test("routes Pages help with exact successful streams", () => { + const result = run(["pages", "--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 pages [options] [command]"); + expect(stdout).toContain("put [options] [dir]"); + }); + + test("returns an exact error status without polluting stdout", () => { + const result = run(["unknown"]); + + expect(result.exitCode).toBe(1); + expect(result.stdout.toString()).toBe(""); + expect(result.stderr.toString()).toBe("Error: unknown tool 'unknown'. Run 'alab --help'.\n"); + }); +}); diff --git a/vendor/pages b/vendor/pages new file mode 160000 index 0000000..c6a71e8 --- /dev/null +++ b/vendor/pages @@ -0,0 +1 @@ +Subproject commit c6a71e826bfecb9f7c65c88c1f1d28ba23f72bd8