diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 700d26f..1bcd5f6 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -43,6 +43,16 @@ jobs: pnpm turbo run typecheck test fi + # apps/cli/README.md is generated from the root README.md, and it is what + # npmjs.com renders for @airshiplabs/cli. Committed rather than built on + # demand so a clean checkout can publish without running the generator. + - name: README is not stale + run: | + if ! node scripts/sync-readme.mjs --check; then + echo "::error file=apps/cli/README.md::The CLI README is stale. Run 'make readme' and commit the result." + exit 1 + fi + # apps/web/src/routeTree.gen.ts is generated but committed, so it drifts. # Regenerated by BUILDING, not by `tsr generate`: two things write this # file and they disagree — the router CLI emits the tree alone, while the diff --git a/Makefile b/Makefile index 12a43aa..51bd3ce 100644 --- a/Makefile +++ b/Makefile @@ -283,7 +283,10 @@ demo: ## One-shot: install + build, then print the two-terminal recipe ##@ Release (@airshiplabs/cli) -.PHONY: release release\:ci release\:version +.PHONY: readme release release\:ci release\:version + +readme: ## Regenerate apps/cli/README.md from the root README.md + @node scripts/sync-readme.mjs release: ## Cut a release locally (bump + commit + tag; stops before push) $(confirm_shared) diff --git a/apps/cli/README.md b/apps/cli/README.md new file mode 100644 index 0000000..1b6de36 --- /dev/null +++ b/apps/cli/README.md @@ -0,0 +1,438 @@ + + +# Airship + +[![npm](https://img.shields.io/npm/v/@airshiplabs/cli)](https://www.npmjs.com/package/@airshiplabs/cli) +[![node](https://img.shields.io/node/v/@airshiplabs/cli)](https://nodejs.org) +[![license](https://img.shields.io/npm/l/@airshiplabs/cli)](https://github.com/0xnyn/airship/blob/main/LICENSE) + +**Visual editor for your codebase.** + +Airship puts an infinite design canvas in front of your dev server. Select an element, describe +the change, and watch Claude Code, Codex or OpenCode update the source — without rebuilding your +UI in a separate design tool. + +![Airship mid-edit: the prompt "Turn this into a github icon" streaming its reads, writes and edits, a desktop and an iPhone frame side by side on the canvas, and the Edit inspector open on the selection](https://raw.githubusercontent.com/0xnyn/airship/main/media/inspector-edit.png) + +```bash +npx @airshiplabs/cli --target 3000 +``` + +No plugin. No config. Nothing added to your dependencies or your bundle. + +[airship.design](https://airship.design) · [CLI reference](#cli-reference) · [Configuration](#configuration) · [Questions](#questions) + +--- + +## Quick start + +![The Airship landing page, with the editor overlay open on a running app at localhost:3000](https://raw.githubusercontent.com/0xnyn/airship/main/media/cover.png) + +**1. Start your app the way you always do.** + +```bash +pnpm dev # http://localhost:3000 +``` + +Works with Vite, Next, Remix, Rails, or anything that serves HTML over HTTP. No plugins +required. + +**2. Point Airship at your dev server.** + +```bash +npx @airshiplabs/cli --target 3000 +``` + +Airship connects to the port you're already running and opens the visual editor on the next +free port. Or install it once and use the `airship` binary: + +```bash +npm i -g @airshiplabs/cli +airship --target 3000 +``` + +**3. Bring your coding agent.** + +```bash +airship --target 3000 --agent codex --safe +``` + +Pick the agent you already use and start making changes without running a separate agent +interface. + +Airship uses the authentication you already have configured for Claude Code, Codex, or +OpenCode. Run `airship doctor` if something does not work. + +### Or let it start your dev server too + +`--exec` starts your dev server and stops it again when Airship exits. Leave `--target` off and +it reads the port from your `package.json`: + +```bash +airship --exec "pnpm dev" +``` + +Run `airship` on its own and it just asks you for the port, the agent and the mode. + +## What it is + +A CLI, and nothing else. + +- **Nothing goes into your project.** Airship runs in front of the dev server you already have. + Your build, your config and your dependencies are untouched. +- **Every frame is a real browser window.** A phone frame behaves like a phone, however far you + zoom out. Mobile and desktop sit side by side — both live, both editable, one source file. +- **Changes land in your code.** Click an element and Airship knows the file and line that drew + it. Describe the change, get the diff, undo it if you don't like it. +- **Nothing leaves your machine.** No account, no telemetry, no service to sign up for. + + +![The agent panel showing the diff of the file it just edited, with the element still selected on the canvas](https://raw.githubusercontent.com/0xnyn/airship/main/media/agent.png) + +## Canvas or inline + +Two ways to look at your app. Same editor either way. + +**`canvas`** (default) — your app on a pannable canvas, one live frame per device size. + +![The canvas, with a desktop frame and an iPhone frame side by side, the agent panel on the left and the inspector on the right](https://raw.githubusercontent.com/0xnyn/airship/main/media/canvas.png) + +**`inline`** — the editor on top of your own page, one window. + +![Inline mode, with the editor panels floating over the real page at localhost:3000](https://raw.githubusercontent.com/0xnyn/airship/main/media/inline.png) + +Pick one at launch with `airship --mode inline`, or switch any time from the bottom bar. Your +choice sticks across reloads and as you click around your app. Add `?__airship=inline` to a URL +to try the other one once, without changing your preference; `?__airship=shell` is the way back +to the canvas — the parameter takes the internal mode name, so it is `shell`, not `canvas`. + +Open any route of your app in Airship — `/pricing`, `/settings` — and every frame opens there. + +On the canvas: + +| Gesture | | +| --- | --- | +| wheel / two-finger | pan | +| ⌘/ctrl-wheel, pinch | zoom at cursor | +| space-drag, middle-drag | pan | +| ⇧1 / ⇧2 / ⇧0 | fit / zoom to selection / 100% | + +## Inspector + +Click an element and it fills the inspector. Every tab is looking at the same thing. + +`Edit` is position, size, spacing and layout — the shot up in [Quick start](#quick-start). `CSS` +is the box model, the rules that are actually hitting the element, and your own tweaks on top: + +![The CSS tab, showing the box model, an empty element.style block and the matched CSS rules for the selection](https://raw.githubusercontent.com/0xnyn/airship/main/media/inspector-css.png) + +`DOM` is the tree. Click a node to select it, or drag one to move it somewhere else: + +![The DOM tab, showing the element tree with the selected text node highlighted](https://raw.githubusercontent.com/0xnyn/airship/main/media/inspector-dom.png) + +## Agents + +Pick one with `--agent`, or switch between them as you go. They are not equal, and Airship tells +you what you're giving up at startup. + +| | `claude` (default) | `codex` | `opencode` | +| --- | --- | --- | --- | +| Watch it write | word by word | the whole reply at once, at the end | word by word | +| Pick up an old chat | yes | yes | yes | +| Branch off a chat | yes | starts fresh, and says so | yes, history kept | +| Shows what it cost | in dollars | tokens only | in dollars | +| `--effort` | yes | yes | **ignored** | +| `--max-turns`, `--max-budget` | yes | **ignored** | **ignored** | +| `--model` | a model name | a model name | needs the `provider/model` form | +| `--safe` | checks each edit and command | **real sandbox** | asks before each edit and command | +| Install | included | included | **you install it yourself** | + +Undo is Airship's, not the agent's. It keeps the previous version of every file it touches, so +undo works on all three. One catch: `codex` and `opencode` get that previous version from Git, +so **undo needs your project to be a Git repo on those two**. Airship warns you at startup. + +### Authentication + +Airship reuses whatever the chosen agent already has, and warns at startup if it finds nothing. + +| Agent | Needs one of | +| --- | --- | +| `claude` | `ANTHROPIC_API_KEY`, `CLAUDE_CODE_OAUTH_TOKEN`, or a `claude` login (`~/.claude`) | +| `codex` | `CODEX_API_KEY`, `OPENAI_API_KEY`, or a `codex login` (`~/.codex/auth.json`) | +| `opencode` | the `opencode` binary on PATH, **plus** a provider key or an `opencode auth login` | + +OpenCode is a separate install — `brew install sst/tap/opencode` or `npm i -g opencode-ai` — +and accepts `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `OPENCODE_API_KEY`, `OPENROUTER_API_KEY`, +`GEMINI_API_KEY`, `GOOGLE_GENERATIVE_AI_API_KEY`, `AWS_BEARER_TOKEN_BEDROCK` or +`AWS_ACCESS_KEY_ID`. + +## Safety + +**By default the agent runs unsandboxed.** It can write anywhere you can and reach the network — +the same access it has when you run it from your terminal. Pass `--safe` to confine it: + +| | default | `--safe` | +| --- | --- | --- | +| `codex` | full access, network on | locked to your project folder, no network, no web search | +| `claude` | full access, no sandbox | edits kept inside your project, dangerous commands blocked | +| `opencode` | full access | asks before every edit and command, checked the same way; no web fetch, no web search, nothing outside your project | + +`--safe` is not equally strong on all three, and the CLI says so at launch: + +- **Only `codex` gets a real sandbox.** The operating system stops it writing outside your + project. On `claude` and `opencode`, Airship checks each edit and command first — a good + check, but a check, not a wall. +- **That check looks for known-dangerous commands; it does not understand shell.** It catches + `rm -rf`. It does not catch a write pointed somewhere else, like `echo x > /elsewhere`. +- **`claude` and `opencode` can still reach the network.** Airship switches off their web tools, + but nothing stops a command they run from opening a connection anyway. Only a sandbox can. + +If a hard guarantee matters more to you than which agent you use, run +`airship --agent codex --safe`. + +Diffs and undo work the same either way — `--safe` has no effect on them. + +## CLI reference + +``` +airship [options] +airship --target [options] +airship [options] +``` + +| Command | | +| --- | --- | +| `airship` | Launch the visual editor against your dev server. Bare at a terminal, it asks first. | +| `airship init` | Create an `airship.config.json` for this project. | +| `airship doctor` | Check your environment and report what is wrong. | + +Flags accept `--flag value` and `--flag=value`, a camelCase spelling of any kebab name +(`--maxTurns` ≡ `--max-turns`), and `--no-` to turn any boolean off. A bare `--` stops flag +parsing — airship takes no positional arguments, so anything after it is ignored rather than +forwarded. + +### Core + +| Flag | | Default | +| --- | --- | --- | +| `-t, --target ` | Port your dev server is already running on. Detected from your `package.json` when omitted. | | +| `-p, --port ` | Port for the airship proxy. | `target + 1` | +| `--cwd ` | Project root for edits. | current directory | +| `--mode ` | Editor mode: `canvas` or `inline`. Switchable from the editor too. | `canvas` | +| `--exec ` | Start your dev server with this command and stop it when airship exits. | | +| `--open` | Open the editor in your browser once it is listening. | | + +### Agent + +| Flag | | Default | +| --- | --- | --- | +| `-a, --agent ` | Coding agent: `claude`, `codex`, `opencode`. | `claude` | +| `-m, --model ` | Model id. | the agent's own default | +| `--effort ` | Reasoning effort: `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. | | +| `--max-turns ` | Cap agent turns per edit (claude only). | `24` | +| `--max-budget ` | Stop an edit if it exceeds this cost in USD (claude only). | | +| `--commit` | Auto-commit each accepted edit (Conventional Commits). | | + +`-m` is `--model`, not `--mode`. `--mode` has no short alias. + +### Sandbox + +| Flag | | Default | +| --- | --- | --- | +| `--safe` | Confine edits to the project directory and cut network access. See [Safety](#safety). | off | + +### Backend + +| Flag | | Default | +| --- | --- | --- | +| `--codex-path ` | Path to the `codex` binary. | bundled | +| `--codex-config ` | Extra `codex --config` pair; repeatable. | | +| `--opencode-path ` | Path to the `opencode` binary. | found on PATH | +| `--opencode-url ` | Attach to a running `opencode serve` instead of starting one. | | +| `--opencode-agent ` | Run as a named opencode agent. | its own | +| `--opencode-config ` | JSON file merged into the opencode server config. | | + +`--codex-config` reads the shape of the value: `true`/`false` become TOML booleans and anything +numeric becomes a number, so `--codex-config network_access=true` sends a boolean, not the string. +Quote it — `--codex-config k='"true"'` — to keep a string a string. + +### Global + +| Flag | | +| --- | --- | +| `--json` | Machine-readable JSON on stdout, no colour and no banner. | +| `-q, --quiet` | Suppress the launch banner. Warnings still print. | +| `--debug` | Print stack traces on failure. | +| `-h, --help` | Show this help. | +| `-v, --version` | Print the version. | + +Banners, warnings and errors go to stderr; `--json` payloads, help and `--version` go to stdout, +so `airship --json | jq` is reliable. + +### `airship init` + +Writes an `airship.config.json` from the same questions the bare `airship` wizard asks, so it +stops asking. Takes `--cwd` and the global flags. Needs a terminal. + +### `airship doctor` + +Checks, in order: `node`, `airship`, `config`, `git repo`, `overlay bundle`, `agent claude`, +`agent codex`, `agent opencode`, `dev server`. Each reports `ok`, `warn` or `fail` with a hint. +Only your preferred agent (`--agent`, default `claude`) can fail the run; the other two warn. + +Exits `1` if any check failed, so `airship doctor && airship` works. Takes `--cwd`, `--target`, +`--agent` and the global flags. + +### Exit codes + +| Code | | +| --- | --- | +| `0` | Fine. | +| `1` | Something failed. | +| `2` | Bad flag, bad value, or a terminal was needed and there wasn't one. | +| `127` | Not an airship command. | +| `130` | Interrupted — Ctrl-C, or a cancelled prompt. | + +## Configuration + +Settings resolve in this order, highest first: + +``` +flags → AIRSHIP_* environment → airship.config.json → defaults +``` + +### The config file + +`airship.config.json`, or an `"airship"` key in your `package.json`. Run `airship init` to write +one. Every key is a flag name, in either kebab or camel case: + +```json +{ + "agent": "claude", + "mode": "canvas", + "target": 3000, + "safe": true +} +``` + +Airship looks for it from `--cwd` upwards and **stops at your repository root**, so a stray +config file somewhere above your repo won't affect you. Misspell a key and it says so, with a +suggestion — it never quietly ignores one. + +### Environment + +Every flag except `--help` and `--version` has an environment variable: `AIRSHIP_` plus the flag +name uppercased, with `-` as `_`. + +``` +AIRSHIP_TARGET AIRSHIP_AGENT AIRSHIP_CODEX_PATH +AIRSHIP_PORT AIRSHIP_MODEL AIRSHIP_CODEX_CONFIG +AIRSHIP_CWD AIRSHIP_EFFORT AIRSHIP_OPENCODE_PATH +AIRSHIP_MODE AIRSHIP_MAX_TURNS AIRSHIP_OPENCODE_URL +AIRSHIP_EXEC AIRSHIP_MAX_BUDGET AIRSHIP_OPENCODE_AGENT +AIRSHIP_OPEN AIRSHIP_COMMIT AIRSHIP_OPENCODE_CONFIG +AIRSHIP_SAFE AIRSHIP_JSON AIRSHIP_QUIET +AIRSHIP_DEBUG +``` + +`AIRSHIP_HELP` and `AIRSHIP_VERSION` are deliberately not read — exporting one would leave the +CLI unable to run anything. Booleans take `1`/`true`/`yes`/`on` or `0`/`false`/`no`/`off`; +anything else is an error rather than a guess. + +Three more are honoured: `AIRSHIP_EDITOR` (`vscode`, `cursor`, `windsurf` or `zed` — which +editor "open in editor" prefers, otherwise probed in that order), and `NO_COLOR` / `FORCE_COLOR`. + +### `--cwd` + +`--cwd` is the folder your dev server treats as its root, which is not always your repository +root. Airship needs it to turn the paths your dev server reports (`/src/app.tsx`) into real +files on disk. In a monorepo where the app lives in `apps/web`, that's `--cwd apps/web`. + +## Port detection + +Leave `--target` off and Airship works it out, trying each likely port in turn and taking the +first one that answers: + +1. **The port in your dev script** — a `--port`, `-p` or `PORT=` in `scripts.dev`, `scripts.start` + or `scripts.serve`. +2. **Your framework's default**, by what is in your dependencies: + + | Dependency | Port | + | --- | --- | + | `next`, `nuxt`, `@remix-run/dev`, `react-scripts` | `3000` | + | `parcel` | `1234` | + | `@angular/cli` | `4200` | + | `astro` | `4321` | + | `@sveltejs/kit`, `vite` | `5173` | + | `storybook` (or any `@storybook/*`) | `6006` | + | `gatsby` | `8000` | + | `@11ty/eleventy` | `8080` | + + Most specific first — a project with both `vite` and `storybook` is a Vite app that also has + a component catalogue, not the other way round. + +3. **Common ports** — `3000`, `5173`, `8080`, `4321`, `4200`. + +With `--exec` it's the opposite: the port has to be *free*, since Airship is about to start your +dev server on it. It won't start one on a port that's already taken. + +## Questions + +Including the ones with unflattering answers. + +**Do I need a plugin, or to change my build?** +No. Airship runs as a reverse proxy in front of your existing dev server. Nothing is added to +your dependencies, config, or bundle. + +**Does it work with Tailwind, CSS Modules or styled-components?** +Yes. Airship works with the styling system already in your project. It works with the tokens and +styling conventions already in your codebase, so visual changes can map back to the values your +design system already uses. + +**How does it know which file an element came from?** +Dev builds already record where each thing on screen came from. Airship reads that to find the +file and line, and hands it to the agent. + +**What exactly gets sent to the agent?** +What you clicked, the file and line it came from, anything you changed in the inspector, the +frames you were working in — and whatever you typed. + +**Can I use my existing coding agent?** +Yes. Airship works with Claude Code, Codex, and OpenCode, so you don't need to change your agent +just to use the visual editor. + +**Is it safe to point at a real repository?** +Airship runs locally and works directly on the repository you point it at. By default your +coding agent has the same access to your files and the network that it always has. `--safe` can +keep edits inside your project and block dangerous commands — read [Safety](#safety) for what it +does and doesn't cover on each agent. + +**How do I undo something?** +Every edit can be undone from Airship, which keeps the previous version of the file. On `claude` +that's all there is to it. On `codex` and `opencode` that previous version comes from Git, so +those two need your project to be a repository. See [Agents](#agents). + +**Can I see how a change affects different devices?** +Yes. Airship runs the same app at several real device sizes at once. Make a change once and +watch it land on desktop, tablet and mobile together, without resizing anything. + +**Can I use Airship without Git?** +Yes, Airship doesn't require it. But undo on `codex` and `opencode` works by asking Git for the +previous version of the file, so without a repository you lose undo on those two. `claude` is +unaffected. See [Agents](#agents). + +**Does my code leave my machine?** +Airship runs entirely on localhost. It has no account, telemetry, or hosted service, and your +code isn't sent to Airship. Your chosen coding agent handles requests using the same credentials +and provider it would use from your terminal. + +## Requirements + +Node 22.13 or later, and one of Claude Code, OpenAI Codex or OpenCode. + +## Links + +- [airship.design](https://airship.design) +- [Issues](https://github.com/0xnyn/airship/issues) · [Releases](https://github.com/0xnyn/airship/releases) +- [CONTRIBUTING.md](https://github.com/0xnyn/airship/blob/main/CONTRIBUTING.md) — architecture, the packages, Storybook, CI and releases +- [MIT](https://github.com/0xnyn/airship/blob/main/LICENSE) diff --git a/apps/cli/package.json b/apps/cli/package.json index 0d060bb..0fda887 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -26,6 +26,7 @@ "airship": "./dist/index.js" }, "files": [ + "README.md", "dist" ], "scripts": { diff --git a/scripts/sync-readme.mjs b/scripts/sync-readme.mjs new file mode 100644 index 0000000..0bf56b5 --- /dev/null +++ b/scripts/sync-readme.mjs @@ -0,0 +1,130 @@ +// Generates apps/cli/README.md from the root README.md. +// +// Why this exists: npm renders a package's README from the PACKAGE directory, +// and apps/cli has never had one — so @airshiplabs/cli's npm page reads "This +// package does not have a README" while a 400-line one sits at the repo root. +// pnpm copies the root LICENSE into a workspace package that lacks one; it does +// not do the same for README, and `files: ["dist"]` does not reach up either. +// +// Copying it verbatim is not enough. npm resolves relative links against +// `repository.directory` (apps/cli), where media/cover.png does not exist — so +// every screenshot would 404 on npmjs.com. This rewrites them to absolute +// GitHub URLs: raw.githubusercontent for images (the blob view serves HTML, not +// an image) and blob for everything else. In-page anchors are left alone; npm's +// renderer generates the same heading slugs. +// +// node scripts/sync-readme.mjs # write apps/cli/README.md +// node scripts/sync-readme.mjs --check # exit 1 if it is stale +// +// The generated file is committed, like apps/web/src/routeTree.gen.ts, and +// checks.yml runs --check on every PR. Root README.md is the only copy anyone +// edits. + +import { readFileSync, writeFileSync } from "node:fs"; + +// Two passes, because a badge is an image nested inside a link and one regex +// that handles both is ambiguous enough to backtrack badly. BADGE_LINK runs +// first and rewrites the OUTER target; LINK then sweeps everything else, +// including the image still sitting inside it. +/** `[![alt](image)](target)` — a badge. Captures the whole image, then target. */ +const BADGE_LINK = /\[(!\[[^\]]*\]\([^)\s]+\))\]\(([^)\s]+)\)/g; +/** Markdown inline link or image — the leading `!` is what tells them apart. */ +const LINK = /(!?)\[([^\]]*)\]\(([^)\s]+)\)/g; +/** ``` or ~~~ opening or closing a fenced block, which is never rewritten. */ +const FENCE = /^\s*(?:```|~~~)/; +/** A scheme, a protocol-relative host, or an anchor — already resolvable. */ +const ABSOLUTE = /^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i; +const LEADING_DOT_SLASH = /^\.\//; +const REPO_URL = /github\.com[/:]([^/]+)\/([^/.]+)/; + +// Both generated forms point at a branch rather than the release tag. A tag +// would be immutable, but it would change this file on every release and the +// drift check would then fight the release. +const REF = "main"; + +const ROOT = new URL("../README.md", import.meta.url); +const OUT = new URL("../apps/cli/README.md", import.meta.url); +const PKG = new URL("../apps/cli/package.json", import.meta.url); + +const BANNER = + ""; + +function die(message) { + process.stderr.write(`sync-readme: ${message}\n`); + process.exit(1); +} + +/** owner/repo from apps/cli's `repository.url`, so a fork needs no edit here. */ +function repoSlug() { + const url = JSON.parse(readFileSync(PKG, "utf8")).repository?.url; + const match = url?.match(REPO_URL); + if (!match) { + die(`could not read a github owner/repo out of repository.url ("${url}")`); + } + return `${match[1]}/${match[2]}`; +} + +function absolutize(target, isImage, slug) { + const path = target.replace(LEADING_DOT_SLASH, ""); + return isImage + ? `https://raw.githubusercontent.com/${slug}/${REF}/${path}` + : `https://github.com/${slug}/blob/${REF}/${path}`; +} + +/** Returns the rewritten markdown and how many targets it touched. */ +function render(source, slug) { + const out = []; + let rewritten = 0; + let inFence = false; + + const rewriteLine = (line) => + line + .replace(BADGE_LINK, (whole, image, target) => { + if (ABSOLUTE.test(target)) { + return whole; + } + rewritten += 1; + return `[${image}](${absolutize(target, false, slug)})`; + }) + .replace(LINK, (whole, bang, label, target) => { + if (ABSOLUTE.test(target)) { + return whole; + } + rewritten += 1; + return `${bang}[${label}](${absolutize(target, bang === "!", slug)})`; + }); + + for (const line of source.split("\n")) { + if (FENCE.test(line)) { + inFence = !inFence; + out.push(line); + continue; + } + out.push(inFence ? line : rewriteLine(line)); + } + + return { rewritten, text: `${BANNER}\n\n${out.join("\n")}` }; +} + +const { rewritten, text } = render(readFileSync(ROOT, "utf8"), repoSlug()); + +if (process.argv.includes("--check")) { + let current = null; + try { + current = readFileSync(OUT, "utf8"); + } catch { + die("apps/cli/README.md is missing — run `node scripts/sync-readme.mjs`"); + } + if (current !== text) { + die( + "apps/cli/README.md is stale — run `node scripts/sync-readme.mjs` and commit the result" + ); + } + process.stdout.write("apps/cli/README.md is up to date\n"); + process.exit(0); +} + +writeFileSync(OUT, text); +process.stdout.write( + `wrote apps/cli/README.md — ${rewritten} links rewritten, ${text.length} bytes\n` +); diff --git a/scripts/verify-tarball.sh b/scripts/verify-tarball.sh index 4b99b11..b97e96c 100755 --- a/scripts/verify-tarball.sh +++ b/scripts/verify-tarball.sh @@ -20,8 +20,14 @@ PKG_NAME="@airshiplabs/cli" # Every path the published CLI resolves at runtime but no bundler can inline. # dist/vendor/ is written by apps/cli/scripts/vendor-assets.mjs; without it the # installed CLI 404s on its own overlay and renders unstyled. +# +# README.md is generated from the root one by scripts/sync-readme.mjs and is the +# only thing npmjs.com has to show for the package. Without it the page reads +# "This package does not have a README" — which is how 0.2.1 shipped — and the +# version number is spent, because npm never lets you republish one. REQUIRED=( "package/package.json" + "package/README.md" "package/dist/index.js" "package/dist/vendor/overlay.global.js" "package/dist/vendor/hook.global.js"