[x] xiaoyu.sh

XIAOYU(1) — THE COMPLETE MANUAL

A reference to keep.

# xiaoyu

```text
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣤⣤⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣾⣿⣿⡿⠿⣿⣷⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⣿⣿⣿⠋⢀⣠⣾⣿⣷⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⡿⣡⣾⣿⣿⣿⣿⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣰⣿⣷⠿⠟⠛⠛⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⢀⣠⣤⣶⣶⣦⣤⡀⢿⣿⡿⣿⣥⣤⣂⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⣠⣶⣿⣿⣿⠟⠛⠛⣻⣿⣿⣏⠁⠀⠹⣿⣿⣿⣷⣦⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠻⣿⣿⠟⠁⢀⣠⣾⣿⡿⣿⣿⣄⠀⠀⠉⠙⠛⠿⠿⠿⠿⠽⢽⣿⣿⡿⢿⣿⣿⣿⣶⣄⠀⠀⠀
⠀⠀⠀⢸⣿⣿⣿⣿⣿⣿⡟⠀⣿⣿⣿⣷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⢿⣿⣿⣧⠀⠀
⠀⠀⠀⠀⢻⣿⣿⣿⣿⠟⠁⠀⢸⣿⡿⠛⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⡀⠀
⠀⠀⠀⠀⠀⠉⠚⠋⠀⠀⢀⣠⣾⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣷⠀
⠀⠀⢀⣠⣴⣶⣶⣶⣿⣿⣿⣿⡿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠹⣿⡟⠋⠓
⠀⣴⣿⠇⠉⠉⠛⠛⠙⠛⠛⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠁⠃⠀⠀
⣴⣿⣿⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⢻⣿⣿⣷⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠳⡏⠋⠉⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
```

**Static sites, tied together.** · xiaoyu.sh

## What it does

`xiaoyu` is a single command-line binary that puts a folder of static files
online at your own domain, over HTTPS, on AWS.

Point it at a directory, tell it your domain, and it creates and maintains
everything needed to serve that directory:

- a **private S3 bucket** (never public, versioned) for the files;
- a **CloudFront distribution** that reads S3 through Origin Access Control,
  with compression, HTTP→HTTPS redirect, TLS 1.2+, and AWS's managed security
  headers;
- a **TLS certificate** from ACM in `us-east-1`, validated by DNS;
- **atomic releases**: each deploy uploads a new immutable, content-addressed
  release, and CloudFront switches to it only once every byte is in place, so
  visitors never see a half-updated site — and you can roll back;
- **verification**: after deploying, it can fetch your live HTTPS URLs and
  compare their SHA-256 with your local files;
- **aliases and redirects**: extra hostnames like `www.`, all redirected to
  one canonical host, plus directory URLs or SPA routing.

It also creates a dedicated, least-privilege IAM user for day-to-day deploys,
so your admin credentials are only needed once.

There is no AWS CLI, no Node.js, no Terraform, no daemon, no database. One
binary, one small YAML file.

**Requirements:** an AWS account (commercial regions only — China and GovCloud
are rejected), a domain you control DNS for, and Go 1.25+ if you build from
source.

## Install

Download a prebuilt binary for macOS or Linux from
xiaoyu.sh, verify it against the published
`SHA256SUMS.txt`, and put it on your PATH. The builds are unsigned, so macOS
quarantines them: clear it with `xattr -c xiaoyu` after extracting.

Or build for your machine:

```sh
make build
./bin/xiaoyu version
```

Or install with Go 1.25 or newer:

```sh
go install github.com/klodio/xiaoyu/cmd/xiaoyu@latest
```

Cross-compile for another machine, for example a Linux CI runner:

```sh
GOOS=linux GOARCH=amd64 go build -trimpath -o xiaoyu-linux-amd64 ./cmd/xiaoyu
```

Copying that one executable to another machine is enough. That machine needs
only system CA certificates and AWS credentials from the standard AWS SDK
chain (environment variables, a profile/SSO session, web identity, or an
instance/task role).

