agent-notify 0.2.5 → 0.2.6
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/agent-notify.js +80 -40
- package/lib/constants.js +19 -0
- package/lib/download.js +46 -0
- package/lib/install.js +61 -0
- package/lib/platform.js +29 -0
- package/lib/release.js +21 -0
- package/lib/run.js +17 -0
- package/lib/version.js +21 -0
- package/package.json +11 -23
- package/test/install.test.js +130 -0
- package/test/launcher.test.js +151 -0
- package/test/platform.test.js +48 -0
- package/test/version.test.js +21 -0
- package/scripts/install.js +0 -291
package/bin/agent-notify.js
CHANGED
|
@@ -1,56 +1,96 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { spawnSync } = require('node:child_process');
|
|
5
|
+
const { getPlatformTarget } = require('../lib/platform');
|
|
6
|
+
const { extractSemver, compareVersions } = require('../lib/version');
|
|
7
|
+
const { getInstalledBinaryPath, TMP_DIR } = require('../lib/constants');
|
|
8
|
+
const { buildAssetName, buildDownloadUrl } = require('../lib/release');
|
|
9
|
+
const { downloadToFile } = require('../lib/download');
|
|
10
|
+
const { installFromArchive } = require('../lib/install');
|
|
11
|
+
const { runBinary } = require('../lib/run');
|
|
2
12
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const os = require('os');
|
|
7
|
-
const fs = require('fs');
|
|
8
|
-
|
|
9
|
-
function getBinaryDir() {
|
|
10
|
-
// Try to get npm global bin directory
|
|
11
|
-
// npm bin -g is deprecated, use npm prefix -g instead
|
|
12
|
-
try {
|
|
13
|
-
const prefix = execSync('npm prefix -g', { encoding: 'utf8' }).trim();
|
|
14
|
-
return path.join(prefix, 'bin');
|
|
15
|
-
} catch (e) {
|
|
16
|
-
// Fallback to ~/.local/bin
|
|
17
|
-
return path.join(os.homedir(), '.local', 'bin');
|
|
18
|
-
}
|
|
13
|
+
function getDesiredVersion() {
|
|
14
|
+
return require('../package.json').version;
|
|
19
15
|
}
|
|
20
16
|
|
|
21
|
-
|
|
17
|
+
function getInstalledVersion(binaryPath) {
|
|
18
|
+
const result = spawnSync(binaryPath, ['--version'], {
|
|
19
|
+
encoding: 'utf8',
|
|
20
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
if (result.error || result.status !== 0) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
return os.platform() === 'win32' ? 'agent-notify.exe' : 'agent-notify';
|
|
27
|
+
return extractSemver(result.stdout);
|
|
25
28
|
}
|
|
26
29
|
|
|
27
|
-
function
|
|
28
|
-
|
|
30
|
+
async function downloadAndInstall(version, target, binaryPath) {
|
|
31
|
+
fs.mkdirSync(TMP_DIR, { recursive: true });
|
|
32
|
+
|
|
33
|
+
const assetName = buildAssetName(version, target);
|
|
34
|
+
const archivePath = path.join(TMP_DIR, assetName);
|
|
35
|
+
const binaryNameInArchive = `agent-notify-v${version}-${target.goos}-${target.goarch}${target.ext}`;
|
|
36
|
+
|
|
37
|
+
await downloadToFile(buildDownloadUrl(version, assetName), archivePath);
|
|
38
|
+
return installFromArchive({
|
|
39
|
+
archivePath,
|
|
40
|
+
installDir: path.dirname(binaryPath),
|
|
41
|
+
binaryNameInArchive,
|
|
42
|
+
finalBinaryName: path.basename(binaryPath),
|
|
43
|
+
});
|
|
29
44
|
}
|
|
30
45
|
|
|
31
|
-
function
|
|
32
|
-
const
|
|
46
|
+
async function main(argv, deps = {}) {
|
|
47
|
+
const desiredVersion = (deps.getDesiredVersion || getDesiredVersion)();
|
|
48
|
+
const target = (deps.getPlatformTarget || getPlatformTarget)();
|
|
49
|
+
const binaryPath = (deps.getInstalledBinaryPath || getInstalledBinaryPath)(target);
|
|
50
|
+
const pathExists = deps.pathExists || fs.existsSync;
|
|
51
|
+
const getVersion = deps.getInstalledVersion || getInstalledVersion;
|
|
52
|
+
const compare = deps.compareVersions || compareVersions;
|
|
53
|
+
const install = deps.downloadAndInstall || ((version, releaseTarget) => downloadAndInstall(version, releaseTarget, binaryPath));
|
|
54
|
+
const run = deps.runBinary || runBinary;
|
|
55
|
+
const warn = deps.warn || ((message) => console.warn(message));
|
|
33
56
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
57
|
+
let installedVersion = null;
|
|
58
|
+
const hasInstalledBinary = pathExists(binaryPath);
|
|
59
|
+
if (hasInstalledBinary) {
|
|
60
|
+
installedVersion = getVersion(binaryPath);
|
|
38
61
|
}
|
|
62
|
+
const canFallbackToInstalledBinary = Boolean(installedVersion);
|
|
39
63
|
|
|
40
|
-
const
|
|
41
|
-
const child = spawn(binaryPath, args, {
|
|
42
|
-
stdio: 'inherit',
|
|
43
|
-
env: process.env
|
|
44
|
-
});
|
|
64
|
+
const needsInstall = !hasInstalledBinary || !installedVersion || compare(installedVersion, desiredVersion) < 0;
|
|
45
65
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
66
|
+
if (needsInstall) {
|
|
67
|
+
try {
|
|
68
|
+
await install(desiredVersion, target, binaryPath);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (!canFallbackToInstalledBinary) {
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
warn(`failed to update agent-notify: ${error.message}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
49
76
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
77
|
+
return run(binaryPath, argv);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (require.main === module) {
|
|
81
|
+
main(process.argv.slice(2))
|
|
82
|
+
.then((code) => {
|
|
83
|
+
process.exitCode = code;
|
|
84
|
+
})
|
|
85
|
+
.catch((error) => {
|
|
86
|
+
console.error(error.message);
|
|
87
|
+
process.exitCode = 1;
|
|
88
|
+
});
|
|
54
89
|
}
|
|
55
90
|
|
|
56
|
-
|
|
91
|
+
module.exports = {
|
|
92
|
+
main,
|
|
93
|
+
downloadAndInstall,
|
|
94
|
+
getDesiredVersion,
|
|
95
|
+
getInstalledVersion,
|
|
96
|
+
};
|
package/lib/constants.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const path = require('node:path');
|
|
2
|
+
const os = require('node:os');
|
|
3
|
+
|
|
4
|
+
const REPO_OWNER = 'hellolib';
|
|
5
|
+
const REPO_NAME = 'agent-notify';
|
|
6
|
+
const INSTALL_DIR = path.join(os.homedir(), '.agent-notify');
|
|
7
|
+
const TMP_DIR = path.join(INSTALL_DIR, 'tmp');
|
|
8
|
+
|
|
9
|
+
function getInstalledBinaryPath(target) {
|
|
10
|
+
return path.join(INSTALL_DIR, `agent-notify${target.ext}`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports = {
|
|
14
|
+
REPO_OWNER,
|
|
15
|
+
REPO_NAME,
|
|
16
|
+
INSTALL_DIR,
|
|
17
|
+
TMP_DIR,
|
|
18
|
+
getInstalledBinaryPath,
|
|
19
|
+
};
|
package/lib/download.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const https = require('node:https');
|
|
3
|
+
|
|
4
|
+
function removeFileIfExists(filePath, callback) {
|
|
5
|
+
fs.rm(filePath, { force: true }, () => callback());
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function downloadToFile(url, destinationPath, client = https) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const file = fs.createWriteStream(destinationPath);
|
|
11
|
+
|
|
12
|
+
const request = client.get(url, (response) => {
|
|
13
|
+
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
14
|
+
const redirectUrl = new URL(response.headers.location, url).toString();
|
|
15
|
+
file.close(() => {
|
|
16
|
+
removeFileIfExists(destinationPath, () => {
|
|
17
|
+
downloadToFile(redirectUrl, destinationPath, client).then(resolve, reject);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (response.statusCode !== 200) {
|
|
24
|
+
file.close(() => {
|
|
25
|
+
removeFileIfExists(destinationPath, () => {
|
|
26
|
+
reject(new Error(`download failed: ${response.statusCode} ${url}`));
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
response.pipe(file);
|
|
33
|
+
file.on('finish', () => file.close(() => resolve(destinationPath)));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
request.on('error', (err) => {
|
|
37
|
+
file.close(() => {
|
|
38
|
+
removeFileIfExists(destinationPath, () => reject(err));
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = {
|
|
45
|
+
downloadToFile,
|
|
46
|
+
};
|
package/lib/install.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const os = require('node:os');
|
|
4
|
+
const tar = require('tar');
|
|
5
|
+
|
|
6
|
+
function isUnsafeArchivePath(entryPath) {
|
|
7
|
+
return path.isAbsolute(entryPath) || entryPath.split('/').includes('..');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function installFromArchive({ archivePath, installDir, binaryNameInArchive, finalBinaryName }) {
|
|
11
|
+
fs.mkdirSync(installDir, { recursive: true });
|
|
12
|
+
|
|
13
|
+
const extractDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-notify-extract-'));
|
|
14
|
+
const finalPath = path.join(installDir, finalBinaryName);
|
|
15
|
+
const tempFinalPath = `${finalPath}.tmp`;
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const entries = [];
|
|
19
|
+
await tar.t({
|
|
20
|
+
file: archivePath,
|
|
21
|
+
gzip: true,
|
|
22
|
+
onReadEntry: (entry) => entries.push({ path: entry.path, type: entry.type }),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const binaryEntry = entries.find((entry) => entry.path === binaryNameInArchive);
|
|
26
|
+
if (!binaryEntry) {
|
|
27
|
+
throw new Error(`binary not found in archive: ${binaryNameInArchive}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (binaryEntry.type !== 'File' || entries.some((entry) => isUnsafeArchivePath(entry.path))) {
|
|
31
|
+
throw new Error('unsafe archive contents');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
await tar.x({
|
|
35
|
+
file: archivePath,
|
|
36
|
+
cwd: extractDir,
|
|
37
|
+
gzip: true,
|
|
38
|
+
filter: (entryPath) => entryPath === binaryNameInArchive,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const extractedPath = path.join(extractDir, binaryNameInArchive);
|
|
42
|
+
if (!fs.existsSync(extractedPath)) {
|
|
43
|
+
throw new Error(`binary not found in archive: ${binaryNameInArchive}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fs.copyFileSync(extractedPath, tempFinalPath);
|
|
47
|
+
if (process.platform !== 'win32') {
|
|
48
|
+
fs.chmodSync(tempFinalPath, 0o755);
|
|
49
|
+
}
|
|
50
|
+
fs.renameSync(tempFinalPath, finalPath);
|
|
51
|
+
|
|
52
|
+
return finalPath;
|
|
53
|
+
} finally {
|
|
54
|
+
fs.rmSync(tempFinalPath, { force: true });
|
|
55
|
+
fs.rmSync(extractDir, { recursive: true, force: true });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = {
|
|
60
|
+
installFromArchive,
|
|
61
|
+
};
|
package/lib/platform.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
function getPlatformTarget({ platform = process.platform, arch = process.arch } = {}) {
|
|
2
|
+
const goosMap = {
|
|
3
|
+
darwin: 'darwin',
|
|
4
|
+
linux: 'linux',
|
|
5
|
+
win32: 'windows',
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const goarchMap = {
|
|
9
|
+
x64: 'amd64',
|
|
10
|
+
arm64: 'arm64',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const goos = goosMap[platform];
|
|
14
|
+
const goarch = goarchMap[arch];
|
|
15
|
+
|
|
16
|
+
if (!goos || !goarch) {
|
|
17
|
+
throw new Error(`unsupported platform: ${platform}/${arch}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
goos,
|
|
22
|
+
goarch,
|
|
23
|
+
ext: goos === 'windows' ? '.exe' : '',
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
getPlatformTarget,
|
|
29
|
+
};
|
package/lib/release.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const { REPO_OWNER, REPO_NAME } = require('./constants');
|
|
2
|
+
|
|
3
|
+
function buildTag(version) {
|
|
4
|
+
return version.startsWith('v') ? version : `v${version}`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function buildAssetName(version, target) {
|
|
8
|
+
const tag = buildTag(version);
|
|
9
|
+
return `agent-notify-${tag}-${target.goos}-${target.goarch}.tar.gz`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function buildDownloadUrl(version, assetName) {
|
|
13
|
+
const tag = buildTag(version);
|
|
14
|
+
return `https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/download/${tag}/${assetName}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = {
|
|
18
|
+
buildTag,
|
|
19
|
+
buildAssetName,
|
|
20
|
+
buildDownloadUrl,
|
|
21
|
+
};
|
package/lib/run.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const { spawn } = require('node:child_process');
|
|
2
|
+
|
|
3
|
+
function runBinary(binaryPath, args) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const child = spawn(binaryPath, args, {
|
|
6
|
+
stdio: 'inherit',
|
|
7
|
+
env: { ...process.env },
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
child.on('error', reject);
|
|
11
|
+
child.on('close', (code) => resolve(code ?? 1));
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = {
|
|
16
|
+
runBinary,
|
|
17
|
+
};
|
package/lib/version.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
function extractSemver(output) {
|
|
2
|
+
const match = String(output).match(/v?(\d+\.\d+\.\d+)/);
|
|
3
|
+
return match ? match[1] : null;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function compareVersions(left, right) {
|
|
7
|
+
const l = left.split('.').map(Number);
|
|
8
|
+
const r = right.split('.').map(Number);
|
|
9
|
+
|
|
10
|
+
for (let i = 0; i < 3; i += 1) {
|
|
11
|
+
if (l[i] < r[i]) return -1;
|
|
12
|
+
if (l[i] > r[i]) return 1;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = {
|
|
19
|
+
extractSemver,
|
|
20
|
+
compareVersions,
|
|
21
|
+
};
|
package/package.json
CHANGED
|
@@ -1,35 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-notify",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"
|
|
5
|
-
"description": "Agent notification tool with Feishu integration",
|
|
3
|
+
"version": "0.2.6",
|
|
4
|
+
"description": "NPX launcher for the agent-notify binary",
|
|
6
5
|
"bin": {
|
|
7
6
|
"agent-notify": "./bin/agent-notify.js"
|
|
8
7
|
},
|
|
9
|
-
"
|
|
10
|
-
"postinstall": "node scripts/install.js"
|
|
11
|
-
},
|
|
8
|
+
"type": "commonjs",
|
|
12
9
|
"files": [
|
|
13
|
-
"bin
|
|
14
|
-
"
|
|
10
|
+
"bin",
|
|
11
|
+
"lib",
|
|
12
|
+
"test"
|
|
15
13
|
],
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"notification",
|
|
19
|
-
"feishu",
|
|
20
|
-
"agent"
|
|
21
|
-
],
|
|
22
|
-
"author": "hellolib",
|
|
23
|
-
"license": "MIT",
|
|
24
|
-
"repository": {
|
|
25
|
-
"type": "git",
|
|
26
|
-
"url": "git+https://github.com/hellolib/agent-notify.git"
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "node --test ./test/*.test.js"
|
|
27
16
|
},
|
|
28
|
-
"
|
|
29
|
-
"
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"tar": "^7.4.3"
|
|
30
19
|
},
|
|
31
|
-
"homepage": "https://github.com/hellolib/agent-notify#readme",
|
|
32
20
|
"engines": {
|
|
33
|
-
"node": ">=
|
|
21
|
+
"node": ">=18"
|
|
34
22
|
}
|
|
35
23
|
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
const test = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { PassThrough } = require('node:stream');
|
|
7
|
+
const tar = require('tar');
|
|
8
|
+
const { installFromArchive } = require('../lib/install');
|
|
9
|
+
const { downloadToFile } = require('../lib/download');
|
|
10
|
+
|
|
11
|
+
test('replaces installed binary with extracted binary', async (t) => {
|
|
12
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-notify-install-'));
|
|
13
|
+
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
14
|
+
const installDir = path.join(root, '.agent-notify');
|
|
15
|
+
const archivePath = path.join(root, 'agent-notify-v0.2.3-linux-amd64.tar.gz');
|
|
16
|
+
const extractedBinaryName = 'agent-notify-v0.2.3-linux-amd64';
|
|
17
|
+
|
|
18
|
+
fs.mkdirSync(path.join(root, 'src'));
|
|
19
|
+
fs.writeFileSync(path.join(root, 'src', extractedBinaryName), 'new-binary');
|
|
20
|
+
|
|
21
|
+
await tar.c(
|
|
22
|
+
{
|
|
23
|
+
gzip: true,
|
|
24
|
+
file: archivePath,
|
|
25
|
+
cwd: path.join(root, 'src'),
|
|
26
|
+
},
|
|
27
|
+
[extractedBinaryName],
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
fs.mkdirSync(installDir, { recursive: true });
|
|
31
|
+
fs.writeFileSync(path.join(installDir, 'agent-notify'), 'old-binary');
|
|
32
|
+
|
|
33
|
+
const installedPath = await installFromArchive({
|
|
34
|
+
archivePath,
|
|
35
|
+
installDir,
|
|
36
|
+
binaryNameInArchive: extractedBinaryName,
|
|
37
|
+
finalBinaryName: 'agent-notify',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
assert.equal(installedPath, path.join(installDir, 'agent-notify'));
|
|
41
|
+
assert.equal(fs.readFileSync(installedPath, 'utf8'), 'new-binary');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('throws when archive does not contain expected binary', async (t) => {
|
|
45
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-notify-install-'));
|
|
46
|
+
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
47
|
+
const installDir = path.join(root, '.agent-notify');
|
|
48
|
+
const archivePath = path.join(root, 'empty.tar.gz');
|
|
49
|
+
|
|
50
|
+
fs.mkdirSync(path.join(root, 'src'));
|
|
51
|
+
fs.writeFileSync(path.join(root, 'src', 'other-file'), 'nope');
|
|
52
|
+
|
|
53
|
+
await tar.c(
|
|
54
|
+
{
|
|
55
|
+
gzip: true,
|
|
56
|
+
file: archivePath,
|
|
57
|
+
cwd: path.join(root, 'src'),
|
|
58
|
+
},
|
|
59
|
+
['other-file'],
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
await assert.rejects(
|
|
63
|
+
installFromArchive({
|
|
64
|
+
archivePath,
|
|
65
|
+
installDir,
|
|
66
|
+
binaryNameInArchive: 'agent-notify-v0.2.3-linux-amd64',
|
|
67
|
+
finalBinaryName: 'agent-notify',
|
|
68
|
+
}),
|
|
69
|
+
/binary not found in archive/,
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('follows relative redirects when downloading', async (t) => {
|
|
74
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-notify-download-'));
|
|
75
|
+
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
76
|
+
const destinationPath = path.join(root, 'archive.tar.gz');
|
|
77
|
+
|
|
78
|
+
const client = {
|
|
79
|
+
get(url, handler) {
|
|
80
|
+
const request = new PassThrough();
|
|
81
|
+
process.nextTick(() => {
|
|
82
|
+
if (url === 'https://example.com/releases/archive.tar.gz') {
|
|
83
|
+
const response = new PassThrough();
|
|
84
|
+
response.statusCode = 302;
|
|
85
|
+
response.headers = { location: '/download/archive.tar.gz' };
|
|
86
|
+
handler(response);
|
|
87
|
+
response.end();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const response = new PassThrough();
|
|
92
|
+
response.statusCode = 200;
|
|
93
|
+
response.headers = {};
|
|
94
|
+
handler(response);
|
|
95
|
+
response.end('payload');
|
|
96
|
+
});
|
|
97
|
+
return request;
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const filePath = await downloadToFile('https://example.com/releases/archive.tar.gz', destinationPath, client);
|
|
102
|
+
assert.equal(filePath, destinationPath);
|
|
103
|
+
assert.equal(fs.readFileSync(destinationPath, 'utf8'), 'payload');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('removes destination file when download fails with non-200 response', async (t) => {
|
|
107
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-notify-download-'));
|
|
108
|
+
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
109
|
+
const destinationPath = path.join(root, 'archive.tar.gz');
|
|
110
|
+
|
|
111
|
+
const client = {
|
|
112
|
+
get(_url, handler) {
|
|
113
|
+
const request = new PassThrough();
|
|
114
|
+
process.nextTick(() => {
|
|
115
|
+
const response = new PassThrough();
|
|
116
|
+
response.statusCode = 404;
|
|
117
|
+
response.headers = {};
|
|
118
|
+
handler(response);
|
|
119
|
+
response.end();
|
|
120
|
+
});
|
|
121
|
+
return request;
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
await assert.rejects(
|
|
126
|
+
downloadToFile('https://example.com/releases/archive.tar.gz', destinationPath, client),
|
|
127
|
+
/download failed: 404/,
|
|
128
|
+
);
|
|
129
|
+
assert.equal(fs.existsSync(destinationPath), false);
|
|
130
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
const test = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { main } = require('../bin/agent-notify');
|
|
7
|
+
|
|
8
|
+
test('downloads when installed binary is missing', async (t) => {
|
|
9
|
+
const calls = [];
|
|
10
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-notify-launcher-'));
|
|
11
|
+
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
12
|
+
const binaryPath = path.join(root, 'agent-notify');
|
|
13
|
+
|
|
14
|
+
const exitCode = await main(['doctor'], {
|
|
15
|
+
getDesiredVersion: () => '0.2.3',
|
|
16
|
+
getPlatformTarget: () => ({ goos: 'linux', goarch: 'amd64', ext: '' }),
|
|
17
|
+
getInstalledBinaryPath: () => binaryPath,
|
|
18
|
+
getInstalledVersion: () => null,
|
|
19
|
+
downloadAndInstall: async () => {
|
|
20
|
+
calls.push('download');
|
|
21
|
+
fs.writeFileSync(binaryPath, 'binary');
|
|
22
|
+
return binaryPath;
|
|
23
|
+
},
|
|
24
|
+
runBinary: async (targetPath, args) => {
|
|
25
|
+
calls.push(['run', targetPath, args]);
|
|
26
|
+
return 0;
|
|
27
|
+
},
|
|
28
|
+
pathExists: (value) => fs.existsSync(value),
|
|
29
|
+
warn: () => {},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
assert.equal(exitCode, 0);
|
|
33
|
+
assert.deepEqual(calls, ['download', ['run', binaryPath, ['doctor']]]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('reuses installed binary when version is current', async () => {
|
|
37
|
+
const calls = [];
|
|
38
|
+
|
|
39
|
+
const exitCode = await main(['doctor'], {
|
|
40
|
+
getDesiredVersion: () => '0.2.3',
|
|
41
|
+
getPlatformTarget: () => ({ goos: 'linux', goarch: 'amd64', ext: '' }),
|
|
42
|
+
getInstalledBinaryPath: () => '/tmp/agent-notify',
|
|
43
|
+
getInstalledVersion: () => '0.2.3',
|
|
44
|
+
downloadAndInstall: async () => {
|
|
45
|
+
throw new Error('should not download');
|
|
46
|
+
},
|
|
47
|
+
runBinary: async (targetPath, args) => {
|
|
48
|
+
calls.push(['run', targetPath, args]);
|
|
49
|
+
return 0;
|
|
50
|
+
},
|
|
51
|
+
pathExists: () => true,
|
|
52
|
+
compareVersions: (left, right) => (left === right ? 0 : -1),
|
|
53
|
+
warn: () => {},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
assert.equal(exitCode, 0);
|
|
57
|
+
assert.deepEqual(calls, [['run', '/tmp/agent-notify', ['doctor']]]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('reuses installed binary when local version is newer', async () => {
|
|
61
|
+
const calls = [];
|
|
62
|
+
|
|
63
|
+
const exitCode = await main(['doctor'], {
|
|
64
|
+
getDesiredVersion: () => '0.2.3',
|
|
65
|
+
getPlatformTarget: () => ({ goos: 'linux', goarch: 'amd64', ext: '' }),
|
|
66
|
+
getInstalledBinaryPath: () => '/tmp/agent-notify',
|
|
67
|
+
getInstalledVersion: () => '0.2.4',
|
|
68
|
+
downloadAndInstall: async () => {
|
|
69
|
+
throw new Error('should not download');
|
|
70
|
+
},
|
|
71
|
+
runBinary: async (targetPath, args) => {
|
|
72
|
+
calls.push(['run', targetPath, args]);
|
|
73
|
+
return 0;
|
|
74
|
+
},
|
|
75
|
+
pathExists: () => true,
|
|
76
|
+
compareVersions: () => 1,
|
|
77
|
+
warn: () => {},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
assert.equal(exitCode, 0);
|
|
81
|
+
assert.deepEqual(calls, [['run', '/tmp/agent-notify', ['doctor']]]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('falls back to old binary when download fails but installed binary exists', async () => {
|
|
85
|
+
const warnings = [];
|
|
86
|
+
const calls = [];
|
|
87
|
+
|
|
88
|
+
const exitCode = await main(['doctor'], {
|
|
89
|
+
getDesiredVersion: () => '0.2.3',
|
|
90
|
+
getPlatformTarget: () => ({ goos: 'linux', goarch: 'amd64', ext: '' }),
|
|
91
|
+
getInstalledBinaryPath: () => '/tmp/agent-notify',
|
|
92
|
+
getInstalledVersion: () => '0.2.2',
|
|
93
|
+
compareVersions: () => -1,
|
|
94
|
+
downloadAndInstall: async () => {
|
|
95
|
+
throw new Error('network down');
|
|
96
|
+
},
|
|
97
|
+
runBinary: async (targetPath, args) => {
|
|
98
|
+
calls.push(['run', targetPath, args]);
|
|
99
|
+
return 0;
|
|
100
|
+
},
|
|
101
|
+
pathExists: () => true,
|
|
102
|
+
warn: (message) => warnings.push(message),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
assert.equal(exitCode, 0);
|
|
106
|
+
assert.equal(warnings.length, 1);
|
|
107
|
+
assert.match(warnings[0], /network down/);
|
|
108
|
+
assert.deepEqual(calls, [['run', '/tmp/agent-notify', ['doctor']]]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('fails when download fails and no installed binary exists', async () => {
|
|
112
|
+
await assert.rejects(
|
|
113
|
+
main(['doctor'], {
|
|
114
|
+
getDesiredVersion: () => '0.2.3',
|
|
115
|
+
getPlatformTarget: () => ({ goos: 'linux', goarch: 'amd64', ext: '' }),
|
|
116
|
+
getInstalledBinaryPath: () => '/tmp/agent-notify',
|
|
117
|
+
getInstalledVersion: () => null,
|
|
118
|
+
compareVersions: () => -1,
|
|
119
|
+
downloadAndInstall: async () => {
|
|
120
|
+
throw new Error('network down');
|
|
121
|
+
},
|
|
122
|
+
runBinary: async () => 0,
|
|
123
|
+
pathExists: () => false,
|
|
124
|
+
warn: () => {},
|
|
125
|
+
}),
|
|
126
|
+
/network down/,
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('fails when installed binary is unreadable and update also fails', async () => {
|
|
131
|
+
const warnings = [];
|
|
132
|
+
|
|
133
|
+
await assert.rejects(
|
|
134
|
+
main(['doctor'], {
|
|
135
|
+
getDesiredVersion: () => '0.2.3',
|
|
136
|
+
getPlatformTarget: () => ({ goos: 'linux', goarch: 'amd64', ext: '' }),
|
|
137
|
+
getInstalledBinaryPath: () => '/tmp/agent-notify',
|
|
138
|
+
getInstalledVersion: () => null,
|
|
139
|
+
compareVersions: () => -1,
|
|
140
|
+
downloadAndInstall: async () => {
|
|
141
|
+
throw new Error('network down');
|
|
142
|
+
},
|
|
143
|
+
runBinary: async () => 0,
|
|
144
|
+
pathExists: () => true,
|
|
145
|
+
warn: (message) => warnings.push(message),
|
|
146
|
+
}),
|
|
147
|
+
/network down/,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
assert.deepEqual(warnings, []);
|
|
151
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const test = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const { getPlatformTarget } = require('../lib/platform');
|
|
4
|
+
const { buildAssetName, buildDownloadUrl } = require('../lib/release');
|
|
5
|
+
|
|
6
|
+
test('maps darwin arm64 to release target', () => {
|
|
7
|
+
assert.deepEqual(getPlatformTarget({ platform: 'darwin', arch: 'arm64' }), {
|
|
8
|
+
goos: 'darwin',
|
|
9
|
+
goarch: 'arm64',
|
|
10
|
+
ext: '',
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('maps win32 x64 to release target', () => {
|
|
15
|
+
assert.deepEqual(getPlatformTarget({ platform: 'win32', arch: 'x64' }), {
|
|
16
|
+
goos: 'windows',
|
|
17
|
+
goarch: 'amd64',
|
|
18
|
+
ext: '.exe',
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('maps win32 arm64 to release target', () => {
|
|
23
|
+
assert.deepEqual(getPlatformTarget({ platform: 'win32', arch: 'arm64' }), {
|
|
24
|
+
goos: 'windows',
|
|
25
|
+
goarch: 'arm64',
|
|
26
|
+
ext: '.exe',
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('throws on unsupported platform', () => {
|
|
31
|
+
assert.throws(() => getPlatformTarget({ platform: 'freebsd', arch: 'x64' }), /unsupported platform/);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('throws on unsupported architecture', () => {
|
|
35
|
+
assert.throws(() => getPlatformTarget({ platform: 'linux', arch: 'ia32' }), /unsupported platform/);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('builds release asset names from version and target', () => {
|
|
39
|
+
const target = { goos: 'linux', goarch: 'arm64', ext: '' };
|
|
40
|
+
assert.equal(buildAssetName('0.2.3', target), 'agent-notify-v0.2.3-linux-arm64.tar.gz');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('builds download URL from version and asset name', () => {
|
|
44
|
+
assert.equal(
|
|
45
|
+
buildDownloadUrl('0.2.3', 'agent-notify-v0.2.3-linux-arm64.tar.gz'),
|
|
46
|
+
'https://github.com/hellolib/agent-notify/releases/download/v0.2.3/agent-notify-v0.2.3-linux-arm64.tar.gz',
|
|
47
|
+
);
|
|
48
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const test = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const { extractSemver, compareVersions } = require('../lib/version');
|
|
4
|
+
|
|
5
|
+
test('extracts plain semver', () => {
|
|
6
|
+
assert.equal(extractSemver('v0.2.3\n'), '0.2.3');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test('extracts semver from prefixed output', () => {
|
|
10
|
+
assert.equal(extractSemver('agent-notify version v1.4.0\n'), '1.4.0');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('returns null when output has no semver', () => {
|
|
14
|
+
assert.equal(extractSemver('development build\n'), null);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('compares versions correctly', () => {
|
|
18
|
+
assert.equal(compareVersions('0.2.3', '0.2.3'), 0);
|
|
19
|
+
assert.equal(compareVersions('0.2.2', '0.2.3'), -1);
|
|
20
|
+
assert.equal(compareVersions('0.3.0', '0.2.9'), 1);
|
|
21
|
+
});
|
package/scripts/install.js
DELETED
|
@@ -1,291 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const https = require('https');
|
|
4
|
-
const http = require('http');
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
const path = require('path');
|
|
7
|
-
const os = require('os');
|
|
8
|
-
const { execSync } = require('child_process');
|
|
9
|
-
|
|
10
|
-
const PACKAGE_VERSION = require('../package.json').agentnotifyversion;
|
|
11
|
-
const GITHUB_REPO = 'hellolib/agent-notify';
|
|
12
|
-
|
|
13
|
-
// Possible locations where agent-notify might be installed
|
|
14
|
-
function getCommonBinaryDirs() {
|
|
15
|
-
const dirs = [];
|
|
16
|
-
const binaryName = os.platform() === 'win32' ? 'agent-notify.exe' : 'agent-notify';
|
|
17
|
-
|
|
18
|
-
// GOPATH/bin
|
|
19
|
-
if (process.env.GOPATH) {
|
|
20
|
-
dirs.push(path.join(process.env.GOPATH, 'bin'));
|
|
21
|
-
}
|
|
22
|
-
// GOBIN
|
|
23
|
-
if (process.env.GOBIN) {
|
|
24
|
-
dirs.push(process.env.GOBIN);
|
|
25
|
-
}
|
|
26
|
-
// Default Go bin directory
|
|
27
|
-
dirs.push(path.join(os.homedir(), 'go', 'bin'));
|
|
28
|
-
// ~/.local/bin
|
|
29
|
-
dirs.push(path.join(os.homedir(), '.local', 'bin'));
|
|
30
|
-
// /usr/local/bin
|
|
31
|
-
dirs.push('/usr/local/bin');
|
|
32
|
-
|
|
33
|
-
return dirs;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function findExistingBinary(binaryName) {
|
|
37
|
-
const dirs = getCommonBinaryDirs();
|
|
38
|
-
for (const dir of dirs) {
|
|
39
|
-
const binaryPath = path.join(dir, binaryName);
|
|
40
|
-
if (fs.existsSync(binaryPath)) {
|
|
41
|
-
return binaryPath;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function getBinaryDir() {
|
|
48
|
-
// Try to get npm global bin directory
|
|
49
|
-
// npm bin -g is deprecated, use npm prefix -g instead
|
|
50
|
-
try {
|
|
51
|
-
const prefix = execSync('npm prefix -g', { encoding: 'utf8' }).trim();
|
|
52
|
-
// On Windows, npm prefix -g returns the bin directory directly
|
|
53
|
-
// On Unix, it returns the parent and bin is in {prefix}/bin
|
|
54
|
-
if (os.platform() === 'win32') {
|
|
55
|
-
return prefix;
|
|
56
|
-
}
|
|
57
|
-
return path.join(prefix, 'bin');
|
|
58
|
-
} catch (e) {
|
|
59
|
-
// Fallback to ~/.local/bin
|
|
60
|
-
return path.join(os.homedir(), '.local', 'bin');
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const BINARY_DIR = getBinaryDir();
|
|
65
|
-
|
|
66
|
-
function getInstalledVersion(binaryPath) {
|
|
67
|
-
try {
|
|
68
|
-
const version = execSync(`"${binaryPath}" --version`, { encoding: 'utf8', timeout: 5000 }).trim();
|
|
69
|
-
// version output like: agent-notify version 0.1.0
|
|
70
|
-
const match = version.match(/(\d+\.\d+\.\d+)/);
|
|
71
|
-
return match ? match[1] : null;
|
|
72
|
-
} catch (e) {
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function getRemoteArchiveName(version) {
|
|
78
|
-
const platform = os.platform();
|
|
79
|
-
const arch = os.arch();
|
|
80
|
-
|
|
81
|
-
const platformMap = {
|
|
82
|
-
darwin: 'darwin',
|
|
83
|
-
linux: 'linux',
|
|
84
|
-
win32: 'windows'
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const archMap = {
|
|
88
|
-
x64: 'amd64',
|
|
89
|
-
arm64: 'arm64'
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const mappedPlatform = platformMap[platform];
|
|
93
|
-
const mappedArch = archMap[arch];
|
|
94
|
-
|
|
95
|
-
if (!mappedPlatform || !mappedArch) {
|
|
96
|
-
throw new Error(`Unsupported platform: ${platform} ${arch}`);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return `agent-notify-v${version}-${mappedPlatform}-${mappedArch}.tar.gz`;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function getBinaryNameInArchive(version) {
|
|
103
|
-
const platform = os.platform();
|
|
104
|
-
const arch = os.arch();
|
|
105
|
-
|
|
106
|
-
const platformMap = {
|
|
107
|
-
darwin: 'darwin',
|
|
108
|
-
linux: 'linux',
|
|
109
|
-
win32: 'windows'
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const archMap = {
|
|
113
|
-
x64: 'amd64',
|
|
114
|
-
arm64: 'arm64'
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
const mappedPlatform = platformMap[platform];
|
|
118
|
-
const mappedArch = archMap[arch];
|
|
119
|
-
|
|
120
|
-
if (platform === 'win32') {
|
|
121
|
-
return `agent-notify-v${version}-${mappedPlatform}-${mappedArch}.exe`;
|
|
122
|
-
}
|
|
123
|
-
return `agent-notify-v${version}-${mappedPlatform}-${mappedArch}`;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function getLocalBinaryName() {
|
|
127
|
-
return os.platform() === 'win32' ? 'agent-notify.exe' : 'agent-notify';
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function getDownloadUrl(archiveName, version) {
|
|
131
|
-
return `https://github.com/${GITHUB_REPO}/releases/download/v${version}/${archiveName}`;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function extractTarGz(archivePath, destDir) {
|
|
135
|
-
// Use tar command (available on macOS, Linux, and Windows 10+)
|
|
136
|
-
execSync(`tar -xzf "${archivePath}" -C "${destDir}"`, { stdio: 'inherit' });
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
async function install() {
|
|
140
|
-
const archiveName = getRemoteArchiveName(PACKAGE_VERSION);
|
|
141
|
-
const binaryNameInArchive = getBinaryNameInArchive(PACKAGE_VERSION);
|
|
142
|
-
const localBinaryName = getLocalBinaryName();
|
|
143
|
-
const binaryPath = path.join(BINARY_DIR, localBinaryName);
|
|
144
|
-
|
|
145
|
-
// Check if binary already exists in npm bin directory with correct version
|
|
146
|
-
if (fs.existsSync(binaryPath)) {
|
|
147
|
-
const installedVersion = getInstalledVersion(binaryPath);
|
|
148
|
-
if (installedVersion === PACKAGE_VERSION) {
|
|
149
|
-
console.log(`Binary already exists at ${binaryPath} (v${PACKAGE_VERSION})`);
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
console.log(`Updating binary from v${installedVersion || 'unknown'} to v${PACKAGE_VERSION}...`);
|
|
153
|
-
} else {
|
|
154
|
-
// Check if binary exists in other common directories
|
|
155
|
-
const existingBinary = findExistingBinary(localBinaryName);
|
|
156
|
-
if (existingBinary) {
|
|
157
|
-
const existingVersion = getInstalledVersion(existingBinary);
|
|
158
|
-
if (existingVersion === PACKAGE_VERSION) {
|
|
159
|
-
// Same version, copy/link to npm bin directory
|
|
160
|
-
console.log(`Found existing binary at ${existingBinary} (v${PACKAGE_VERSION})`);
|
|
161
|
-
console.log(`Copying to npm bin directory: ${binaryPath}`);
|
|
162
|
-
|
|
163
|
-
if (!fs.existsSync(BINARY_DIR)) {
|
|
164
|
-
fs.mkdirSync(BINARY_DIR, { recursive: true });
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
fs.copyFileSync(existingBinary, binaryPath);
|
|
168
|
-
if (os.platform() !== 'win32') {
|
|
169
|
-
fs.chmodSync(binaryPath, 0o755);
|
|
170
|
-
}
|
|
171
|
-
console.log('Done!');
|
|
172
|
-
return;
|
|
173
|
-
} else if (existingVersion) {
|
|
174
|
-
console.log(`Found existing binary at ${existingBinary} (v${existingVersion}), but need v${PACKAGE_VERSION}`);
|
|
175
|
-
console.log('Installing new version to npm bin directory...');
|
|
176
|
-
} else {
|
|
177
|
-
console.log(`Found existing binary at ${existingBinary}, but version unknown`);
|
|
178
|
-
console.log('Installing to npm bin directory...');
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Create binary directory
|
|
184
|
-
if (!fs.existsSync(BINARY_DIR)) {
|
|
185
|
-
fs.mkdirSync(BINARY_DIR, { recursive: true });
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const url = getDownloadUrl(archiveName, PACKAGE_VERSION);
|
|
189
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-notify-'));
|
|
190
|
-
const archivePath = path.join(tempDir, archiveName);
|
|
191
|
-
|
|
192
|
-
try {
|
|
193
|
-
// Download archive
|
|
194
|
-
await downloadFile(url, archivePath);
|
|
195
|
-
|
|
196
|
-
// Extract archive
|
|
197
|
-
console.log('Extracting...');
|
|
198
|
-
extractTarGz(archivePath, tempDir);
|
|
199
|
-
|
|
200
|
-
// Move binary to destination (use copy + unlink for cross-device support)
|
|
201
|
-
const extractedBinaryPath = path.join(tempDir, binaryNameInArchive);
|
|
202
|
-
if (fs.existsSync(binaryPath)) {
|
|
203
|
-
fs.unlinkSync(binaryPath);
|
|
204
|
-
}
|
|
205
|
-
fs.copyFileSync(extractedBinaryPath, binaryPath);
|
|
206
|
-
fs.unlinkSync(extractedBinaryPath);
|
|
207
|
-
|
|
208
|
-
// Make binary executable (Unix)
|
|
209
|
-
if (os.platform() !== 'win32') {
|
|
210
|
-
fs.chmodSync(binaryPath, 0o755);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
console.log(`Successfully installed binary to ${binaryPath}`);
|
|
214
|
-
console.log(`Ensure ${BINARY_DIR} is in your PATH.`);
|
|
215
|
-
} catch (err) {
|
|
216
|
-
console.error(`Failed to download binary: ${err.message}`);
|
|
217
|
-
console.error('');
|
|
218
|
-
console.error('Please ensure the release exists at:');
|
|
219
|
-
console.error(` ${url}`);
|
|
220
|
-
|
|
221
|
-
// Don't fail the install, let user download manually
|
|
222
|
-
process.exit(0);
|
|
223
|
-
} finally {
|
|
224
|
-
// Cleanup temp directory
|
|
225
|
-
try {
|
|
226
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
227
|
-
} catch (e) {
|
|
228
|
-
// Ignore cleanup errors
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function downloadFile(url, dest) {
|
|
234
|
-
return new Promise((resolve, reject) => {
|
|
235
|
-
const protocol = url.startsWith('https') ? https : http;
|
|
236
|
-
|
|
237
|
-
console.log(`Downloading: ${url}`);
|
|
238
|
-
|
|
239
|
-
const request = protocol.get(url, (response) => {
|
|
240
|
-
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
241
|
-
downloadFile(response.headers.location, dest).then(resolve).catch(reject);
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (response.statusCode !== 200) {
|
|
246
|
-
reject(new Error(`Failed to download: HTTP ${response.statusCode}`));
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const totalSize = parseInt(response.headers['content-length'], 10);
|
|
251
|
-
let downloadedSize = 0;
|
|
252
|
-
let lastPercent = 0;
|
|
253
|
-
|
|
254
|
-
const file = fs.createWriteStream(dest);
|
|
255
|
-
|
|
256
|
-
response.on('data', (chunk) => {
|
|
257
|
-
downloadedSize += chunk.length;
|
|
258
|
-
if (totalSize) {
|
|
259
|
-
const percent = Math.floor((downloadedSize / totalSize) * 100);
|
|
260
|
-
if (percent >= lastPercent + 10) {
|
|
261
|
-
process.stdout.write(`\rDownloading: ${percent}%`);
|
|
262
|
-
lastPercent = percent;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
response.pipe(file);
|
|
268
|
-
|
|
269
|
-
file.on('finish', () => {
|
|
270
|
-
file.close();
|
|
271
|
-
process.stdout.write('\n');
|
|
272
|
-
resolve();
|
|
273
|
-
});
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
request.on('error', (err) => {
|
|
277
|
-
fs.unlink(dest, () => {});
|
|
278
|
-
reject(err);
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
request.setTimeout(120000, () => {
|
|
282
|
-
request.destroy();
|
|
283
|
-
reject(new Error('Download timeout'));
|
|
284
|
-
});
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
install().catch((err) => {
|
|
289
|
-
console.error(err.message);
|
|
290
|
-
process.exit(1);
|
|
291
|
-
});
|