From c52a59c9272b919b92750e38a3bc746ae21b1603 Mon Sep 17 00:00:00 2001 From: Erik Thorelli Date: Tue, 6 Oct 2026 18:45:18 -0700 Subject: [PATCH] Rename the CLI to allthings, keeping atw as an alias for now The package and binary are allthings (cli/), at 2.0.0-alpha.2. The old name still runs, from the package's atw bin or the installer's atw symlink, and prints a note on stderr. The site it reads is one value, siteOrigin in cli/src/config.ts, which stays on allthingsweb.dev until allthings.dev serves the new site. ALLTHINGS_MCP_URL replaces ATW_MCP_URL, which is still read. Releases ship allthings- binaries, and a new installer puts allthings and atw in ~/.allthings/bin, falling back to the old asset names for earlier releases. --- .github/workflows/cli-release.yaml | 10 ++-- .github/workflows/cli.yaml | 23 +++++---- .github/workflows/lint.yaml | 2 +- Makefile | 6 +-- README.md | 2 +- atw-cli/src/index.ts | 28 ----------- {atw-cli => cli}/.gitignore | 5 ++ cli/README.md | 44 +++++++++++++++++ {atw-cli => cli}/bun.lock | 2 +- {atw-cli => cli}/package.json | 9 ++-- cli/src/atw.ts | 5 ++ {atw-cli => cli}/src/cli.ts | 48 ++++++++++++------- {atw-cli => cli}/src/client.ts | 5 +- cli/src/config.ts | 37 +++++++++++++++ {atw-cli => cli}/src/errors.ts | 0 {atw-cli => cli}/src/format.ts | 0 cli/src/index.ts | 7 +++ cli/src/main.ts | 32 +++++++++++++ {atw-cli => cli}/src/schemas.ts | 0 {atw-cli => cli}/tests/cli.test.ts | 37 +++++++++++++-- {atw-cli => cli}/tests/client.test.ts | 2 +- cli/tests/config.test.ts | 62 ++++++++++++++++++++++++ {atw-cli => cli}/tests/contract.test.ts | 0 {atw-cli => cli}/tests/fixtures.ts | 0 {atw-cli => cli}/tsconfig.json | 0 infra/docs/r2-migration.md | 2 +- pages/atw-install.bash | 44 ++--------------- pages/install.bash | 63 +++++++++++++++++++++++++ web/src/mcp/tools.ts | 2 +- web/tests/support/legacy-urls.ts | 6 +-- 30 files changed, 359 insertions(+), 124 deletions(-) delete mode 100644 atw-cli/src/index.ts rename {atw-cli => cli}/.gitignore (80%) create mode 100644 cli/README.md rename {atw-cli => cli}/bun.lock (99%) rename {atw-cli => cli}/package.json (60%) create mode 100644 cli/src/atw.ts rename {atw-cli => cli}/src/cli.ts (73%) rename {atw-cli => cli}/src/client.ts (96%) create mode 100644 cli/src/config.ts rename {atw-cli => cli}/src/errors.ts (100%) rename {atw-cli => cli}/src/format.ts (100%) create mode 100644 cli/src/index.ts create mode 100644 cli/src/main.ts rename {atw-cli => cli}/src/schemas.ts (100%) rename {atw-cli => cli}/tests/cli.test.ts (79%) rename {atw-cli => cli}/tests/client.test.ts (98%) create mode 100644 cli/tests/config.test.ts rename {atw-cli => cli}/tests/contract.test.ts (100%) rename {atw-cli => cli}/tests/fixtures.ts (100%) rename {atw-cli => cli}/tsconfig.json (100%) create mode 100644 pages/install.bash diff --git a/.github/workflows/cli-release.yaml b/.github/workflows/cli-release.yaml index 4eddbcd4..e36bec7f 100644 --- a/.github/workflows/cli-release.yaml +++ b/.github/workflows/cli-release.yaml @@ -25,15 +25,15 @@ jobs: - name: ๐Ÿ”Ž Check the tag matches the CLI version run: | - VERSION=$(jq -r .version atw-cli/package.json) + VERSION=$(jq -r .version cli/package.json) if [ "$GITHUB_REF_NAME" != "$VERSION" ]; then - echo "::error::Tag $GITHUB_REF_NAME does not match atw-cli/package.json version $VERSION" + echo "::error::Tag $GITHUB_REF_NAME does not match cli/package.json version $VERSION" exit 1 fi - name: ๐Ÿ“ฅ Download deps run: bash "$GITHUB_WORKSPACE/.github/scripts/bun-install.sh" - working-directory: atw-cli + working-directory: cli - name: ๐Ÿ”จ Compiling the different versions run: make build-all-cli @@ -53,8 +53,8 @@ jobs: run: | ASSET_PLATFORMS=("bun-linux-x64" "bun-linux-arm64" "bun-windows-x64.exe" "bun-darwin-x64" "bun-darwin-arm64") for platform in "${ASSET_PLATFORMS[@]}"; do - if [ -f "atw-cli/atw-cli-$platform" ]; then - gh release upload "${GITHUB_REF_NAME}" "atw-cli/atw-cli-$platform" --clobber + if [ -f "cli/allthings-$platform" ]; then + gh release upload "${GITHUB_REF_NAME}" "cli/allthings-$platform" --clobber echo "โœ… Uploaded file for platform $platform" else echo "โŒ File for platform $platform not found, skipping." diff --git a/.github/workflows/cli.yaml b/.github/workflows/cli.yaml index 973adbd3..9cd7bc7e 100644 --- a/.github/workflows/cli.yaml +++ b/.github/workflows/cli.yaml @@ -4,7 +4,7 @@ on: push: branches: ["main"] paths: - - "atw-cli/**" + - "cli/**" - "app/src/lib/public-api/**" - "app/package.json" - "app/bun.lock" @@ -13,7 +13,7 @@ on: pull_request: types: [opened, synchronize, reopened] paths: - - "atw-cli/**" + - "cli/**" - "app/src/lib/public-api/**" - "app/package.json" - "app/bun.lock" @@ -44,24 +44,27 @@ jobs: run: bash "$GITHUB_WORKSPACE/.github/scripts/bun-install.sh" - name: ๐Ÿ“ฅ Download CLI deps - working-directory: atw-cli + working-directory: cli run: bash "$GITHUB_WORKSPACE/.github/scripts/bun-install.sh" - name: ๐Ÿ’„ Format check - working-directory: atw-cli + working-directory: cli run: bun run check - name: ๐Ÿ” Type check - working-directory: atw-cli + working-directory: cli run: bun run typecheck - name: ๐Ÿ„ Run the tests - working-directory: atw-cli + working-directory: cli run: bun run test - name: ๐Ÿ“ฆ Compile and smoke-test the binary - working-directory: atw-cli + working-directory: cli run: | - bun build --compile --minify src/index.ts --outfile atw - ./atw --version - ./atw --help + bun build --compile --minify src/index.ts --outfile allthings + ./allthings --version + ./allthings --help + # The old name still runs, with its note on stderr. + ln -s allthings atw + ./atw --version 2>&1 >/dev/null | grep -q "atw is now allthings" diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index a2fd4153..bc01296b 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -39,7 +39,7 @@ jobs: run: bash "$GITHUB_WORKSPACE/.github/scripts/bun-install.sh" - name: ๐Ÿ“ฅ Download CLI deps - working-directory: atw-cli + working-directory: cli run: bash "$GITHUB_WORKSPACE/.github/scripts/bun-install.sh" - name: ๐Ÿ“ฅ Download infra deps diff --git a/Makefile b/Makefile index c205e747..6f6c944d 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ PACKAGE_MANAGER := bun CURRENT_DIR := $(shell pwd) DEPENDENCIES := bun git APP_DIR := $(CURRENT_DIR)/app -CLI_DIR := $(CURRENT_DIR)/atw-cli +CLI_DIR := $(CURRENT_DIR)/cli .PHONY: list list: @@ -59,13 +59,13 @@ serve: app/.env ## Serve the Next.js application .PHONY: build-cli build-cli: ## Build CLI - @cd $(CLI_DIR) && bun build --compile --minify src/index --outfile atw-cli + @cd $(CLI_DIR) && bun build --compile --minify src/index --outfile allthings @cd $(CLI_DIR) && rm -f .*.bun-build .PHONY: build-all-cli build-all-cli: @cd $(CLI_DIR) && for target in bun-linux-x64 bun-linux-arm64 bun-windows-x64 bun-darwin-x64 bun-darwin-arm64; do \ - bun build --compile --minify src/index --outfile atw-cli-$$target --target=$$target; \ + bun build --compile --minify src/index --outfile allthings-$$target --target=$$target; \ done @cd $(CLI_DIR) && rm -f .*.bun-build diff --git a/README.md b/README.md index ce035090..adf907aa 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ ### ๐Ÿ“ Directories - `app`: the Next.js web application -- `atw-cli`: the CLI tool +- `cli`: `allthings`, the command-line client for people and agents (see [cli/README.md](cli/README.md)) - `core`: the Effect domain models, public contract and data access, shared by the Worker and the CLI - `web`: the Cloudflare Worker replacing the app: the public API and the MCP server so far. `core` and `web` form a bun workspace: run `bun install` at the repository root - `infra`: Cloudflare infrastructure as code, with Alchemy diff --git a/atw-cli/src/index.ts b/atw-cli/src/index.ts deleted file mode 100644 index bb5cbacb..00000000 --- a/atw-cli/src/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bun -import { run } from "./cli.ts"; - -const opener = - process.platform === "darwin" - ? ["open"] - : process.platform === "win32" - ? ["cmd", "/c", "start", ""] - : ["xdg-open"]; - -const exitCode = await run(process.argv.slice(2), { - stdout: (text) => process.stdout.write(text), - stderr: (text) => process.stderr.write(text), - // oxlint-disable-next-line typescript/no-unnecessary-boolean-literal-compare -- @types/node says boolean, but non-TTY streams leave isTTY undefined - isTTY: process.stdout.isTTY === true, - env: process.env, - openUrl: async (url) => { - // The link is already printed, so a missing opener (no xdg-open) is fine. - try { - await Bun.spawn([...opener, url], { stdout: "ignore", stderr: "ignore" }) - .exited; - } catch { - return; - } - }, -}); - -process.exit(exitCode); diff --git a/atw-cli/.gitignore b/cli/.gitignore similarity index 80% rename from atw-cli/.gitignore rename to cli/.gitignore index a14702c4..b3c9bbc7 100644 --- a/atw-cli/.gitignore +++ b/cli/.gitignore @@ -32,3 +32,8 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # Finder (MacOS) folder config .DS_Store + +# compiled binaries (make build-cli, make build-all-cli, CI) +/allthings +/allthings-bun-* +/atw diff --git a/cli/README.md b/cli/README.md new file mode 100644 index 00000000..2b0999ac --- /dev/null +++ b/cli/README.md @@ -0,0 +1,44 @@ +# allthings + +all things from your terminal: what's coming up, who's on stage and how to get in. It reads the same public MCP server agents use, so what you see here is what they see. + +```sh +curl -fsSL https://allthingsweb-dev.github.io/allthingsweb/install.bash | bash +``` + +That installs the latest stable release in `~/.allthings/bin`. `allthings` 2 is in prerelease until the launch, so ask for one by name: + +```sh +curl -fsSL https://allthingsweb-dev.github.io/allthingsweb/install.bash | ALLTHINGS_VERSION=2.0.0-alpha.2 bash +``` + +## Use + +```sh +allthings events # what's coming up +allthings events --past --limit 5 # the five most recent evenings +allthings event # one evening: when, where, the talks +allthings speakers react # who has been on stage about react +allthings rsvp # open the evening's page to say you're in +allthings about # where to find everyone between evenings +``` + +Add `--json` to any of them for the public contract, unformatted, for scripts and agents. Exit codes are part of that contract: 0 ok, 1 service error, 2 usage error, 3 no such evening. + +`ALLTHINGS_MCP_URL` points it at another server, such as a local Worker. + +## The old name + +This CLI used to be `atw`. The installer still puts an `atw` beside `allthings`, and the package still has an `atw` bin. Both run the same CLI and print a note on stderr, so scripts reading stdout keep working. The old name goes away in a later release. `ATW_MCP_URL` and `ATW_VERSION` are still read when the new names are unset. + +## Working on it + +```sh +bun install +bun run check && bun run typecheck && bun test +bun run src/index.ts events +``` + +`src/config.ts` holds the site it talks to, `siteOrigin`. It stays on allthingsweb.dev until allthings.dev serves the new site: until then allthings.dev only redirects, and a redirect drops an MCP POST. It flips with the domain move (`infra/docs/r2-migration.md`, Phase 4). + +Releases are GitHub releases, cut by pushing a tag that matches `version` in `package.json` (`.github/workflows/cli-release.yaml`). A hyphenated tag (`2.0.0-alpha.2`) is a prerelease, and the installer skips it unless asked. Nothing is published to npm: the package is private. diff --git a/atw-cli/bun.lock b/cli/bun.lock similarity index 99% rename from atw-cli/bun.lock rename to cli/bun.lock index d612ed94..2b8e8187 100644 --- a/atw-cli/bun.lock +++ b/cli/bun.lock @@ -3,7 +3,7 @@ "configVersion": 1, "workspaces": { "": { - "name": "atw-cli", + "name": "allthings", "dependencies": { "zod": "^4.6.5", }, diff --git a/atw-cli/package.json b/cli/package.json similarity index 60% rename from atw-cli/package.json rename to cli/package.json index 50842dd4..f457cb0a 100644 --- a/atw-cli/package.json +++ b/cli/package.json @@ -1,11 +1,12 @@ { - "name": "atw-cli", - "version": "2.0.0-alpha.1", - "description": "All Things Web from your terminal and your agents.", + "name": "allthings", + "version": "2.0.0-alpha.2", + "description": "all things from your terminal and your agents: the evenings, who is on stage and how to get in.", "type": "module", "private": true, "bin": { - "atw": "./src/index.ts" + "allthings": "./src/index.ts", + "atw": "./src/atw.ts" }, "scripts": { "fmt": "prettier --write .", diff --git a/cli/src/atw.ts b/cli/src/atw.ts new file mode 100644 index 00000000..12a5ca0a --- /dev/null +++ b/cli/src/atw.ts @@ -0,0 +1,5 @@ +#!/usr/bin/env bun +import { main } from "./main.ts"; + +// The package's `atw` bin: the old name, kept as an alias for now. +await main(true); diff --git a/atw-cli/src/cli.ts b/cli/src/cli.ts similarity index 73% rename from atw-cli/src/cli.ts rename to cli/src/cli.ts index 82e7fc9a..2b890e8c 100644 --- a/atw-cli/src/cli.ts +++ b/cli/src/cli.ts @@ -1,6 +1,13 @@ import { parseArgs } from "node:util"; import packageJson from "../package.json" with { type: "json" }; -import { createClient, defaultEndpoint, type Client } from "./client.ts"; +import { createClient, type Client } from "./client.ts"; +import { + commandName, + defaultEndpoint, + endpointFrom, + endpointVariable, + legacyCommandName, +} from "./config.ts"; import { CliError, ExitCode } from "./errors.ts"; import { formatCommunity, @@ -17,23 +24,25 @@ export type Io = { env: Record; openUrl: (url: string) => Promise; client?: Client; + /** True when started by its old name, `atw`, which still works for now. */ + legacy?: boolean; }; -const help = `atw ยท All Things Web from your terminal and your agents +const help = `${commandName} ยท evenings for people who build software, from your terminal and your agents Usage - atw events [--past | --all] [--limit ] [--json] - atw event [--json] - atw speakers [search] [--limit ] [--json] - atw rsvp [--print] - atw about [--json] + ${commandName} events [--past | --all] [--limit ] [--json] + ${commandName} event [--json] + ${commandName} speakers [search] [--limit ] [--json] + ${commandName} rsvp [--print] + ${commandName} about [--json] Examples - atw events What's coming up - atw events --past --limit 5 The five most recent events - atw event --json Full details for scripts and agents - atw speakers react Past speakers matching "react" - atw rsvp Open the event's RSVP page + ${commandName} events What's coming up + ${commandName} events --past --limit 5 The five most recent evenings + ${commandName} event --json One evening in full, for scripts and agents + ${commandName} speakers react Who has been on stage about react + ${commandName} rsvp Open the evening's page to say you're in Options --json Print the public JSON contract instead of text @@ -41,12 +50,12 @@ Options -v, --version Show the version Exit codes: 0 ok, 1 service error, 2 usage error, 3 event not found. -Set ATW_MCP_URL to use another server (default ${defaultEndpoint}). +Set ${endpointVariable} to use another server (default ${defaultEndpoint}). `; function usage(message: string): never { throw new CliError( - `${message}\nRun "atw --help" for usage.`, + `${message}\nRun "${commandName} --help" for usage.`, ExitCode.UsageError, ); } @@ -60,7 +69,11 @@ function parseLimit(value: string | undefined, fallback: number, max: number) { return limit; } +/** What the old name prints, on stderr so scripts reading stdout are unaffected. */ +export const legacyNote = `${legacyCommandName} is now ${commandName}. The old name still works for now and goes away in a later release.\n`; + export async function run(argv: string[], io: Io): Promise { + if (io.legacy === true) io.stderr(legacyNote); try { const parsed = (() => { try { @@ -96,8 +109,7 @@ export async function run(argv: string[], io: Io): Promise { const color = io.isTTY && !io.env["NO_COLOR"] && !values.json; const client = - io.client ?? - createClient({ endpoint: io.env["ATW_MCP_URL"] ?? defaultEndpoint }); + io.client ?? createClient({ endpoint: endpointFrom(io.env) }); const print = (text: string, data: unknown) => io.stdout(values.json ? `${JSON.stringify(data, null, 2)}\n` : text); @@ -156,11 +168,11 @@ export async function run(argv: string[], io: Io): Promise { } } catch (error) { if (error instanceof CliError) { - io.stderr(`atw: ${error.message}\n`); + io.stderr(`${commandName}: ${error.message}\n`); return error.exitCode; } io.stderr( - `atw: ${error instanceof Error ? error.message : String(error)}\n`, + `${commandName}: ${error instanceof Error ? error.message : String(error)}\n`, ); return ExitCode.ServiceError; } diff --git a/atw-cli/src/client.ts b/cli/src/client.ts similarity index 96% rename from atw-cli/src/client.ts rename to cli/src/client.ts index ad8cb884..eaa03d9b 100644 --- a/atw-cli/src/client.ts +++ b/cli/src/client.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { commandName, defaultEndpoint } from "./config.ts"; import { CliError, ExitCode } from "./errors.ts"; import { communitySchema, @@ -11,8 +12,6 @@ import { type Speaker, } from "./schemas.ts"; -export const defaultEndpoint = "https://allthingsweb.dev/mcp"; - /** * Whether a get_event tool error means the slug has no published event. The * contract test checks this against the server's own message. @@ -146,7 +145,7 @@ export function createClient({ } catch (error) { if (error instanceof CliError && error.exitCode === ExitCode.NotFound) { throw new CliError( - `No published event has the slug "${slug}". Run "atw events --all" to find one.`, + `No published event has the slug "${slug}". Run "${commandName} events --all" to find one.`, ExitCode.NotFound, ); } diff --git a/cli/src/config.ts b/cli/src/config.ts new file mode 100644 index 00000000..848eefc8 --- /dev/null +++ b/cli/src/config.ts @@ -0,0 +1,37 @@ +/** + * The site the CLI talks to, and the one value that moves it. allthingsweb.dev + * serves the MCP server until allthings.dev does: before the domain moves, + * allthings.dev only redirects there, and a redirect drops an MCP POST. Flip + * this to "https://allthings.dev" once the new site answers on it + * (infra/docs/r2-migration.md, Phase 4). + */ +export const siteOrigin: "https://allthingsweb.dev" | "https://allthings.dev" = + "https://allthingsweb.dev"; + +/** The public MCP server every command reads from. */ +export const defaultEndpoint = `${siteOrigin}/mcp`; + +/** The command's name, and the one it replaces, kept as an alias for now. */ +export const commandName = "allthings"; +export const legacyCommandName = "atw"; + +/** The environment variable that points the CLI at another MCP server. */ +export const endpointVariable = "ALLTHINGS_MCP_URL"; +/** Its old name, still read when the new one is unset. */ +export const legacyEndpointVariable = "ATW_MCP_URL"; + +/** + * Whether the CLI was started by its old name: an `atw` symlink to the + * compiled binary, or `atw.exe`. `argv0` is the name it was invoked as. + */ +export function invokedAsLegacy(argv0: string): boolean { + const base = argv0.split(/[\\/]/).at(-1) ?? ""; + return base.replace(/\.exe$/i, "") === legacyCommandName; +} + +/** The MCP server to use: the environment's choice, else {@link defaultEndpoint}. */ +export function endpointFrom(env: Record): string { + return ( + env[endpointVariable] ?? env[legacyEndpointVariable] ?? defaultEndpoint + ); +} diff --git a/atw-cli/src/errors.ts b/cli/src/errors.ts similarity index 100% rename from atw-cli/src/errors.ts rename to cli/src/errors.ts diff --git a/atw-cli/src/format.ts b/cli/src/format.ts similarity index 100% rename from atw-cli/src/format.ts rename to cli/src/format.ts diff --git a/cli/src/index.ts b/cli/src/index.ts new file mode 100644 index 00000000..e69d7b96 --- /dev/null +++ b/cli/src/index.ts @@ -0,0 +1,7 @@ +#!/usr/bin/env bun +import { invokedAsLegacy } from "./config.ts"; +import { main } from "./main.ts"; + +// The compiled binary is also installed as an `atw` symlink, so the name it +// was started by decides whether to print the old name's note. +await main(invokedAsLegacy(process.argv0)); diff --git a/cli/src/main.ts b/cli/src/main.ts new file mode 100644 index 00000000..08bab349 --- /dev/null +++ b/cli/src/main.ts @@ -0,0 +1,32 @@ +import { run } from "./cli.ts"; + +const opener = + process.platform === "darwin" + ? ["open"] + : process.platform === "win32" + ? ["cmd", "/c", "start", ""] + : ["xdg-open"]; + +/** Runs the CLI against this process; `legacy` is true when started as `atw`. */ +export async function main(legacy: boolean): Promise { + const exitCode = await run(process.argv.slice(2), { + stdout: (text) => process.stdout.write(text), + stderr: (text) => process.stderr.write(text), + // oxlint-disable-next-line typescript/no-unnecessary-boolean-literal-compare -- @types/node says boolean, but non-TTY streams leave isTTY undefined + isTTY: process.stdout.isTTY === true, + env: process.env, + legacy, + openUrl: async (url) => { + // The link is already printed, so a missing opener (no xdg-open) is fine. + try { + await Bun.spawn([...opener, url], { + stdout: "ignore", + stderr: "ignore", + }).exited; + } catch { + return; + } + }, + }); + process.exit(exitCode); +} diff --git a/atw-cli/src/schemas.ts b/cli/src/schemas.ts similarity index 100% rename from atw-cli/src/schemas.ts rename to cli/src/schemas.ts diff --git a/atw-cli/tests/cli.test.ts b/cli/tests/cli.test.ts similarity index 79% rename from atw-cli/tests/cli.test.ts rename to cli/tests/cli.test.ts index 945795d8..fe8fa023 100644 --- a/atw-cli/tests/cli.test.ts +++ b/cli/tests/cli.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { run, type Io } from "../src/cli.ts"; +import { legacyNote, run, type Io } from "../src/cli.ts"; import type { Client } from "../src/client.ts"; import { CliError, ExitCode } from "../src/errors.ts"; import { event, fakeClient, summary } from "./fixtures.ts"; @@ -34,12 +34,12 @@ async function exec( return { code, stdout, stderr, opened }; } -describe("atw", () => { +describe("allthings", () => { test("prints help with examples and exit codes", async () => { for (const argv of [[], ["--help"], ["help"]]) { const { code, stdout } = await exec(argv); expect(code).toBe(ExitCode.Ok); - expect(stdout).toContain("atw events [--past | --all]"); + expect(stdout).toContain("allthings events [--past | --all]"); expect(stdout).toContain( "Exit codes: 0 ok, 1 service error, 2 usage error, 3 event not found.", ); @@ -63,7 +63,7 @@ describe("atw", () => { ]) { const { code, stderr } = await exec(argv); expect(code).toBe(ExitCode.UsageError); - expect(stderr).toContain('Run "atw --help" for usage.'); + expect(stderr).toContain('Run "allthings --help" for usage.'); } }); @@ -117,7 +117,7 @@ describe("atw", () => { }); const { code, stderr } = await exec(["event", "x"], { client }); expect(code).toBe(ExitCode.NotFound); - expect(stderr).toBe('atw: No published event has the slug "x".\n'); + expect(stderr).toBe('allthings: No published event has the slug "x".\n'); }); test("rsvp prints the link and opens it only for a person at a terminal", async () => { @@ -149,3 +149,30 @@ describe("atw", () => { expect(about.stdout).toContain("https://discord.gg/B3Sm4b5mfD"); }); }); + +describe("atw, the old name", () => { + test("prints a note on stderr and otherwise behaves the same", async () => { + const io = (legacy: boolean) => { + let stdout = ""; + let stderr = ""; + const value: Io = { + stdout: (text) => (stdout += text), + stderr: (text) => (stderr += text), + isTTY: false, + env: {}, + openUrl: async () => {}, + client: fakeClient(), + legacy, + }; + return { value, out: () => ({ stdout, stderr }) }; + }; + const legacy = io(true); + const current = io(false); + expect(await run(["events", "--json"], legacy.value)).toBe(ExitCode.Ok); + expect(await run(["events", "--json"], current.value)).toBe(ExitCode.Ok); + expect(legacy.out().stdout).toBe(current.out().stdout); + expect(legacy.out().stderr).toBe(legacyNote); + expect(current.out().stderr).toBe(""); + expect(legacyNote).toContain("atw is now allthings"); + }); +}); diff --git a/atw-cli/tests/client.test.ts b/cli/tests/client.test.ts similarity index 98% rename from atw-cli/tests/client.test.ts rename to cli/tests/client.test.ts index 715456a7..cbebdb69 100644 --- a/atw-cli/tests/client.test.ts +++ b/cli/tests/client.test.ts @@ -78,7 +78,7 @@ describe("MCP client", () => { ExitCode.NotFound, ); expect(notFound.message).toBe( - `No published event has the slug "x". Run "atw events --all" to find one.`, + `No published event has the slug "x". Run "allthings events --all" to find one.`, ); const down = respondWith({ diff --git a/cli/tests/config.test.ts b/cli/tests/config.test.ts new file mode 100644 index 00000000..2342d574 --- /dev/null +++ b/cli/tests/config.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, test } from "bun:test"; +import { + defaultEndpoint, + endpointFrom, + invokedAsLegacy, + siteOrigin, +} from "../src/config.ts"; +import packageJson from "../package.json"; + +describe("config", () => { + test("the default endpoint is the site's MCP server", () => { + expect(defaultEndpoint).toBe(`${siteOrigin}/mcp`); + // Until allthings.dev serves the new site, it only redirects, and a + // redirect drops an MCP POST. + expect(["https://allthingsweb.dev", "https://allthings.dev"]).toContain( + siteOrigin, + ); + }); + + test("ALLTHINGS_MCP_URL wins, then ATW_MCP_URL, then the default", () => { + expect(endpointFrom({})).toBe(defaultEndpoint); + expect(endpointFrom({ ATW_MCP_URL: "https://old.test/mcp" })).toBe( + "https://old.test/mcp", + ); + expect( + endpointFrom({ + ALLTHINGS_MCP_URL: "https://new.test/mcp", + ATW_MCP_URL: "https://old.test/mcp", + }), + ).toBe("https://new.test/mcp"); + }); + + test("knows when it was started by its old name", () => { + for (const argv0 of [ + "atw", + "./atw", + "/home/me/.allthings/bin/atw", + "C:\\bin\\atw.exe", + "ATW.EXE".toLowerCase(), + ]) { + expect(invokedAsLegacy(argv0)).toBe(true); + } + for (const argv0 of [ + "allthings", + "/usr/local/bin/allthings", + "bun", + "allthings.exe", + "atw-cli", + "", + ]) { + expect(invokedAsLegacy(argv0)).toBe(false); + } + }); + + test("the package installs allthings, with atw as its alias", () => { + expect(packageJson.name).toBe("allthings"); + expect(packageJson.bin).toEqual({ + allthings: "./src/index.ts", + atw: "./src/atw.ts", + }); + }); +}); diff --git a/atw-cli/tests/contract.test.ts b/cli/tests/contract.test.ts similarity index 100% rename from atw-cli/tests/contract.test.ts rename to cli/tests/contract.test.ts diff --git a/atw-cli/tests/fixtures.ts b/cli/tests/fixtures.ts similarity index 100% rename from atw-cli/tests/fixtures.ts rename to cli/tests/fixtures.ts diff --git a/atw-cli/tsconfig.json b/cli/tsconfig.json similarity index 100% rename from atw-cli/tsconfig.json rename to cli/tsconfig.json diff --git a/infra/docs/r2-migration.md b/infra/docs/r2-migration.md index b9786e76..90e80472 100644 --- a/infra/docs/r2-migration.md +++ b/infra/docs/r2-migration.md @@ -124,7 +124,7 @@ From activation until step 2's deploy, allthings.dev and www don't answer. Their 6. Run `copy` again for anything uploaded to the old bucket before the redeploy, then `verify --public`. 7. Run an end-to-end check: one upload through the admin, visible on media.allthings.dev and as an `/img` variant. 8. **Point clients at the new host.** These are small PRs once step 3 passes, not before. Before activation, allthings.dev still redirects to allthingsweb.dev, and a redirect drops an MCP POST. After it, the new site answers on allthings.dev and allthingsweb.dev keeps serving the old app, so clients can switch to `https://allthings.dev/mcp`. - - The CLI's default endpoint becomes `https://allthings.dev/mcp` (`atw-cli/src/client.ts` and `core/scripts/fixtures.ts`). + - The CLI's default endpoint becomes `https://allthings.dev/mcp` (`siteOrigin` in `cli/src/config.ts`, and `core/scripts/fixtures.ts`). - `infra/README.md`'s "default profile until the domain moves" sentence becomes past tense. **Phase 5: after** diff --git a/pages/atw-install.bash b/pages/atw-install.bash index 1bbc35cb..e4adabd1 100644 --- a/pages/atw-install.bash +++ b/pages/atw-install.bash @@ -1,40 +1,6 @@ #!/usr/bin/env bash - -ATW_HOME="$HOME/.allthingsweb" -mkdir -p $ATW_HOME -cd $ATW_HOME - -OS=$(uname -s | tr '[:upper:]' '[:lower:]') -ARCH=$(uname -m) - -if [ "$ARCH" = "x86_64" ]; then - ARCH="x64" -elif [ "$ARCH" = "arm64" ] || [ "$ARCH" = "aarch64" ]; then - ARCH="arm64" -else - echo "โŒ Unsupported architecture: $ARCH" - exit 1 -fi - -FILE="bun-${OS}-${ARCH}" -# Install a specific (pre)release with ATW_VERSION=2.0.0-alpha.1; defaults to the latest stable release. -LATEST_RELEASE="${ATW_VERSION:-$(curl -s "https://api.github.com/repos/allthingsweb-dev/allthingsweb/releases/latest" | grep tag_name | cut -d'"' -f 4)}" -URL="https://github.com/allthingsweb-dev/allthingsweb/releases/download/${LATEST_RELEASE}/atw-cli-${FILE}" - -if curl -fLO "${URL}"; then - echo "โœ… Successfully downloaded ${FILE}" -else - echo "โŒ Failed to download ${FILE}. Please check the URL or platform." - exit 1 -fi - -ln -sf $ATW_HOME/atw-cli-${FILE} $HOME/atw -chmod +x $HOME/atw - -echo "You can now use ATW CLI by running: ~/atw" -echo "" -echo "- You may want to put ~/atw in you PATH" -echo "- You may want to creat an alias (in your .zshrc or .bashrc) alias atw='~/atw'" - -~/atw - +# The old installer's address. The CLI is now `allthings`; this runs its +# installer, which also keeps `atw` as an alias for now. +set -euo pipefail +echo "atw is now allthings. Installing it from install.bash." +curl -fsSL https://allthingsweb-dev.github.io/allthingsweb/install.bash | bash diff --git a/pages/install.bash b/pages/install.bash new file mode 100644 index 00000000..fe041187 --- /dev/null +++ b/pages/install.bash @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Installs the all things CLI, `allthings`, in ~/.allthings/bin, with `atw` +# beside it as the old name for now. +# +# curl -fsSL https://allthingsweb-dev.github.io/allthingsweb/install.bash | bash +# +# ALLTHINGS_VERSION=2.0.0-alpha.2 installs that (pre)release; the default is +# the latest stable release. ALLTHINGS_HOME moves the install (~/.allthings). +set -euo pipefail + +repo="allthingsweb-dev/allthingsweb" +home_dir="${ALLTHINGS_HOME:-$HOME/.allthings}" +bin_dir="$home_dir/bin" +version="${ALLTHINGS_VERSION:-${ATW_VERSION:-}}" + +os=$(uname -s | tr '[:upper:]' '[:lower:]') +case "$os" in + linux | darwin) ;; + *) + echo "No installer for $os. Download the binary from https://github.com/$repo/releases." >&2 + exit 1 + ;; +esac +case "$(uname -m)" in + x86_64 | amd64) arch="x64" ;; + arm64 | aarch64) arch="arm64" ;; + *) + echo "Unsupported architecture: $(uname -m)" >&2 + exit 1 + ;; +esac +platform="bun-$os-$arch" + +if [[ -z "$version" ]]; then + version=$(curl -fsSL "https://api.github.com/repos/$repo/releases/latest" | grep '"tag_name"' | cut -d '"' -f 4) +fi +if [[ -z "$version" ]]; then + echo "Could not find the latest release." >&2 + exit 1 +fi + +mkdir -p "$bin_dir" +download=$(mktemp "$bin_dir/.allthings.XXXXXX") +trap 'rm -f "$download"' EXIT +base="https://github.com/$repo/releases/download/$version" +# Releases before the rename ship the binary as atw-cli-. +if ! curl -fsL -o "$download" "$base/allthings-$platform" && + ! curl -fsSL -o "$download" "$base/atw-cli-$platform"; then + echo "Release $version has no binary for $platform." >&2 + exit 1 +fi +chmod 755 "$download" +mv "$download" "$bin_dir/allthings" +trap - EXIT +ln -sf allthings "$bin_dir/atw" + +echo "Installed allthings $version in $bin_dir." +case ":$PATH:" in + *":$bin_dir:"*) ;; + *) echo "Add it to your PATH: export PATH=\"$bin_dir:\$PATH\"" ;; +esac +echo +"$bin_dir/allthings" --help diff --git a/web/src/mcp/tools.ts b/web/src/mcp/tools.ts index 38a19be1..724f9cd7 100644 --- a/web/src/mcp/tools.ts +++ b/web/src/mcp/tools.ts @@ -17,7 +17,7 @@ import { community } from "./community.ts"; /** * The public MCP tools, as app/src/lib/mcp/tools.ts declares them: the same * names, titles, descriptions, annotations, input schemas and error texts, - * because agents and the CLI (atw-cli) rely on them. + * because agents and the CLI (cli/, `allthings`) rely on them. * * Input schemas stay in zod, the MCP SDK's own schema library and so already * in the bundle: they are what clients see in tools/list and they word the diff --git a/web/tests/support/legacy-urls.ts b/web/tests/support/legacy-urls.ts index 9dc3662e..c62cb455 100644 --- a/web/tests/support/legacy-urls.ts +++ b/web/tests/support/legacy-urls.ts @@ -211,19 +211,19 @@ export const legacyUrls: ReadonlyArray = [ // The public API and MCP { pattern: "/api/v1/events", - source: "app route, atw-cli", + source: "app route, the CLI", example: "/api/v1/events", worker: json, }, { pattern: "/api/v1/events/[id]", - source: "app route, atw-cli", + source: "app route, the CLI", example: "/api/v1/events/e0000000-0000-4000-8000-000000000503", worker: json, }, { pattern: "/api/v1/speakers", - source: "app route, atw-cli", + source: "app route, the CLI", example: "/api/v1/speakers", worker: json, },