@seemseam/architec 0.2.10 → 0.2.11
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 +130 -140
- package/README.zh-CN.md +114 -117
- package/bin/archi.js +9 -131
- package/docs/npm-release.md +75 -55
- package/lib/archi-dispatcher.js +291 -0
- package/package.json +14 -9
- package/scripts/check-release-assets.js +101 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("node:crypto");
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const http = require("node:http");
|
|
6
|
+
const https = require("node:https");
|
|
7
|
+
const os = require("node:os");
|
|
8
|
+
const path = require("node:path");
|
|
9
|
+
const { spawn } = require("node:child_process");
|
|
10
|
+
const { fileURLToPath } = require("node:url");
|
|
11
|
+
|
|
12
|
+
const PACKAGE_ROOT = path.resolve(__dirname, "..");
|
|
13
|
+
const PACKAGE_JSON = JSON.parse(
|
|
14
|
+
fs.readFileSync(path.join(PACKAGE_ROOT, "package.json"), "utf8"),
|
|
15
|
+
);
|
|
16
|
+
const VERSION = PACKAGE_JSON.version;
|
|
17
|
+
const OWNER = "SeemSeam";
|
|
18
|
+
const REPO = "architec";
|
|
19
|
+
const SUPPORTED_TRIPLETS = new Set([
|
|
20
|
+
"linux-x64",
|
|
21
|
+
"linux-arm64",
|
|
22
|
+
"darwin-x64",
|
|
23
|
+
"darwin-arm64",
|
|
24
|
+
"win32-x64",
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
function fail(message) {
|
|
28
|
+
throw new Error(message);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function platformTriplet(platform = process.platform, arch = process.arch) {
|
|
32
|
+
const mappedArch = arch === "x64" || arch === "arm64" ? arch : "";
|
|
33
|
+
if (!mappedArch) {
|
|
34
|
+
fail(`unsupported CPU architecture: ${arch}`);
|
|
35
|
+
}
|
|
36
|
+
if (platform === "linux" || platform === "darwin") {
|
|
37
|
+
return `${platform}-${mappedArch}`;
|
|
38
|
+
}
|
|
39
|
+
if (platform === "win32" && mappedArch === "x64") {
|
|
40
|
+
return "win32-x64";
|
|
41
|
+
}
|
|
42
|
+
fail(`unsupported platform: ${platform}-${arch}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function assetNameFor(triplet, version = VERSION) {
|
|
46
|
+
if (!SUPPORTED_TRIPLETS.has(triplet)) {
|
|
47
|
+
fail(`unsupported binary target: ${triplet}`);
|
|
48
|
+
}
|
|
49
|
+
const extension = triplet.startsWith("win32-") ? ".exe" : "";
|
|
50
|
+
return `archi-v${version}-${triplet}${extension}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function checksumFileName(version = VERSION) {
|
|
54
|
+
return `archi-v${version}-checksums.txt`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function defaultCacheBase() {
|
|
58
|
+
if (process.platform === "win32") {
|
|
59
|
+
return (
|
|
60
|
+
process.env.LOCALAPPDATA ||
|
|
61
|
+
path.join(os.homedir(), "AppData", "Local")
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
if (process.platform === "darwin") {
|
|
65
|
+
return path.join(os.homedir(), "Library", "Caches");
|
|
66
|
+
}
|
|
67
|
+
return process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function cacheRoot() {
|
|
71
|
+
return (
|
|
72
|
+
process.env.ARCHITEC_NPM_CACHE_DIR ||
|
|
73
|
+
path.join(defaultCacheBase(), "architec", "npm")
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function releaseBaseUrl() {
|
|
78
|
+
return (
|
|
79
|
+
process.env.ARCHITEC_NPM_RELEASE_BASE_URL ||
|
|
80
|
+
`https://github.com/${OWNER}/${REPO}/releases/download/v${VERSION}/`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function joinUrl(base, name) {
|
|
85
|
+
const normalized = base.endsWith("/") ? base : `${base}/`;
|
|
86
|
+
return new URL(name, normalized).toString();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function sha256File(filePath) {
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
const hash = crypto.createHash("sha256");
|
|
92
|
+
const stream = fs.createReadStream(filePath);
|
|
93
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
94
|
+
stream.on("error", reject);
|
|
95
|
+
stream.on("end", () => resolve(hash.digest("hex")));
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function request(url, redirects = 0) {
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
const parsed = new URL(url);
|
|
102
|
+
const client = parsed.protocol === "http:" ? http : https;
|
|
103
|
+
const req = client.get(
|
|
104
|
+
parsed,
|
|
105
|
+
{
|
|
106
|
+
headers: {
|
|
107
|
+
"User-Agent": `${PACKAGE_JSON.name}/${VERSION}`,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
(res) => {
|
|
111
|
+
if (
|
|
112
|
+
res.statusCode >= 300 &&
|
|
113
|
+
res.statusCode < 400 &&
|
|
114
|
+
res.headers.location
|
|
115
|
+
) {
|
|
116
|
+
res.resume();
|
|
117
|
+
if (redirects >= 5) {
|
|
118
|
+
reject(new Error(`too many redirects while fetching ${url}`));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
resolve(request(new URL(res.headers.location, url).toString(), redirects + 1));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (res.statusCode !== 200) {
|
|
125
|
+
res.resume();
|
|
126
|
+
reject(new Error(`HTTP ${res.statusCode} while fetching ${url}`));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
resolve(res);
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
req.on("error", reject);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function readUrl(url) {
|
|
137
|
+
const parsed = new URL(url);
|
|
138
|
+
if (parsed.protocol === "file:") {
|
|
139
|
+
return fs.promises.readFile(fileURLToPath(parsed));
|
|
140
|
+
}
|
|
141
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
142
|
+
fail(`unsupported URL protocol: ${parsed.protocol}`);
|
|
143
|
+
}
|
|
144
|
+
const res = await request(url);
|
|
145
|
+
const chunks = [];
|
|
146
|
+
for await (const chunk of res) {
|
|
147
|
+
chunks.push(chunk);
|
|
148
|
+
}
|
|
149
|
+
return Buffer.concat(chunks);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function downloadUrl(url, targetPath) {
|
|
153
|
+
const parsed = new URL(url);
|
|
154
|
+
if (parsed.protocol === "file:") {
|
|
155
|
+
await fs.promises.copyFile(fileURLToPath(parsed), targetPath);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
159
|
+
fail(`unsupported URL protocol: ${parsed.protocol}`);
|
|
160
|
+
}
|
|
161
|
+
const res = await request(url);
|
|
162
|
+
await new Promise((resolve, reject) => {
|
|
163
|
+
const stream = fs.createWriteStream(targetPath, { mode: 0o755 });
|
|
164
|
+
res.pipe(stream);
|
|
165
|
+
res.on("error", reject);
|
|
166
|
+
stream.on("error", reject);
|
|
167
|
+
stream.on("finish", resolve);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function parseChecksums(text) {
|
|
172
|
+
const checksums = new Map();
|
|
173
|
+
for (const line of text.split(/\r?\n/)) {
|
|
174
|
+
const trimmed = line.trim();
|
|
175
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const parts = trimmed.split(/\s+/);
|
|
179
|
+
if (parts.length < 2) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (/^[a-fA-F0-9]{64}$/.test(parts[0])) {
|
|
183
|
+
checksums.set(parts[1].replace(/^\*/, ""), parts[0].toLowerCase());
|
|
184
|
+
} else if (/^[a-fA-F0-9]{64}$/.test(parts[1])) {
|
|
185
|
+
checksums.set(parts[0].replace(/^\*/, ""), parts[1].toLowerCase());
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return checksums;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function expectedChecksum(assetName) {
|
|
192
|
+
const checksumUrl =
|
|
193
|
+
process.env.ARCHITEC_NPM_CHECKSUM_URL ||
|
|
194
|
+
joinUrl(releaseBaseUrl(), checksumFileName());
|
|
195
|
+
const checksumText = (await readUrl(checksumUrl)).toString("utf8");
|
|
196
|
+
const checksums = parseChecksums(checksumText);
|
|
197
|
+
const expected = checksums.get(assetName);
|
|
198
|
+
if (!expected) {
|
|
199
|
+
fail(`checksum for ${assetName} was not found in ${checksumUrl}`);
|
|
200
|
+
}
|
|
201
|
+
return expected;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function markerMatches(markerPath, assetName, expected) {
|
|
205
|
+
try {
|
|
206
|
+
const marker = JSON.parse(fs.readFileSync(markerPath, "utf8"));
|
|
207
|
+
return (
|
|
208
|
+
marker.version === VERSION &&
|
|
209
|
+
marker.assetName === assetName &&
|
|
210
|
+
marker.sha256 === expected
|
|
211
|
+
);
|
|
212
|
+
} catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function ensureBinary() {
|
|
218
|
+
const configuredBinary = process.env.ARCHITEC_NPM_BINARY_PATH;
|
|
219
|
+
if (configuredBinary) {
|
|
220
|
+
const resolved = path.resolve(configuredBinary);
|
|
221
|
+
if (!fs.existsSync(resolved)) {
|
|
222
|
+
fail(`ARCHITEC_NPM_BINARY_PATH does not exist: ${resolved}`);
|
|
223
|
+
}
|
|
224
|
+
return resolved;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const triplet = platformTriplet();
|
|
228
|
+
const assetName = assetNameFor(triplet);
|
|
229
|
+
const targetDir = path.join(cacheRoot(), VERSION, triplet);
|
|
230
|
+
const targetPath = path.join(targetDir, process.platform === "win32" ? "archi.exe" : "archi");
|
|
231
|
+
const markerPath = path.join(targetDir, "download.json");
|
|
232
|
+
const expected = await expectedChecksum(assetName);
|
|
233
|
+
|
|
234
|
+
if (fs.existsSync(targetPath) && markerMatches(markerPath, assetName, expected)) {
|
|
235
|
+
return targetPath;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
await fs.promises.mkdir(targetDir, { recursive: true });
|
|
239
|
+
if (fs.existsSync(targetPath)) {
|
|
240
|
+
const actual = await sha256File(targetPath);
|
|
241
|
+
if (actual === expected) {
|
|
242
|
+
await fs.promises.chmod(targetPath, 0o755);
|
|
243
|
+
await fs.promises.writeFile(
|
|
244
|
+
markerPath,
|
|
245
|
+
`${JSON.stringify({ version: VERSION, assetName, sha256: expected }, null, 2)}\n`,
|
|
246
|
+
);
|
|
247
|
+
return targetPath;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const tempPath = path.join(targetDir, `${assetName}.${process.pid}.download`);
|
|
252
|
+
await downloadUrl(joinUrl(releaseBaseUrl(), assetName), tempPath);
|
|
253
|
+
const actual = await sha256File(tempPath);
|
|
254
|
+
if (actual !== expected) {
|
|
255
|
+
await fs.promises.rm(tempPath, { force: true });
|
|
256
|
+
fail(`checksum mismatch for ${assetName}: expected ${expected}, got ${actual}`);
|
|
257
|
+
}
|
|
258
|
+
await fs.promises.chmod(tempPath, 0o755);
|
|
259
|
+
await fs.promises.rename(tempPath, targetPath);
|
|
260
|
+
await fs.promises.writeFile(
|
|
261
|
+
markerPath,
|
|
262
|
+
`${JSON.stringify({ version: VERSION, assetName, sha256: expected }, null, 2)}\n`,
|
|
263
|
+
);
|
|
264
|
+
return targetPath;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function runBinary(binaryPath, args) {
|
|
268
|
+
return new Promise((resolve, reject) => {
|
|
269
|
+
const child = spawn(binaryPath, args, {
|
|
270
|
+
stdio: "inherit",
|
|
271
|
+
env: process.env,
|
|
272
|
+
});
|
|
273
|
+
child.on("error", reject);
|
|
274
|
+
child.on("exit", (code) => resolve(code === null ? 1 : code));
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function main(args = process.argv.slice(2)) {
|
|
279
|
+
const binaryPath = await ensureBinary();
|
|
280
|
+
return runBinary(binaryPath, args);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
module.exports = {
|
|
284
|
+
VERSION,
|
|
285
|
+
assetNameFor,
|
|
286
|
+
checksumFileName,
|
|
287
|
+
ensureBinary,
|
|
288
|
+
main,
|
|
289
|
+
parseChecksums,
|
|
290
|
+
platformTriplet,
|
|
291
|
+
};
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seemseam/architec",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.11",
|
|
4
|
+
"description": "Binary dispatcher for the Architec architecture-review CLI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"architecture",
|
|
7
|
-
"
|
|
7
|
+
"architecture-review",
|
|
8
|
+
"binary",
|
|
8
9
|
"cli",
|
|
9
|
-
"
|
|
10
|
-
"
|
|
10
|
+
"code-analysis",
|
|
11
|
+
"llm"
|
|
11
12
|
],
|
|
12
13
|
"homepage": "https://github.com/SeemSeam/architec#readme",
|
|
13
14
|
"author": "SeemSeam",
|
|
@@ -16,7 +17,7 @@
|
|
|
16
17
|
},
|
|
17
18
|
"repository": {
|
|
18
19
|
"type": "git",
|
|
19
|
-
"url": "https://github.com/SeemSeam/architec
|
|
20
|
+
"url": "https://github.com/SeemSeam/architec"
|
|
20
21
|
},
|
|
21
22
|
"license": "MIT",
|
|
22
23
|
"bin": {
|
|
@@ -24,7 +25,10 @@
|
|
|
24
25
|
},
|
|
25
26
|
"files": [
|
|
26
27
|
"bin/",
|
|
27
|
-
"
|
|
28
|
+
"lib/",
|
|
29
|
+
"scripts/check-release-assets.js",
|
|
30
|
+
"docs/npm-release.md",
|
|
31
|
+
"LICENSE"
|
|
28
32
|
],
|
|
29
33
|
"engines": {
|
|
30
34
|
"node": ">=18"
|
|
@@ -33,7 +37,8 @@
|
|
|
33
37
|
"access": "public"
|
|
34
38
|
},
|
|
35
39
|
"scripts": {
|
|
36
|
-
"test": "node --check bin/archi.js",
|
|
37
|
-
"pack:dry-run": "npm pack --dry-run --json"
|
|
40
|
+
"test": "node --check bin/archi.js && node --check lib/archi-dispatcher.js && node scripts/smoke-dispatcher.js",
|
|
41
|
+
"pack:dry-run": "npm pack --dry-run --json",
|
|
42
|
+
"release-assets:check": "node scripts/check-release-assets.js"
|
|
38
43
|
}
|
|
39
44
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const https = require("node:https");
|
|
4
|
+
const { URL } = require("node:url");
|
|
5
|
+
const {
|
|
6
|
+
VERSION,
|
|
7
|
+
assetNameFor,
|
|
8
|
+
checksumFileName,
|
|
9
|
+
parseChecksums,
|
|
10
|
+
} = require("../lib/archi-dispatcher");
|
|
11
|
+
|
|
12
|
+
const OWNER = "SeemSeam";
|
|
13
|
+
const REPO = "architec";
|
|
14
|
+
const DEFAULT_REQUIRED_TRIPLETS = [
|
|
15
|
+
"linux-x64",
|
|
16
|
+
"darwin-x64",
|
|
17
|
+
"darwin-arm64",
|
|
18
|
+
"win32-x64",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function requiredTriplets() {
|
|
22
|
+
const raw = process.env.ARCHITEC_NPM_REQUIRED_TRIPLETS || "";
|
|
23
|
+
if (!raw.trim()) {
|
|
24
|
+
return DEFAULT_REQUIRED_TRIPLETS;
|
|
25
|
+
}
|
|
26
|
+
return raw.split(",").map((value) => value.trim()).filter(Boolean);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function requestJson(url) {
|
|
30
|
+
return request(url).then((body) => JSON.parse(body.toString("utf8")));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function request(url) {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const headers = {
|
|
36
|
+
Accept: "application/vnd.github+json",
|
|
37
|
+
"User-Agent": `@seemseam/architec/${VERSION}`,
|
|
38
|
+
};
|
|
39
|
+
if (process.env.GITHUB_TOKEN) {
|
|
40
|
+
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
|
41
|
+
}
|
|
42
|
+
https
|
|
43
|
+
.get(new URL(url), { headers }, (res) => {
|
|
44
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
45
|
+
res.resume();
|
|
46
|
+
resolve(request(new URL(res.headers.location, url).toString()));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (res.statusCode !== 200) {
|
|
50
|
+
res.resume();
|
|
51
|
+
reject(new Error(`HTTP ${res.statusCode} while fetching ${url}`));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const chunks = [];
|
|
55
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
56
|
+
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
57
|
+
})
|
|
58
|
+
.on("error", reject);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function main() {
|
|
63
|
+
const release = await requestJson(
|
|
64
|
+
`https://api.github.com/repos/${OWNER}/${REPO}/releases/tags/v${VERSION}`,
|
|
65
|
+
);
|
|
66
|
+
if (release.draft) {
|
|
67
|
+
throw new Error(`GitHub Release v${VERSION} is still a draft`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const assetByName = new Map((release.assets || []).map((asset) => [asset.name, asset]));
|
|
71
|
+
const checksumsName = checksumFileName();
|
|
72
|
+
const checksumsAsset = assetByName.get(checksumsName);
|
|
73
|
+
if (!checksumsAsset) {
|
|
74
|
+
throw new Error(`missing GitHub Release checksum asset: ${checksumsName}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const checksums = parseChecksums(
|
|
78
|
+
(await request(checksumsAsset.browser_download_url)).toString("utf8"),
|
|
79
|
+
);
|
|
80
|
+
const missing = [];
|
|
81
|
+
for (const triplet of requiredTriplets()) {
|
|
82
|
+
const assetName = assetNameFor(triplet);
|
|
83
|
+
if (!assetByName.has(assetName)) {
|
|
84
|
+
missing.push(assetName);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (!checksums.has(assetName)) {
|
|
88
|
+
missing.push(`${assetName} checksum`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (missing.length) {
|
|
93
|
+
throw new Error(`missing required release assets: ${missing.join(", ")}`);
|
|
94
|
+
}
|
|
95
|
+
console.log(`GitHub Release v${VERSION} has required npm binary assets.`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
main().catch((error) => {
|
|
99
|
+
console.error(error.message);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
});
|