## Quick start

Run everything from the folder that contains your website files.

### 1. Create the configuration

```sh
xiaoyu init
```

An interactive wizard explains every setting, recommends a default, validates
your answers, and writes `.xiaoyu.yaml` after you confirm. It never asks for
secrets. Prefer editing by hand? Copy
`.xiaoyu.example.yaml` to `.xiaoyu.yaml` instead.

Then check it — this is offline and needs no credentials:

```sh
xiaoyu config check
```

### 2. Get provisioning credentials

AWS will not let an unauthenticated tool create an IAM user, so the first-time
setup needs an identity that already exists. Root credentials are refused
either way. Pick one of these:

**A. Launch the bootstrap stack** (no IAM work by hand). The launch link on
xiaoyu.sh opens CloudFormation with
[`cloudformation/bootstrap.yaml`](cloudformation/bootstrap.yaml), which creates
one temporary IAM user scoped to this account and outputs its access key. Then
hand that key to xiaoyu:

```sh
xiaoyu login
```

`login` prompts for the key (the secret is not echoed), proves it works with
`sts:GetCallerIdentity`, and offers to save it with mode `0600` outside your
site folder. `--no-save` verifies without writing anything.

The stack's identity can create this site's infrastructure and the runtime IAM
user — it is scoped, not harmless. **Delete the stack once step 5 finishes**;
that deletes the user and revokes the key. It deliberately cannot run
`xiaoyu destroy`. The stack is created in `ap-northeast-2`, where the template
is hosted; that is unrelated to your site's region.

**B. Use your own administrator credentials**, ideally temporary ones from an
admin role:

```sh
export AWS_ACCESS_KEY_ID='temporary-bootstrap-key'
export AWS_SECRET_ACCESS_KEY='temporary-bootstrap-secret'
export AWS_SESSION_TOKEN='temporary-bootstrap-token'   # if applicable
```

`xiaoyu login` accepts these too, if you would rather keep them in a file than
in your shell history.

### 3. Provision the site

```sh
xiaoyu bootstrap-user --deploy
```

The first run normally stops with **exit code 2**: ACM is waiting for DNS
validation. It prints the exact CNAME record to add. No IAM user or access key
is created yet.

### 4. Add the certificate-validation DNS record

Add the printed CNAME at your DNS provider. If you use Namecheap, the tool can
do it for you (see Namecheap DNS). Keep this record forever
so ACM can renew the certificate automatically.

### 5. Finish provisioning and get your deploy credentials

Once ACM reports `ISSUED`, run the same command again:

```sh
xiaoyu bootstrap-user --deploy
```

It finishes CloudFront, publishes the first release, then creates an IAM user
scoped to exactly this site's bucket, certificate, distribution, OAC, and
routing function. It verifies that user's new key actually works, writes it
with mode `0600` to your OS user-config directory (outside your website
folder), and prints the path.

Finally, add the printed site DNS record (ALIAS/CNAME to CloudFront) for each
domain and alias.

### 6. Use the dedicated credentials from now on

```sh
source '/path/printed/by/xiaoyu/example.com-xxxxxxxxxxxx.env'
xiaoyu status
```

That file exports the AWS key plus the site settings that were in effect, so a
fresh shell is enough to deploy. Keep it private; it holds a long-lived secret.

### 7. Verify the live site

```sh
xiaoyu verify --wait         # index plus one sample file
xiaoyu verify --all --wait   # every file
```

Verification downloads your public HTTPS URLs and compares SHA-256 digests
with the local files, checking every alias redirect on the way. `--wait` waits
for CloudFront to finish deploying and invalidating first.

Leave `verify_after_deploy: false` until DNS resolves; then turn it on so
every deploy checks itself.

That's it. The everyday loop afterwards is: edit files → `xiaoyu deploy`.

## Everyday use

