archgate 0.39.0 → 0.40.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archgate",
3
- "version": "0.39.0",
3
+ "version": "0.40.0",
4
4
  "description": "Enforce Architecture Decision Records as executable rules — for both humans and AI agents",
5
5
  "keywords": [
6
6
  "adr",
@@ -24,10 +24,10 @@
24
24
  "url": "git+https://github.com/archgate/cli.git"
25
25
  },
26
26
  "bin": {
27
- "archgate": "bin/archgate.cjs"
27
+ "archgate": "shims/npm/archgate.cjs"
28
28
  },
29
29
  "files": [
30
- "bin/archgate.cjs"
30
+ "shims/npm/archgate.cjs"
31
31
  ],
32
32
  "os": [
33
33
  "darwin",
@@ -67,11 +67,11 @@
67
67
  "czg": "1.13.1",
68
68
  "fast-check": "4.8.0",
69
69
  "inquirer": "13.4.3",
70
- "knip": "6.14.1",
70
+ "knip": "6.14.2",
71
71
  "meriyah": "7.1.0",
72
72
  "oxfmt": "0.51.0",
73
73
  "oxlint": "1.66.0",
74
- "posthog-node": "5.35.0",
74
+ "posthog-node": "5.35.1",
75
75
  "zod": "4.4.3"
76
76
  },
