Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/deploy-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,36 @@ concurrency:
cancel-in-progress: true

jobs:
# Dependabot bumps package.json but cannot regenerate bun.lock, so a merged
# dependency PR leaves the two out of sync and every later `bun install
# --frozen-lockfile` fails. Catch that drift on the PR that introduces it.
lockfile:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.3.14

- name: Resolve dependencies without the lockfile pin
run: bun install --no-frozen-lockfile

- name: Check bun.lock matches package.json
run: |
if ! git diff --quiet -- bun.lock; then
echo "::error file=bun.lock::bun.lock is out of sync with package.json. Run 'bun install' locally and commit the updated bun.lock."
git --no-pager diff --stat -- bun.lock
exit 1
fi
echo "bun.lock is in sync with package.json."

build:
runs-on: ubuntu-latest
permissions:
Expand All @@ -36,6 +66,9 @@ jobs:
- name: Install dependencies
run: bun install --frozen-lockfile

- name: Type-check content and components
run: bun run check

- name: Build documentation
run: bun run build

Expand Down
196 changes: 173 additions & 23 deletions bun.lock

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"build": "astro build",
"preview": "astro preview",
"astro": "astro",
"check": "astro check",
"lint": "oxlint .",
"lint:fix": "oxlint --fix .",
"format": "oxfmt --write .",
Expand All @@ -20,7 +21,9 @@
"sharp": "^0.35.3"
},
"devDependencies": {
"oxfmt": "^0.64.0",
"oxlint": "^1.78.0"
"@astrojs/check": "^0.9.10",
"oxfmt": "^0.65",
"oxlint": "^1.78.0",
"typescript": "^6"
}
}
88 changes: 85 additions & 3 deletions src/content/docs/guides/command-execution.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ title: Command Execution
description: How Ahoy finds and runs your commands
---

import { Aside } from "@astrojs/starlight/components";

## How Ahoy finds your config

When you run an Ahoy command, it:
Expand All @@ -27,7 +29,7 @@ commands:
cmd: cp $1 $2

run-in-container:
cmd: docker-compose exec app $@
cmd: docker-compose exec app "$@"
```

```bash
Expand All @@ -38,9 +40,27 @@ ahoy run-in-container npm test

`$@` passes all arguments through, which is handy for wrapper commands. Use `$#` to check the argument count, and `$0` for the command name.

Everything after the command name reaches your command verbatim, flags
included, so wrapping a tool that takes its own options needs nothing special:

```yaml
commands:
phpunit:
cmd: docker compose exec app vendor/bin/phpunit "$@"
```

```bash
ahoy phpunit --filter=MyTest --stop-on-failure
```

Ahoy's own flags - `--verbose`, `--file` and friends - are only recognised
before the command name, so they never collide with your command's options.

## Interactive commands

Ahoy's stdin is connected directly to your terminal, so interactive commands work without any special setup:
All three standard streams - stdin, stdout and stderr - are inherited from the
process that invoked Ahoy. Ahoy does not read, buffer or copy them, so a command
runs exactly as it would if you had typed it yourself:

```yaml
commands:
Expand All @@ -53,7 +73,44 @@ commands:
cmd: mysql -u$DB_USER -p$DB_PASS $DB_NAME
```

These run fully interactively - you get a real prompt, readline support, and piping all work as expected.
When you run these from a terminal, the inherited streams are attached to a
TTY, so they run fully interactively - you get a real prompt, readline support,
and piping all work as expected. Invoked from a script or CI job, where the
streams are pipes or files instead, the command sees exactly what it would see
there without Ahoy.

### Full-screen and TUI commands

Because the streams are the inherited descriptors rather than a copy of them,
commands can ask about them directly - whether they are a TTY at all, and if so
the terminal size - and get a true answer. When Ahoy is run from a terminal that
is what full-screen and menu-driven tools need in order to draw:

```yaml
commands:
pick-env:
usage: Choose an environment to deploy to
cmd: gum choose staging production

logs:
usage: Browse container logs interactively
cmd: docker compose logs --tail=1000 | fzf --tac
```