```sh
xiaoyu deploy                  # publish the current folder as a new release
xiaoyu upload                  # upload all files (no AWS config reconciliation)
xiaoyu upload --file app.css   # upload a single file
xiaoyu sync --dry-run          # show exactly what would change
xiaoyu sync                    # publish changes
xiaoyu sync --delete --yes     # also drop files that no longer exist locally
```

`upload` and `sync` never delete anything unless you pass `--delete`. With
`atomic_deploy: true` (the default) all three create an immutable release and
switch CloudFront only when it is complete.

Changed files are detected by SHA-256, stored as object metadata — not by
timestamps or ETags. Unchanged files are copied server-side rather than
re-uploaded.

If you changed settings that affect AWS itself (new alias, new routing mode,
changed security headers), the dedicated user is intentionally not allowed to
do that. Restore admin credentials and run:

```sh
xiaoyu bootstrap-user --provision
```

which reprovisions and refreshes the user's policy without rotating its key.

## Configuration

`.xiaoyu.yaml` in the current directory holds non-secret settings.
`XIAOYU_CONFIG` selects a different file. Every setting has an environment
override, which wins over the YAML value. Unknown YAML keys are rejected.

```yaml
domain: example.com
aliases:
  - www.example.com
canonical_domain: example.com
region: eu-west-1
source: .
routing: directory
```

| YAML | Environment override | Default |
|---|---|---|
| `domain` | `XIAOYU_DOMAIN` | required |
| `aliases` | `XIAOYU_ALIASES` (comma-separated) | none |
| `canonical_domain` | `XIAOYU_CANONICAL_DOMAIN` | primary domain |
| `region` | `AWS_REGION` | required |
| `source` | `XIAOYU_SOURCE_DIR` | current directory |
| `bucket` | `XIAOYU_BUCKET` | deterministic account/domain name |
| `routing` | `XIAOYU_ROUTING` | `files` |
| `security_headers` | `XIAOYU_SECURITY_HEADERS` | `true` |
| `atomic_deploy` | `XIAOYU_ATOMIC_DEPLOY` | `true` |
| `retain_releases` | `XIAOYU_RETAIN_RELEASES` | `5` |
| `verify_after_deploy` | `XIAOYU_VERIFY_AFTER_DEPLOY` | `false` |
| `verify_all_files` | `XIAOYU_VERIFY_ALL_FILES` | `false` |
| `index_document` | `XIAOYU_INDEX_DOCUMENT` | `index.html` |
| `error_document` | `XIAOYU_ERROR_DOCUMENT` | `404.html` when present |
| `price_class` | `XIAOYU_PRICE_CLASS` | `PriceClass_100` |
| `asset_cache_control` | `XIAOYU_CACHE_CONTROL` | `public,max-age=300` |
| `html_cache_control` | `XIAOYU_HTML_CACHE_CONTROL` | `no-cache` |
| `exclude` | `XIAOYU_EXCLUDE` (comma-separated) | none |
| `log_bucket` | `XIAOYU_LOG_BUCKET` | disabled |
| `log_prefix` | `XIAOYU_LOG_PREFIX` | empty |

Exclude patterns use Go `path.Match`. Some things are never uploaded, whatever
you configure: `.git`, `.DS_Store`, `.env*`, `.xiaoyu.env*`, `.xiaoyu.yaml`,
`.xiaoyu.yml`, `.release.json`, the `xiaoyu`/`xiaoyu.exe` binaries, the
the tool's internal `_xiaoyu/` and `_s3site/` namespaces, Open Design directories (`.od/`, `.od-skills/`,
`open-design/`, `.open-design/`), symlinks, and non-regular files. Similarly
named public assets such as `xiaoyu.css` upload normally.

### Routing

- `files` — direct object paths only; `/` serves `index_document`.
- `directory` — `/about` and `/about/` serve `/about/index.html`.
- `spa` — extensionless paths serve the root `index_document`.

