import { Callout } from "zudoku/ui/Callout";

# Get started with the CLI

## 1. Install

The CLI ships as a .NET global tool. It targets **.NET 8 (LTS)** and runs on the .NET 8, 9 or 10 runtime.

```bash
dotnet tool install --global businessdev.ALbuild.Cli
# later: dotnet tool update --global businessdev.ALbuild.Cli
```

This installs the **`albuild`** command. Make sure the global tools folder is on your `PATH` (`~/.dotnet/tools` on macOS/Linux; `%USERPROFILE%\.dotnet\tools` on Windows).

<Callout type="info" title="Need the .NET SDK?">
The tool needs a .NET runtime present. Install the SDK or runtime from [dotnet.microsoft.com](https://dotnet.microsoft.com/download) if `dotnet --version` fails.
</Callout>

## 2. Prerequisites

The CLI drives the `businessdev.ALbuild` PowerShell module, so it needs:

- **PowerShell 7+** (`pwsh`) on `PATH`.
- The **`businessdev.ALbuild` module**: install it with the CLI itself:

```bash
albuild module install     # installs businessdev.ALbuild from the PowerShell Gallery
albuild doctor             # verifies PowerShell + module + Docker
```

- **Docker** in the right mode only on a host that will *run* BC containers (Windows containers on Windows). A Mac/Linux client that talks to a remote MCP host does not need Docker locally.

<Callout type="caution" title="macOS: 'You must install or update .NET to run this application' from pwsh">
If `albuild` commands fail with a runtime error pointing at `pwsh.dll` (e.g. it asks for `Microsoft.NETCore.App 8.0.0` while you only have 9.x), your PowerShell was installed as a **net8 dotnet-tool** but only a newer .NET runtime is present. Fixes, best first:

1. Install PowerShell as a real package so it's self-contained: `brew install powershell`.
2. Or let it roll forward onto the newer runtime: `export DOTNET_ROLL_FORWARD=Major` (add to `~/.zshrc`). Recent CLI builds set this for the `pwsh` child automatically.
</Callout>

## 3. Configure dependencies — `albuild.json`

This is the step that pays off the most. ALbuild's headline feature is a **real transitive dependency resolver**: it reads your project's `app.json` `dependencies`, resolves a *mutually-compatible, transitively-complete* set of packages from your configured feeds (plus Microsoft's first-party feeds, registered automatically), downloads them into `.alpackages`, and writes a `dependencies.lock.json` for reproducible builds. No hand-curated `.alpackages`, no "works on my machine".

You drive it with one committed file at the repo root — **`albuild.json`** — which ALbuild reads automatically, so you never repeat `--country`/`--bc-version`/feeds on the command line or in a pipeline.

Scaffold it, then add your feeds:

```bash
albuild init --country de --bc-version 28.2
albuild feeds add https://api.nuget.org/v3/index.json
albuild feeds add https://pkgs.dev.azure.com/365businessdev/Public/_packaging/MSDyn365BCRuntimeApps/nuget/v3/index.json \
  --name 365businessdev --kind runtime
albuild feeds list
```

The result is a small, source-controlled file:

```jsonc
{
  "country": "de",
  "artifactType": "Sandbox",
  "bcVersion": "28.2",
  "select": "Latest",
  "feeds": [
    // a plain string is the simplest form (defaults to kind 'apps')
    "https://api.nuget.org/v3/index.json",
    // an object lets you name the feed and set its kind
    {
      "url": "https://pkgs.dev.azure.com/365businessdev/Public/_packaging/MSDyn365BCRuntimeApps/nuget/v3/index.json",
      "name": "365businessdev",
      "kind": "runtime"
    }
  ]
}
```

- **`kind`** tells the resolver what a feed serves: `apps` (default, installable apps), `symbols` (compile-only symbol packages) or `runtime` (sealed runtime packages). Here, **NuGet.org** carries app/symbol packages and the **365 business development** feed serves runtime packages.
- **Microsoft's feeds are automatic.** The resolver always registers Microsoft's public symbol/app feeds (MSSymbols, AppSourceSymbols, MSApps), so you only list *your* ISV/partner feeds.
- **Private feeds keep their key out of the file.** Add `--api-key-env <VAR>` (or `"tokenEnv": "<VAR>"` in the object) — it records the *name* of an environment variable holding the token, resolved at run time:

```bash
albuild feeds add https://pkgs.dev.azure.com/acme/_packaging/private/nuget/v3/index.json \
  --name acme --kind apps --api-key-env ACME_PAT
```

Preview what *would* be resolved (downloads nothing), then restore for real:

```bash
albuild deps plan    --project ./app --workspace . --bc-version 28.2   # dry-run: the exact set + versions
albuild deps install --project ./app --workspace . --container <name>  # resolve + install into a container
```

<Callout type="info" title="Mixed sources, one resolver">
The same resolver also pulls **Azure DevOps Universal Packages** and **committed local `.app` files**, alongside NuGet v3 feeds, and pins Microsoft first-party apps to the target build. See the [dependency resolver](../concepts/dependency-resolver) deep dive and the full [`albuild.json` reference](../concepts/project-config).
</Callout>

## 4. Check AL logic without a container

Before any of the walkthroughs below: not every question needs a container.
[`albuild probe`](./probe) interprets your AL locally, so "what does this actually return?" is
answered in a fraction of a second instead of a publish-and-test cycle.

```bash
albuild probe scope --project ./app                       # what can be probed here?
albuild probe call --project ./app \
  --target '"bdev.BNK Bank Formatter"::FormatIBAN' \
  --args '["DE89370400440532013000"]'                      # call one procedure
albuild probe eval --vars 'd: Date' \
  --code "d := CalcDate('<CM>', 20260115D);"               # evaluate a few statements
```

Probe **refuses rather than guesses**: anything it cannot model exactly returns a stable error code
instead of a plausible-looking wrong answer. Treat it as a fast pre-check, then confirm with the
real test run below. See [Probing AL](./probe).

---

## 5. Walkthroughs

### 5.1 Local Windows developer — a container for local development

On a Windows box with Docker (Windows-container mode), everything runs locally — no `--server`.

```bash
albuild doctor                                  # PowerShell + module + Docker all green?
albuild init --country de --bc-version 28.2      # write albuild.json (commit it) — see §3
albuild container new --country de --bc-version 28.2
```

`container new` prints **how to connect** the moment the container is ready:

```text
  Connect to 'albd2030d4':
    Web client:   https://albd2030d4/BC/
    Container IP: 172.19.126.46   (add '172.19.126.46  albd2030d4' to your hosts file if the name does not resolve)
    Username:     admin
    Password:     Bc!JXyggABrCJqLCRKkhA5C
```

Open the web client in your browser and sign in with those credentials. To restore your app's dependencies into the container and run the inner loop, then tear it down when you're done:

```bash
albuild container new --project ./app --workspace .          # provision + install the dependency closure
albuild run pipeline  --workspace . --container albd2030d4    # build → publish → test
albuild app test      --container albd2030d4 --coverage       # re-run tests with code coverage
albuild coverage threshold --path ./CodeCoverage --workspace ./app --min 80   # gate (exit 1 if below)
albuild container remove --container albd2030d4               # tear down when finished
```

<Callout type="tip" title="Prefer the editor? Use the VS Code extension">
The [ALbuild VS Code extension](../vscode-extension) creates and inspects BC containers, restores dependencies and runs the pipeline from the sidebar — the same engine, without leaving the editor. Great if you'd rather click than type.
</Callout>

### 5.2 GitHub Copilot (VS Code) — let the agent drive the CLI

Give Copilot's **agent mode** an instructions file so it runs the build/test loop through `albuild` instead of hand-writing `pwsh`/`docker`, and manages a single warm container per task. Commit `.github/copilot-instructions.md`:

```md
## Validating AL changes (Business Central) — use the albuild CLI

Never hand-write pwsh or docker; use the `albuild` CLI and read its `--json` output.

0. **Check the cheap way first.** For a question about what AL logic returns, run
   `albuild probe call ... --json` or `albuild probe eval ... --json` — no container, answered in
   a fraction of a second. Only fall back to the container loop when the answer depends on the
   real platform, or to confirm the final state.
1. **One warm container per task.** Create it once and remember its name:
   `albuild container new --country de --bc-version 28.2 --json`  → read `data.containerName`.
   Reuse that **same** container for every iteration below — it stays warm, do not recreate it.
2. **After each change:** `albuild run pipeline --workspace . --container <name> --json`
   then read `data.tests` ({ passed, failed, failures[] }). Fix failures and repeat.
3. **When the task is finished:** `albuild container remove --container <name> --yes`.
```

Open the repo in VS Code, switch Copilot Chat to **Agent** mode, and ask it to implement and validate a change. It runs the commands in the integrated terminal and reads the structured results — create once, iterate on the warm container, drop it at the end.

<Callout type="info" title="Want an automatic warm pool — or the native VS Code path?">
For a real warm **pool** across sessions (automatic reuse + LRU eviction), point the CLI at a local MCP server with `--backend mcp`: `container new` then warm-reuses a matching container from the pool instead of building a new one. Alternatively, the [ALbuild VS Code extension](../vscode-extension) contributes the MCP server to Copilot automatically, so the agent gets typed `ensure-container`/`deploy-and-test` tools (warm pool + auto-cleanup) — see the [MCP server guide](../mcp-server/get-started).
</Callout>

### 5.3 Claude Code — let the agent drive the CLI

Claude Code drives the CLI through its shell. Commit a `CLAUDE.md` at the repo root with the same loop:

```md
## Business Central inner loop — use the albuild CLI

- Before reaching for a container, check whether `albuild probe` can answer it:
  `albuild probe call --project ./app --target '"<object>"::<procedure>' --args '[...]' --json`.
  It interprets the AL locally in a fraction of a second and refuses rather than guessing.
- Create one container at the start and reuse it for the whole task (do not recreate it each iteration):
  `albuild container new --country de --bc-version 28.2 --json`  → keep `data.containerName`.
- Validate a change: `albuild run pipeline --workspace . --container <name> --json`  → read `data.tests`.
- Tear down when finished: `albuild container remove --container <name> --yes`.
```

Claude Code reads `CLAUDE.md` at session start and runs these via its shell tool, iterating build → publish → test against the one warm container and removing it when the task is done.

<Callout type="info" title="Or use the MCP server directly">
Claude Code can also drive the ALbuild **MCP server** (typed tools, a warm pool and removal governance) instead of the CLI — drop a `.mcp.json` pointing at `npx -y @365businessdev/albuild-mcp`. See [MCP server → Get started](../mcp-server/get-started). On a Mac, point both the CLI and the MCP server at a remote host (next walkthrough).
</Callout>

### 5.4 macOS / Linux — drive a remote MCP host

You can't run a BC container on a Mac, but you can still compile. Run the heavy container work on a shared **Windows MCP host** and point the CLI at it. Two sides — the **host** (Windows server) and each **client**. The bearer token is generated on the host and handed to the client out-of-band; it is **never** written into `mcp.json`.

**On the host (Windows + Docker):**

```bash
albuild mcp install                                   # npm i -g @365businessdev/albuild-mcp (needs Node.js)
albuild agents add linus --tools build-app,run-tests,publish-app
albuild agents token linus                            # prints linus's bearer token → copy it to the client
albuild mcp serve --http --port 5020                  # host it; tokens + agent roster wired in automatically
#   or as an auto-starting Windows service (needs NSSM + an elevated shell):
albuild mcp service install --port 5020
```

**On each client (your Mac/Linux box):**

```bash
albuild mcp configure --client claude-code --url http://winsrv:5020/mcp
#   writes mcp.json with  "Authorization": "Bearer ${ALBUILD_MCP_TOKEN}"
export ALBUILD_MCP_TOKEN='<linus token from the host>'   # PowerShell: $env:ALBUILD_MCP_TOKEN = '...'
albuild mcp status --server http://winsrv:5020/mcp       # verify the client can reach the host
albuild run pipeline --workspace . --container <name> --server http://winsrv:5020/mcp   # build local → test remote
```

The token is read from `ALBUILD_MCP_TOKEN` at run time (`--token-env <VAR>` to choose another variable). Keep it in the environment / a secret store, not in the committed `mcp.json`.

<Callout type="info" title="Compilation is always local">
Even when you target a remote host, `albuild app build` (and the build step of `run pipeline`) **compile on the client**: your AL source and the compiler stay on your machine. Only the container ops (publish, install, test, unpublish) go to the host, and the locally-built `.app` files travel there as uploads. This is the classic macOS flow: compile here, run there.
</Callout>

#### Make the remote host the default

So you don't repeat `--server` on every command, save it as the default. `albuild mcp configure --server <name> --url <url>` records a named profile in `~/.albuild/config.json` and the first one configured becomes the default; pass `--default` to force it. When `--server` is omitted, the CLI resolves it in this order: **`--server` → `ALBUILD_SERVER` env var → configured default → local**.

```bash
albuild mcp configure --server prod --url http://winsrv:5020/mcp --default
albuild mcp default                  # show the current default + known profiles
albuild run pipeline --workspace .   # no --server needed → uses 'prod'
albuild app build    --workspace .   # still compiles LOCALLY (server only redirects container ops)
albuild mcp status --server local    # force local for a one-off, ignoring the default
albuild mcp default --unset          # clear the default → commands run local again
```

This gives you the natural split: on a **Mac**, set the default once and every command goes remote; on **Windows**, leave it unset to run local by default and opt into a host per-command with `--server prod` (or tell your agent to). `ALBUILD_SERVER` is the per-shell override, handy in CI.

---

Next: the full **[command reference](commands)**.
