Files
airship/Makefile
T
Nayan 0b863148cd docs: explain the two catalogues, and gate the files they generate
Documents what the preceding commits added, so it lands after them rather than
describing things that are not there yet. Both new generators are wired up here
too, because a generated file that nothing checks is a file that drifts.

The controls table in `README.md` is now generated from the command catalogue,
which is the point of having one — the six hand-written rows it replaces had
already drifted in the copy under `apps/cli/`. `make controls` rewrites it and
`CONTROLS.md`, then syncs the CLI's README, and it has to run in that order
because `sync-readme.mjs` copies the file the first step writes into. Both
`make preflight` and the PR gate fail on a stale one.

`make models:refresh` is deliberately not in either. Every other generated file
in this repo derives from something committed beside it, so its drift gate can
only fire when a human changed the input. This one derives from models.dev,
which changes whenever a vendor ships a model, so gating on it would need
network to pass and would turn PRs that touched nothing red. Treat a refresh
like a lockfile bump.

Both generated tables spell each chord for both platforms. A single column was
Mac-only exactly where it mattered, which is the bug the catalogue exists to
end, and `README.md` is the highest-traffic place to make it.

The model flags get their own rows, the config file keys are documented as
per-backend and why, and the two capability matrices gain a line for what each
backend can enumerate about itself — the asymmetry the seed list exists for.
2026-08-16 13:05:37 +05:30

341 lines
14 KiB
Makefile

