@frumu/tandem-enterprise 0.5.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 +5 -0
- package/bin/tandem-engine.js +100 -0
- package/package.json +31 -0
- package/scripts/binary-installer.js +249 -0
- package/scripts/install.js +25 -0
package/README.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# @frumu/tandem-enterprise
|
|
2
|
+
|
|
3
|
+
Hosted enterprise Tandem engine binary distribution for Linux x64.
|
|
4
|
+
|
|
5
|
+
This package installs a `tandem-engine` command backed by the `tandem-engine-enterprise-linux-x64.tar.gz` GitHub release asset. It is intended for hosted `tandem-agents` deployments that need enterprise routes compiled into the engine.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const https = require("https");
|
|
5
|
+
const { spawnSync } = require("child_process");
|
|
6
|
+
|
|
7
|
+
const binaryPath = path.join(__dirname, "native", "tandem-engine");
|
|
8
|
+
const packageInfo = require("../package.json");
|
|
9
|
+
const UPDATE_CHECK_TIMEOUT_MS = 1200;
|
|
10
|
+
|
|
11
|
+
function parseVersion(version) {
|
|
12
|
+
const core = String(version || "").split("-")[0];
|
|
13
|
+
return core.split(".").map((part) => {
|
|
14
|
+
const value = Number.parseInt(part, 10);
|
|
15
|
+
return Number.isFinite(value) ? value : 0;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isNewerVersion(latest, current) {
|
|
20
|
+
const a = parseVersion(latest);
|
|
21
|
+
const b = parseVersion(current);
|
|
22
|
+
const length = Math.max(a.length, b.length);
|
|
23
|
+
|
|
24
|
+
for (let i = 0; i < length; i += 1) {
|
|
25
|
+
const left = a[i] || 0;
|
|
26
|
+
const right = b[i] || 0;
|
|
27
|
+
if (left > right) return true;
|
|
28
|
+
if (left < right) return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function fetchLatestVersion(packageName, timeoutMs) {
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
const encodedName = encodeURIComponent(packageName);
|
|
37
|
+
const url = `https://registry.npmjs.org/${encodedName}/latest`;
|
|
38
|
+
const request = https.get(url, { headers: { Accept: "application/json" } }, (response) => {
|
|
39
|
+
if (response.statusCode !== 200) {
|
|
40
|
+
response.resume();
|
|
41
|
+
resolve(null);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let body = "";
|
|
46
|
+
response.setEncoding("utf8");
|
|
47
|
+
response.on("data", (chunk) => {
|
|
48
|
+
body += chunk;
|
|
49
|
+
});
|
|
50
|
+
response.on("end", () => {
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(body);
|
|
53
|
+
resolve(typeof parsed.version === "string" ? parsed.version : null);
|
|
54
|
+
} catch {
|
|
55
|
+
resolve(null);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
request.on("error", () => resolve(null));
|
|
61
|
+
request.setTimeout(timeoutMs, () => {
|
|
62
|
+
request.destroy();
|
|
63
|
+
resolve(null);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function notifyIfUpdateAvailable() {
|
|
69
|
+
const latestVersion = await fetchLatestVersion(packageInfo.name, UPDATE_CHECK_TIMEOUT_MS);
|
|
70
|
+
if (!latestVersion || !isNewerVersion(latestVersion, packageInfo.version)) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.error(`[${packageInfo.name}] Update available: ${packageInfo.version} -> ${latestVersion}`);
|
|
75
|
+
console.error(`Run: npm i -g ${packageInfo.name}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function main() {
|
|
79
|
+
await notifyIfUpdateAvailable();
|
|
80
|
+
|
|
81
|
+
const child = spawnSync(binaryPath, process.argv.slice(2), { stdio: "inherit" });
|
|
82
|
+
|
|
83
|
+
if (child.error) {
|
|
84
|
+
console.error("tandem-engine binary is missing. Reinstall with: npm i -g @frumu/tandem-enterprise");
|
|
85
|
+
console.error(child.error.message);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
process.exit(child.status ?? 1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
main().catch(() => {
|
|
93
|
+
const child = spawnSync(binaryPath, process.argv.slice(2), { stdio: "inherit" });
|
|
94
|
+
if (child.error) {
|
|
95
|
+
console.error("tandem-engine binary is missing. Reinstall with: npm i -g @frumu/tandem-enterprise");
|
|
96
|
+
console.error(child.error.message);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
process.exit(child.status ?? 1);
|
|
100
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@frumu/tandem-enterprise",
|
|
3
|
+
"version": "0.5.11",
|
|
4
|
+
"description": "Hosted enterprise Tandem engine binary distribution for Linux x64",
|
|
5
|
+
"homepage": "https://tandem.ac",
|
|
6
|
+
"bin": {
|
|
7
|
+
"tandem-engine": "bin/tandem-engine.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"postinstall": "node scripts/install.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin",
|
|
14
|
+
"scripts"
|
|
15
|
+
],
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/frumu-ai/tandem.git"
|
|
19
|
+
},
|
|
20
|
+
"author": "Frumu Ltd",
|
|
21
|
+
"license": "MIT OR Apache-2.0",
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"os": [
|
|
26
|
+
"linux"
|
|
27
|
+
],
|
|
28
|
+
"cpu": [
|
|
29
|
+
"x64"
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const https = require("https");
|
|
4
|
+
const { execFileSync, execSync } = require("child_process");
|
|
5
|
+
|
|
6
|
+
const DEFAULT_REPO = "frumu-ai/tandem";
|
|
7
|
+
const DEFAULT_MIN_SIZE = 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
const PLATFORM_MAP = {
|
|
10
|
+
win32: { os: "windows", ext: ".exe", archive: "zip" },
|
|
11
|
+
darwin: { os: "darwin", ext: "", archive: "zip" },
|
|
12
|
+
linux: { os: "linux", ext: "", archive: "tar.gz" },
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const ARCH_MAP = {
|
|
16
|
+
x64: "x64",
|
|
17
|
+
arm64: "arm64",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function resolveArtifactInfo(config = {}, runtime = process) {
|
|
21
|
+
const platform = PLATFORM_MAP[runtime.platform];
|
|
22
|
+
const arch = ARCH_MAP[runtime.arch];
|
|
23
|
+
|
|
24
|
+
if (!platform || !arch) {
|
|
25
|
+
throw new Error(`Unsupported platform: ${runtime.platform}-${runtime.arch}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (config.supported) {
|
|
29
|
+
const supported = config.supported.some((entry) => {
|
|
30
|
+
const platformMatches = !entry.platform || entry.platform === runtime.platform;
|
|
31
|
+
const archMatches = !entry.arch || entry.arch === runtime.arch;
|
|
32
|
+
return platformMatches && archMatches;
|
|
33
|
+
});
|
|
34
|
+
if (!supported) {
|
|
35
|
+
throw new Error(`Unsupported platform for ${config.packageName || "this package"}: ${runtime.platform}-${runtime.arch}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const binaryBaseName = config.binaryBaseName || config.assetBaseName || "tandem-engine";
|
|
40
|
+
const binaryName = `${binaryBaseName}${platform.ext}`;
|
|
41
|
+
const archive = config.archive || platform.archive;
|
|
42
|
+
const assetPrefix = config.assetPrefix || `${binaryBaseName}-${platform.os}-${arch}`;
|
|
43
|
+
const artifactName = archive === "zip" ? `${assetPrefix}.zip` : `${assetPrefix}.tar.gz`;
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
artifactName,
|
|
47
|
+
binaryName,
|
|
48
|
+
isWindows: platform.os === "windows",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildHeaders(userAgent) {
|
|
53
|
+
const headers = { "User-Agent": userAgent };
|
|
54
|
+
const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
55
|
+
if (token) {
|
|
56
|
+
headers.Authorization = `Bearer ${token}`;
|
|
57
|
+
}
|
|
58
|
+
return headers;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseVersion(raw) {
|
|
62
|
+
const match = String(raw || "").match(/\b(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\b/);
|
|
63
|
+
return match ? match[1] : "";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function installedBinaryVersion(binaryPath, execFile = execFileSync) {
|
|
67
|
+
if (!fs.existsSync(binaryPath)) return "";
|
|
68
|
+
try {
|
|
69
|
+
const output = execFile(binaryPath, ["--version"], {
|
|
70
|
+
encoding: "utf8",
|
|
71
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
72
|
+
timeout: 5000,
|
|
73
|
+
});
|
|
74
|
+
return parseVersion(output);
|
|
75
|
+
} catch {
|
|
76
|
+
return "";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function shouldDownloadBinary(binaryPath, packageVersion, readVersion = installedBinaryVersion, minSize = DEFAULT_MIN_SIZE) {
|
|
81
|
+
if (!fs.existsSync(binaryPath)) {
|
|
82
|
+
return { download: true, reason: "missing" };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const stats = fs.statSync(binaryPath);
|
|
86
|
+
if (stats.size < minSize) {
|
|
87
|
+
return { download: true, reason: `too small (${stats.size} bytes)` };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const installedVersion = readVersion(binaryPath);
|
|
91
|
+
if (!installedVersion) {
|
|
92
|
+
return { download: true, reason: "version check failed" };
|
|
93
|
+
}
|
|
94
|
+
if (installedVersion !== packageVersion) {
|
|
95
|
+
return {
|
|
96
|
+
download: true,
|
|
97
|
+
reason: `version mismatch (${installedVersion} != ${packageVersion})`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { download: false, reason: `version ${installedVersion} already installed` };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function fetchJson(url, userAgent) {
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
https
|
|
107
|
+
.get(url, { headers: buildHeaders(userAgent) }, (res) => {
|
|
108
|
+
if (res.statusCode !== 200) {
|
|
109
|
+
if (res.statusCode === 302 || res.statusCode === 301) {
|
|
110
|
+
return fetchJson(res.headers.location, userAgent).then(resolve).catch(reject);
|
|
111
|
+
}
|
|
112
|
+
return reject(new Error(`GitHub API HTTP ${res.statusCode}`));
|
|
113
|
+
}
|
|
114
|
+
let data = "";
|
|
115
|
+
res.on("data", (chunk) => (data += chunk));
|
|
116
|
+
res.on("end", () => {
|
|
117
|
+
try {
|
|
118
|
+
resolve(JSON.parse(data));
|
|
119
|
+
} catch (e) {
|
|
120
|
+
reject(e);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
})
|
|
124
|
+
.on("error", reject);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function downloadReleaseAsset({ repo, artifactName, packageVersion, binDir, userAgent }) {
|
|
129
|
+
console.log(`Checking releases for ${repo}...`);
|
|
130
|
+
const releases = await fetchJson(`https://api.github.com/repos/${repo}/releases`, `${userAgent}-installer`);
|
|
131
|
+
const targetTag = `v${packageVersion}`;
|
|
132
|
+
|
|
133
|
+
console.log(`Filtering releases for ${repo} (Target: ${targetTag})...`);
|
|
134
|
+
let release = releases.find((r) => r.tag_name === targetTag);
|
|
135
|
+
|
|
136
|
+
if (!release) {
|
|
137
|
+
console.warn(`Warning: No release found for tag ${targetTag}. Checking for latest compatible assets...`);
|
|
138
|
+
release = releases.find((r) => r.assets.some((a) => a.name === artifactName));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!release) {
|
|
142
|
+
console.error(`Status: No release found with asset ${artifactName}`);
|
|
143
|
+
console.error("Available assets in latest:", releases[0]?.assets?.map((a) => a.name));
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const asset = release.assets.find((a) => a.name === artifactName);
|
|
148
|
+
if (!asset) {
|
|
149
|
+
throw new Error(`Release ${release.tag_name} does not contain ${artifactName}`);
|
|
150
|
+
}
|
|
151
|
+
console.log(`Downloading ${asset.name} from ${release.tag_name}...`);
|
|
152
|
+
|
|
153
|
+
const archivePath = path.join(binDir, artifactName);
|
|
154
|
+
const file = fs.createWriteStream(archivePath);
|
|
155
|
+
|
|
156
|
+
return new Promise((resolve, reject) => {
|
|
157
|
+
const request = (url) => {
|
|
158
|
+
https
|
|
159
|
+
.get(url, { headers: buildHeaders(userAgent) }, (res) => {
|
|
160
|
+
if (res.statusCode === 302 || res.statusCode === 301) {
|
|
161
|
+
return request(res.headers.location);
|
|
162
|
+
}
|
|
163
|
+
if (res.statusCode !== 200) return reject(new Error(`Download failed: HTTP ${res.statusCode}`));
|
|
164
|
+
res.pipe(file);
|
|
165
|
+
file.on("finish", () => {
|
|
166
|
+
file.close();
|
|
167
|
+
resolve(archivePath);
|
|
168
|
+
});
|
|
169
|
+
})
|
|
170
|
+
.on("error", (err) => {
|
|
171
|
+
fs.unlink(archivePath, () => {});
|
|
172
|
+
reject(err);
|
|
173
|
+
});
|
|
174
|
+
};
|
|
175
|
+
request(asset.browser_download_url);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function extractArchive({ archivePath, artifactName, binDir, destPath, isWindows }) {
|
|
180
|
+
console.log("Extracting...");
|
|
181
|
+
if (isWindows) {
|
|
182
|
+
execSync(`powershell -Command "Expand-Archive -Path '${archivePath}' -DestinationPath '${binDir}' -Force"`);
|
|
183
|
+
} else if (artifactName.endsWith(".zip")) {
|
|
184
|
+
execSync(`unzip -o "${archivePath}" -d "${binDir}"`);
|
|
185
|
+
} else {
|
|
186
|
+
execSync(`tar -xzf "${archivePath}" -C "${binDir}"`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
fs.unlinkSync(archivePath);
|
|
190
|
+
|
|
191
|
+
if (fs.existsSync(destPath)) {
|
|
192
|
+
console.log("Verified binary extracted.");
|
|
193
|
+
if (!isWindows) fs.chmodSync(destPath, 0o755);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
console.error("Binary not found at expected path:", destPath);
|
|
198
|
+
console.log("Files in bin:", fs.readdirSync(binDir));
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function warnAndExit(binaryBaseName, err) {
|
|
203
|
+
const detail = err && err.message ? err.message : String(err);
|
|
204
|
+
console.warn(`Warning: ${binaryBaseName} binary download skipped: ${detail}`);
|
|
205
|
+
console.warn(
|
|
206
|
+
`Install completed without a bundled binary. Runtime commands will require a later reinstall or a preinstalled ${binaryBaseName} binary.`
|
|
207
|
+
);
|
|
208
|
+
process.exit(0);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function installBinary(config = {}) {
|
|
212
|
+
const packageInfo = require(path.join(config.packageRoot, "package.json"));
|
|
213
|
+
const repo = config.repo || DEFAULT_REPO;
|
|
214
|
+
const minSize = config.minSize || DEFAULT_MIN_SIZE;
|
|
215
|
+
const binaryBaseName = config.binaryBaseName || "tandem-engine";
|
|
216
|
+
const { artifactName, binaryName, isWindows } = resolveArtifactInfo(config);
|
|
217
|
+
const binDir = path.join(config.packageRoot, "bin", "native");
|
|
218
|
+
const destPath = path.join(binDir, binaryName);
|
|
219
|
+
|
|
220
|
+
if (!fs.existsSync(binDir)) {
|
|
221
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const decision = shouldDownloadBinary(destPath, packageInfo.version, installedBinaryVersion, minSize);
|
|
225
|
+
if (!decision.download) {
|
|
226
|
+
console.log(`Binary already present (${decision.reason}).`);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (decision.reason !== "missing") {
|
|
230
|
+
console.log(`Existing binary will be replaced: ${decision.reason}.`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const archivePath = await downloadReleaseAsset({
|
|
234
|
+
repo,
|
|
235
|
+
artifactName,
|
|
236
|
+
packageVersion: packageInfo.version,
|
|
237
|
+
binDir,
|
|
238
|
+
userAgent: config.userAgent || binaryBaseName,
|
|
239
|
+
});
|
|
240
|
+
await extractArchive({ archivePath, artifactName, binDir, destPath, isWindows });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
module.exports = {
|
|
244
|
+
installBinary,
|
|
245
|
+
installedBinaryVersion,
|
|
246
|
+
parseVersion,
|
|
247
|
+
resolveArtifactInfo,
|
|
248
|
+
shouldDownloadBinary,
|
|
249
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const { installBinary } = require("./binary-installer");
|
|
3
|
+
|
|
4
|
+
const config = {
|
|
5
|
+
packageRoot: path.join(__dirname, ".."),
|
|
6
|
+
packageName: "@frumu/tandem-enterprise",
|
|
7
|
+
binaryBaseName: "tandem-engine",
|
|
8
|
+
assetPrefix: "tandem-engine-enterprise-linux-x64",
|
|
9
|
+
archive: "tar.gz",
|
|
10
|
+
userAgent: "tandem-engine-enterprise",
|
|
11
|
+
supported: [{ platform: "linux", arch: "x64" }],
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
if (require.main === module) {
|
|
15
|
+
installBinary(config).catch((err) => {
|
|
16
|
+
const detail = err && err.message ? err.message : String(err);
|
|
17
|
+
console.error(`Error: @frumu/tandem-enterprise could not install tandem-engine: ${detail}`);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = {
|
|
23
|
+
config,
|
|
24
|
+
...require("./binary-installer"),
|
|
25
|
+
};
|