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
95 changes: 73 additions & 22 deletions .archgate/adrs/ARCH-001-command-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,31 +20,34 @@ The explicit register pattern strikes the right balance: each command owns its r

## Decision

Commands live in `src/commands/` and export a `register*Command(program)` function. The main entry point (`src/cli.ts`) explicitly imports and calls each register function. Subcommands (e.g., `adr create`, `adr list`) use nested directories with an `index.ts` that composes the subcommand group.
Commands live in src/commands/ and export a register\*Command(program) function. The main entry point (src/cli.ts) explicitly imports and calls each register function. Subcommands (e.g., adr create, adr list) use nested directories with an index.ts that composes the subcommand group.

**Key constraints:**

1. **One command per file** — Each `.ts` file in `src/commands/` defines exactly one command (or one command group via its `index.ts`)
2. **Explicit registration** — Every command must be manually imported and registered in `src/cli.ts`. No auto-discovery.
1. **One command per file** — Each .ts file in src/commands/ defines exactly one command (or one command group via its index.ts)
2. **Explicit registration** — Every command must be manually imported and registered in src/cli.ts. No auto-discovery.
3. **Thin commands** — Command files handle I/O only: parse arguments, call engine/helpers, format output. No business logic.
4. **In-process execution** — Commands run in the same Bun process as the CLI entry point. No child process spawning.
5. **main() wrapper in entry point** — All async bootstrap logic in src/cli.ts MUST be wrapped in an async function main() called via .catch(). Top-level await is forbidden in the entry point.

## Do's and Don'ts

### Do

- Export a `register*Command` function from each command module
- Export a register\*Command function from each command module
- Keep commands thin: parse args, call helpers/engine, format output
- Use `src/commands/<name>.ts` for top-level commands
- Use `src/commands/<name>/index.ts` for command groups with subcommands
- Import the register function explicitly in `src/cli.ts`
- Use src/commands/<name>.ts for top-level commands
- Use src/commands/<name>/index.ts for command groups with subcommands
- Import the register function explicitly in src/cli.ts
- Wrap all async logic in src/cli.ts in an async function main() and call it as main().catch((err) => { logError(String(err)); process.exit(2); }) — this is required for bun build --compile --bytecode compatibility

### Don't

- Don't put business logic in command files — move it to `src/engine/`, `src/helpers/`, or `src/formats/`
- Don't use `executableDir()` for command discovery
- Don't call `.parse()` in command files — the entry point handles parsing
- Don't put business logic in command files — move it to src/engine/, src/helpers/, or src/formats/
- Don't use executableDir() for command discovery
- Don't call .parse() in command files — the entry point handles parsing
- Don't create commands that spawn child processes for subcommand execution
- Don't use top-level await in src/cli.ts — bun build --compile --bytecode (the binary compiler) rejects it even though bun run and tsc accept it. The symptom is a build-time parse error: "await" can only be used inside an "async" function

## Implementation Pattern

Expand Down Expand Up @@ -105,6 +108,49 @@ export function registerCheckCommand(program: Command) {
}
```

### Entry Point main() Pattern

bun build --compile --bytecode — the command used to produce standalone binaries — rejects top-level await at parse time, even though bun run and tsc both accept it. All async bootstrap logic in src/cli.ts MUST be wrapped in an async function main().

```typescript
// src/cli.ts — GOOD: all async logic wrapped in main()
import { logError } from "./helpers/log";

// Synchronous bootstrap checks can remain at top level
if (!semver.satisfies(Bun.version, ">=1.2.21"))
throw new Error("You need to update Bun to version 1.2.21 or higher");

createPathIfNotExists(paths.cacheFolder);

async function main() {
await installGit(); // async logic goes inside main()

const program = new Command().name("archgate").version(packageJson.version);
registerInitCommand(program);
// ... register other commands ...

const updateCheckPromise = checkForUpdatesIfNeeded(packageJson.version);
await program.parseAsync(process.argv);
const notice = await updateCheckPromise;
if (notice) console.log(notice);
}

main().catch((err) => {
logError(String(err));
process.exit(2);
});
```

```typescript
// src/cli.ts — BAD: top-level await breaks bun build --compile --bytecode
createPathIfNotExists(paths.cacheFolder);

await installGit(); // ERROR: "await" can only be used inside an "async" function