Rewrites and canonical-host redirects run in one managed CloudFront Function.
Every alias goes into both the certificate and the distribution; requests to a
non-canonical hostname get a `301` that preserves path and query string.
`directory` and `spa` reject extensionless source files whose public paths the
rewrite would capture.

## Commands

| Command | Purpose |
|---|---|
| `xiaoyu init` | Interactive configuration wizard (alias of `config init`) |
| `xiaoyu config check` | Validate local configuration and files, offline |
| `xiaoyu login` | Verify pasted provisioning credentials and store them privately |
| `xiaoyu bootstrap-user` | Provision AWS and create/rotate/repair the dedicated user |
| `xiaoyu deploy` | Reconcile AWS settings and publish a release |
| `xiaoyu plan` | Preview AWS and content changes, read-only |
| `xiaoyu status` | AWS, DNS, HTTPS, release, and 24h CloudWatch metrics |
| `xiaoyu doctor` | Diagnose local, AWS, DNS, HTTPS, redirect, and content health |
| `xiaoyu upload` | Upload all files, or one with `--file` |
| `xiaoyu sync` | Checksum sync, optionally with `--delete` |
| `xiaoyu verify` | Compare live HTTPS responses with local SHA-256 |
| `xiaoyu releases` | List releases, or `--prune --yes` old ones |
| `xiaoyu rollback --yes <id>` | Point CloudFront back at a retained release |
| `xiaoyu dns` | Plan or apply Namecheap DNS records |
| `xiaoyu destroy` | Plan, or confirm, guarded teardown |
| `xiaoyu version` | Print the version |

`status`, `releases`, `rollback`, `dns`, and `destroy` work without the local
website folder. `plan`, `deploy`, `upload`, `sync`, `verify`, and
`bootstrap-user --deploy` need it.

Run `xiaoyu` with no arguments for the full usage text.

## Releases and rollback

Each release is stored under a content-addressed prefix:

```text
_xiaoyu/releases/<release-id>/
```

Sites provisioned before the rename keep `_s3site/` instead; both are read, and
neither is ever published.

CloudFront's origin path is switched only after the complete release and its
manifest are verified in S3. Old releases stay immutable, so rollback is just
a pointer change:

```sh
xiaoyu releases                      # list, with integrity status
xiaoyu rollback --yes <release-id>   # go back
xiaoyu releases --prune --yes        # delete inactive ones beyond retain_releases
```

Pruning keeps every release younger than one hour, re-checks the active
release before each deletion, and waits for CloudFront and its invalidations
to settle. Deploy, rollback, prune, and destroy coordinate through a renewable
S3 lease so two of them can never act on a half-written release.

## Checking a site

```sh
xiaoyu plan      # what would change, read-only
xiaoyu status    # AWS/DNS/HTTPS state and CloudFront metrics
xiaoyu doctor    # actionable health checks, including a live byte comparison
xiaoyu verify    # SHA-256 check of the live site
```

For subdomains, DNS status requires an exact CNAME match to the distribution.
At an apex domain, ALIAS/ANAME flattening hides the target, so a successful
lookup is reported as indeterminate rather than as proof.

## Namecheap DNS

Every command that needs DNS prints records you can add by hand. If your
domain is at Namecheap, automation is available and always plan-first:

```sh
export NAMECHEAP_API_USER='...'
export NAMECHEAP_API_KEY='...'
export NAMECHEAP_USERNAME='...'
export NAMECHEAP_CLIENT_IP='your-whitelisted-public-ip'

xiaoyu dns                 # show the plan
xiaoyu dns --apply --yes   # apply it
```

It reads your complete current record set, replaces only conflicting records
for the hosts it needs, keeps everything else (MX, TXT, …), and re-reads all
records immediately before writing so it aborts if anything changed since the
plan. Namecheap has no conditional-write token, so a small unavoidable race
remains. All aliases must currently share one registrable domain; manual
instructions support aliases in different zones.

## Access logs

