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
165 changes: 155 additions & 10 deletions bin/archgate.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"use strict";

const { execFileSync } = require("child_process");
const https = require("https");
const zlib = require("zlib");
const path = require("path");
const fs = require("fs");

Expand All @@ -15,26 +17,169 @@ function getPlatformPackageName() {
);
}

function getBinaryName() {
return process.platform === "win32" ? "archgate.exe" : "archgate";
}

function getPackageVersion() {
const pkg = JSON.parse(
fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8")
);
return pkg.version;
}

function getBinaryPath() {
const pkgName = getPlatformPackageName();
const binaryName = process.platform === "win32" ? "archgate.exe" : "archgate";
const binaryName = getBinaryName();

// 1. Try the platform-specific optional dependency (normal path).
try {
const pkgDir = path.dirname(require.resolve(`${pkgName}/package.json`));
const binaryPath = path.join(pkgDir, "bin", binaryName);
if (fs.existsSync(binaryPath)) return binaryPath;
} catch {
/* platform package not installed */
}
throw new Error(
`archgate binary not found. "${getPlatformPackageName()}" may not be installed.\nTry reinstalling: npm install -g archgate`

// 2. Fallback: binary downloaded into our own bin/ by postinstall or a
// previous on-demand download.
const fallbackPath = path.join(__dirname, binaryName);
if (fs.existsSync(fallbackPath)) return fallbackPath;

return null;
}

// ---------------------------------------------------------------------------
// On-demand download from the npm registry
// ---------------------------------------------------------------------------

function fetchWithRedirects(url) {
return new Promise((resolve, reject) => {
https
.get(url, (res) => {
if (
res.statusCode >= 300 &&
res.statusCode < 400 &&
res.headers.location
) {
fetchWithRedirects(res.headers.location).then(resolve, reject);
return;
}
if (res.statusCode !== 200) {
reject(new Error(`GET ${url} returned status ${res.statusCode}`));
return;
}
resolve(res);
})
.on("error", reject);
});
}

/** Strip null bytes from a buffer-decoded string. */
function stripNulls(str) {
const idx = str.indexOf(String.fromCodePoint(0));
return idx === -1 ? str : str.slice(0, idx);
}

/**
* Download the platform-specific npm package tarball and extract the binary.
* Returns the path to the downloaded binary.
*/
async function downloadBinary() {
const pkgName = getPlatformPackageName();
const version = getPackageVersion();
const binaryName = getBinaryName();

const url = `https://registry.npmjs.org/${pkgName}/-/${pkgName}-${version}.tgz`;
console.error(
`archgate: binary not found, downloading ${pkgName}@${version}...`
);

const res = await fetchWithRedirects(url);

const binDir = __dirname;
const destPath = path.join(binDir, binaryName);

return new Promise((resolve, reject) => {
const gunzip = zlib.createGunzip();
const chunks = [];
const expectedSuffix = `bin/${binaryName}`;
let found = false;

res.pipe(gunzip);
gunzip.on("data", (chunk) => chunks.push(chunk));
gunzip.on("end", () => {
const data = Buffer.concat(chunks);
let offset = 0;

while (offset + 512 <= data.length) {
const header = data.subarray(offset, offset + 512);
offset += 512;

// Empty header block signals end of archive
if (header.every((b) => b === 0)) break;

let name = stripNulls(header.subarray(0, 100).toString("utf8"));
const prefix = stripNulls(header.subarray(345, 500).toString("utf8"));
if (prefix) name = `${prefix}/${name}`;

const sizeStr = stripNulls(
header.subarray(124, 136).toString("utf8")
).trim();
const size = parseInt(sizeStr, 8) || 0;
const blocks = Math.ceil(size / 512);
const fileData = data.subarray(offset, offset + size);
offset += blocks * 512;

if (name.endsWith(expectedSuffix)) {
fs.writeFileSync(destPath, fileData, { mode: 0o755 });
found = true;
break;
}
}

if (found) {
console.error(`archgate: binary downloaded successfully.`);
resolve(destPath);
} else {
reject(
new Error(`Could not find ${expectedSuffix} in tarball from ${url}`)
);
}
});

gunzip.on("error", reject);
});
}

try {
const binary = getBinaryPath();
execFileSync(binary, process.argv.slice(2), { stdio: "inherit" });
} catch (e) {
if (typeof e.status === "number") process.exit(e.status);
console.error(e.message);
process.exit(2);
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------

async function main() {
let binary = getBinaryPath();

// If the binary is missing (optional dep skipped AND postinstall blocked),
// download it on-demand from the npm registry.
if (!binary) {
try {
binary = await downloadBinary();
} catch (err) {
console.error(
`archgate: failed to download binary: ${err.message}\n` +
`Try reinstalling: npm install archgate`
);
process.exit(2);
}
}

try {
execFileSync(binary, process.argv.slice(2), { stdio: "inherit" });
} catch (e) {
if (typeof e.status === "number") process.exit(e.status);
console.error(e.message);
process.exit(2);
}
}

main();
4 changes: 4 additions & 0 deletions docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export default defineConfig({
"Enforce Architecture Decision Records as executable rules — for both humans and AI agents.",
customCss: ["./src/styles/custom.css"],
expressiveCode: {
// Use the pure-JS regex engine instead of the WASM-based oniguruma
// engine. Bun's WASM support on Cloudflare Pages triggers a
// "call_indirect to a null table entry" crash in oniguruma.
shiki: { engine: "javascript" },
// Disable Starlight's automatic UI color overrides so our
// hand-picked colors from archgate.dev take full effect.
useStarlightUiThemeColors: false,
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"archgate": "bin/archgate.cjs"
},
"files": [
"bin/archgate.cjs"
"bin/archgate.cjs",
"scripts/postinstall.cjs"
],
"os": [
"darwin",
Expand All @@ -47,6 +48,7 @@
"format": "oxfmt --write .",
"format:check": "oxfmt --check .",
"lint": "oxlint --deny-warnings .",
"postinstall": "node scripts/postinstall.cjs",
"test": "bun test --timeout 60000",
"test:watch": "bun test --watch --timeout 60000",
"typecheck": "tsc --build",
Expand Down
54 changes: 54 additions & 0 deletions scripts/postinstall.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env node
"use strict";

// Best-effort pre-download: tries to fetch the binary during install so the
// first `archgate` invocation is instant. If this script is blocked by the
// package manager (--ignore-scripts, etc.), the bin shim will download
// on-demand at runtime instead.

const path = require("path");
const fs = require("fs");

const PACKAGE_NAME_MAP = {
"darwin-arm64": "archgate-darwin-arm64",
"linux-x64": "archgate-linux-x64",
"win32-x64": "archgate-win32-x64",
};

function isBinaryPresent() {
const key = `${process.platform}-${process.arch}`;
const pkgName = PACKAGE_NAME_MAP[key];
if (!pkgName) return true; // unsupported platform — skip silently

const binaryName = process.platform === "win32" ? "archgate.exe" : "archgate";

// Check optional dependency
try {
const pkgDir = path.dirname(require.resolve(`${pkgName}/package.json`));
if (fs.existsSync(path.join(pkgDir, "bin", binaryName))) return true;
} catch {
/* not installed */
}

// Check local fallback
if (fs.existsSync(path.join(__dirname, "..", "bin", binaryName))) return true;

return false;
}

if (!isBinaryPresent()) {
// Delegate to the bin shim's download logic by running it with --version.
// This triggers the on-demand download without side effects.
try {
require("child_process").execFileSync(
process.execPath,
[path.join(__dirname, "..", "bin", "archgate.cjs"), "--version"],
{ stdio: "inherit" }
);
} catch {
// Postinstall must never block `npm install`.
console.warn(
"archgate: could not pre-download binary. It will be downloaded on first run."
);
}
}
Loading