archgate 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -26
- package/bin/archgate.cjs +155 -10
- package/package.json +7 -9
- package/scripts/postinstall.cjs +54 -0
- package/src/formats/rules.ts +0 -68
package/README.md
CHANGED
|
@@ -42,10 +42,13 @@ When a rule is violated, `archgate check` reports the file, line, and which ADR
|
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
44
|
# macOS / Linux
|
|
45
|
-
curl -fsSL https://
|
|
45
|
+
curl -fsSL https://cli.archgate.dev/install-unix | sh
|
|
46
46
|
|
|
47
47
|
# Windows (PowerShell)
|
|
48
|
-
irm https://
|
|
48
|
+
irm https://cli.archgate.dev/install-windows | iex
|
|
49
|
+
|
|
50
|
+
# Windows (Git Bash / MSYS2)
|
|
51
|
+
curl -fsSL https://cli.archgate.dev/install-unix | sh
|
|
49
52
|
```
|
|
50
53
|
|
|
51
54
|
**Via npm** (or any Node.js package manager):
|
|
@@ -77,7 +80,7 @@ npx archgate check # run via package manager
|
|
|
77
80
|
|
|
78
81
|
```bash
|
|
79
82
|
# 1. Install
|
|
80
|
-
curl -fsSL https://
|
|
83
|
+
curl -fsSL https://cli.archgate.dev/install-unix | sh # or: npm install -g archgate
|
|
81
84
|
|
|
82
85
|
# 2. Initialize governance in your project
|
|
83
86
|
cd my-project
|
|
@@ -95,29 +98,7 @@ archgate check
|
|
|
95
98
|
|
|
96
99
|
## Writing rules
|
|
97
100
|
|
|
98
|
-
Each ADR can have a companion `.rules.ts` file that exports checks
|
|
99
|
-
|
|
100
|
-
See the [writing rules guide](https://cli.archgate.dev/guides/writing-rules/) for examples and the full [rule API reference](https://cli.archgate.dev/reference/rule-api/).
|
|
101
|
-
|
|
102
|
-
## Commands
|
|
103
|
-
|
|
104
|
-
| Command | Description |
|
|
105
|
-
| ------------------------ | ---------------------------------------------------------- |
|
|
106
|
-
| `archgate init` | Initialize `.archgate/` with example ADR and editor config |
|
|
107
|
-
| `archgate check` | Run ADR compliance checks (`--staged` for pre-commit) |
|
|
108
|
-
| `archgate adr create` | Create a new ADR interactively |
|
|
109
|
-
| `archgate adr list` | List all ADRs (`--json`, `--domain`) |
|
|
110
|
-
| `archgate adr show <id>` | Print a specific ADR |
|
|
111
|
-
| `archgate adr update` | Update an ADR's frontmatter |
|
|
112
|
-
| `archgate login` | Authenticate with GitHub for editor plugins |
|
|
113
|
-
| `archgate upgrade` | Upgrade to the latest release |
|
|
114
|
-
| `archgate clean` | Remove the CLI cache (`~/.archgate/`) |
|
|
115
|
-
|
|
116
|
-
See the [CLI reference](https://cli.archgate.dev/reference/cli-commands/) for full usage and options.
|
|
117
|
-
|
|
118
|
-
## CI and pre-commit hooks
|
|
119
|
-
|
|
120
|
-
Add `archgate check` to your CI pipeline or pre-commit hooks to block merges that violate ADRs. See the [CI integration guide](https://cli.archgate.dev/guides/ci-integration/) and [pre-commit hooks guide](https://cli.archgate.dev/guides/pre-commit-hooks/) for setup instructions.
|
|
101
|
+
Each ADR can have a companion `.rules.ts` file that exports automated checks. See the [writing rules guide](https://cli.archgate.dev/guides/writing-rules/) for examples and the full [rule API reference](https://cli.archgate.dev/reference/rule-api/).
|
|
121
102
|
|
|
122
103
|
## Supercharge with AI plugins
|
|
123
104
|
|
package/bin/archgate.cjs
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
const { execFileSync } = require("child_process");
|
|
5
|
+
const https = require("https");
|
|
6
|
+
const zlib = require("zlib");
|
|
5
7
|
const path = require("path");
|
|
6
8
|
const fs = require("fs");
|
|
7
9
|
|
|
@@ -15,9 +17,22 @@ function getPlatformPackageName() {
|
|
|
15
17
|
);
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
function getBinaryName() {
|
|
21
|
+
return process.platform === "win32" ? "archgate.exe" : "archgate";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getPackageVersion() {
|
|
25
|
+
const pkg = JSON.parse(
|
|
26
|
+
fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8")
|
|
27
|
+
);
|
|
28
|
+
return pkg.version;
|
|
29
|
+
}
|
|
30
|
+
|
|
18
31
|
function getBinaryPath() {
|
|
19
32
|
const pkgName = getPlatformPackageName();
|
|
20
|
-
const binaryName =
|
|
33
|
+
const binaryName = getBinaryName();
|
|
34
|
+
|
|
35
|
+
// 1. Try the platform-specific optional dependency (normal path).
|
|
21
36
|
try {
|
|
22
37
|
const pkgDir = path.dirname(require.resolve(`${pkgName}/package.json`));
|
|
23
38
|
const binaryPath = path.join(pkgDir, "bin", binaryName);
|
|
@@ -25,16 +40,146 @@ function getBinaryPath() {
|
|
|
25
40
|
} catch {
|
|
26
41
|
/* platform package not installed */
|
|
27
42
|
}
|
|
28
|
-
|
|
29
|
-
|
|
43
|
+
|
|
44
|
+
// 2. Fallback: binary downloaded into our own bin/ by postinstall or a
|
|
45
|
+
// previous on-demand download.
|
|
46
|
+
const fallbackPath = path.join(__dirname, binaryName);
|
|
47
|
+
if (fs.existsSync(fallbackPath)) return fallbackPath;
|
|
48
|
+
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// On-demand download from the npm registry
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
function fetchWithRedirects(url) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
https
|
|
59
|
+
.get(url, (res) => {
|
|
60
|
+
if (
|
|
61
|
+
res.statusCode >= 300 &&
|
|
62
|
+
res.statusCode < 400 &&
|
|
63
|
+
res.headers.location
|
|
64
|
+
) {
|
|
65
|
+
fetchWithRedirects(res.headers.location).then(resolve, reject);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (res.statusCode !== 200) {
|
|
69
|
+
reject(new Error(`GET ${url} returned status ${res.statusCode}`));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
resolve(res);
|
|
73
|
+
})
|
|
74
|
+
.on("error", reject);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Strip null bytes from a buffer-decoded string. */
|
|
79
|
+
function stripNulls(str) {
|
|
80
|
+
const idx = str.indexOf(String.fromCodePoint(0));
|
|
81
|
+
return idx === -1 ? str : str.slice(0, idx);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Download the platform-specific npm package tarball and extract the binary.
|
|
86
|
+
* Returns the path to the downloaded binary.
|
|
87
|
+
*/
|
|
88
|
+
async function downloadBinary() {
|
|
89
|
+
const pkgName = getPlatformPackageName();
|
|
90
|
+
const version = getPackageVersion();
|
|
91
|
+
const binaryName = getBinaryName();
|
|
92
|
+
|
|
93
|
+
const url = `https://registry.npmjs.org/${pkgName}/-/${pkgName}-${version}.tgz`;
|
|
94
|
+
console.error(
|
|
95
|
+
`archgate: binary not found, downloading ${pkgName}@${version}...`
|
|
30
96
|
);
|
|
97
|
+
|
|
98
|
+
const res = await fetchWithRedirects(url);
|
|
99
|
+
|
|
100
|
+
const binDir = __dirname;
|
|
101
|
+
const destPath = path.join(binDir, binaryName);
|
|
102
|
+
|
|
103
|
+
return new Promise((resolve, reject) => {
|
|
104
|
+
const gunzip = zlib.createGunzip();
|
|
105
|
+
const chunks = [];
|
|
106
|
+
const expectedSuffix = `bin/${binaryName}`;
|
|
107
|
+
let found = false;
|
|
108
|
+
|
|
109
|
+
res.pipe(gunzip);
|
|
110
|
+
gunzip.on("data", (chunk) => chunks.push(chunk));
|
|
111
|
+
gunzip.on("end", () => {
|
|
112
|
+
const data = Buffer.concat(chunks);
|
|
113
|
+
let offset = 0;
|
|
114
|
+
|
|
115
|
+
while (offset + 512 <= data.length) {
|
|
116
|
+
const header = data.subarray(offset, offset + 512);
|
|
117
|
+
offset += 512;
|
|
118
|
+
|
|
119
|
+
// Empty header block signals end of archive
|
|
120
|
+
if (header.every((b) => b === 0)) break;
|
|
121
|
+
|
|
122
|
+
let name = stripNulls(header.subarray(0, 100).toString("utf8"));
|
|
123
|
+
const prefix = stripNulls(header.subarray(345, 500).toString("utf8"));
|
|
124
|
+
if (prefix) name = `${prefix}/${name}`;
|
|
125
|
+
|
|
126
|
+
const sizeStr = stripNulls(
|
|
127
|
+
header.subarray(124, 136).toString("utf8")
|
|
128
|
+
).trim();
|
|
129
|
+
const size = parseInt(sizeStr, 8) || 0;
|
|
130
|
+
const blocks = Math.ceil(size / 512);
|
|
131
|
+
const fileData = data.subarray(offset, offset + size);
|
|
132
|
+
offset += blocks * 512;
|
|
133
|
+
|
|
134
|
+
if (name.endsWith(expectedSuffix)) {
|
|
135
|
+
fs.writeFileSync(destPath, fileData, { mode: 0o755 });
|
|
136
|
+
found = true;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (found) {
|
|
142
|
+
console.error(`archgate: binary downloaded successfully.`);
|
|
143
|
+
resolve(destPath);
|
|
144
|
+
} else {
|
|
145
|
+
reject(
|
|
146
|
+
new Error(`Could not find ${expectedSuffix} in tarball from ${url}`)
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
gunzip.on("error", reject);
|
|
152
|
+
});
|
|
31
153
|
}
|
|
32
154
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Main
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
async function main() {
|
|
160
|
+
let binary = getBinaryPath();
|
|
161
|
+
|
|
162
|
+
// If the binary is missing (optional dep skipped AND postinstall blocked),
|
|
163
|
+
// download it on-demand from the npm registry.
|
|
164
|
+
if (!binary) {
|
|
165
|
+
try {
|
|
166
|
+
binary = await downloadBinary();
|
|
167
|
+
} catch (err) {
|
|
168
|
+
console.error(
|
|
169
|
+
`archgate: failed to download binary: ${err.message}\n` +
|
|
170
|
+
`Try reinstalling: npm install archgate`
|
|
171
|
+
);
|
|
172
|
+
process.exit(2);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
execFileSync(binary, process.argv.slice(2), { stdio: "inherit" });
|
|
178
|
+
} catch (e) {
|
|
179
|
+
if (typeof e.status === "number") process.exit(e.status);
|
|
180
|
+
console.error(e.message);
|
|
181
|
+
process.exit(2);
|
|
182
|
+
}
|
|
40
183
|
}
|
|
184
|
+
|
|
185
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "archgate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Enforce Architecture Decision Records as executable rules — for both humans and AI agents",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"adr",
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"archgate": "bin/archgate.cjs"
|
|
28
28
|
},
|
|
29
29
|
"files": [
|
|
30
|
-
"
|
|
31
|
-
"
|
|
30
|
+
"bin/archgate.cjs",
|
|
31
|
+
"scripts/postinstall.cjs"
|
|
32
32
|
],
|
|
33
33
|
"os": [
|
|
34
34
|
"darwin",
|
|
@@ -36,9 +36,6 @@
|
|
|
36
36
|
"win32"
|
|
37
37
|
],
|
|
38
38
|
"type": "module",
|
|
39
|
-
"exports": {
|
|
40
|
-
"./rules": "./src/formats/rules.ts"
|
|
41
|
-
},
|
|
42
39
|
"scripts": {
|
|
43
40
|
"build:check": "bun build src/cli.ts --compile --bytecode --outfile dist/.build-check && rm -f dist/.build-check dist/.build-check.exe",
|
|
44
41
|
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
|
|
@@ -51,6 +48,7 @@
|
|
|
51
48
|
"format": "oxfmt --write .",
|
|
52
49
|
"format:check": "oxfmt --check .",
|
|
53
50
|
"lint": "oxlint --deny-warnings .",
|
|
51
|
+
"postinstall": "node scripts/postinstall.cjs",
|
|
54
52
|
"test": "bun test --timeout 60000",
|
|
55
53
|
"test:watch": "bun test --watch --timeout 60000",
|
|
56
54
|
"typecheck": "tsc --build",
|
|
@@ -75,9 +73,9 @@
|
|
|
75
73
|
"typescript": "^5"
|
|
76
74
|
},
|
|
77
75
|
"optionalDependencies": {
|
|
78
|
-
"archgate-darwin-arm64": "0.
|
|
79
|
-
"archgate-linux-x64": "0.
|
|
80
|
-
"archgate-win32-x64": "0.
|
|
76
|
+
"archgate-darwin-arm64": "0.16.0",
|
|
77
|
+
"archgate-linux-x64": "0.16.0",
|
|
78
|
+
"archgate-win32-x64": "0.16.0"
|
|
81
79
|
},
|
|
82
80
|
"readme": "README.md"
|
|
83
81
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// Best-effort pre-download: tries to fetch the binary during install so the
|
|
5
|
+
// first `archgate` invocation is instant. If this script is blocked by the
|
|
6
|
+
// package manager (--ignore-scripts, etc.), the bin shim will download
|
|
7
|
+
// on-demand at runtime instead.
|
|
8
|
+
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const fs = require("fs");
|
|
11
|
+
|
|
12
|
+
const PACKAGE_NAME_MAP = {
|
|
13
|
+
"darwin-arm64": "archgate-darwin-arm64",
|
|
14
|
+
"linux-x64": "archgate-linux-x64",
|
|
15
|
+
"win32-x64": "archgate-win32-x64",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function isBinaryPresent() {
|
|
19
|
+
const key = `${process.platform}-${process.arch}`;
|
|
20
|
+
const pkgName = PACKAGE_NAME_MAP[key];
|
|
21
|
+
if (!pkgName) return true; // unsupported platform — skip silently
|
|
22
|
+
|
|
23
|
+
const binaryName = process.platform === "win32" ? "archgate.exe" : "archgate";
|
|
24
|
+
|
|
25
|
+
// Check optional dependency
|
|
26
|
+
try {
|
|
27
|
+
const pkgDir = path.dirname(require.resolve(`${pkgName}/package.json`));
|
|
28
|
+
if (fs.existsSync(path.join(pkgDir, "bin", binaryName))) return true;
|
|
29
|
+
} catch {
|
|
30
|
+
/* not installed */
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Check local fallback
|
|
34
|
+
if (fs.existsSync(path.join(__dirname, "..", "bin", binaryName))) return true;
|
|
35
|
+
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!isBinaryPresent()) {
|
|
40
|
+
// Delegate to the bin shim's download logic by running it with --version.
|
|
41
|
+
// This triggers the on-demand download without side effects.
|
|
42
|
+
try {
|
|
43
|
+
require("child_process").execFileSync(
|
|
44
|
+
process.execPath,
|
|
45
|
+
[path.join(__dirname, "..", "bin", "archgate.cjs"), "--version"],
|
|
46
|
+
{ stdio: "inherit" }
|
|
47
|
+
);
|
|
48
|
+
} catch {
|
|
49
|
+
// Postinstall must never block `npm install`.
|
|
50
|
+
console.warn(
|
|
51
|
+
"archgate: could not pre-download binary. It will be downloaded on first run."
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/formats/rules.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
// --- Severity ---
|
|
2
|
-
|
|
3
|
-
export type Severity = "error" | "warning" | "info";
|
|
4
|
-
|
|
5
|
-
// --- Grep Match ---
|
|
6
|
-
|
|
7
|
-
export interface GrepMatch {
|
|
8
|
-
file: string;
|
|
9
|
-
line: number;
|
|
10
|
-
column: number;
|
|
11
|
-
content: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// --- Violation Detail ---
|
|
15
|
-
|
|
16
|
-
export interface ViolationDetail {
|
|
17
|
-
ruleId: string;
|
|
18
|
-
adrId: string;
|
|
19
|
-
message: string;
|
|
20
|
-
file?: string;
|
|
21
|
-
line?: number;
|
|
22
|
-
fix?: string;
|
|
23
|
-
severity: Severity;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// --- Report interface (side-effect based) ---
|
|
27
|
-
|
|
28
|
-
export interface RuleReport {
|
|
29
|
-
violation(
|
|
30
|
-
detail: Omit<ViolationDetail, "ruleId" | "adrId" | "severity">
|
|
31
|
-
): void;
|
|
32
|
-
warning(detail: Omit<ViolationDetail, "ruleId" | "adrId" | "severity">): void;
|
|
33
|
-
info(detail: Omit<ViolationDetail, "ruleId" | "adrId" | "severity">): void;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// --- Rule Context ---
|
|
37
|
-
|
|
38
|
-
export interface RuleContext {
|
|
39
|
-
projectRoot: string;
|
|
40
|
-
scopedFiles: string[];
|
|
41
|
-
changedFiles: string[];
|
|
42
|
-
glob(pattern: string): Promise<string[]>;
|
|
43
|
-
grep(file: string, pattern: RegExp): Promise<GrepMatch[]>;
|
|
44
|
-
grepFiles(pattern: RegExp, fileGlob: string): Promise<GrepMatch[]>;
|
|
45
|
-
readFile(path: string): Promise<string>;
|
|
46
|
-
readJSON(path: string): Promise<unknown>;
|
|
47
|
-
report: RuleReport;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// --- Rule Config ---
|
|
51
|
-
|
|
52
|
-
export interface RuleConfig {
|
|
53
|
-
description: string;
|
|
54
|
-
severity?: Severity;
|
|
55
|
-
check: (ctx: RuleContext) => Promise<void>;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// --- Rule Set ---
|
|
59
|
-
|
|
60
|
-
export type RuleSet = { rules: Record<string, RuleConfig> };
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Define rules in a .rules.ts companion file.
|
|
64
|
-
* Keys become rule IDs, preventing duplicates.
|
|
65
|
-
*/
|
|
66
|
-
export function defineRules(rules: Record<string, RuleConfig>): RuleSet {
|
|
67
|
-
return { rules };
|
|
68
|
-
}
|