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
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Enforce LF line endings for all text files to avoid CRLF issues on Windows.
* text=auto eol=lf
38 changes: 28 additions & 10 deletions .github/workflows/release-binaries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,15 @@ jobs:
- target: bun-darwin-arm64
artifact: archgate-darwin-arm64
os: macos-latest
binary_name: archgate
- target: bun-linux-x64
artifact: archgate-linux-x64
os: ubuntu-latest
binary_name: archgate
- target: bun-windows-x64
artifact: archgate-win32-x64
os: windows-latest
binary_name: archgate.exe
runs-on: ${{ matrix.os }}

steps:
Expand All @@ -39,26 +45,38 @@ jobs:

- run: bun install --frozen-lockfile

- run: bun run validate
- name: Validate (Unix)
if: runner.os != 'Windows'
run: bun run validate

- name: Validate (Windows)
if: runner.os == 'Windows'
run: bun run lint && bun run typecheck && bun run format:check && bun test

- name: Build binary
run: |
bun build src/cli.ts \
--compile \
--bytecode \
--minify \
--target ${{ matrix.target }} \
--outfile dist/${{ matrix.artifact }}
bun build src/cli.ts --compile --bytecode --minify --target ${{ matrix.target }} --outfile dist/${{ matrix.artifact }}

- name: Copy binary into npm package
- name: Copy binary into npm package (Unix)
if: runner.os != 'Windows'
run: |
TAG="${{ github.event.release.tag_name || inputs.tag }}"
VERSION="${TAG#v}"
cp dist/${{ matrix.artifact }} packages/${{ matrix.artifact }}/bin/archgate
chmod +x packages/${{ matrix.artifact }}/bin/archgate
cp dist/${{ matrix.artifact }} packages/${{ matrix.artifact }}/bin/${{ matrix.binary_name }}
chmod +x packages/${{ matrix.artifact }}/bin/${{ matrix.binary_name }}
cd packages/${{ matrix.artifact }}
npm version "$VERSION" --no-git-tag-version