SHELL := /bin/bash
.DEFAULT_GOAL := help
# Target naming convention: <surface>:<action>
#
# airship has no deploy environments — one workspace, one site, one binary — so
# unlike a multi-env repo there is no env axis to lead with. What varies here is
# the SURFACE: the repo, the site, the storybook, the CLI, the release. So
# repo-wide operations stay bare (install, build, test, check, preflight) and
# everything else is prefixed with the surface it acts on (web:dev, run:codex).
#
# The Makefile is a dumb runner. Every recipe is one pnpm/turbo/script call.
# Colors
CYAN := \033[0;36m
BLUE := \033[0;34m
GREEN := \033[0;32m
YELLOW := \033[1;33m
RED := \033[0;31m
MAGENTA := \033[0;35m
RESET := \033[0m
# The port apps/web's dev server listens on. airship's overlay always takes
# TARGET + 1, so the editor lands on 5174.
#
# Override to drive airship against something else entirely:
# make run TARGET=3000 APP=../my-app
TARGET ?= 5173
# The dev server's ROOT — not the repo root. airship resolves the root-relative
# URLs the dev server reports (/src/app.tsx) against this.
APP := apps/web
CLI := apps/cli/dist/index.js
# Generated, and committed on purpose — the route tree is checked in so a clean
# checkout typechecks without a build first (see .gitignore). Committed
# generated files drift, so preflight regenerates and diffs them.
#
# Regenerated by BUILDING apps/web, not by `make web:routes`. Two things write
# this file and they disagree: `tsr generate` emits the route tree alone, while
# the TanStack Start Vite plugin appends the `declare module` block that
# registers the router type for SSR. The build's output is the committed one, so
# the build is what the drift check has to run.
GENERATED := apps/web/src/routeTree.gen.ts
# Reusable Y/n gate for targets that touch something published or shared — npm,
# Cloudflare. Anything other than y/Y aborts.
define confirm_shared
@printf "$(YELLOW)⚠ This affects a shared/published target — continue? [y/N] $(RESET)"; \
read REPLY; \
case "$$REPLY" in [yY]) ;; *) printf "$(RED)Aborted.$(RESET)\n"; exit 1 ;; esac
endef
.PHONY: help
help:
@printf "$(CYAN)"
@printf " ▗▄▖ ▗▄▄▄▖▗▄▄▖ ▗▄▄▖▗▖ ▗▖▗▄▄▄▖▗▄▄▖ \n"
@printf " ▐▌ ▐▌ █ ▐▌ ▐▌▐▌ ▐▌ ▐▌ █ ▐▌ ▐▌\n"
@printf " ▐▛▀▜▌ █ ▐▛▀▚▖ ▝▀▚▖▐▛▀▜▌ █ ▐▛▀▘ \n"
@printf " ▐▌ ▐▌▗▄█▄▖▐▌ ▐▌▗▄▄▞▘▐▌ ▐▌▗▄█▄▖▐▌ \n"
@printf "$(RESET)\n"
@awk ' \
/^##@/ { printf "\n\033[0;36m%s\033[0m\n", substr($$0, 5); next } \
/^[a-zA-Z_\\][a-zA-Z0-9_\\:.-]*:.*## / { \
target = $$0; sub(/: .*/, "", target); gsub(/\\/, "", target); \
n = index($$0, "## "); desc = substr($$0, n + 3); \
printf " \033[0;32m%-24s\033[0m %s\n", target, desc \
}' $(MAKEFILE_LIST)
@printf "\n$(YELLOW)Convention:$(RESET) $(CYAN)<surface>:<action>$(RESET) — repo-wide ops stay bare.\n"
@printf "$(YELLOW)Shortcuts:$(RESET) $(CYAN)i$(RESET)=install $(CYAN)d$(RESET)=dev $(CYAN)b$(RESET)=build $(CYAN)l$(RESET)=lint $(CYAN)f$(RESET)=fix $(CYAN)t$(RESET)=typecheck $(CYAN)c$(RESET)=check\n"
@printf "$(YELLOW)Context:$(RESET) TARGET=$(TARGET) APP=$(APP) (the overlay takes TARGET+1)\n\n"
##@ Repo
.PHONY: install update build dev dev\:pkgs clean distclean
install: ## Install workspace dependencies
@printf "$(BLUE)Installing dependencies...$(RESET)\n"
@pnpm install
@printf "$(GREEN)Dependencies installed!$(RESET)\n"
update: ## Update dependencies interactively
@pnpm update --interactive --latest
build: ## Build every package and the site (topological)
@pnpm build
dev: ## Watch-build packages and serve the site
@pnpm dev
dev\:pkgs: ## Watch-build the packages only, without the site
@pnpm dev:pkgs
clean: ## Remove build output and turbo caches
@pnpm clean
distclean: clean ## clean, plus every node_modules in the tree
@rm -rf node_modules apps/*/node_modules packages/*/node_modules
##@ Quality
.PHONY: lint fix fmt typecheck test test\:browser browsers check commitlint
.PHONY: preflight preflight\:fix
lint: ## Lint everything (Biome via Ultracite)
@pnpm lint
fix: ## Auto-fix formatting and lint across the repo
@pnpm fix
fmt: fix ## Alias for fix
typecheck: ## tsc --noEmit across the workspace
@pnpm typecheck
test: ## Run the test suite
@pnpm test
test\:browser: ## Run every story as a test in real Chromium (needs `make browsers`)
@pnpm turbo run test:browser --filter=@airship/overlay
browsers: ## Download the Chromium build the browser tier runs in
@pnpm --filter @airship/overlay exec playwright install chromium
check: lint typecheck test ## Lint, typecheck and test
commitlint: ## Check this branch's commits against the Conventional Commits rules
@pnpm commitlint --from origin/main --to HEAD
# Everything checks.yml gates a PR on, in one command, plus the drift check it
# layers on top. Worth running before you open a PR: a stale route tree fails
# there, and it is far cheaper to find here.
#
# Two flavours, same gate:
# preflight verifies only — safe on a clean tree, fails on anything CI would
# preflight:fix autofixes what is autofixable first, then verifies the rest
#
# One deliberate difference from CI: typecheck and test run repo-wide. CI scopes
# them with `turbo --affected` against the PR base; locally there is no base.
#
# Sub-makes rather than prerequisites: GNU make 3.81 (what macOS ships) never
# matches an escaped-colon name in a PREREQUISITE list against the target of the
# same name — it quietly "remakes" a nameless target instead, so the recipe
# passes having run nothing. Passing the goal to a sub-make quotes it past the
# escaping, and a failure still stops the recipe.
preflight: ## Run the full CI gate locally (lint + typecheck + test + route drift)
@printf "$(CYAN)» Lint$(RESET)\n"
@$(MAKE) lint
@printf "$(CYAN)» Typecheck$(RESET)\n"
@$(MAKE) typecheck
@printf "$(CYAN)» Test$(RESET)\n"
@$(MAKE) test
@printf "$(CYAN)» Controls reference$(RESET)\n"
@node --experimental-strip-types --no-warnings=ExperimentalWarning scripts/gen-controls.mjs --check
@node scripts/sync-readme.mjs --check
@printf "$(CYAN)» Route tree$(RESET)\n"
@$(MAKE) "web:build"
@if ! git diff --quiet -- $(GENERATED); then \
printf "$(RED)The route tree is stale — run 'make web:build' and commit the result:$(RESET)\n"; \
git diff --stat -- $(GENERATED); \
exit 1; \
fi
@printf "$(GREEN)Preflight passed — ready for a PR.$(RESET)\n"
# Best-effort: applies every fix this repo can apply on its own, then runs the
# same verification tail. The regenerated route tree is REPORTED rather than
# failed on — this target just produced it, so failing on its own output would
# be pointless. Typecheck and test still fail; nothing autofixes those.
preflight\:fix: ## preflight, but autofix first (ultracite fix + regenerate routes)
@printf "$(CYAN)» Fix (ultracite)$(RESET)\n"
@$(MAKE) fix
@printf "$(CYAN)» Controls reference$(RESET)\n"
@$(MAKE) controls
@printf "$(CYAN)» Route tree$(RESET)\n"
@$(MAKE) "web:build"
@printf "$(CYAN)» Typecheck$(RESET)\n"
@$(MAKE) typecheck
@printf "$(CYAN)» Test$(RESET)\n"
@$(MAKE) test
@if git diff --quiet -- $(GENERATED); then \
printf "$(GREEN)Preflight passed — ready for a PR.$(RESET)\n"; \
else \
printf "$(YELLOW)Preflight passed. The regenerated route tree still needs committing:$(RESET)\n"; \
git diff --stat -- $(GENERATED); \
fi
##@ Site (apps/web)
.PHONY: web\:dev web\:build web\:preview web\:tokens web\:og web\:routes
.PHONY: web\:deploy
web\:dev: ## Start apps/web's dev server (see TARGET below)
# Through turbo, not `pnpm --filter … dev`: @airship/web#dev depends on
# @airship/site-tokens#build, which is what emits dist/tokens.css. Vite has
# nothing to import without it.
@pnpm turbo run dev --filter=@airship/web
web\:build: ## Production-build apps/web
@pnpm turbo run build --filter=@airship/web
web\:preview: ## Serve the built worker locally, exactly as Cloudflare will run it
@pnpm --filter @airship/web exec wrangler dev
web\:tokens: ## Regenerate the site's design tokens from DESIGN.md
@pnpm --filter @airship/site-tokens build
web\:og: ## Regenerate the social card (public/og.png) after changing the copy
# Needs site-tokens built: the card embeds Inter and JetBrains Mono from
# that package's dist/fonts, and falls back to system faces without them.
@pnpm turbo run build --filter=@airship/site-tokens
@pnpm --filter @airship/web og
web\:routes: ## Scaffold apps/web's route tree after adding a route (build is authoritative)
# `tsr generate` writes the route tree but NOT the `declare module` block
# that registers the router type for SSR — the Start Vite plugin appends
# that during a build. Running this alone therefore leaves the committed
# file 9 lines short; `make web:build` puts them back. Fine for scaffolding
# a new route mid-edit, but build before you commit.
@pnpm --filter @airship/web routes
web\:deploy: ## Deploy apps/web to Cloudflare Workers (break-glass; CI normally does this)
# Cloudflare Workers Builds deploys the site on every push to main, so this
# target is the manual override — a hotfix when the build lane is down or a
# first deploy before the repo is connected. It authenticates as *you*
# (`wrangler login`), not as CI, and there is only one worker to hit: no
# --env, because wrangler.jsonc no longer declares any.
$(confirm_shared)
@printf "$(BLUE)Deploying apps/web to Cloudflare...$(RESET)\n"
@$(MAKE) "web:build"
@pnpm --filter @airship/web exec wrangler deploy
@printf "$(GREEN)Deployed!$(RESET)\n"
##@ Overlay (storybook)
.PHONY: storybook storybook\:build
storybook: ## Browse the overlay's controls, sections and panel (:6006)
# Through turbo, and for the same reason web:dev is: the story config maps
# @airship/editor-tokens' dist/fonts onto /__airship/fonts, and Storybook
# refuses to start when a staticDirs source is missing. A bare
# `pnpm --filter @airship/overlay storybook` therefore fails on a clean
# checkout, where that dist does not exist yet.
@pnpm turbo run storybook --filter=@airship/overlay
storybook\:build: ## Build the static story catalogue into storybook-static/
@pnpm turbo run build-storybook --filter=@airship/overlay
##@ CLI (drive airship against APP)
.PHONY: run run\:safe run\:codex run\:opencode run\:inline run\:solo doctor demo
run: $(CLI) ## Point airship at apps/web (its dev server must be up)
@node $(CLI) --target $(TARGET) --cwd $(APP)
run\:safe: $(CLI) ## Same as run, with the agent confined to the project
@node $(CLI) --target $(TARGET) --cwd $(APP) --safe
run\:codex: $(CLI) ## Same as run, on Codex instead of Claude
@node $(CLI) --target $(TARGET) --cwd $(APP) --agent codex
run\:opencode: $(CLI) ## Same as run, on OpenCode (needs `opencode` on PATH)
@node $(CLI) --target $(TARGET) --cwd $(APP) --agent opencode
run\:inline: $(CLI) ## Same as run, on the inline surface instead of the canvas
@node $(CLI) --target $(TARGET) --cwd $(APP) --mode inline
run\:solo: $(CLI) ## Run apps/web's dev server and airship together, one terminal
@node $(CLI) --cwd $(APP) --exec "pnpm turbo run dev --filter=@airship/web"
doctor: $(CLI) ## Check the environment and report what is wrong
@node $(CLI) doctor --cwd $(APP)
$(CLI):
@pnpm build
demo: ## One-shot: install + build, then print the two-terminal recipe
@$(MAKE) install build
@echo
@echo " Ready. In one terminal:"
@echo " make web:dev # http://localhost:$(TARGET)"
@echo " then in another:"
@echo " make run # airship, editing that same app"
@echo " and open http://localhost:$$(( $(TARGET) + 1 ))"
@echo
@echo " That is airship editing its own home page. Pick the hero's button,"
@echo " ask for a change, and watch the diff land in apps/web/src/."
##@ Release (@airshiplabs/cli)
.PHONY: controls models\:refresh readme release release\:ci release\:retry release\:version
# Before `readme`, always: this writes the short table into the root README.md,
# and `readme` copies the root README.md to apps/cli. Run the other way round
# and the CLI's copy is one generation behind.
controls: ## Regenerate CONTROLS.md and the README table from the command catalog
@node --experimental-strip-types --no-warnings=ExperimentalWarning scripts/gen-controls.mjs
@$(MAKE) readme
# Deliberately NOT in `preflight`, unlike `controls`. Every other generated file
# here derives from something committed beside it, so its drift gate can only
# fire when a human changed the input. This one derives from models.dev, which
# changes whenever a vendor ships a model — gating on it would need network to
# pass and would go red on PRs that touched nothing. See the header of
# scripts/gen-models.mjs, and NEXT-STEPS §7 for what that costs.
models\:refresh: ## Refresh the seed model catalogue from models.dev
@node scripts/gen-models.mjs
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)
@bash scripts/release.sh
release\:ci: ## Trigger the CI publish workflow. BUMP=patch|minor|major, VERSION=x.y.z, DRY=1
@gh workflow run publish.yml --ref "$$(git rev-parse --abbrev-ref HEAD)" \
-f bump="$(or $(BUMP),patch)" -f version="$(VERSION)" -f dry_run="$(if $(DRY),true,false)"
@echo "Dispatched publish on $$(git rev-parse --abbrev-ref HEAD). Watch: gh run watch"
release\:retry: ## Publish a tag that never reached npm. TAG=cli-vX.Y.Z
@test -n "$(TAG)" || { echo "release:retry needs TAG=cli-vX.Y.Z"; exit 1; }
@gh workflow run release.yml -f tag="$(TAG)"
@echo "Dispatched release for $(TAG). Watch: gh run watch"
release\:version: ## Print the version the next release would get (BUMP=, VERSION=)
@node scripts/next-version.mjs $(if $(VERSION),--version "$(VERSION)",--bump "$(or $(BUMP),patch)")
# Single-letter shortcuts. Deliberately undocumented — a `##` here would render
# as an empty section in help, and the footer already advertises them.
.PHONY: i d b l f t c
i: install
d: dev
b: build
l: lint
f: fix
t: typecheck
c: check