# Build, Test, and Publish Your Own aux4 Package ![banner](https://articles.aux4.blog/data/posts/aux4/build-test-and-publish-your-own-aux4-package/banner.png) ## Introduction In this tutorial you'll build a real aux4 package from an empty folder: a small `greet` CLI defined entirely in a JSON manifest. You'll write markdown-based tests for it, measure coverage, lint it, install it locally, and prepare it for publishing to [hub.aux4.io](https://hub.aux4.io). The package is deliberately tiny so the *workflow* stays in focus — the same loop scales to any package you build later. ## Install the tools Everything you need is part of the aux4 ecosystem: ```bash aux4 aux4 pkger install aux4/test aux4/lint aux4/package-releaser ``` - **aux4/test** — a markdown-based test runner for CLI commands. - **aux4/lint** — catches structural mistakes in your `.aux4` file. - **aux4/package-releaser** — versions, builds, installs, and publishes packages. ## Lay out the package An aux4 package lives inside a `package/` directory: ```bash mkdir -p greet/package/test cd greet ``` Create `package/.aux4` — metadata plus two profiles. The `main` profile is the required entry point; it routes into a `greet` profile that holds the actual command: ```json { "scope": "myorg", "name": "greet", "version": "0.0.1", "description": "A friendly greeter", "license": "MIT", "tags": ["greeting", "example"], "profiles": [ { "name": "main", "commands": [ { "name": "greet", "execute": ["profile:greet"], "help": { "text": "Greeting commands" } } ] }, { "name": "greet", "commands": [ { "name": "hello", "execute": ["echo \"Hello, ${name}!\""], "help": { "text": "Say hello to someone", "variables": [ { "name": "name", "text": "Name to greet", "arg": true, "default": "World" } ] } } ] } ] } ``` A few conventions the linter enforces later: - The `main` profile is mandatory — it's where aux4 starts. - A profile that a command routes into (`profile:greet`) shares the command's name. - Command and profile names use dashes (`send-email`); only **variable** names use camelCase (`firstName`). - Variables are referenced with `${name}` inside `execute`. One variable per command can be `"arg": true` (positional). The installer also requires a `LICENSE` and a `README.md` next to the manifest — without them, install fails with `LICENSE file not found` or `README.md file not found`. Add a `package/LICENSE` (standard MIT text is fine) and a short `package/README.md`: ````markdown # myorg/greet A friendly greeter. ## greet hello Say hello to someone. ```bash aux4 greet hello John ``` ```` ## Write tests in markdown aux4 tests are just markdown files ending in `.test.md`. The runner walks the headings to build scenarios, and reads fenced code blocks whose **language tag** is a keyword: `execute` runs a command, `expect` checks stdout, `error` checks stderr. Create `package/test/greet__hello.test.md` (the `__` mirrors the command hierarchy `greet hello`): ````markdown # greet hello ## Greets a named person ```execute aux4 greet hello John ``` ```expect Hello, John! ``` ## Falls back to a default ```execute aux4 greet hello ``` ```expect Hello, World! ``` ```` Tests in `package/test/` call `aux4 ` directly — the runner auto-discovers the `package/.aux4` next to them, so there's nothing to wire up. `expect` does an exact match by default, but you can relax it by appending modifiers to the tag — `expect:partial` for substrings and wildcards, `expect:ignoreCase`, `expect:regex`, or `expect:json` to compare JSON regardless of formatting. ## Install locally Install the package so `aux4 greet` resolves to your code. Run this from the `greet/` directory (the one containing `package/`): ```bash aux4 aux4 releaser install --dir ./package --noBuild true ``` `--noBuild true` skips cross-compilation — there's nothing to compile in a pure-`.aux4` package. ```text Building aux4 package myorg/greet 0.0.1-local Creating zip file myorg_greet_0.0.1-local.zip + adding file myorg/greet/.aux4 + adding file myorg/greet/LICENSE + adding file myorg/greet/README.md + adding file myorg/greet/test/greet__hello.test.md Unzipping package myorg greet 0.0.1-local Loading package myorg greet 0.0.1-local Installed packages: ✓ myorg/greet 0.0.1-local The package myorg/greet:0.0.1-local has been installed ``` The command now works: ```bash aux4 greet hello John aux4 greet hello ``` ```text Hello, John! Hello, World! ``` ## Run the tests From inside `package/`, run the suite: ```bash aux4 test run test/ ``` ```text PASS engine/aux4.test.js Test file greet__hello.test.md 1. greet hello 1.1. Greets a named person ✓ 1. should print output (19 ms) 1.2. Falls back to a default ✓ 1. should print output (17 ms) Test Suites: 1 passed, 1 total Tests: 2 passed, 2 total Snapshots: 0 total Time: 0.134 s ``` Want to know how much of your package the tests exercise? Add a coverage gate: ```bash aux4 test coverage test/ --threshold 80 ``` ```text Coverage Report ====================================================================== Package: myorg/greet@0.0.1 main/greet 1/1 steps ████████████ 100% 5ms greet/hello 1/1 steps ████████████ 100% 5ms ---------------------------------------------------------------------- Summary: Commands: 2/2 (100%) Steps: 2/2 (100%) ====================================================================== ``` It exits non-zero if step coverage drops below the threshold — exactly what you want guarding a CI pipeline. ## Lint before you ship The linter flags structural problems — missing `main` profile, duplicate command names, broken `profile:` references, dangling `${variables}`, naming-convention slips, and more: ```bash aux4 lint run --strict true ``` ```text No issues found. ``` `--strict true` promotes `-local` version suffixes to errors, so a package only ever installed locally can't accidentally be published. Exit code `0` means no errors; `1` means fix something. ## Publish to the hub When the tests are green and the linter is happy, releasing is a single command. The releaser bumps the version, builds, publishes to the hub, commits, tags, and pushes: ```bash aux4 aux4 releaser release --level patch ``` `--level` is `patch`, `minor`, or `major`. The first time you publish you'll authenticate to the hub with your access token. Prefer to automate it? The official actions run test and publish on every push to `main`. Add `.github/workflows/publish.yml`: ```yaml name: Publish Package on: push: branches: [main] workflow_dispatch: inputs: level: description: 'Release level (patch, minor, major)' default: 'patch' type: choice options: [patch, minor, major] permissions: contents: write jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: aux4/test-package-action@v1 publish: needs: [test] runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - uses: aux4/publish-package-action@v1 with: working_directory: package package_directory: . level: ${{ inputs.level || 'patch' }} aux4_token: ${{ secrets.AUX4_ACCESS_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }} ``` Now every merge to `main` runs the tests and publishes a new version automatically. ## Conclusion That's the entire arc of building an aux4 package: 1. Describe commands in `package/.aux4`. 2. Write `.test.md` tests and run them with `aux4 test run`. 3. Gate quality with `aux4 test coverage` and `aux4 lint run`. 4. Ship with `aux4 aux4 releaser release` (or let GitHub Actions do it). No scaffolding generators, no framework — just a JSON manifest and the same tools the ecosystem uses on itself. Swap `echo` for any command, add more profiles, and the loop stays identical. ## See Also - [aux4/test](https://hub.aux4.io/r/public/packages/aux4/test) - [aux4/lint](https://hub.aux4.io/r/public/packages/aux4/lint) - [aux4/package-releaser](https://hub.aux4.io/r/public/packages/aux4/package-releaser) - [aux4 Package Hub](https://hub.aux4.io)