fitguard 0.1.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/bin/cli.js +31 -0
- package/package.json +26 -0
- package/scripts/platform.js +49 -0
- package/scripts/postinstall.js +113 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const { spawnSync } = require("child_process");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const { binPath } = require("../scripts/platform");
|
|
7
|
+
|
|
8
|
+
const bin = binPath();
|
|
9
|
+
|
|
10
|
+
if (!fs.existsSync(bin)) {
|
|
11
|
+
console.error(
|
|
12
|
+
`fitguard binary not found at ${bin}.\n` +
|
|
13
|
+
"The postinstall download may have failed. Try: npm install fitguard --force"
|
|
14
|
+
);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// stdio: "inherit" hands the real terminal (including the masked
|
|
19
|
+
// password prompts `fitguard init` uses) straight to the Go binary,
|
|
20
|
+
// rather than this wrapper trying to proxy stdin/stdout itself.
|
|
21
|
+
const result = spawnSync(bin, process.argv.slice(2), { stdio: "inherit" });
|
|
22
|
+
|
|
23
|
+
if (result.error) {
|
|
24
|
+
console.error(`fitguard: ${result.error.message}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// status is null when the child was killed by a signal (e.g. Ctrl+C)
|
|
29
|
+
// rather than exiting normally; process.exit() requires a number, so
|
|
30
|
+
// that case falls back to a generic failure code.
|
|
31
|
+
process.exit(result.status === null ? 1 : result.status);
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fitguard",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Self-hosted, OpenAI-compatible AI gateway that stops runaway LLM bills before they happen.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"homepage": "https://github.com/Oluiy/ai-cost-guard",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/Oluiy/ai-cost-guard.git"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"fitguard": "bin/cli.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"bin",
|
|
16
|
+
"scripts"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"postinstall": "node scripts/postinstall.js"
|
|
20
|
+
},
|
|
21
|
+
"os": ["darwin", "linux", "win32"],
|
|
22
|
+
"cpu": ["x64", "arm64"],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=14"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Shared between postinstall.js (downloads the binary) and bin/cli.js
|
|
3
|
+
// (runs it), so the two can never disagree on where it lives or what
|
|
4
|
+
// GoReleaser named it.
|
|
5
|
+
|
|
6
|
+
const path = require("path");
|
|
7
|
+
|
|
8
|
+
// Node's process.platform/arch values vs. GoOS/GoArch, which is what
|
|
9
|
+
// .goreleaser.yaml's archive name_template uses (fitguard_<os>_<arch>).
|
|
10
|
+
const OS_MAP = { darwin: "darwin", linux: "linux", win32: "windows" };
|
|
11
|
+
const ARCH_MAP = { x64: "amd64", arm64: "arm64" };
|
|
12
|
+
|
|
13
|
+
// Must match .goreleaser.yaml's `ignore:` list: every GOOS/GOARCH pair
|
|
14
|
+
// that isn't built has no release asset to download.
|
|
15
|
+
const UNSUPPORTED = new Set(["windows/arm64"]);
|
|
16
|
+
|
|
17
|
+
function resolvePlatform() {
|
|
18
|
+
const goos = OS_MAP[process.platform];
|
|
19
|
+
const goarch = ARCH_MAP[process.arch];
|
|
20
|
+
if (!goos || !goarch) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`fitguard has no prebuilt binary for ${process.platform}/${process.arch}. ` +
|
|
23
|
+
"Build from source instead: https://github.com/Oluiy/ai-cost-guard#installation"
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
if (UNSUPPORTED.has(`${goos}/${goarch}`)) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`fitguard does not build for ${goos}/${goarch}. ` +
|
|
29
|
+
"Build from source instead: https://github.com/Oluiy/ai-cost-guard#installation"
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return { goos, goarch };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function archiveName(goos, goarch) {
|
|
36
|
+
const ext = goos === "windows" ? "zip" : "tar.gz";
|
|
37
|
+
return `fitguard_${goos}_${goarch}.${ext}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function binDir() {
|
|
41
|
+
return path.join(__dirname, "..", "dist");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function binPath() {
|
|
45
|
+
const ext = process.platform === "win32" ? ".exe" : "";
|
|
46
|
+
return path.join(binDir(), `fitguard${ext}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { resolvePlatform, archiveName, binDir, binPath };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
// Downloads the fitguard binary matching this machine from GitHub
|
|
4
|
+
// Releases, verifies it against the published checksums (same check
|
|
5
|
+
// install.sh does), and extracts it into npm/dist/. No dependencies:
|
|
6
|
+
// https (built-in) for the download, and the system `tar` for
|
|
7
|
+
// extraction — present by default on macOS, Linux, and Windows 10+
|
|
8
|
+
// (bsdtar, which also opens .zip), so there's nothing extra to install
|
|
9
|
+
// just to install this.
|
|
10
|
+
|
|
11
|
+
const fs = require("fs");
|
|
12
|
+
const path = require("path");
|
|
13
|
+
const http = require("http");
|
|
14
|
+
const https = require("https");
|
|
15
|
+
const crypto = require("crypto");
|
|
16
|
+
const { execFileSync } = require("child_process");
|
|
17
|
+
|
|
18
|
+
const { resolvePlatform, archiveName, binDir, binPath } = require("./platform");
|
|
19
|
+
const pkg = require("../package.json");
|
|
20
|
+
|
|
21
|
+
const REPO = "Oluiy/ai-cost-guard";
|
|
22
|
+
// The npm package version and the git tag are kept in lockstep (1.2.3 <-> v1.2.3),
|
|
23
|
+
// so there's one version number to bump per release rather than two to keep in sync.
|
|
24
|
+
const VERSION = process.env.FITGUARD_VERSION || `v${pkg.version}`;
|
|
25
|
+
const BASE_URL =
|
|
26
|
+
process.env.FITGUARD_BASE_URL || `https://github.com/${REPO}/releases/download`;
|
|
27
|
+
|
|
28
|
+
function get(url, redirects) {
|
|
29
|
+
redirects = redirects || 0;
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
if (redirects > 5) return reject(new Error(`too many redirects fetching ${url}`));
|
|
32
|
+
// GitHub always serves https, but FITGUARD_BASE_URL is also the hook
|
|
33
|
+
// this postinstall step is tested against locally, so the transport
|
|
34
|
+
// follows the URL rather than being hardcoded to https.
|
|
35
|
+
const transport = url.startsWith("http://") ? http : https;
|
|
36
|
+
transport
|
|
37
|
+
.get(url, { headers: { "User-Agent": "fitguard-npm-installer" } }, (res) => {
|
|
38
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
39
|
+
res.resume();
|
|
40
|
+
return resolve(get(res.headers.location, redirects + 1));
|
|
41
|
+
}
|
|
42
|
+
if (res.statusCode !== 200) {
|
|
43
|
+
res.resume();
|
|
44
|
+
return reject(new Error(`GET ${url} -> HTTP ${res.statusCode}`));
|
|
45
|
+
}
|
|
46
|
+
const chunks = [];
|
|
47
|
+
res.on("data", (c) => chunks.push(c));
|
|
48
|
+
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
49
|
+
res.on("error", reject);
|
|
50
|
+
})
|
|
51
|
+
.on("error", reject);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function sha256(buf) {
|
|
56
|
+
return crypto.createHash("sha256").update(buf).digest("hex");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function main() {
|
|
60
|
+
const { goos, goarch } = resolvePlatform();
|
|
61
|
+
const archive = archiveName(goos, goarch);
|
|
62
|
+
const archiveUrl = `${BASE_URL}/${VERSION}/${archive}`;
|
|
63
|
+
const checksumsUrl = `${BASE_URL}/${VERSION}/checksums.txt`;
|
|
64
|
+
|
|
65
|
+
console.log(`fitguard: downloading ${archive} (${VERSION})...`);
|
|
66
|
+
const [archiveBuf, checksumsBuf] = await Promise.all([
|
|
67
|
+
get(archiveUrl),
|
|
68
|
+
get(checksumsUrl),
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
const line = checksumsBuf
|
|
72
|
+
.toString("utf8")
|
|
73
|
+
.split("\n")
|
|
74
|
+
.find((l) => l.trim().endsWith(archive));
|
|
75
|
+
if (!line) {
|
|
76
|
+
throw new Error(`no checksum entry for ${archive} in checksums.txt`);
|
|
77
|
+
}
|
|
78
|
+
const expected = line.trim().split(/\s+/)[0];
|
|
79
|
+
const actual = sha256(archiveBuf);
|
|
80
|
+
if (expected !== actual) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`checksum mismatch for ${archive}: expected ${expected}, got ${actual}`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const dist = binDir();
|
|
87
|
+
fs.mkdirSync(dist, { recursive: true });
|
|
88
|
+
const archivePath = path.join(dist, archive);
|
|
89
|
+
fs.writeFileSync(archivePath, archiveBuf);
|
|
90
|
+
|
|
91
|
+
console.log("fitguard: extracting...");
|
|
92
|
+
// bsdtar (macOS/BSD tar, and Windows 10 1803+'s tar.exe) opens .zip
|
|
93
|
+
// through the same `tar -xf`, so this one call covers every platform
|
|
94
|
+
// this package targets without a separate zip codepath.
|
|
95
|
+
execFileSync("tar", ["-xf", archivePath, "-C", dist, "fitguard" + (goos === "windows" ? ".exe" : "")], {
|
|
96
|
+
stdio: "inherit",
|
|
97
|
+
});
|
|
98
|
+
fs.unlinkSync(archivePath);
|
|
99
|
+
|
|
100
|
+
if (goos !== "windows") {
|
|
101
|
+
fs.chmodSync(binPath(), 0o755);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
console.log(`fitguard: installed to ${binPath()}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
main().catch((err) => {
|
|
108
|
+
console.error(`fitguard: install failed: ${err.message}`);
|
|
109
|
+
console.error(
|
|
110
|
+
"You can install manually instead: https://github.com/Oluiy/ai-cost-guard#installation"
|
|
111
|
+
);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
});
|