Set `log_bucket` to enable CloudFront standard (legacy) access logging:

```yaml
log_bucket: my-cloudfront-log-bucket
log_prefix: example.com/
```

The bucket must already exist, be separate from the site bucket, and be
configured for CloudFront log delivery. Legacy logging needs bucket-ACL
permissions, so it does **not** work with the dedicated runtime user — use it
only if you deploy under an administrator-managed role. The site bucket's
private, bucket-owner-enforced configuration is never weakened for logging.

## Tearing down

`destroy` is plan-only unless you confirm:

```sh
xiaoyu destroy                       # show what would be deleted
xiaoyu destroy --retain-bucket --yes # delete the edge, keep the bucket
xiaoyu destroy --yes                 # delete everything it owns
```

It only touches resources whose ownership tags and private ownership marker
prove they belong to this site, and re-checks that immediately before each
deletion. Namecheap DNS records and the IAM user are left behind and reported
so you can clean them up deliberately. The dedicated deploy user has no
deletion rights — run destroy with administrator credentials.

After `--retain-bucket`, the bucket and all its releases remain but the
distribution, OAC, certificate, and routing functions are gone. To bring the
site back, restore the same configuration and administrator credentials, keep
`XIAOYU_IAM_USER` pointing at the existing user, and run:

```sh
xiaoyu bootstrap-user --provision
```

This rebuilds the edge around the retained release without needing your local
files at all. If the retained release is missing or unverifiable, it fails
rather than silently publishing something else. You may need to complete a new
ACM validation and rerun the command.

## How it stays safe

A few design choices worth knowing:

- **S3 is never public.** The public site is CloudFront, which signs its
  origin requests with Origin Access Control. The S3 website endpoint is never
  enabled. The bucket policy requires TLS, requests are bound to the expected
  AWS account, and deployment refuses to preserve unexpected access-granting
  policy statements.
- **Admin credentials are temporary.** They provision infrastructure once; the
  long-lived user created afterwards is scoped to the exact ARNs of this site
  and can only deploy content. It cannot create certificates, distributions,
  IAM resources, or delete infrastructure.
- **Deploys are all-or-nothing.** Content is verified in S3 before CloudFront
  ever points at it, and routing code is content-addressed so a new rewrite is
  never served against an old release.
- **Verification is byte-exact.** Comparisons use SHA-256 of the actual HTTPS
  response body, not ETags or timestamps.

## Exit codes

- `0` — success
- `1` — configuration, verification, or AWS operation failed
- `2` — waiting for ACM DNS validation (add the printed CNAME and rerun)

## Development

```sh
make build              # ./bin/xiaoyu
make release VERSION=v1.2.3
make test               # never contacts AWS
make vet fmt
```

`make release` requires a strict SemVer 2.0 version with a leading `v` and
checks the finished binary.

### Real-AWS integration tests

These are behind a build tag and an explicit opt-in because they create
billable resources. Use a dedicated test domain and account.

```sh
export AWS_ACCESS_KEY_ID='...'
export AWS_SECRET_ACCESS_KEY='...'
export AWS_REGION='eu-west-1'
export XIAOYU_INTEGRATION_DOMAIN='xiaoyu-test.example.com'
export XIAOYU_RUN_AWS_INTEGRATION=1

make test-integration
make test-integration-compile   # compile only, no AWS
```

The first run may stop for ACM validation; add the printed CNAME and rerun,
then add the site DNS record. Use
`XIAOYU_INTEGRATION_SKIP_CUSTOM_DOMAIN=1` only while that record is pending.
The suite covers provisioning idempotency, anonymous S3 access returning
`403`, HTTP→HTTPS redirects, byte-exact content over the custom domain, sync
and single-file invalidation, atomic releases and rollback, and resource
reuse. Resources are deliberately left in place; run `xiaoyu destroy --yes`
when finished.

Additional gated suites:

