@frumu/tandem 0.5.9 → 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/package.json +2 -2
- package/scripts/binary-installer.js +249 -0
- package/scripts/install.js +12 -241
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frumu/tandem",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "Tandem
|
|
3
|
+
"version": "0.5.11",
|
|
4
|
+
"description": "Tandem authority-layer runtime CLI and engine binary distribution",
|
|
5
5
|
"homepage": "https://tandem.ac",
|
|
6
6
|
"bin": {
|
|
7
7
|
"tandem": "bin/tandem.js",
|
|
@@ -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
|
+
};
|
package/scripts/install.js
CHANGED
|
@@ -1,253 +1,24 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
3
|
-
const https = require('https');
|
|
4
|
-
const { execFileSync, execSync } = require('child_process');
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const { installBinary } = require("./binary-installer");
|
|
5
3
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
// Platform mapping
|
|
11
|
-
const PLATFORM_MAP = {
|
|
12
|
-
'win32': { os: 'windows', ext: '.exe' },
|
|
13
|
-
'darwin': { os: 'darwin', ext: '' },
|
|
14
|
-
'linux': { os: 'linux', ext: '' }
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const ARCH_MAP = {
|
|
18
|
-
'x64': 'x64',
|
|
19
|
-
'arm64': 'arm64'
|
|
4
|
+
const config = {
|
|
5
|
+
packageRoot: path.join(__dirname, ".."),
|
|
6
|
+
binaryBaseName: "tandem-engine",
|
|
7
|
+
userAgent: "tandem-engine",
|
|
20
8
|
};
|
|
21
9
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const arch = ARCH_MAP[process.arch];
|
|
25
|
-
|
|
26
|
-
if (!platform || !arch) {
|
|
27
|
-
throw new Error(`Unsupported platform: ${process.platform}-${process.arch}`);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
let artifactName = `tandem-engine-${platform.os}-${arch}`;
|
|
31
|
-
// Handle specific artifact naming conventions (zip vs tar.gz)
|
|
32
|
-
if (platform.os === 'windows') {
|
|
33
|
-
artifactName += '.zip';
|
|
34
|
-
} else if (platform.os === 'darwin') {
|
|
35
|
-
artifactName += '.zip';
|
|
36
|
-
} else {
|
|
37
|
-
artifactName += '.tar.gz';
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return {
|
|
41
|
-
artifactName,
|
|
42
|
-
binaryName: `tandem-engine${platform.ext}`,
|
|
43
|
-
isWindows: platform.os === 'windows'
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const { artifactName, binaryName, isWindows } = getArtifactInfo();
|
|
48
|
-
const binDir = path.join(__dirname, '..', 'bin', 'native');
|
|
49
|
-
const destPath = path.join(binDir, binaryName);
|
|
50
|
-
|
|
51
|
-
function buildHeaders(userAgent) {
|
|
52
|
-
const headers = { 'User-Agent': userAgent };
|
|
53
|
-
const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
54
|
-
if (token) {
|
|
55
|
-
headers.Authorization = `Bearer ${token}`;
|
|
56
|
-
}
|
|
57
|
-
return headers;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function warnAndExit(err) {
|
|
10
|
+
if (require.main === module) {
|
|
11
|
+
installBinary(config).catch((err) => {
|
|
61
12
|
const detail = err && err.message ? err.message : String(err);
|
|
62
13
|
console.warn(`Warning: tandem-engine binary download skipped: ${detail}`);
|
|
63
14
|
console.warn(
|
|
64
|
-
|
|
15
|
+
"Install completed without a bundled binary. Runtime commands will require a later reinstall or a preinstalled tandem-engine binary."
|
|
65
16
|
);
|
|
66
17
|
process.exit(0);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (!fs.existsSync(binDir)) {
|
|
70
|
-
fs.mkdirSync(binDir, { recursive: true });
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function parseVersion(raw) {
|
|
74
|
-
const match = String(raw || '').match(/\b(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\b/);
|
|
75
|
-
return match ? match[1] : "";
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function installedBinaryVersion(binaryPath, execFile = execFileSync) {
|
|
79
|
-
if (!fs.existsSync(binaryPath)) return "";
|
|
80
|
-
try {
|
|
81
|
-
const output = execFile(binaryPath, ['--version'], {
|
|
82
|
-
encoding: 'utf8',
|
|
83
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
84
|
-
timeout: 5000,
|
|
85
|
-
});
|
|
86
|
-
return parseVersion(output);
|
|
87
|
-
} catch {
|
|
88
|
-
return "";
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function shouldDownloadBinary(binaryPath, packageVersion, readVersion = installedBinaryVersion) {
|
|
93
|
-
if (!fs.existsSync(binaryPath)) {
|
|
94
|
-
return { download: true, reason: "missing" };
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const stats = fs.statSync(binaryPath);
|
|
98
|
-
if (stats.size < MIN_SIZE) {
|
|
99
|
-
return { download: true, reason: `too small (${stats.size} bytes)` };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const installedVersion = readVersion(binaryPath);
|
|
103
|
-
if (!installedVersion) {
|
|
104
|
-
return { download: true, reason: "version check failed" };
|
|
105
|
-
}
|
|
106
|
-
if (installedVersion !== packageVersion) {
|
|
107
|
-
return {
|
|
108
|
-
download: true,
|
|
109
|
-
reason: `version mismatch (${installedVersion} != ${packageVersion})`,
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return { download: false, reason: `version ${installedVersion} already installed` };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Helper to fetch JSON from GitHub API
|
|
117
|
-
function fetchJson(url) {
|
|
118
|
-
return new Promise((resolve, reject) => {
|
|
119
|
-
https.get(url, { headers: buildHeaders('tandem-engine-installer') }, (res) => {
|
|
120
|
-
if (res.statusCode !== 200) {
|
|
121
|
-
if (res.statusCode === 302 || res.statusCode === 301) {
|
|
122
|
-
return fetchJson(res.headers.location).then(resolve).catch(reject);
|
|
123
|
-
}
|
|
124
|
-
return reject(new Error(`GitHub API HTTP ${res.statusCode}`));
|
|
125
|
-
}
|
|
126
|
-
let data = '';
|
|
127
|
-
res.on('data', chunk => data += chunk);
|
|
128
|
-
res.on('end', () => {
|
|
129
|
-
try { resolve(JSON.parse(data)); } catch (e) { reject(e); }
|
|
130
|
-
});
|
|
131
|
-
}).on('error', reject);
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Simplified download
|
|
136
|
-
async function download() {
|
|
137
|
-
console.log(`Checking releases for ${REPO}...`);
|
|
138
|
-
const releases = await fetchJson(`https://api.github.com/repos/${REPO}/releases`);
|
|
139
|
-
|
|
140
|
-
// Get the version from package.json
|
|
141
|
-
const packageVersion = require('../package.json').version;
|
|
142
|
-
const targetTag = `v${packageVersion}`;
|
|
143
|
-
|
|
144
|
-
console.log(`Filtering releases for ${REPO} (Target: ${targetTag})...`);
|
|
145
|
-
// const releases = await fetchJson... <--- REMOVED DUPLICATE
|
|
146
|
-
|
|
147
|
-
// 1. Try to find the exact release for this package version
|
|
148
|
-
let release = releases.find(r => r.tag_name === targetTag);
|
|
149
|
-
|
|
150
|
-
if (!release) {
|
|
151
|
-
console.warn(`Warning: No release found for tag ${targetTag}. Checking for latest compatible assets...`);
|
|
152
|
-
// 2. Fallback: Find LATEST release that contains our asset (useful for nightly/beta where tags might differ)
|
|
153
|
-
release = releases.find(r => r.assets.some(a => a.name === artifactName));
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (!release) {
|
|
157
|
-
// Fallback: Check prereleases explicitly if strict filtering was on (it wasn't here)
|
|
158
|
-
// If not found, maybe name changed?
|
|
159
|
-
console.error(`Status: No release found with asset ${artifactName}`);
|
|
160
|
-
console.error("Available assets in latest:", releases[0]?.assets?.map(a => a.name));
|
|
161
|
-
process.exit(1);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const asset = release.assets.find(a => a.name === artifactName);
|
|
165
|
-
console.log(`Downloading ${asset.name} from ${release.tag_name}...`);
|
|
166
|
-
|
|
167
|
-
const file = fs.createWriteStream(path.join(binDir, artifactName));
|
|
168
|
-
|
|
169
|
-
return new Promise((resolve, reject) => {
|
|
170
|
-
const downloadUrl = asset.browser_download_url;
|
|
171
|
-
|
|
172
|
-
const request = (url) => {
|
|
173
|
-
https.get(url, { headers: buildHeaders('tandem-installer') }, (res) => {
|
|
174
|
-
if (res.statusCode === 302 || res.statusCode === 301) {
|
|
175
|
-
return request(res.headers.location);
|
|
176
|
-
}
|
|
177
|
-
if (res.statusCode !== 200) return reject(new Error(`Download failed: HTTP ${res.statusCode}`));
|
|
178
|
-
res.pipe(file);
|
|
179
|
-
file.on('finish', () => {
|
|
180
|
-
file.close();
|
|
181
|
-
resolve(path.join(binDir, artifactName));
|
|
182
|
-
});
|
|
183
|
-
}).on('error', err => {
|
|
184
|
-
fs.unlink(path.join(binDir, artifactName), () => { }); // cleanup
|
|
185
|
-
reject(err);
|
|
186
|
-
});
|
|
187
|
-
};
|
|
188
|
-
request(downloadUrl);
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Extract
|
|
193
|
-
async function extract(archivePath) {
|
|
194
|
-
console.log("Extracting...");
|
|
195
|
-
if (isWindows) {
|
|
196
|
-
execSync(`powershell -Command "Expand-Archive -Path '${archivePath}' -DestinationPath '${binDir}' -Force"`);
|
|
197
|
-
} else {
|
|
198
|
-
if (artifactName.endsWith('.zip')) {
|
|
199
|
-
execSync(`unzip -o "${archivePath}" -d "${binDir}"`);
|
|
200
|
-
} else {
|
|
201
|
-
execSync(`tar -xzf "${archivePath}" -C "${binDir}"`);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Cleanup archive
|
|
206
|
-
fs.unlinkSync(archivePath);
|
|
207
|
-
|
|
208
|
-
// Locate binary (it might be inside a folder?)
|
|
209
|
-
// Our release workflow:
|
|
210
|
-
// Windows: dist/tandem-engine.exe -> zipped -> dist/tandem-engine.exe
|
|
211
|
-
// Linux: dist/tandem-engine -> tar -> dist/tandem-engine
|
|
212
|
-
// So on extraction, it might extract 'dist/tandem-engine' or just 'tandem-engine'.
|
|
213
|
-
// We should check.
|
|
214
|
-
|
|
215
|
-
// If extraction creates a folder (common behavior), we need to find it.
|
|
216
|
-
// Assuming root extraction for now based on GHA inspect.
|
|
217
|
-
// 'dist' folder? Yes. The GHA zips "dist/*". So it extracts "tandem-engine.exe" directly if zip didn't preserve root?
|
|
218
|
-
// "Compress-Archive -Path "dist/*" ... " -> This usually puts files at root of zip.
|
|
219
|
-
|
|
220
|
-
if (fs.existsSync(destPath)) {
|
|
221
|
-
console.log("Verified binary extracted.");
|
|
222
|
-
if (!isWindows) fs.chmodSync(destPath, 0o755);
|
|
223
|
-
} else {
|
|
224
|
-
console.error("Binary not found at expected path:", destPath);
|
|
225
|
-
// List files
|
|
226
|
-
console.log("Files in bin:", fs.readdirSync(binDir));
|
|
227
|
-
process.exit(1);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
async function main() {
|
|
232
|
-
const packageVersion = require('../package.json').version;
|
|
233
|
-
const decision = shouldDownloadBinary(destPath, packageVersion);
|
|
234
|
-
if (!decision.download) {
|
|
235
|
-
console.log(`Binary already present (${decision.reason}).`);
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
if (decision.reason !== "missing") {
|
|
239
|
-
console.log(`Existing binary will be replaced: ${decision.reason}.`);
|
|
240
|
-
}
|
|
241
|
-
const archivePath = await download();
|
|
242
|
-
await extract(archivePath);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (require.main === module) {
|
|
246
|
-
main().catch(warnAndExit);
|
|
18
|
+
});
|
|
247
19
|
}
|
|
248
20
|
|
|
249
21
|
module.exports = {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
shouldDownloadBinary,
|
|
22
|
+
config,
|
|
23
|
+
...require("./binary-installer"),
|
|
253
24
|
};
|