- name: Copy binary into npm package (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$tag = "${{ github.event.release.tag_name || inputs.tag }}"
$version = $tag -replace '^v', ''
Copy-Item "dist/${{ matrix.artifact }}.exe" "packages/${{ matrix.artifact }}/bin/${{ matrix.binary_name }}"
Set-Location "packages/${{ matrix.artifact }}"
npm version "$version" --no-git-tag-version

- uses: actions/setup-node@v4
with:
registry-url: "https://registry.npmjs.org"
Expand Down
60 changes: 60 additions & 0 deletions .github/workflows/windows-smoke-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Windows Smoke Test

on:
pull_request:
types: [opened, edited, synchronize, reopened]
branches:
- main

concurrency:
group: windows-smoke-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
smoke-test:
name: Windows Smoke Test
runs-on: windows-latest
timeout-minutes: 15
if: github.event.pull_request.draft == false
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: moonrepo/setup-toolchain@v0
with:
auto-install: true

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Lint
run: bun run lint

- name: Typecheck
run: bun run typecheck

- name: Format check
run: bun run format:check

- name: Tests
run: bun test --timeout 60000

- name: Build Windows binary
run: bun build src/cli.ts --compile --bytecode --outfile dist/archgate-smoke-test

- name: Smoke test binary
shell: pwsh
run: |
$binary = "dist/archgate-smoke-test.exe"
if (-not (Test-Path $binary)) {
Write-Error "Binary not found at $binary"
exit 1
}
$output = & $binary --version 2>&1
Write-Host "archgate version: $output"
if ($LASTEXITCODE -ne 0) {
Write-Error "Binary exited with code $LASTEXITCODE"
exit 1
}
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ 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 + build check
bun run build # binaries → dist/ (darwin-arm64, linux-x64)
bun run build # binaries → dist/ (darwin-arm64, linux-x64, win32-x64)
bun run commit # conventional commit wizard
```

Expand Down Expand Up @@ -60,7 +60,7 @@ Zod schemas are the single source of truth. Types derived via `z.infer<>` — ne
## Conventions

- Commands export `register*Command(program)`, handle I/O only — no business logic
- OS: macOS and Linux only (Windows blocked, WSL2 recommended)
- OS: macOS, Linux, and Windows
- Output: `styleText()` from `node:util`; `--json` for machine-readable; no emoji
- Exit codes: 0 = success, 1 = violation, 2 = internal error
- Deps: minimal; prefer Bun built-ins (see ARCH-006)
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Thank you for your interest in contributing to Archgate CLI! We welcome all kinds of contributions.

> **Note:** Development is only supported on macOS, Linux, or Windows via WSL2. Native Windows development is not supported.
> **Note:** Development is supported on macOS, Linux, and Windows.

## 🚀 Quick Start

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ When a rule is violated, `archgate check` reports the file, line, and which ADR
npm install -g archgate
```

**Requirements:** macOS (arm64) or Linux (x86_64). Node.js is only needed to run the wrapper — the CLI itself is a standalone binary.
**Requirements:** macOS (arm64), Linux (x86_64), or Windows (x86_64). Node.js is only needed to run the wrapper — the CLI itself is a standalone binary.

> **Using [proto](https://moonrepo.dev/proto)?** Add the following to `~/.proto/config.toml` and your shell profile so globals persist across Node.js version switches:
>
Expand Down
6 changes: 4 additions & 2 deletions bin/archgate.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,18 @@ function getPlatformPackageName() {
const { platform, arch } = process;
if (platform === "darwin" && arch === "arm64") return "archgate-darwin-arm64";
if (platform === "linux" && arch === "x64") return "archgate-linux-x64";
if (platform === "win32" && arch === "x64") return "archgate-win32-x64";
throw new Error(
`Unsupported platform: ${platform}/${arch}\narchgate supports darwin/arm64 and linux/x64 only.`
`Unsupported platform: ${platform}/${arch}\narchgate supports darwin/arm64, linux/x64, and win32/x64.`
);
}

function getBinaryPath() {
const pkgName = getPlatformPackageName();
const binaryName = process.platform === "win32" ? "archgate.exe" : "archgate";
try {
const pkgDir = path.dirname(require.resolve(`${pkgName}/package.json`));
const binaryPath = path.join(pkgDir, "bin", "archgate");
const binaryPath = path.join(pkgDir, "bin", binaryName);
if (fs.existsSync(binaryPath)) return binaryPath;
} catch {
/* platform package not installed */
Expand Down
1 change: 1 addition & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
},
"os": [
"darwin",
"linux"
"linux",
"win32"
],
"scripts": {
"cli": "bun run src/cli.ts",
Expand All @@ -44,7 +45,7 @@
"format:check": "prettier --check .",
"test": "bun test --timeout 60000",
"test:watch": "bun test --watch --timeout 60000",
"build:check": "bun build src/cli.ts --compile --bytecode --outfile dist/.build-check && rm -f dist/.build-check",
"build:check": "bun build src/cli.ts --compile --bytecode --outfile dist/.build-check && rm -f dist/.build-check dist/.build-check.exe",
"validate": "bun run lint && bun run typecheck && bun run format:check && bun test && bun run check && bun run build:check",
"commit": "cz",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0"
Expand All @@ -71,6 +72,7 @@
},
"optionalDependencies": {
"archgate-darwin-arm64": ">=0.1.1",
"archgate-linux-x64": ">=0.1.1"
"archgate-linux-x64": ">=0.1.1",
"archgate-win32-x64": ">=0.1.1"
}
}
19 changes: 19 additions & 0 deletions packages/archgate-win32-x64/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "archgate-win32-x64",
"version": "0.0.0",
"description": "windows x64 binary for archgate",
"os": [
"win32"
],
"cpu": [
"x64"
],
"files": [
"bin/archgate.exe"
],
"license": "FSL-1.1-ALv2",
"repository": {
"type": "git",
"url": "git+https://github.com/archgate/cli.git"
}
}
4 changes: 2 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ if (typeof Bun === "undefined")
if (!semver.satisfies(Bun.version, ">=1.2.21"))
throw new Error("You need to update Bun to version 1.2.21 or higher");

if (!["darwin", "linux"].includes(process.platform))
throw new Error("Archgate only supports macOS and Linux");
if (!["darwin", "linux", "win32"].includes(process.platform))
throw new Error("Archgate only supports macOS, Linux, and Windows");

createPathIfNotExists(paths.cacheFolder);

Expand Down
5 changes: 3 additions & 2 deletions src/engine/git-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ export async function resolveScopedFiles(
const glob = new Bun.Glob(pattern);
// oxlint-disable-next-line no-await-in-loop -- async iterator
for await (const file of glob.scan({ cwd: projectRoot, dot: false })) {
if (trackedFiles && !trackedFiles.has(file)) continue;
if (!allFiles.includes(file)) allFiles.push(file);
const normalized = file.replaceAll("\\", "/");
if (trackedFiles && !trackedFiles.has(normalized)) continue;
if (!allFiles.includes(normalized)) allFiles.push(normalized);
}
}
return allFiles.sort();
Expand Down
5 changes: 4 additions & 1 deletion src/engine/loader.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { readdirSync } from "node:fs";
import { join, basename } from "node:path";
import { pathToFileURL } from "node:url";
import { parseAdr } from "../formats/adr";
import type { AdrDocument } from "../formats/adr";
import { type RuleSet } from "../formats/rules";
Expand Down Expand Up @@ -86,8 +87,10 @@ export async function loadRuleAdrs(
try {
// Cache-bust: Bun caches import() per-process, so append a timestamp
// to force re-reading from disk on every call (critical for MCP server).
// Use file:// URL to handle Windows backslash paths in import().
const rulesUrl = `${pathToFileURL(rulesFile).href}?t=${Date.now()}`;
// oxlint-disable-next-line no-await-in-loop -- dynamic import must be sequential
const mod = await import(`${rulesFile}?t=${Date.now()}`);
const mod = await import(rulesUrl);
const parsed = RuleSetSchema.safeParse(mod.default);

if (!parsed.success) {
Expand Down
7 changes: 4 additions & 3 deletions src/engine/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ function createRuleContext(
const g = new Bun.Glob(pattern);
const results: string[] = [];
for await (const file of g.scan({ cwd: projectRoot, dot: false })) {
results.push(file);
results.push(file.replaceAll("\\", "/"));
}
return results.sort();
},
Expand All @@ -73,7 +73,7 @@ function createRuleContext(
const match = lines[i].match(pattern);
if (match) {
matches.push({
file: relative(projectRoot, absPath),
file: relative(projectRoot, absPath).replaceAll("\\", "/"),
line: i + 1,
column: (match.index ?? 0) + 1,
content: lines[i],
Expand All @@ -89,6 +89,7 @@ function createRuleContext(
const allMatches: GrepMatch[] = [];

for await (const file of g.scan({ cwd: projectRoot, dot: false })) {
const normalized = file.replaceAll("\\", "/");
const absPath = join(projectRoot, file);
try {
const content = await Bun.file(absPath).text();
Expand All @@ -98,7 +99,7 @@ function createRuleContext(
const match = lines[i].match(pattern);
if (match) {
allMatches.push({
file,
file: normalized,
line: i + 1,
column: (match.index ?? 0) + 1,
content: lines[i],
Expand Down
4 changes: 2 additions & 2 deletions src/helpers/adr-writer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { existsSync, readdirSync } from "node:fs";
import { join } from "node:path";
import { join, basename } from "node:path";
import {
DOMAIN_PREFIXES,
type AdrDomain,
Expand Down Expand Up @@ -163,7 +163,7 @@ export async function updateAdrFile(
rules,
});

const fileName = existing.filePath.split("/").pop()!;
const fileName = basename(existing.filePath);
await Bun.write(existing.filePath, content);

return { id: opts.id, fileName, filePath: existing.filePath };
Expand Down
4 changes: 4 additions & 0 deletions src/helpers/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ export function installGit() {
console.log("Git is not installed. Installing...");
if (process.platform === "darwin") return $`brew install git`;
if (process.platform === "linux") return $`sudo apt-get install -y git`;
if (process.platform === "win32")
throw new Error(
"Git is not installed. Install it from https://git-scm.com/download/win and make sure it is on your PATH."
);
throw new Error("Unsupported platform");
}

Expand Down
4 changes: 2 additions & 2 deletions tests/commands/check.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ import { loadRuleAdrs } from "../../src/engine/loader";
import { runChecks } from "../../src/engine/runner";
import { getExitCode } from "../../src/engine/reporter";

// Absolute path to the real defineRules module
// Absolute path to the real defineRules module (forward slashes for import specifiers)
const RULES_MODULE_PATH = join(
import.meta.dir,
"..",
"..",
"src",
"formats",
"rules.ts"
);
).replaceAll("\\", "/");

describe("check command integration", () => {
let tempDir: string;
Expand Down
4 changes: 2 additions & 2 deletions tests/engine/loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ import { join } from "node:path";
import { tmpdir } from "node:os";
import { loadRuleAdrs } from "../../src/engine/loader";

// Absolute path to the real defineRules module
// Absolute path to the real defineRules module (forward slashes for import specifiers)
const RULES_MODULE_PATH = join(
import.meta.dir,
"..",
"..",
"src",
"formats",
"rules.ts"
);
).replaceAll("\\", "/");

describe("loadRuleAdrs", () => {
let tempDir: string;
Expand Down