- `XIAOYU_RUN_RETAIN_RECOVERY_INTEGRATION=1` — destructive
  `destroy --retain-bucket` and recovery round-trip.
- `XIAOYU_RUN_IAM_INTEGRATION=1` — creates and cleans up a dedicated IAM user
  (the test domain's distribution must already exist).
- `XIAOYU_RUN_IAM_DEPLOY_INTEGRATION=1` — additionally provisions and proves
  the generated user can deploy.

`XIAOYU_INTEGRATION_BUCKET` overrides the test bucket,
`XIAOYU_INTEGRATION_TIMEOUT` (default `30m`) sets the lifecycle timeout, and
`XIAOYU_INTEGRATION_TEST_TIMEOUT` (default `35m`) sets Make's outer Go test
timeout, which must exceed the lifecycle timeout by more than two minutes.

### The project website

[`website/`](website/) holds the xiaoyu.sh site: plain HTML/CSS, system
monospace fonts, no dependencies or external assets. The online manual is
generated from this README, so documentation stays in sync.

```sh
make website                      # downloads + pages, into dist/website/
make website VERSION=v1.2.3       # stamp a real version into the binaries
make website-preview              # http://127.0.0.1:8080
```

`make website` runs `make downloads` first, which cross-compiles the binary for
macOS and Linux into `dist/website/download/`, packages the tracked source with
`git archive`, and writes `SHA256SUMS.txt`. The page build refuses to run if
those artifacts are missing, so its download links can never 404. Artifacts are
deliberately named `xiaoyu-<os>-<arch>.tar.gz`: a file named exactly `xiaoyu`
is always excluded from uploads and could never be published. The version in
`dist/website-version.txt` is shown on the page; it is `dev` unless you pass
`VERSION`.

#### Publishing the CloudFormation template

The build copies [`cloudformation/bootstrap.yaml`](cloudformation/bootstrap.yaml)
to `dist/website/aws/bootstrap.yaml` as the readable copy. The launch button
cannot point there: CloudFormation only accepts a `TemplateURL` served from an
S3 bucket, and its docs state that "URLs from S3 static websites are not
supported". So the machine-readable copy needs a small public bucket of its
own. The published copy lives at:

```text
https://xiaoyu-bootstrap.s3.ap-northeast-2.amazonaws.com/bootstrap.yaml
```

That is the default the page is built with; set `XIAOYU_TEMPLATE_URL` at build
time to point somewhere else. The launch link creates the stack in the region
parsed from that URL — `ap-northeast-2` here — because CloudFormation reads the
template from S3 and cross-region reads are not dependably supported. The stack
holds only a global IAM user, so its region says nothing about where the site
lives; override it with `XIAOYU_STACK_REGION` if you move the template.

The key is not versioned, so re-uploading changes what every existing launch
link creates. Edit the template in place only for fixes you want everyone to
get; publish a new key for anything else.

`dist/website/` is a self-contained site root: upload it to any static host, or
publish it with xiaoyu from inside the folder. The build writes a
`.xiaoyu.yaml` there (copied from [`website.xiaoyu.yaml`](website.xiaoyu.yaml)
with `source: .`), and that filename is always excluded from uploads, so it is
never published.

```sh
make build website
cd dist/website
xiaoyu config check
xiaoyu bootstrap-user --deploy   # first time, with temporary admin credentials
xiaoyu deploy                    # every time after that
```

Edit `website.xiaoyu.yaml` in the repository root to change site settings; the
copy in `dist/website/` is regenerated by every build.

Then follow the quick start from step 4. `xiaoyu.sh` is an
apex domain, so use a DNS provider with ALIAS/ANAME or CNAME flattening.
Building or previewing the website never touches AWS or DNS.

## Migrating from s3site

This tool used to be called `s3site`. Existing `.s3site.yaml` files and
`S3SITE_` environment variables still work. See
[MIGRATING-FROM-S3SITE.md](MIGRATING-FROM-S3SITE.md).