Skip to content

Commit 9eb15c1

Browse files
authored
fix: improve packaging (#4)
* refactor: restructure main function and improve error handling * chore: update project description and enhance validation script * chore: update bun version to 1.3.9 * chore: update validation steps to include build check * chore: remove duplicate version entry from CHANGELOG * fix: formatting
1 parent 09c312c commit 9eb15c1

7 files changed

Lines changed: 112 additions & 48 deletions

File tree

.archgate/adrs/ARCH-001-command-structure.md

Lines changed: 73 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,31 +20,34 @@ The explicit register pattern strikes the right balance: each command owns its r
2020

2121
## Decision
2222

23-
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.
23+
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.
2424

2525
**Key constraints:**
2626

27-
1. **One command per file** — Each `.ts` file in `src/commands/` defines exactly one command (or one command group via its `index.ts`)
28-
2. **Explicit registration** — Every command must be manually imported and registered in `src/cli.ts`. No auto-discovery.
27+
1. **One command per file** — Each .ts file in src/commands/ defines exactly one command (or one command group via its index.ts)
28+
2. **Explicit registration** — Every command must be manually imported and registered in src/cli.ts. No auto-discovery.
2929
3. **Thin commands** — Command files handle I/O only: parse arguments, call engine/helpers, format output. No business logic.
3030
4. **In-process execution** — Commands run in the same Bun process as the CLI entry point. No child process spawning.
31+
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.
3132

3233
## Do's and Don'ts
3334

3435
### Do
3536

36-
- Export a `register*Command` function from each command module
37+
- Export a register\*Command function from each command module
3738
- Keep commands thin: parse args, call helpers/engine, format output
38-
- Use `src/commands/<name>.ts` for top-level commands
39-
- Use `src/commands/<name>/index.ts` for command groups with subcommands
40-
- Import the register function explicitly in `src/cli.ts`
39+
- Use src/commands/<name>.ts for top-level commands
40+
- Use src/commands/<name>/index.ts for command groups with subcommands
41+
- Import the register function explicitly in src/cli.ts
42+
- 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
4143

4244
### Don't
4345

44-
- Don't put business logic in command files — move it to `src/engine/`, `src/helpers/`, or `src/formats/`
45-
- Don't use `executableDir()` for command discovery
46-
- Don't call `.parse()` in command files — the entry point handles parsing
46+
- Don't put business logic in command files — move it to src/engine/, src/helpers/, or src/formats/
47+
- Don't use executableDir() for command discovery
48+
- Don't call .parse() in command files — the entry point handles parsing
4749
- Don't create commands that spawn child processes for subcommand execution
50+
- 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
4851

4952
## Implementation Pattern
5053

@@ -105,6 +108,49 @@ export function registerCheckCommand(program: Command) {
105108
}
106109
```
107110

111+
### Entry Point main() Pattern
112+
113+
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().
114+
115+
```typescript
116+
// src/cli.ts — GOOD: all async logic wrapped in main()
117+
import { logError } from "./helpers/log";
118+
119+
// Synchronous bootstrap checks can remain at top level
120+
if (!semver.satisfies(Bun.version, ">=1.2.21"))
121+
throw new Error("You need to update Bun to version 1.2.21 or higher");
122+
123+
createPathIfNotExists(paths.cacheFolder);
124+
125+
async function main() {
126+
await installGit(); // async logic goes inside main()
127+
128+
const program = new Command().name("archgate").version(packageJson.version);
129+
registerInitCommand(program);
130+
// ... register other commands ...
131+
132+
const updateCheckPromise = checkForUpdatesIfNeeded(packageJson.version);
133+
await program.parseAsync(process.argv);
134+
const notice = await updateCheckPromise;
135+
if (notice) console.log(notice);
136+
}
137+
138+
main().catch((err) => {
139+
logError(String(err));
140+
process.exit(2);
141+
});
142+
```
143+
144+
```typescript
145+
// src/cli.ts — BAD: top-level await breaks bun build --compile --bytecode
146+
createPathIfNotExists(paths.cacheFolder);
147+
148+
await installGit(); // ERROR: "await" can only be used inside an "async" function
149+
150+
const program = new Command().name("archgate").version(packageJson.version);
151+
await program.parseAsync(process.argv); // also breaks
152+
```
153+
108154
### Subcommand Group Pattern
109155

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

132178
### Positive
133179

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

139186
### Negative
140187

141-
- **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.
188+
- **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.
142189
- **No hot-reload of commands** — Adding a new command requires restarting the CLI process. Acceptable for a development tool.
143190

144191
### Risks
145192

146-
- **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.
147-
- **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.
193+
- **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.
194+
- **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.
195+
- **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.
148196

149197
## Compliance and Enforcement
150198

151199
### Automated Enforcement
152200

153-
- **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`.
154-
- **Archgate rule** `ARCH-001/no-business-logic`: Detects complex data transformation patterns in command files that should be in helpers. Severity: `error`.
201+
- **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.
202+
- **Archgate rule** ARCH-001/no-business-logic: Detects complex data transformation patterns in command files that should be in helpers. Severity: error.
203+
- **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.
155204

156205
### Manual Enforcement
157206

158207
Code reviewers MUST verify:
159208

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

164214
## References
165215

166216
- [Commander.js documentation](https://github.com/tj/commander.js)
167-
- [ARCH-004 — No Barrel Files](./ARCH-004-no-barrel-files.md) — Permits `index.ts` with logic, forbids re-export-only barrels
217+
- [ARCH-004 — No Barrel Files](./ARCH-004-no-barrel-files.md) — Permits index.ts with logic, forbids re-export-only barrels
218+
- [ARCH-002 — Error Handling](./ARCH-002-error-handling.md) — logError and exit code conventions used in the main().catch() handler

.claude/agent-memory/archgate-developer/MEMORY.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,19 @@ Skipping steps 2 or 3 is a workflow violation. The user should NEVER have to inv
3434
- **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.
3535
- **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.
3636
- **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>`.
37+
- **`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.
38+
- **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.
3739

3840
## Validation Pipeline
3941

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

46+
## CLI Repo Quirk
47+
48+
- **`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`.
49+
4450
## MCP Tools Structure
4551

4652
- MCP tools live in `src/mcp/tools/` — one file per tool (check, list-adrs, review-context, session-context)

.prototools

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
bun = "1.3.8"
1+
bun = "1.3.9"
22
gh = "^2.83.2"
33

44
[settings]

CHANGELOG.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
11
## 0.1.0 (2026-02-23)
2-
3-
# 0.1.0 (2026-02-23)

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ bun run typecheck # tsc --build
1818
bun run format # prettier --write
1919
bun run format:check # prettier --check
2020
bun test # all tests
21-
bun run validate # MANDATORY: lint + typecheck + format + test + ADR check
21+
bun run validate # MANDATORY: lint + typecheck + format + test + ADR check + build check
2222
bun run build # binaries → dist/ (darwin-arm64, linux-x64)
2323
bun run commit # conventional commit wizard
2424
```
2525

2626
## Validation Gate
2727

28-
**`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`.
28+
**`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`.
2929

3030
## Architecture
3131

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "archgate",
33
"version": "0.1.0",
4-
"description": "AI governance for software development",
4+
"description": "Enforce Architecture Decision Records as executable rules — for both humans and AI agents",
55
"readme": "README.md",
66
"license": "FSL-1.1-ALv2",
77
"homepage": "https://archgate.dev",
@@ -40,7 +40,8 @@
4040
"format:check": "prettier --check .",
4141
"test": "bun test --timeout 60000",
4242
"test:watch": "bun test --watch --timeout 60000",
43-
"validate": "bun run lint && bun run typecheck && bun run format:check && bun test && bun run check",
43+
"build:check": "bun build src/cli.ts --compile --bytecode --outfile dist/.build-check && rm -f dist/.build-check",
44+
"validate": "bun run lint && bun run typecheck && bun run format:check && bun test && bun run check && bun run build:check",
4445
"commit": "cz",
4546
"build": "bun run scripts/build.ts",
4647
"build:darwin": "bun run scripts/build.ts --target darwin-arm64",

src/cli.ts

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { registerCleanCommand } from "./commands/clean";
1111
import { registerCheckCommand } from "./commands/check";
1212
import { registerMcpCommand } from "./commands/mcp";
1313
import { checkForUpdatesIfNeeded } from "./helpers/update-check";
14+
import { logError } from "./helpers/log";
1415

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

2627
createPathIfNotExists(paths.cacheFolder);
2728

28-
await installGit();
29-
30-
const program = new Command()
31-
.name("archgate")
32-
.version(packageJson.version)
33-
.description("AI governance for software development");
34-
35-
registerInitCommand(program);
36-
registerAdrCommand(program);
37-
registerCheckCommand(program);
38-
registerMcpCommand(program);
39-
registerUpgradeCommand(program);
40-
registerCleanCommand(program);
41-
42-
const updateCheckPromise = checkForUpdatesIfNeeded(packageJson.version);
43-
await program.parseAsync(process.argv);
44-
const notice = await updateCheckPromise;
45-
if (notice) console.log(notice);
29+
async function main() {
30+
await installGit();
31+
32+
const program = new Command()
33+
.name("archgate")
34+
.version(packageJson.version)
35+
.description("AI governance for software development");
36+
37+
registerInitCommand(program);
38+
registerAdrCommand(program);
39+
registerCheckCommand(program);
40+
registerMcpCommand(program);
41+
registerUpgradeCommand(program);
42+
registerCleanCommand(program);
43+
44+
const updateCheckPromise = checkForUpdatesIfNeeded(packageJson.version);
45+
await program.parseAsync(process.argv);
46+
const notice = await updateCheckPromise;
47+
if (notice) console.log(notice);
48+
}
49+
50+
main().catch((err: unknown) => {
51+
logError(String(err));
52+
process.exit(2);
53+
});

0 commit comments

Comments
 (0)