The contributor's path. Where modules live in the asd-cli source, how to add one, how to add a plugin to the bundled set, and how the test + release cycle works.
This page assumes you're working in the
asd-engineering/asd-cli
source tree. If you just want to add automation hooks to your own
project, learn/07-build-a-module
is the right page — plugin authoring doesn't require any of this.
asd-cli/
├── src/ # the CLI dispatcher + core
│ ├── cli/ # `asd <verb>` entry points
│ ├── core/ # cross-cutting helpers
│ └── …
├── modules/
│ ├── <name>/ # one subsystem per directory
│ │ ├── <name>.manifest.yaml # module identity + activation
│ │ ├── config.json
│ │ ├── docker/ # compose files if any
│ │ ├── index.just # entry point for the module
│ │ ├── map.env # env var declarations
│ │ ├── map.targets # service-target mappings
│ │ ├── scripts/
│ │ │ └── api.just # just recipes wrapping the CLI
│ │ ├── tpl.env # env template fragment
│ │ └── workspace.dirs # workspace directories the module owns
│ └── plugin/
│ └── <name>/ # one plugin per directory
│ ├── tpl.asd.yml # merged into user asd.yaml on activation
│ └── scripts/
│ └── config.schema.ts # Zod schema for plugins_config.<name>
├── tests/ # integration tests
└── …
| Module | Plugin | |
|---|---|---|
| Files | Subsystem code, manifest, scripts/api.just | tpl.asd.yml + config schema |
| Adds CLI commands | Yes — defined in src/cli/<module>.ts and wired up at compile |
No — declares hooks consumed by existing commands |
| Compiled into the binary | Yes | No (read at runtime) |
| Examples | auth, net, caddy, vault, code |
angular, supabase |
If you're adding a new subsystem (a new way to do something at the
command line), it's a module. If you're packaging an existing
stack's automation + services for reuse, it's a plugin.
Create the plugin directory.
modules/plugin/<your-plugin>/
├── tpl.asd.yml
└── scripts/
└── config.schema.ts
Write tpl.asd.yml. Declare automation hooks,
network.services, default values for plugins_config. Use
${{ env.X }} macros for templating. See the bundled
supabase plugin for the full pattern.
Write config.schema.ts. Export a Zod schema that
validates the plugins_config.<your-plugin> slot:
import { z } from "zod";
export const Config = z.object({ /* … */ }).strict();
(Optional) Add a preflight command to src/cli/<your-plugin>.ts
that imports the schema and runs it against the project's
config. Convention is asd <your-plugin> preflight.
Open a PR. CI runs the plugin against a fixture project to
make sure activation merges cleanly with no other plugin.
Module directory under modules/<your-module>/ with the
layout above. The manifest must declare an id, name,
endpoint, and activation policy:
# modules/<your-module>/<your-module>.manifest.yaml
id: your-module
name: Your Module
description: One-line summary
activation:
policy: auto # or "manual"
checks:
- type: http
url: "http://127.0.0.1:${{ env.YOUR_PORT }}/health"
expectedStatus: [200]
CLI registration. Add src/cli/<your-module>.ts exporting
the subcommands. Wire them into the dispatcher. Convention:
one verb per subcommand, kebab-case.
scripts/api.just. Just recipes that wrap each CLI command
you registered, so external tooling (Justfiles, scripts) can
call them by recipe name.
Tests. Add tests/<your-module>.test.ts covering happy
path + the obvious failure modes. CI runs bun test across
the whole tree.
Schema updates. If your module reads from asd.yaml, add
the fields to the project schema. The contract is enforced via
Zod; run asd schema after changes to confirm the new fields
appear.
asd help entry. Once your module is wired, asd help
lists its commands automatically. No separate registration.
Open a PR. Maintainer review focuses on: subcommand naming
consistency, schema additions, test coverage, and bundle-size
impact (modules ship inside the binary).
These come up in code review repeatedly:
Commands are nouns + verbs. asd <noun> <verb> is the
pattern — asd net apply, asd auth refresh, asd vault set.
Avoid top-level verbs except for the bare-verb shortcuts in
core (up, down, doctor, services).
Idempotency over transactionality. Commands should converge,
not fail-fast on partial state. Re-running should be a no-op
when the world matches the input.
Zod at the boundary. Anything read from disk or the network
passes through a Zod schema. Errors should surface as readable
validation messages, not stack traces.
No magic env vars. If you need configuration, declare it
in <module>.manifest.yaml and add it to the project schema.
process.env.MY_RANDOM_VAR reads in module code are blocked in
review.
Stateful daemons go through the process manager. Don't
child_process.spawn and forget about it. The asd process
manager handles PID tracking, restart-on-fingerprint-change,
and graceful shutdown — use it.
TUI uses a shared TUI primitive. If your command has an
interactive mode, build on the existing TUI menu module.
Don't roll your own.
# Build
bun run build
# Smoke
./bin/asd <your-module> <command>
# Run the test suite
bun test
For end-to-end testing against a real tunnel server, the CI fixture
project (tests/fixtures/project-basic) gives you a known-good
asd.yaml. Run it through your changes and verify the resulting
registry.json matches the expected snapshot.
main trigger a beta release (vX.Y.Z-beta.N).vX.Y.Z) trigger production releases.install.sh, install.ps1, install.cmd) and agithub.com/asd-engineering/asd-cli/releases/<tag>.https://asd.host/install.sharchitecture — what you're extending.learn/07-build-a-module — the user-facing plugin path (no asd-cli fork needed).registry — the durable state your module may need to read.