Tools built on [Bubble Tea](https://github.com/charmbracelet/bubbletea) - `gum`,
`glow` and similar - draw on stderr specifically, as do progress bars, spinners
and anything that repaints in place.

<Aside type="caution" title="Fixed in v3.0.1">
Ahoy v3.0.0 routed stderr through an internal pipe. Bytes still reached your
terminal, but the terminal itself was no longer on the other end, so these
tools measured a 0x0 screen and drew nothing - or, with `gum` on Bubble Tea
v2, crashed outright. If you are on v3.0.0 and an interactive command renders
blank, upgrade. See [issue #180](https://github.com/ahoy-cli/ahoy/issues/180).
</Aside>

Output is not buffered either - a long-running command's output appears as it
is produced, not in a burst when the command exits.

## Chaining commands

Expand Down Expand Up @@ -120,3 +177,28 @@ Ahoy passes exit codes through unchanged. If a command exits non-zero, Ahoy exit
```bash
ahoy test && ahoy deploy
```

## Controlling execution within a command

Because the command body is bash, the usual operators control what runs next:

- `&&` - run the next command only if the previous one succeeded (exit `0`)
- `||` - run the next command only if the previous one failed (non-zero exit)
- `;` - always run the next command

```yaml
commands:
only-run-1-2-and-4:
cmd: |
echo "1 - If this passes" &&
echo "2 - Then do this" ||
echo "3 - Or do this if 1 or 2 fails" ;
echo "4 - Do this no matter what"
```

## Debugging and gotchas

- **Use `--verbose` to see what actually ran.** It prints the resolved command before executing it, which is the fastest way to diagnose an unexpected result.
- **Commands run in a subshell.** Environment variables are copied in, but nothing a command does can change your parent shell - it cannot change your current directory or set variables for your session.
- **Quotes can be tricky.** When passing a command through to a subcommand, quotes can be lost along the way. Check `--verbose` first, then experiment with single versus double quotes. Prefer `cmd: |` for anything complex so YAML quoting stays out of the way.
- **Check your YAML if Ahoy complains.** Indentation and structure errors surface as parse failures; `ahoy config validate` reports them along with unsupported fields.
2 changes: 1 addition & 1 deletion src/content/docs/guides/getting-started.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ commands:

logs:
usage: Follow container logs
cmd: docker-compose logs -f $@
cmd: docker-compose logs -f "$@"
```

Run your commands from anywhere in the project tree:
Expand Down
42 changes: 41 additions & 1 deletion src/content/docs/guides/writing-commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ commands:

run:
usage: Run any command in the app container
cmd: docker compose exec app $@
cmd: docker compose exec app "$@"
```

`$1`, `$2` for specific positions; `$@` to forward everything. The last pattern is great for transparent wrappers:
Expand All @@ -47,6 +47,24 @@ commands:
ahoy run npm test -- --watch
```

Flags count as arguments. Everything after the command name reaches your
command exactly as typed, in order, and Ahoy never claims any of it:

```bash
ahoy run npm test --watch
ahoy run chown -R www-data:www-data .
```

Ahoy consumes a `--` only when it is the first thing after the command name,
where it has always just meant "the rest is arguments". A `--` anywhere else
belongs to your command, so wrapping a tool that needs its own separator works
as you would expect:

```bash
ahoy k exec mypod -- ls /app # kubectl gets its separator
ahoy run npm test -- --watch # npm passes --watch to the test script
```

## Multi-line scripts

Use the YAML block scalar (`|`) for anything beyond a single line. Standard bash applies:
Expand Down Expand Up @@ -180,6 +198,28 @@ commands:

Then your team just runs `ahoy drush cache:rebuild` or `ahoy composer require vendor/package`.

The wrapped tool keeps its own flags, including the ones that would otherwise
be Ahoy's:

```bash
ahoy drush --help # Drush's help
ahoy composer require --dev vendor/pkg
ahoy drush sql:query --extra=-A "SELECT 1"
ahoy node --version
```

The one thing to know: since `--help` belongs to the wrapped tool, Ahoy's own
help for a command is `ahoy help drush`.

<Aside type="caution" title="Fixed in v3.0.1">
Ahoy v3.0.0 parsed a command's arguments before running it and silently
dropped any flag it did not recognise, along with the token following it -
so `ahoy fix-perms chown -R www-data:www-data .` arrived as `chown .`. The
wrapped tool ran successfully with different arguments, which made this
quiet rather than loud. Upgrade if you wrap anything that takes flags. See
[issue #182](https://github.com/ahoy-cli/ahoy/issues/182).
</Aside>

## Environment-specific commands <Badge text="v2.3.0+" variant="tip" />

Use the `env` field for commands that need their own configuration:
Expand Down
107 changes: 106 additions & 1 deletion src/content/docs/reference/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,22 @@ ahoy [flags] [command] [args...]

## Global flags

Global flags belong **before** the command name. Everything after the command
name belongs to that command - see
[Passing arguments to commands](#passing-arguments-to-commands).

| Flag | Short | Description |
| --------------- | ----- | --------------------------------------------------------------- |
| `--file <path>` | `-f` | Use a specific config file instead of searching for `.ahoy.yml` |
| `--verbose` | | Enable verbose output |
| `--verbose` | `-v` | Enable verbose output |
| `--version` | | Print the Ahoy version and exit |
| `--help` | `-h` | Show help |

```bash
ahoy --verbose deploy # --verbose is Ahoy's
ahoy deploy --verbose # --verbose is passed to the deploy command
```

## Built-in commands

### `ahoy config` <Badge text="v3" variant="success" />
Expand Down Expand Up @@ -61,6 +70,47 @@ ahoy help <command> # Help for a specific command

`ahoy init` still works but is deprecated. It redirects to `ahoy config init` with a notice. Prefer `ahoy config init` in any new scripts or documentation.

## Conventional commands

### `ahoy confirm <message>`

`confirm` is not built in - it is a convention: a small command you define in your own config (and can import from [confirmation.ahoy.yml](https://github.com/ahoy-cli/ahoy/blob/master/examples/confirmation.ahoy.yml)) to guard destructive operations. Because Ahoy commands can call other Ahoy commands, it behaves like a reusable function.

It prompts with your message and exits `0` when the answer is `y`, or `1` otherwise, so it chains with `&&`, `||` and `|| exit 0`:

```bash
ahoy confirm "Are you sure you want to do this?"
# >> Are you sure you want to do this? [y/N]

ahoy confirm "Delete the build directory?" && rm -rf ./build || echo "Skipping..."
```

A minimal definition:

```yaml
commands:
confirm:
hide: true
cmd: |
read -r -p ">> $1 [y/N] " response
[ "$response" = "y" ]
```

`hide: true` keeps the helper out of the command listing without disabling it.

The [examples file](https://github.com/ahoy-cli/ahoy/blob/master/examples/examples.ahoy.yml) carries a fuller version that also works unattended, controlled by two environment variables you set yourself:

| Variable | Effect |
| ----------------------- | ----------------------------------------------------------------- |
| `AHOY_CONFIRM_RESPONSE` | Answers the prompt instead of reading from the terminal (e.g. `y`) |
| `AHOY_CONFIRM_WAIT_SKIP` | Set to `1` to skip the 3-second grace pause in automated mode |

```bash
AHOY_CONFIRM_RESPONSE=y AHOY_CONFIRM_WAIT_SKIP=1 ahoy db:reset
```

See [Writing Commands](/guides/writing-commands) for the full pattern.

## Config file discovery

Ahoy searches for `.ahoy.yml` in the following order:
Expand All @@ -81,6 +131,61 @@ ahoy copy a.txt b.txt # $1 = "a.txt", $2 = "b.txt"
ahoy run npm test # $@ = "npm test"
```

Everything after the command name is passed through **verbatim and in order**,
including anything that looks like a flag. Ahoy never claims a flag there, even
one of its own - which is what makes Ahoy usable as a wrapper around other
tools:

```bash
ahoy test --filter=MyTest --stop-on-failure # all four tokens reach the command
ahoy fix-perms chown -R www-data:www-data . # -R is not Ahoy's to take
ahoy phpunit --help # PHPUnit's help, not Ahoy's
```

To reach Ahoy's own help for a command, use `ahoy help <command>`.

A leading `--` is accepted for readability and removed before the arguments are
passed on. Any later `--` is the command's own data and reaches it untouched,
which matters for the many tools that need a separator of their own:

```bash
ahoy run -- npm test # $@ = "npm test"
ahoy k exec mypod -- ls /app # $@ = "exec mypod -- ls /app"
ahoy git checkout -- src/main.go # $@ = "checkout -- src/main.go"
```

### Where each token goes

| Typed | Read by | Reaches the command as |
| ---------------------------------- | ------- | ------------------------ |
| `ahoy --verbose test` | Ahoy | - |
| `ahoy -f build.yml test` | Ahoy | - |
| `ahoy -f=build.yml test` | Ahoy | - |
| `ahoy test --verbose` | Command | `--verbose` |
| `ahoy test --filter=X` | Command | `--filter=X` |
| `ahoy test -R -p /tmp/logs` | Command | `-R -p /tmp/logs` |
| `ahoy test --help` | Command | `--help` |
| `ahoy test -- --anything` | Command | `--anything` |
| `ahoy test a -- b` | Command | `a -- b` |
| `ahoy help test` | Ahoy | - |

## Standard streams

Ahoy passes stdin, stdout and stderr through unchanged - each command inherits
the streams of the process that invoked Ahoy, whether those are a terminal,
pipes, or files. It does not read, buffer, tee or copy them, so:

- Interactive commands get a real prompt and readline support when stdin is
attached to a TTY
- Full-screen and TUI commands (`gum`, `fzf`, anything on Bubble Tea) can query
the terminal size and draw correctly when the relevant streams are a TTY
- Output appears as it is produced rather than when the command exits
- Redirection behaves as it would without Ahoy - `ahoy test 2>errors.log`
sends the command's stderr to the file, unbuffered, while stdout stays on
the terminal

Exit codes pass through unchanged - see [Exit codes](#exit-codes).

## Shell completion

### Bash
Expand Down