const program = new Command().name("archgate").version(packageJson.version);
await program.parseAsync(process.argv); // also breaks
```

### Subcommand Group Pattern

```typescript
Expand All @@ -131,37 +177,42 @@ export function registerAdrCommand(program: Command) {

### Positive

- **In-process execution enables testing** — Commands can be tested by calling `register*Command()` directly, without spawning subprocesses or mocking executables
- **Explicit imports make dependencies clear** — Opening `src/cli.ts` shows every command the CLI supports. No hidden commands loaded at runtime.
- **Subcommand nesting is straightforward** — Command groups use the same pattern as top-level commands, with an `index.ts` that composes children
- **Type-safe registration** — Commander.js `@commander-js/extra-typings` provides full type inference for options and arguments within each register function
- **In-process execution enables testing** — Commands can be tested by calling register\*Command() directly, without spawning subprocesses or mocking executables
- **Explicit imports make dependencies clear** — Opening src/cli.ts shows every command the CLI supports. No hidden commands loaded at runtime.
- **Subcommand nesting is straightforward** — Command groups use the same pattern as top-level commands, with an index.ts that composes children
- **Type-safe registration** — Commander.js @commander-js/extra-typings provides full type inference for options and arguments within each register function
- **Binary-compatible entry point** — The main() wrapper pattern ensures src/cli.ts compiles cleanly with bun build --compile --bytecode for standalone binary distribution

### Negative

- **Manual import bookkeeping** — Each new command requires adding an import and registration call in `src/cli.ts`. This is a minor overhead for a CLI with fewer than 15 commands.
- **Manual import bookkeeping** — Each new command requires adding an import and registration call in src/cli.ts. This is a minor overhead for a CLI with fewer than 15 commands.
- **No hot-reload of commands** — Adding a new command requires restarting the CLI process. Acceptable for a development tool.

### Risks

- **Stale imports when commands are removed** — If a command file is deleted but its import in `src/cli.ts` is not removed, TypeScript will catch the error at compile time. The `bun run typecheck` step in the validation pipeline prevents this from reaching production.
- **Command group index.ts confused with barrels** — The `index.ts` files in command group directories (e.g., `src/commands/adr/index.ts`) contain real composition logic, not re-exports. [ARCH-004 — No Barrel Files](./ARCH-004-no-barrel-files.md) explicitly permits `index.ts` files with logic.
- **Stale imports when commands are removed** — If a command file is deleted but its import in src/cli.ts is not removed, TypeScript will catch the error at compile time. The bun run typecheck step in the validation pipeline prevents this from reaching production.
- **Command group index.ts confused with barrels** — The index.ts files in command group directories (e.g., src/commands/adr/index.ts) contain real composition logic, not re-exports. ARCH-004 No Barrel Files explicitly permits index.ts files with logic.
- **Top-level await regression** — A developer unfamiliar with the --bytecode constraint may introduce top-level await back into src/cli.ts. Mitigation: The bun run build:check step in the validate pipeline catches this immediately — bun run validate will fail locally before the code reaches CI.

## Compliance and Enforcement

### Automated Enforcement

- **Archgate rule** `ARCH-001/register-function-export`: Scans all command files under `src/commands/` (excluding `index.ts` group files) and verifies each exports a `register*Command` function. Severity: `error`.
- **Archgate rule** `ARCH-001/no-business-logic`: Detects complex data transformation patterns in command files that should be in helpers. Severity: `error`.
- **Archgate rule** ARCH-001/register-function-export: Scans all command files under src/commands/ (excluding index.ts group files) and verifies each exports a register\*Command function. Severity: error.
- **Archgate rule** ARCH-001/no-business-logic: Detects complex data transformation patterns in command files that should be in helpers. Severity: error.
- **Build check** bun run build:check: Compiles src/cli.ts with bun build --compile --bytecode as part of bun run validate. A top-level await regression causes an immediate, descriptive parse error.

### Manual Enforcement

Code reviewers MUST verify:

1. New commands are imported and registered in `src/cli.ts`
1. New commands are imported and registered in src/cli.ts
2. Command files delegate to engine/helpers for business logic
3. Command group `index.ts` files contain composition logic, not just re-exports
3. Command group index.ts files contain composition logic, not just re-exports
4. No top-level await has been introduced in src/cli.ts — all async logic must be inside main()

## References

- [Commander.js documentation](https://github.com/tj/commander.js)
- [ARCH-004 — No Barrel Files](./ARCH-004-no-barrel-files.md) — Permits `index.ts` with logic, forbids re-export-only barrels
- [ARCH-004 — No Barrel Files](./ARCH-004-no-barrel-files.md) — Permits index.ts with logic, forbids re-export-only barrels
- [ARCH-002 — Error Handling](./ARCH-002-error-handling.md) — logError and exit code conventions used in the main().catch() handler
8 changes: 7 additions & 1 deletion .claude/agent-memory/archgate-developer/MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,19 @@ Skipping steps 2 or 3 is a workflow violation. The user should NEVER have to inv
- **oxlint `no-negated-condition`** — Always write ternaries with the positive condition first: `x === null ? A : B` not `x !== null ? B : A`. Applies to both `if/else` blocks and ternary expressions.
- **oxlint `no-unused-vars` on catch parameters** — Use bare `catch { }` (no parameter) when the caught error is not used. `catch (err) { }` with unused `err` triggers the rule.
- **oxlint `no-await-in-loop`** — Sequential `await` inside a `for` loop is flagged (warning). When the sequential order is intentional (e.g., build steps with per-step output), suppress with `// oxlint-disable-next-line no-await-in-loop -- <reason>`.
- **`bun build --compile --bytecode` rejects top-level `await`** — Even though `bun run` and `tsc` handle top-level `await` fine, the Bun bytecode compiler (`--bytecode`) does not. In `src/cli.ts`, all async bootstrap logic MUST be wrapped in `async function main() { ... }` and called as `main().catch((err) => { logError(String(err)); process.exit(2); })`. Never use top-level `await` in the CLI entry point. See ARCH-001 Do's for the documented pattern.
- **npm `main` field always gets included in publish** — `"main"` in `package.json` is always included in `npm publish` regardless of the `files` array. If the package doesn't need a default entry point (only sub-path exports like `./rules`), remove `main` entirely to avoid bundling the CLI entry point into the npm package.

## Validation Pipeline

- `bun run validate` is the mandatory gate: lint → typecheck → format:check → test → ADR check
- `bun run validate` is the mandatory gate: lint → typecheck → format:check → test → ADR check → build:check
- All ADR rule severities are `error` (not `warning`) — violations are hard blockers
- The pipeline is fail-fast — fix failures in order

## CLI Repo Quirk

- **`archgate` command = `bun run cli`** — This is the CLI repo itself, so the `archgate` binary is not installed in PATH. Use `bun run cli <command>` (e.g., `bun run cli check`, `bun run cli adr list`) instead of `archgate <command>`. The `bun run cli` script maps to `bun run src/cli.ts`.

## MCP Tools Structure

- MCP tools live in `src/mcp/tools/` — one file per tool (check, list-adrs, review-context, session-context)
Expand Down
2 changes: 1 addition & 1 deletion .prototools
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
bun = "1.3.8"
bun = "1.3.9"
gh = "^2.83.2"

[settings]
Expand Down
2 changes: 0 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
## 0.1.0 (2026-02-23)

# 0.1.0 (2026-02-23)
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ bun run typecheck # tsc --build
bun run format # prettier --write
bun run format:check # prettier --check
bun test # all tests
bun run validate # MANDATORY: lint + typecheck + format + test + ADR check
bun run validate # MANDATORY: lint + typecheck + format + test + ADR check + build check
bun run build # binaries → dist/ (darwin-arm64, linux-x64)
bun run commit # conventional commit wizard
```

## Validation Gate

**`bun run validate` must pass before any task is considered complete.** Fail-fast pipeline: lint → typecheck → format → test → ADR check. Mirrors CI in `.github/workflows/code-pull-request.yml`.
**`bun run validate` must pass before any task is considered complete.** Fail-fast pipeline: lint → typecheck → format → test → ADR check → build check. Mirrors CI in `.github/workflows/code-pull-request.yml`.

## Architecture

Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "archgate",
"version": "0.1.0",
"description": "AI governance for software development",
"description": "Enforce Architecture Decision Records as executable rules — for both humans and AI agents",
"readme": "README.md",
"license": "FSL-1.1-ALv2",
"homepage": "https://archgate.dev",
Expand Down Expand Up @@ -40,7 +40,8 @@
"format:check": "prettier --check .",
"test": "bun test --timeout 60000",
"test:watch": "bun test --watch --timeout 60000",
"validate": "bun run lint && bun run typecheck && bun run format:check && bun test && bun run check",
"build:check": "bun build src/cli.ts --compile --bytecode --outfile dist/.build-check && rm -f dist/.build-check",
"validate": "bun run lint && bun run typecheck && bun run format:check && bun test && bun run check && bun run build:check",
"commit": "cz",
"build": "bun run scripts/build.ts",
"build:darwin": "bun run scripts/build.ts --target darwin-arm64",
Expand Down
44 changes: 26 additions & 18 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { registerCleanCommand } from "./commands/clean";
import { registerCheckCommand } from "./commands/check";
import { registerMcpCommand } from "./commands/mcp";
import { checkForUpdatesIfNeeded } from "./helpers/update-check";
import { logError } from "./helpers/log";

if (typeof Bun === "undefined")
throw new Error(
Expand All @@ -25,21 +26,28 @@ if (!["darwin", "linux"].includes(process.platform))

createPathIfNotExists(paths.cacheFolder);

await installGit();

const program = new Command()
.name("archgate")
.version(packageJson.version)
.description("AI governance for software development");

registerInitCommand(program);
registerAdrCommand(program);
registerCheckCommand(program);
registerMcpCommand(program);
registerUpgradeCommand(program);
registerCleanCommand(program);

const updateCheckPromise = checkForUpdatesIfNeeded(packageJson.version);
await program.parseAsync(process.argv);
const notice = await updateCheckPromise;
if (notice) console.log(notice);
async function main() {
await installGit();

const program = new Command()
.name("archgate")
.version(packageJson.version)
.description("AI governance for software development");

registerInitCommand(program);
registerAdrCommand(program);
registerCheckCommand(program);
registerMcpCommand(program);
registerUpgradeCommand(program);
registerCleanCommand(program);

const updateCheckPromise = checkForUpdatesIfNeeded(packageJson.version);
await program.parseAsync(process.argv);
const notice = await updateCheckPromise;
if (notice) console.log(notice);
}

main().catch((err: unknown) => {
logError(String(err));
process.exit(2);
});