77
77
  "peerDependencies": {
@@ -2,24 +2,32 @@
2
2
  "use strict";
3
3
 
4
4
  const { execFileSync, execSync } = require("child_process");
5
+ const crypto = require("crypto");
5
6
  const https = require("https");
6
7
  const zlib = require("zlib");
7
8
  const path = require("path");
8
9
  const fs = require("fs");
9
10
  const os = require("os");
10
11
 
11
- function getArtifactName() {
12
- const { platform, arch } = process;
13
- if (platform === "darwin" && arch === "arm64") return "archgate-darwin-arm64";
14
- if (platform === "linux" && arch === "x64") return "archgate-linux-x64";
15
- if (platform === "win32" && arch === "x64") return "archgate-win32-x64";
12
+ function getArtifactName(platform, arch) {
13
+ const p = platform || process.platform;
14
+ const a = arch || process.arch;
15
+ if (p === "darwin" && a === "arm64") return "archgate-darwin-arm64";
16
+ if (p === "linux" && a === "x64") return "archgate-linux-x64";
17
+ if (p === "win32" && a === "x64") return "archgate-win32-x64";
16
18
  throw new Error(
17
- `Unsupported platform: ${platform}/${arch}\narchgate supports darwin/arm64, linux/x64, and win32/x64.`
19
+ `Unsupported platform: ${p}/${a}\narchgate supports darwin/arm64, linux/x64, and win32/x64.`
18
20
  );
19
21
  }
20
22
 
21
- function getBinaryName() {
22
- return process.platform === "win32" ? "archgate.exe" : "archgate";
23
+ function getBinaryName(platform) {
24
+ return (platform || process.platform) === "win32"
25
+ ? "archgate.exe"
26
+ : "archgate";
27
+ }
28
+
29
+ function getArchiveExt(platform) {
30
+ return (platform || process.platform) === "win32" ? "zip" : "tar.gz";
23
31
  }
24
32
 
25
33
  function getCacheDir() {
@@ -28,7 +36,7 @@ function getCacheDir() {
28
36
 
29
37
  function getPackageVersion() {
30
38
  const pkg = JSON.parse(
31
- fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8")
39
+ fs.readFileSync(path.join(__dirname, "..", "..", "package.json"), "utf8")
32
40
  );
33
41
  return pkg.version;
34
42
  }
@@ -72,6 +80,48 @@ function stripNulls(str) {
72
80
  return idx === -1 ? str : str.slice(0, idx);
73
81
  }
74
82
 
83
+ /**
84
+ * Download a URL into a Buffer, following redirects.
85
+ */
86
+ async function downloadToBuffer(url) {
87
+ const res = await fetchWithRedirects(url);
88
+ const chunks = [];
89
+ await new Promise((resolve, reject) => {
90
+ res.on("data", (chunk) => chunks.push(chunk));
91
+ res.on("end", resolve);
92
+ res.on("error", reject);
93
+ });
94
+ return Buffer.concat(chunks);
95
+ }
96
+
97
+ /**
98
+ * Verify SHA256 checksum of a buffer against the companion .sha256 file.
99
+ * The .sha256 file format is: "<hex-sha256> <filename>\n"
100
+ */
101
+ async function verifySha256(archiveBuffer, checksumUrl, version, fetchFn) {
102
+ const download = fetchFn || downloadToBuffer;
103
+ let checksumData;
104
+ try {
105
+ checksumData = await download(checksumUrl);
106
+ } catch {
107
+ // If checksum file is unavailable, skip verification with a warning
108
+ console.error(
109
+ "archgate: warning: checksum file not available, skipping verification"
110
+ );
111
+ return;
112
+ }
113
+ const expectedHash = checksumData.toString("utf8").trim().split(/\s+/u)[0];
114
+ const actualHash = crypto
115
+ .createHash("sha256")
116
+ .update(archiveBuffer)
117
+ .digest("hex");
118
+ if (actualHash !== expectedHash) {
119
+ throw new Error(
120
+ `checksum verification failed for v${version} (expected ${expectedHash}, got ${actualHash})`
121
+ );
122
+ }
123
+ }
124
+
75
125
  /**
76
126
  * Download the platform binary from GitHub Releases and cache it.
77
127
  * Returns the path to the downloaded binary.
@@ -81,27 +131,26 @@ async function downloadBinary() {
81
131
  const version = getPackageVersion();
82
132
  const binaryName = getBinaryName();
83
133
  const isWin = process.platform === "win32";
84
- const ext = isWin ? "zip" : "tar.gz";
134
+ const ext = getArchiveExt();
85
135
 
86
- const url = `https://github.com/archgate/cli/releases/download/v${version}/${artifactName}.${ext}`;
136
+ const baseUrl = `https://github.com/archgate/cli/releases/download/v${version}/${artifactName}`;
137
+ const url = `${baseUrl}.${ext}`;
138
+ const checksumUrl = `${baseUrl}.${ext}.sha256`;
87
139
  console.error(`archgate: binary not found, downloading v${version}...`);
88
140
 
89
141
  const cacheDir = getCacheDir();
90
142
  fs.mkdirSync(cacheDir, { recursive: true });
91
143
  const destPath = path.join(cacheDir, binaryName);
92
144
 
145
+ // Download archive and verify checksum (shared across platforms)
146
+ const archiveBuffer = await downloadToBuffer(url);
147
+ await verifySha256(archiveBuffer, checksumUrl, version);
148
+
93
149
  if (isWin) {
94
- // Download zip to temp file, extract with PowerShell
95
- const res = await fetchWithRedirects(url);
96
- const chunks = [];
97
- await new Promise((resolve, reject) => {
98
- res.on("data", (chunk) => chunks.push(chunk));
99
- res.on("end", resolve);
100
- res.on("error", reject);
101
- });
150
+ // Extract zip with PowerShell
102
151
  const tmpZip = path.join(cacheDir, "archgate-download.zip");
103
152
  const tmpExtract = path.join(cacheDir, "archgate-extract");
104
- fs.writeFileSync(tmpZip, Buffer.concat(chunks));
153
+ fs.writeFileSync(tmpZip, archiveBuffer);
105
154
  try {
106
155
  fs.mkdirSync(tmpExtract, { recursive: true });
107
156
  execSync(
@@ -114,17 +163,24 @@ async function downloadBinary() {
114
163
  }
115
164
  fs.copyFileSync(extractedBinary, destPath);
116
165
  } finally {
117
- try { fs.unlinkSync(tmpZip); } catch { /* cleanup */ }
118
- try { fs.rmSync(tmpExtract, { recursive: true, force: true }); } catch { /* cleanup */ }
166
+ try {
167
+ fs.unlinkSync(tmpZip);
168
+ } catch {
169
+ /* cleanup */
170
+ }
171
+ try {
172
+ fs.rmSync(tmpExtract, { recursive: true, force: true });
173
+ } catch {
174
+ /* cleanup */
175
+ }
119
176
  }
120
177
  } else {
121
- // Download tar.gz, extract binary using inline tar parser
122
- const res = await fetchWithRedirects(url);
178
+ // Extract binary from tar.gz using inline tar parser
123
179
  await new Promise((resolve, reject) => {
124
180
  const gunzip = zlib.createGunzip();
125
181
  const chunks = [];
126
182
 
127
- res.pipe(gunzip);
183
+ gunzip.end(archiveBuffer);
128
184
  gunzip.on("data", (chunk) => chunks.push(chunk));
129
185
  gunzip.on("end", () => {
130
186
  const data = Buffer.concat(chunks);
@@ -138,9 +194,7 @@ async function downloadBinary() {
138
194
  if (header.every((b) => b === 0)) break;
139
195
 
140
196
  let name = stripNulls(header.subarray(0, 100).toString("utf8"));
141
- const prefix = stripNulls(
142
- header.subarray(345, 500).toString("utf8")
143
- );
197
+ const prefix = stripNulls(header.subarray(345, 500).toString("utf8"));
144
198
  if (prefix) name = `${prefix}/${name}`;
145
199
 
146
200
  const sizeStr = stripNulls(
@@ -204,4 +258,17 @@ async function main() {
204
258
  }
205
259
  }
206
260
 
207
- main();
261
+ // When required as a module (tests), export helpers without running main().
262
+ // When executed directly (npm bin), run the CLI passthrough.
263
+ if (require.main === module) {
264
+ main();
265
+ }
266
+
267
+ module.exports = {
268
+ getArtifactName,
269
+ getBinaryName,
270
+ getArchiveExt,
271
+ getCacheDir,
272
+ stripNulls,
273
+ verifySha256,
274
+ };