@etherscan-npm/cli 1.0.3 → 1.0.4
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 +14 -3
- package/npm/bin/etherscan.js +32 -9
- package/npm/platform.js +16 -0
- package/package.json +10 -6
- package/npm/postinstall.js +0 -58
- package/npm/prepublish-check.js +0 -81
- package/scripts/install.ps1 +0 -362
- package/scripts/install.sh +0 -340
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ Or run it once without keeping a global installation:
|
|
|
52
52
|
npx @etherscan-npm/cli version
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
The npm package
|
|
55
|
+
The npm package selects a platform-specific optional dependency containing the native binary. It does not run installation lifecycle scripts. Installing with `--omit=optional` is not supported.
|
|
56
56
|
|
|
57
57
|
> The package is currently published as `@etherscan-npm/cli` while the `@etherscan` npm scope is being transferred. The `etherscan` command is unchanged.
|
|
58
58
|
|
|
@@ -488,9 +488,20 @@ go build -o etherscan ./cmd/etherscan
|
|
|
488
488
|
|
|
489
489
|
Installer changes can be checked with `sh scripts/test-install.sh` on macOS/Linux or `./scripts/test-install.ps1` in PowerShell on Windows.
|
|
490
490
|
|
|
491
|
-
The npm distribution can be checked with `sh scripts/test-npm.sh` on macOS/Linux or `./scripts/test-npm.ps1` in PowerShell. These tests pack and install the
|
|
491
|
+
The npm distribution can be checked with `sh scripts/test-npm.sh` on macOS/Linux or `./scripts/test-npm.ps1` in PowerShell. These tests pack and install the umbrella and current-platform packages with lifecycle scripts disabled; they do not publish to npm.
|
|
492
492
|
|
|
493
|
-
|
|
493
|
+
The first seven-package npm release requires a one-time bootstrap by an npm administrator with publish access to the `@etherscan-npm` scope. After the matching GitHub release succeeds, check out its exact tag and run:
|
|
494
|
+
|
|
495
|
+
```sh
|
|
496
|
+
gh release download v1.0.4 --dir dist
|
|
497
|
+
npm login
|
|
498
|
+
npm whoami
|
|
499
|
+
VERSION=1.0.4 node npm/publish.js
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
The publisher verifies all six archives against `checksums.txt`, creates the six public platform packages by publishing them first, and publishes `@etherscan-npm/cli` last. It safely skips exact versions that already exist so a partial publication can be retried. Replace `1.0.4` with the actual unused release version if necessary.
|
|
503
|
+
|
|
504
|
+
After the bootstrap, configure npm trusted publishing for the umbrella and all six platform packages, targeting `etherscan/etherscan-cli` and `.github/workflows/release.yml`. Set the `NPM_PUBLISH_ENABLED` repository variable to `true`; subsequent tagged releases publish through GitHub Actions with provenance.
|
|
494
505
|
|
|
495
506
|
## API coverage and support
|
|
496
507
|
|
package/npm/bin/etherscan.js
CHANGED
|
@@ -3,22 +3,45 @@
|
|
|
3
3
|
"use strict";
|
|
4
4
|
|
|
5
5
|
const fs = require("node:fs");
|
|
6
|
+
const os = require("node:os");
|
|
6
7
|
const path = require("node:path");
|
|
7
8
|
const { spawnSync } = require("node:child_process");
|
|
8
9
|
const packageInfo = require("../../package.json");
|
|
10
|
+
const { PLATFORMS, platformPackage } = require("../platform");
|
|
9
11
|
|
|
10
12
|
const packageRoot = path.resolve(__dirname, "..", "..");
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
|
|
14
|
+
// Resolve these once so the package lookup and executable name cannot disagree.
|
|
15
|
+
const platform = os.platform();
|
|
16
|
+
const arch = os.arch();
|
|
17
|
+
const binaryName = platform === "win32" ? "etherscan.exe" : "etherscan";
|
|
18
|
+
const reinstallHint = `Reinstall ${packageInfo.name} without --omit=optional.`;
|
|
19
|
+
|
|
20
|
+
function getExecutable() {
|
|
21
|
+
const packageName = platformPackage(platform, arch);
|
|
22
|
+
if (!packageName) {
|
|
23
|
+
console.error(
|
|
24
|
+
`Etherscan CLI does not support ${platform} ${arch}. ` +
|
|
25
|
+
`Supported platforms: ${Object.keys(PLATFORMS).join(", ")}.`,
|
|
26
|
+
);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const manifest = require.resolve(`${packageName}/package.json`, {
|
|
32
|
+
paths: [packageRoot],
|
|
33
|
+
});
|
|
34
|
+
return path.join(path.dirname(manifest), binaryName);
|
|
35
|
+
} catch {
|
|
36
|
+
console.error(`The platform package ${packageName} is not installed. ${reinstallHint}`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const executable = getExecutable();
|
|
16
42
|
|
|
17
43
|
if (!fs.existsSync(executable)) {
|
|
18
|
-
console.error(
|
|
19
|
-
"Etherscan CLI is not installed in this npm package. " +
|
|
20
|
-
"Reinstall @etherscan-npm/cli without --ignore-scripts.",
|
|
21
|
-
);
|
|
44
|
+
console.error(`The platform package executable is missing: ${executable}. ${reinstallHint}`);
|
|
22
45
|
process.exit(1);
|
|
23
46
|
}
|
|
24
47
|
|
package/npm/platform.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const PLATFORMS = Object.freeze({
|
|
4
|
+
"darwin arm64": "@etherscan-npm/cli-darwin-arm64",
|
|
5
|
+
"darwin x64": "@etherscan-npm/cli-darwin-x64",
|
|
6
|
+
"linux arm64": "@etherscan-npm/cli-linux-arm64",
|
|
7
|
+
"linux x64": "@etherscan-npm/cli-linux-x64",
|
|
8
|
+
"win32 arm64": "@etherscan-npm/cli-win32-arm64",
|
|
9
|
+
"win32 x64": "@etherscan-npm/cli-win32-x64",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
function platformPackage(platform, arch) {
|
|
13
|
+
return PLATFORMS[`${platform} ${arch}`] || null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = { PLATFORMS, platformPackage };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@etherscan-npm/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Command-line client and interactive explorer for the Etherscan V2 API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/etherscan/etherscan-cli",
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
"etherscan": "npm/bin/etherscan.js"
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
|
-
"postinstall": "node npm/postinstall.js",
|
|
20
19
|
"prepublishOnly": "node npm/prepublish-check.js",
|
|
21
20
|
"test:package": "npm pack --dry-run",
|
|
22
21
|
"test:release": "node npm/publish.test.js",
|
|
@@ -24,13 +23,18 @@
|
|
|
24
23
|
},
|
|
25
24
|
"files": [
|
|
26
25
|
"npm/bin/etherscan.js",
|
|
27
|
-
"npm/
|
|
28
|
-
"npm/prepublish-check.js",
|
|
29
|
-
"scripts/install.sh",
|
|
30
|
-
"scripts/install.ps1",
|
|
26
|
+
"npm/platform.js",
|
|
31
27
|
"README.md",
|
|
32
28
|
"LICENSE"
|
|
33
29
|
],
|
|
30
|
+
"optionalDependencies": {
|
|
31
|
+
"@etherscan-npm/cli-darwin-arm64": "1.0.4",
|
|
32
|
+
"@etherscan-npm/cli-darwin-x64": "1.0.4",
|
|
33
|
+
"@etherscan-npm/cli-linux-arm64": "1.0.4",
|
|
34
|
+
"@etherscan-npm/cli-linux-x64": "1.0.4",
|
|
35
|
+
"@etherscan-npm/cli-win32-arm64": "1.0.4",
|
|
36
|
+
"@etherscan-npm/cli-win32-x64": "1.0.4"
|
|
37
|
+
},
|
|
34
38
|
"engines": {
|
|
35
39
|
"node": ">=18"
|
|
36
40
|
},
|
package/npm/postinstall.js
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const fs = require("node:fs");
|
|
4
|
-
const path = require("node:path");
|
|
5
|
-
const { spawnSync } = require("node:child_process");
|
|
6
|
-
const packageInfo = require("../package.json");
|
|
7
|
-
|
|
8
|
-
const packageRoot = path.resolve(__dirname, "..");
|
|
9
|
-
const installDir = path.join(packageRoot, "vendor");
|
|
10
|
-
const version = `v${packageInfo.version}`;
|
|
11
|
-
|
|
12
|
-
let command;
|
|
13
|
-
let args;
|
|
14
|
-
if (process.platform === "win32") {
|
|
15
|
-
command = "powershell.exe";
|
|
16
|
-
args = [
|
|
17
|
-
"-NoLogo",
|
|
18
|
-
"-NoProfile",
|
|
19
|
-
"-NonInteractive",
|
|
20
|
-
"-ExecutionPolicy",
|
|
21
|
-
"Bypass",
|
|
22
|
-
"-File",
|
|
23
|
-
path.join(packageRoot, "scripts", "install.ps1"),
|
|
24
|
-
"-Version",
|
|
25
|
-
version,
|
|
26
|
-
"-InstallDir",
|
|
27
|
-
installDir,
|
|
28
|
-
"-NoPathUpdate",
|
|
29
|
-
];
|
|
30
|
-
} else {
|
|
31
|
-
command = "sh";
|
|
32
|
-
args = [
|
|
33
|
-
path.join(packageRoot, "scripts", "install.sh"),
|
|
34
|
-
"--version",
|
|
35
|
-
version,
|
|
36
|
-
"--install-dir",
|
|
37
|
-
installDir,
|
|
38
|
-
"--no-path-update",
|
|
39
|
-
];
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const result = spawnSync(command, args, { stdio: "inherit", env: process.env });
|
|
43
|
-
if (result.error) {
|
|
44
|
-
console.error(`Unable to run the Etherscan CLI installer: ${result.error.message}`);
|
|
45
|
-
process.exit(1);
|
|
46
|
-
}
|
|
47
|
-
if (result.status !== 0) {
|
|
48
|
-
process.exit(result.status === null ? 1 : result.status);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const executable = path.join(
|
|
52
|
-
installDir,
|
|
53
|
-
process.platform === "win32" ? "etherscan.exe" : "etherscan",
|
|
54
|
-
);
|
|
55
|
-
if (!fs.existsSync(executable)) {
|
|
56
|
-
console.error(`The installer did not create ${executable}.`);
|
|
57
|
-
process.exit(1);
|
|
58
|
-
}
|
package/npm/prepublish-check.js
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const fs = require("node:fs");
|
|
4
|
-
const path = require("node:path");
|
|
5
|
-
const packageInfo = require("../package.json");
|
|
6
|
-
|
|
7
|
-
const versionPattern =
|
|
8
|
-
/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
9
|
-
|
|
10
|
-
// npm treats these as glob metacharacters in package.json "files".
|
|
11
|
-
const globPattern = /[*?[\]{}]/;
|
|
12
|
-
|
|
13
|
-
function checkVersion(version) {
|
|
14
|
-
if (version === "0.0.0-development") {
|
|
15
|
-
throw new Error("the development placeholder version cannot be published");
|
|
16
|
-
}
|
|
17
|
-
if (!versionPattern.test(version)) {
|
|
18
|
-
throw new Error(`invalid release version: ${version}`);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// `npm pack` copies the working tree, not the committed blobs. On Windows a
|
|
23
|
-
// checkout with core.autocrlf=true turns the LF blobs into CRLF, and a CRLF
|
|
24
|
-
// scripts/install.sh cannot be run by /bin/sh on Linux or macOS, which is how
|
|
25
|
-
// @etherscan-npm/cli@1.0.1 shipped an installer that failed on both.
|
|
26
|
-
function checkFileLineEndings(filePath, label) {
|
|
27
|
-
const contents = fs.readFileSync(filePath);
|
|
28
|
-
// Skip binaries using the same NUL-byte heuristic git applies for text=auto.
|
|
29
|
-
if (contents.includes(0x00)) {
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
if (contents.includes(0x0d)) {
|
|
33
|
-
throw new Error(`${label} contains CRLF line endings`);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Checks the literal entries of package.json "files". This fails closed: npm
|
|
38
|
-
// also accepts globs and directories there, and silently skipping what it cannot
|
|
39
|
-
// resolve would let a future "files" edit disable the gate while leaving the
|
|
40
|
-
// publish green — the same silent-pass shape that let 1.0.1 ship.
|
|
41
|
-
function checkLineEndings(baseDir, files = packageInfo.files) {
|
|
42
|
-
for (const entry of files) {
|
|
43
|
-
if (globPattern.test(entry)) {
|
|
44
|
-
throw new Error(
|
|
45
|
-
`${entry} is a glob pattern; this check only understands literal file paths, so update it before publishing`,
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
const filePath = path.join(baseDir, entry);
|
|
49
|
-
let stats;
|
|
50
|
-
try {
|
|
51
|
-
stats = fs.statSync(filePath);
|
|
52
|
-
} catch (error) {
|
|
53
|
-
if (error.code === "ENOENT") {
|
|
54
|
-
throw new Error(`${entry} is listed in package.json "files" but does not exist`);
|
|
55
|
-
}
|
|
56
|
-
throw error;
|
|
57
|
-
}
|
|
58
|
-
if (stats.isDirectory()) {
|
|
59
|
-
throw new Error(
|
|
60
|
-
`${entry} is a directory; this check only understands literal file paths, so update it before publishing`,
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
checkFileLineEndings(filePath, entry);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function main() {
|
|
68
|
-
checkVersion(packageInfo.version);
|
|
69
|
-
checkLineEndings(path.resolve(__dirname, ".."));
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
module.exports = { checkVersion, checkFileLineEndings, checkLineEndings };
|
|
73
|
-
|
|
74
|
-
if (require.main === module) {
|
|
75
|
-
try {
|
|
76
|
-
main();
|
|
77
|
-
} catch (error) {
|
|
78
|
-
console.error(`Refusing to publish @etherscan/cli: ${error.message}`);
|
|
79
|
-
process.exit(1);
|
|
80
|
-
}
|
|
81
|
-
}
|
package/scripts/install.ps1
DELETED
|
@@ -1,362 +0,0 @@
|
|
|
1
|
-
[CmdletBinding()]
|
|
2
|
-
param(
|
|
3
|
-
[string]$Version = $env:ETHERSCAN_VERSION,
|
|
4
|
-
[string]$InstallDir = $env:ETHERSCAN_INSTALL_DIR,
|
|
5
|
-
[switch]$NoPathUpdate,
|
|
6
|
-
[int]$WaitForProcessId = 0,
|
|
7
|
-
[switch]$CleanupScript,
|
|
8
|
-
[switch]$Uninstall
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
$ErrorActionPreference = "Stop"
|
|
12
|
-
$ProgressPreference = "SilentlyContinue"
|
|
13
|
-
|
|
14
|
-
$Repository = "etherscan/etherscan-cli"
|
|
15
|
-
$DownloadBaseUrl = $env:ETHERSCAN_INSTALL_TEST_DOWNLOAD_BASE_URL
|
|
16
|
-
$InstallMarkerName = ".etherscan-cli-path-added"
|
|
17
|
-
$InstallMarkerContent = "etherscan-cli:path-added:v1"
|
|
18
|
-
|
|
19
|
-
function Get-EtherscanArchitecture {
|
|
20
|
-
$architecture = $env:PROCESSOR_ARCHITEW6432
|
|
21
|
-
if ([string]::IsNullOrWhiteSpace($architecture)) {
|
|
22
|
-
$architecture = $env:PROCESSOR_ARCHITECTURE
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
switch -Regex ($architecture) {
|
|
26
|
-
"^(AMD64|x86_64)$" { return "amd64" }
|
|
27
|
-
"^(ARM64|aarch64)$" { return "arm64" }
|
|
28
|
-
default { throw "Unsupported Windows architecture: $architecture. Etherscan CLI supports amd64 and arm64." }
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function Get-GitHubApiHeaders {
|
|
33
|
-
$headers = @{
|
|
34
|
-
Accept = "application/vnd.github+json"
|
|
35
|
-
"User-Agent" = "etherscan-cli-installer"
|
|
36
|
-
"X-GitHub-Api-Version" = "2022-11-28"
|
|
37
|
-
}
|
|
38
|
-
if (-not [string]::IsNullOrWhiteSpace($env:ETHERSCAN_GITHUB_TOKEN)) {
|
|
39
|
-
$headers.Authorization = "Bearer $($env:ETHERSCAN_GITHUB_TOKEN)"
|
|
40
|
-
}
|
|
41
|
-
return $headers
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function Resolve-EtherscanVersion {
|
|
45
|
-
param([string]$RequestedVersion)
|
|
46
|
-
|
|
47
|
-
if (-not [string]::IsNullOrWhiteSpace($RequestedVersion) -and $RequestedVersion -ne "latest") {
|
|
48
|
-
$tag = if ($RequestedVersion.StartsWith("v")) { $RequestedVersion } else { "v$RequestedVersion" }
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
if (-not [string]::IsNullOrWhiteSpace($DownloadBaseUrl)) {
|
|
52
|
-
throw "A version is required when the installer test download source is used."
|
|
53
|
-
}
|
|
54
|
-
$release = Invoke-RestMethod `
|
|
55
|
-
-Uri "https://api.github.com/repos/$Repository/releases/latest" `
|
|
56
|
-
-Headers (Get-GitHubApiHeaders)
|
|
57
|
-
$tag = [string]$release.tag_name
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if ($tag -notmatch '^v[0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$') {
|
|
61
|
-
throw "Invalid release version: $tag"
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return @{
|
|
65
|
-
Tag = $tag
|
|
66
|
-
Version = $tag.Substring(1)
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function Copy-InstallerFile {
|
|
71
|
-
param(
|
|
72
|
-
[string]$Base,
|
|
73
|
-
[string]$Name,
|
|
74
|
-
[string]$Destination
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
if (Test-Path -LiteralPath $Base -PathType Container) {
|
|
78
|
-
Copy-Item -LiteralPath (Join-Path $Base $Name) -Destination $Destination
|
|
79
|
-
return
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
$uri = "$($Base.TrimEnd('/'))/$Name"
|
|
83
|
-
$parsedUri = [Uri]$uri
|
|
84
|
-
if ($parsedUri.Scheme -ne "https") {
|
|
85
|
-
throw "Remote downloads must use HTTPS: $uri"
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
$headers = @{
|
|
89
|
-
"User-Agent" = "etherscan-cli-installer"
|
|
90
|
-
}
|
|
91
|
-
if ($parsedUri.Host -in @("github.com", "api.github.com") -and
|
|
92
|
-
-not [string]::IsNullOrWhiteSpace($env:ETHERSCAN_GITHUB_TOKEN)) {
|
|
93
|
-
$headers.Authorization = "Bearer $($env:ETHERSCAN_GITHUB_TOKEN)"
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
Invoke-WebRequest -Uri $uri -OutFile $Destination -Headers $headers -UseBasicParsing
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function Get-SHA256FileHash {
|
|
100
|
-
param([string]$Path)
|
|
101
|
-
|
|
102
|
-
$sha256 = [Security.Cryptography.SHA256]::Create()
|
|
103
|
-
try {
|
|
104
|
-
$stream = [IO.File]::OpenRead($Path)
|
|
105
|
-
try {
|
|
106
|
-
return ([BitConverter]::ToString($sha256.ComputeHash($stream))).Replace("-", "").ToLowerInvariant()
|
|
107
|
-
}
|
|
108
|
-
finally {
|
|
109
|
-
$stream.Dispose()
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
finally {
|
|
113
|
-
$sha256.Dispose()
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function Add-EtherscanToUserPath {
|
|
118
|
-
param([string]$Directory)
|
|
119
|
-
|
|
120
|
-
$fullDirectory = [IO.Path]::GetFullPath($Directory).TrimEnd('\')
|
|
121
|
-
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
|
122
|
-
$entries = @($userPath -split ';' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
|
123
|
-
$alreadyPresent = $entries | Where-Object {
|
|
124
|
-
$entry = $_
|
|
125
|
-
try {
|
|
126
|
-
$expandedEntry = [Environment]::ExpandEnvironmentVariables($entry)
|
|
127
|
-
[IO.Path]::GetFullPath($expandedEntry).TrimEnd('\').Equals($fullDirectory, [StringComparison]::OrdinalIgnoreCase)
|
|
128
|
-
}
|
|
129
|
-
catch {
|
|
130
|
-
$entry.TrimEnd('\').Equals($fullDirectory, [StringComparison]::OrdinalIgnoreCase)
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (-not $alreadyPresent) {
|
|
135
|
-
$newUserPath = if ([string]::IsNullOrWhiteSpace($userPath)) {
|
|
136
|
-
$fullDirectory
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
"$($userPath.TrimEnd(';'));$fullDirectory"
|
|
140
|
-
}
|
|
141
|
-
[Environment]::SetEnvironmentVariable("Path", $newUserPath, "User")
|
|
142
|
-
Write-Host "Added $fullDirectory to your user PATH."
|
|
143
|
-
$pathAdded = $true
|
|
144
|
-
}
|
|
145
|
-
else {
|
|
146
|
-
$pathAdded = $false
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
$processEntries = @($env:Path -split ';')
|
|
150
|
-
if (-not ($processEntries | Where-Object { $_.TrimEnd('\').Equals($fullDirectory, [StringComparison]::OrdinalIgnoreCase) })) {
|
|
151
|
-
$env:Path = "$env:Path;$fullDirectory"
|
|
152
|
-
}
|
|
153
|
-
return $pathAdded
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function Remove-EtherscanFromUserPath {
|
|
157
|
-
param([string]$Directory)
|
|
158
|
-
|
|
159
|
-
$fullDirectory = [IO.Path]::GetFullPath($Directory).TrimEnd('\')
|
|
160
|
-
$entries = @([Environment]::GetEnvironmentVariable("Path", "User") -split ';' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
|
161
|
-
$kept = @($entries | Where-Object {
|
|
162
|
-
$entry = $_
|
|
163
|
-
try {
|
|
164
|
-
$expandedEntry = [Environment]::ExpandEnvironmentVariables($entry)
|
|
165
|
-
-not [IO.Path]::GetFullPath($expandedEntry).TrimEnd('\').Equals($fullDirectory, [StringComparison]::OrdinalIgnoreCase)
|
|
166
|
-
}
|
|
167
|
-
catch {
|
|
168
|
-
-not $entry.TrimEnd('\').Equals($fullDirectory, [StringComparison]::OrdinalIgnoreCase)
|
|
169
|
-
}
|
|
170
|
-
})
|
|
171
|
-
if ($kept.Count -ne $entries.Count) {
|
|
172
|
-
[Environment]::SetEnvironmentVariable("Path", ($kept -join ';'), "User")
|
|
173
|
-
Write-Host "Removed $fullDirectory from your user PATH."
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function Test-EtherscanInstallMarker {
|
|
178
|
-
param([string]$Path)
|
|
179
|
-
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $false }
|
|
180
|
-
$item = Get-Item -LiteralPath $Path -Force
|
|
181
|
-
if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { return $false }
|
|
182
|
-
return (Get-Content -LiteralPath $Path -Raw) -eq $InstallMarkerContent
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function Get-EtherscanConfigDirectory {
|
|
186
|
-
if (-not [string]::IsNullOrWhiteSpace($env:XDG_CONFIG_HOME)) {
|
|
187
|
-
return Join-Path $env:XDG_CONFIG_HOME "etherscan"
|
|
188
|
-
}
|
|
189
|
-
return Join-Path $env:USERPROFILE ".etherscan"
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if ($env:OS -ne "Windows_NT") {
|
|
193
|
-
throw "This installer supports Windows only. Use install.sh on macOS or Linux."
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if ([string]::IsNullOrWhiteSpace($InstallDir)) {
|
|
197
|
-
$localAppData = [Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData)
|
|
198
|
-
$InstallDir = Join-Path $localAppData "Programs\Etherscan\bin"
|
|
199
|
-
}
|
|
200
|
-
if ($InstallDir.Contains(';')) {
|
|
201
|
-
throw "The installation directory cannot contain a semicolon."
|
|
202
|
-
}
|
|
203
|
-
if ($InstallDir.IndexOfAny([char[]]"`r`n") -ge 0) {
|
|
204
|
-
throw "The installation directory cannot contain a line break."
|
|
205
|
-
}
|
|
206
|
-
if ($WaitForProcessId -gt 0) {
|
|
207
|
-
Wait-Process -Id $WaitForProcessId -ErrorAction SilentlyContinue
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if ($Uninstall) {
|
|
211
|
-
try {
|
|
212
|
-
$targetExecutable = Join-Path $InstallDir "etherscan.exe"
|
|
213
|
-
$marker = Join-Path $InstallDir $InstallMarkerName
|
|
214
|
-
$validMarker = Test-EtherscanInstallMarker -Path $marker
|
|
215
|
-
$localAppData = [Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData)
|
|
216
|
-
$defaultInstallDir = Join-Path $localAppData "Programs\Etherscan\bin"
|
|
217
|
-
$legacyDefault = [IO.Path]::GetFullPath($InstallDir).TrimEnd('\').Equals(
|
|
218
|
-
[IO.Path]::GetFullPath($defaultInstallDir).TrimEnd('\'), [StringComparison]::OrdinalIgnoreCase)
|
|
219
|
-
$removed = $false
|
|
220
|
-
|
|
221
|
-
if (Test-Path -LiteralPath $targetExecutable) {
|
|
222
|
-
Remove-Item -LiteralPath $targetExecutable -Force
|
|
223
|
-
Write-Host "Removed $targetExecutable"
|
|
224
|
-
$removed = $true
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
$otherEntries = @()
|
|
228
|
-
if (Test-Path -LiteralPath $InstallDir -PathType Container) {
|
|
229
|
-
$otherEntries = @(Get-ChildItem -LiteralPath $InstallDir -Force | Where-Object {
|
|
230
|
-
-not ($validMarker -and $_.FullName -eq $marker)
|
|
231
|
-
} | Select-Object -First 1)
|
|
232
|
-
}
|
|
233
|
-
if (-not $NoPathUpdate -and ($validMarker -or $legacyDefault) -and $otherEntries.Count -eq 0) {
|
|
234
|
-
Remove-EtherscanFromUserPath -Directory $InstallDir
|
|
235
|
-
if ($validMarker) { Remove-Item -LiteralPath $marker -Force }
|
|
236
|
-
Remove-Item -LiteralPath $InstallDir -Force -ErrorAction SilentlyContinue
|
|
237
|
-
$removed = $true
|
|
238
|
-
}
|
|
239
|
-
elseif (-not $NoPathUpdate -and (Test-Path -LiteralPath $InstallDir)) {
|
|
240
|
-
Write-Host "Left $InstallDir on PATH because ownership was not proven or the directory is shared."
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
$configDirectory = Get-EtherscanConfigDirectory
|
|
244
|
-
if (Test-Path -LiteralPath $configDirectory) {
|
|
245
|
-
Remove-Item -LiteralPath $configDirectory -Recurse -Force
|
|
246
|
-
Write-Host "Removed $configDirectory"
|
|
247
|
-
$removed = $true
|
|
248
|
-
}
|
|
249
|
-
if ($removed) { Write-Host "Etherscan CLI uninstalled." } else { Write-Host "Nothing to remove." }
|
|
250
|
-
if (-not [string]::IsNullOrWhiteSpace($env:ETHERSCAN_API_KEY)) {
|
|
251
|
-
Write-Warning "ETHERSCAN_API_KEY remains set; unset it in your shell."
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
finally {
|
|
255
|
-
if ($CleanupScript -and -not [string]::IsNullOrWhiteSpace($PSCommandPath)) {
|
|
256
|
-
Remove-Item -LiteralPath $PSCommandPath -Force -ErrorAction SilentlyContinue
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
return
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
$resolved = Resolve-EtherscanVersion -RequestedVersion $Version
|
|
263
|
-
$architecture = Get-EtherscanArchitecture
|
|
264
|
-
$archiveName = "etherscan_$($resolved.Version)_windows_$architecture.zip"
|
|
265
|
-
$baseUrl = if ([string]::IsNullOrWhiteSpace($DownloadBaseUrl)) {
|
|
266
|
-
"https://github.com/$Repository/releases/download/$($resolved.Tag)"
|
|
267
|
-
}
|
|
268
|
-
else {
|
|
269
|
-
$DownloadBaseUrl
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
$tempDirectory = Join-Path ([IO.Path]::GetTempPath()) "etherscan-install-$PID-$([Guid]::NewGuid().ToString('N'))"
|
|
273
|
-
$archivePath = Join-Path $tempDirectory $archiveName
|
|
274
|
-
$checksumPath = Join-Path $tempDirectory "checksums.txt"
|
|
275
|
-
$sourceExecutable = Join-Path $tempDirectory "etherscan.exe"
|
|
276
|
-
|
|
277
|
-
try {
|
|
278
|
-
New-Item -ItemType Directory -Path $tempDirectory -Force | Out-Null
|
|
279
|
-
|
|
280
|
-
Write-Host "Downloading Etherscan CLI $($resolved.Version) for windows/$architecture..."
|
|
281
|
-
Copy-InstallerFile -Base $baseUrl -Name $archiveName -Destination $archivePath
|
|
282
|
-
Copy-InstallerFile -Base $baseUrl -Name "checksums.txt" -Destination $checksumPath
|
|
283
|
-
|
|
284
|
-
$pattern = '^([0-9A-Fa-f]{64})\s+\*?' + [Regex]::Escape($archiveName) + '$'
|
|
285
|
-
$checksumLine = Get-Content -LiteralPath $checksumPath | Where-Object { $_ -match $pattern } | Select-Object -First 1
|
|
286
|
-
if (-not $checksumLine -or $checksumLine -notmatch $pattern) {
|
|
287
|
-
throw "No checksum was published for $archiveName."
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
$expectedHash = $Matches[1].ToLowerInvariant()
|
|
291
|
-
$actualHash = Get-SHA256FileHash -Path $archivePath
|
|
292
|
-
if ($actualHash -ne $expectedHash) {
|
|
293
|
-
throw "Checksum verification failed for $archiveName. Expected $expectedHash, received $actualHash."
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
|
297
|
-
$zip = [IO.Compression.ZipFile]::OpenRead($archivePath)
|
|
298
|
-
try {
|
|
299
|
-
$executableEntries = @($zip.Entries | Where-Object { $_.FullName.Replace('\', '/') -eq "etherscan.exe" })
|
|
300
|
-
if ($executableEntries.Count -ne 1) {
|
|
301
|
-
throw "$archiveName must contain exactly one root-level etherscan.exe."
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
$inputStream = $executableEntries[0].Open()
|
|
305
|
-
$outputStream = [IO.File]::Create($sourceExecutable)
|
|
306
|
-
try {
|
|
307
|
-
$inputStream.CopyTo($outputStream)
|
|
308
|
-
}
|
|
309
|
-
finally {
|
|
310
|
-
$outputStream.Dispose()
|
|
311
|
-
$inputStream.Dispose()
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
finally {
|
|
315
|
-
$zip.Dispose()
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
|
|
319
|
-
$targetExecutable = Join-Path $InstallDir "etherscan.exe"
|
|
320
|
-
$stagedExecutable = Join-Path $InstallDir ".etherscan.exe.new-$PID"
|
|
321
|
-
$backupExecutable = Join-Path $InstallDir ".etherscan.exe.old-$PID"
|
|
322
|
-
Copy-Item -LiteralPath $sourceExecutable -Destination $stagedExecutable -Force
|
|
323
|
-
|
|
324
|
-
try {
|
|
325
|
-
if (Test-Path -LiteralPath $targetExecutable) {
|
|
326
|
-
Move-Item -LiteralPath $targetExecutable -Destination $backupExecutable -Force
|
|
327
|
-
}
|
|
328
|
-
Move-Item -LiteralPath $stagedExecutable -Destination $targetExecutable -Force
|
|
329
|
-
Remove-Item -LiteralPath $backupExecutable -Force -ErrorAction SilentlyContinue
|
|
330
|
-
}
|
|
331
|
-
catch {
|
|
332
|
-
Remove-Item -LiteralPath $stagedExecutable -Force -ErrorAction SilentlyContinue
|
|
333
|
-
if ((Test-Path -LiteralPath $backupExecutable) -and -not (Test-Path -LiteralPath $targetExecutable)) {
|
|
334
|
-
Move-Item -LiteralPath $backupExecutable -Destination $targetExecutable -Force
|
|
335
|
-
}
|
|
336
|
-
throw
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (-not $NoPathUpdate) {
|
|
340
|
-
$pathAdded = Add-EtherscanToUserPath -Directory $InstallDir
|
|
341
|
-
if ($pathAdded) {
|
|
342
|
-
Set-Content -LiteralPath (Join-Path $InstallDir $InstallMarkerName) -Value $InstallMarkerContent -NoNewline
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
Write-Host ""
|
|
347
|
-
Write-Host "Etherscan CLI $($resolved.Version) installed successfully."
|
|
348
|
-
Write-Host "Installed to: $targetExecutable"
|
|
349
|
-
if ($NoPathUpdate) {
|
|
350
|
-
Write-Host "Add $InstallDir to PATH to run etherscan from any directory."
|
|
351
|
-
}
|
|
352
|
-
else {
|
|
353
|
-
Write-Host "Run 'etherscan version' to verify the installation."
|
|
354
|
-
Write-Host "Open a new terminal if the command is not yet available."
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
finally {
|
|
358
|
-
Remove-Item -LiteralPath $tempDirectory -Recurse -Force -ErrorAction SilentlyContinue
|
|
359
|
-
if ($CleanupScript -and -not [string]::IsNullOrWhiteSpace($PSCommandPath)) {
|
|
360
|
-
Remove-Item -LiteralPath $PSCommandPath -Force -ErrorAction SilentlyContinue
|
|
361
|
-
}
|
|
362
|
-
}
|
package/scripts/install.sh
DELETED
|
@@ -1,340 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
|
|
3
|
-
set -eu
|
|
4
|
-
|
|
5
|
-
repository="etherscan/etherscan-cli"
|
|
6
|
-
version=${ETHERSCAN_VERSION:-}
|
|
7
|
-
install_dir=${ETHERSCAN_INSTALL_DIR:-"$HOME/.local/bin"}
|
|
8
|
-
download_base=${ETHERSCAN_INSTALL_TEST_DOWNLOAD_BASE_URL:-}
|
|
9
|
-
update_path=1
|
|
10
|
-
uninstall=0
|
|
11
|
-
marker_name=.etherscan-cli-path-added
|
|
12
|
-
marker_content=etherscan-cli:path-added:v1
|
|
13
|
-
|
|
14
|
-
usage() {
|
|
15
|
-
cat <<'EOF'
|
|
16
|
-
Install Etherscan CLI.
|
|
17
|
-
|
|
18
|
-
Usage: install.sh [options]
|
|
19
|
-
|
|
20
|
-
Options:
|
|
21
|
-
--version VERSION Install a specific version (for example, v1.1.0).
|
|
22
|
-
--install-dir DIRECTORY Install into DIRECTORY (default: ~/.local/bin).
|
|
23
|
-
--no-path-update Do not update the shell profile.
|
|
24
|
-
--uninstall Remove the CLI and saved configuration.
|
|
25
|
-
-h, --help Show this help.
|
|
26
|
-
EOF
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
die() {
|
|
30
|
-
printf 'error: %s\n' "$*" >&2
|
|
31
|
-
exit 1
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
while [ "$#" -gt 0 ]; do
|
|
35
|
-
case "$1" in
|
|
36
|
-
--version)
|
|
37
|
-
[ "$#" -ge 2 ] || die "--version requires a value"
|
|
38
|
-
version=$2
|
|
39
|
-
shift 2
|
|
40
|
-
;;
|
|
41
|
-
--install-dir)
|
|
42
|
-
[ "$#" -ge 2 ] || die "--install-dir requires a value"
|
|
43
|
-
install_dir=$2
|
|
44
|
-
shift 2
|
|
45
|
-
;;
|
|
46
|
-
--no-path-update)
|
|
47
|
-
update_path=0
|
|
48
|
-
shift
|
|
49
|
-
;;
|
|
50
|
-
--uninstall)
|
|
51
|
-
uninstall=1
|
|
52
|
-
shift
|
|
53
|
-
;;
|
|
54
|
-
-h|--help)
|
|
55
|
-
usage
|
|
56
|
-
exit 0
|
|
57
|
-
;;
|
|
58
|
-
*)
|
|
59
|
-
die "unknown option: $1"
|
|
60
|
-
;;
|
|
61
|
-
esac
|
|
62
|
-
done
|
|
63
|
-
|
|
64
|
-
if printf '%s' "$install_dir" | LC_ALL=C grep '[[:cntrl:]]' >/dev/null 2>&1; then
|
|
65
|
-
die "the installation directory cannot contain control characters"
|
|
66
|
-
fi
|
|
67
|
-
|
|
68
|
-
config_dir() {
|
|
69
|
-
if [ -n "${XDG_CONFIG_HOME:-}" ]; then
|
|
70
|
-
printf '%s\n' "$XDG_CONFIG_HOME/etherscan"
|
|
71
|
-
else
|
|
72
|
-
printf '%s\n' "$HOME/.etherscan"
|
|
73
|
-
fi
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
profile_has_block() {
|
|
77
|
-
profile=$1
|
|
78
|
-
target=$2
|
|
79
|
-
[ -f "$profile" ] && [ ! -L "$profile" ] || return 1
|
|
80
|
-
ETHERSCAN_PROFILE_TARGET=$target awk '
|
|
81
|
-
BEGIN { target = ENVIRON["ETHERSCAN_PROFILE_TARGET"] }
|
|
82
|
-
previous == "# Etherscan CLI" && $0 == target { found = 1; exit }
|
|
83
|
-
{ previous = $0 }
|
|
84
|
-
END { exit found ? 0 : 1 }
|
|
85
|
-
' "$profile"
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
remove_profile_block() {
|
|
89
|
-
profile=$1
|
|
90
|
-
target=$2
|
|
91
|
-
profile_has_block "$profile" "$target" || return 0
|
|
92
|
-
tmp=$(mktemp "${profile}.etherscan-uninstall.XXXXXX") || die "could not create a profile temporary file"
|
|
93
|
-
if ! cp -p "$profile" "$tmp"; then
|
|
94
|
-
rm -f "$tmp"
|
|
95
|
-
die "could not preserve permissions for $profile"
|
|
96
|
-
fi
|
|
97
|
-
if ! ETHERSCAN_PROFILE_TARGET=$target awk '
|
|
98
|
-
BEGIN { target = ENVIRON["ETHERSCAN_PROFILE_TARGET"] }
|
|
99
|
-
{
|
|
100
|
-
if (pending) {
|
|
101
|
-
if ($0 == target) { pending = 0; next }
|
|
102
|
-
print "# Etherscan CLI"
|
|
103
|
-
pending = 0
|
|
104
|
-
}
|
|
105
|
-
if ($0 == "# Etherscan CLI") { pending = 1; next }
|
|
106
|
-
print
|
|
107
|
-
}
|
|
108
|
-
END { if (pending) print "# Etherscan CLI" }
|
|
109
|
-
' "$profile" >"$tmp"; then
|
|
110
|
-
rm -f "$tmp"
|
|
111
|
-
die "could not update $profile"
|
|
112
|
-
fi
|
|
113
|
-
mv -f "$tmp" "$profile"
|
|
114
|
-
printf 'Removed the PATH entry from %s\n' "$profile"
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
directory_empty_except_marker() {
|
|
118
|
-
directory=$1
|
|
119
|
-
marker=$2
|
|
120
|
-
[ -d "$directory" ] || return 0
|
|
121
|
-
for entry in "$directory"/.[!.]* "$directory"/..?* "$directory"/*; do
|
|
122
|
-
[ -e "$entry" ] || [ -L "$entry" ] || continue
|
|
123
|
-
[ "$entry" = "$marker" ] && continue
|
|
124
|
-
return 1
|
|
125
|
-
done
|
|
126
|
-
return 0
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if [ "$uninstall" -eq 1 ]; then
|
|
130
|
-
binary="$install_dir/etherscan"
|
|
131
|
-
marker="$install_dir/$marker_name"
|
|
132
|
-
marker_valid=0
|
|
133
|
-
if [ -f "$marker" ] && [ ! -L "$marker" ] && [ "$(cat "$marker")" = "$marker_content" ]; then
|
|
134
|
-
marker_valid=1
|
|
135
|
-
fi
|
|
136
|
-
|
|
137
|
-
escaped_install_dir=$(printf '%s' "$install_dir" | sed 's/[\\"$`]/\\&/g')
|
|
138
|
-
posix_path_line="export PATH=\"$escaped_install_dir:\$PATH\""
|
|
139
|
-
fish_path_line="fish_add_path \"$escaped_install_dir\""
|
|
140
|
-
legacy_provenance=0
|
|
141
|
-
for candidate in "$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.profile"; do
|
|
142
|
-
if profile_has_block "$candidate" "$posix_path_line"; then
|
|
143
|
-
legacy_provenance=1
|
|
144
|
-
fi
|
|
145
|
-
done
|
|
146
|
-
if profile_has_block "$HOME/.config/fish/config.fish" "$fish_path_line"; then
|
|
147
|
-
legacy_provenance=1
|
|
148
|
-
fi
|
|
149
|
-
|
|
150
|
-
removed=0
|
|
151
|
-
if [ -e "$binary" ] || [ -L "$binary" ]; then
|
|
152
|
-
rm -f "$binary"
|
|
153
|
-
printf 'Removed %s\n' "$binary"
|
|
154
|
-
removed=1
|
|
155
|
-
fi
|
|
156
|
-
|
|
157
|
-
if [ "$update_path" -eq 1 ] && { [ "$marker_valid" -eq 1 ] || [ "$legacy_provenance" -eq 1 ]; } && directory_empty_except_marker "$install_dir" "$marker"; then
|
|
158
|
-
for candidate in "$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.profile"; do
|
|
159
|
-
remove_profile_block "$candidate" "$posix_path_line"
|
|
160
|
-
done
|
|
161
|
-
remove_profile_block "$HOME/.config/fish/config.fish" "$fish_path_line"
|
|
162
|
-
if [ "$marker_valid" -eq 1 ]; then
|
|
163
|
-
rm -f "$marker"
|
|
164
|
-
fi
|
|
165
|
-
rmdir "$install_dir" 2>/dev/null || true
|
|
166
|
-
removed=1
|
|
167
|
-
elif [ "$update_path" -eq 1 ] && [ -d "$install_dir" ]; then
|
|
168
|
-
printf 'Left %s on PATH because ownership was not proven or the directory is shared.\n' "$install_dir"
|
|
169
|
-
fi
|
|
170
|
-
|
|
171
|
-
etherscan_config=$(config_dir)
|
|
172
|
-
if [ -e "$etherscan_config" ] || [ -L "$etherscan_config" ]; then
|
|
173
|
-
rm -rf "$etherscan_config"
|
|
174
|
-
printf 'Removed %s\n' "$etherscan_config"
|
|
175
|
-
removed=1
|
|
176
|
-
fi
|
|
177
|
-
if [ "$removed" -eq 1 ]; then
|
|
178
|
-
printf 'Etherscan CLI uninstalled.\n'
|
|
179
|
-
else
|
|
180
|
-
printf 'Nothing to remove.\n'
|
|
181
|
-
fi
|
|
182
|
-
if [ -n "${ETHERSCAN_API_KEY:-}" ]; then
|
|
183
|
-
printf 'note: ETHERSCAN_API_KEY remains set; unset it in your shell.\n' >&2
|
|
184
|
-
fi
|
|
185
|
-
exit 0
|
|
186
|
-
fi
|
|
187
|
-
|
|
188
|
-
fetch_stdout() {
|
|
189
|
-
url=$1
|
|
190
|
-
if command -v curl >/dev/null 2>&1; then
|
|
191
|
-
curl -fsSL -A etherscan-cli-installer "$url"
|
|
192
|
-
elif command -v wget >/dev/null 2>&1; then
|
|
193
|
-
wget -qO- --user-agent=etherscan-cli-installer "$url"
|
|
194
|
-
else
|
|
195
|
-
die "curl or wget is required"
|
|
196
|
-
fi
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
fetch_file() {
|
|
200
|
-
base=$1
|
|
201
|
-
name=$2
|
|
202
|
-
destination=$3
|
|
203
|
-
|
|
204
|
-
if [ -d "$base" ]; then
|
|
205
|
-
cp "$base/$name" "$destination"
|
|
206
|
-
return
|
|
207
|
-
fi
|
|
208
|
-
|
|
209
|
-
case "$base" in
|
|
210
|
-
file://*)
|
|
211
|
-
cp "${base#file://}/$name" "$destination"
|
|
212
|
-
;;
|
|
213
|
-
https://*)
|
|
214
|
-
if command -v curl >/dev/null 2>&1; then
|
|
215
|
-
curl -fsSL -A etherscan-cli-installer "$base/$name" -o "$destination"
|
|
216
|
-
elif command -v wget >/dev/null 2>&1; then
|
|
217
|
-
wget -q --user-agent=etherscan-cli-installer "$base/$name" -O "$destination"
|
|
218
|
-
else
|
|
219
|
-
die "curl or wget is required"
|
|
220
|
-
fi
|
|
221
|
-
;;
|
|
222
|
-
*)
|
|
223
|
-
die "invalid download base URL or directory: $base"
|
|
224
|
-
;;
|
|
225
|
-
esac
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if [ -z "$version" ] || [ "$version" = latest ]; then
|
|
229
|
-
[ -z "$download_base" ] || die "a version is required with the installer test download source"
|
|
230
|
-
release_json=$(fetch_stdout "https://api.github.com/repos/$repository/releases/latest")
|
|
231
|
-
version=$(printf '%s\n' "$release_json" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | sed -n '1p')
|
|
232
|
-
[ -n "$version" ] || die "could not resolve the latest Etherscan CLI version"
|
|
233
|
-
fi
|
|
234
|
-
|
|
235
|
-
case "$version" in
|
|
236
|
-
v*) tag=$version; release_version=${version#v} ;;
|
|
237
|
-
*) tag="v$version"; release_version=$version ;;
|
|
238
|
-
esac
|
|
239
|
-
|
|
240
|
-
printf '%s\n' "$tag" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]+)?$' || die "invalid release version: $tag"
|
|
241
|
-
|
|
242
|
-
system_name=${ETHERSCAN_INSTALL_TEST_OS:-$(uname -s)}
|
|
243
|
-
case "$system_name" in
|
|
244
|
-
Linux|linux) os=linux ;;
|
|
245
|
-
Darwin|darwin) os=darwin ;;
|
|
246
|
-
*) die "unsupported operating system: $system_name" ;;
|
|
247
|
-
esac
|
|
248
|
-
|
|
249
|
-
machine_arch=${ETHERSCAN_INSTALL_TEST_ARCH:-$(uname -m)}
|
|
250
|
-
case "$machine_arch" in
|
|
251
|
-
x86_64|amd64) arch=amd64 ;;
|
|
252
|
-
arm64|aarch64) arch=arm64 ;;
|
|
253
|
-
*) die "unsupported architecture: $machine_arch. Etherscan CLI supports amd64 and arm64." ;;
|
|
254
|
-
esac
|
|
255
|
-
|
|
256
|
-
archive_name="etherscan_${release_version}_${os}_${arch}.tar.gz"
|
|
257
|
-
if [ -z "$download_base" ]; then
|
|
258
|
-
download_base="https://github.com/$repository/releases/download/$tag"
|
|
259
|
-
fi
|
|
260
|
-
|
|
261
|
-
temp_dir=$(mktemp -d 2>/dev/null || mktemp -d -t etherscan-install)
|
|
262
|
-
trap 'rm -rf "$temp_dir"' EXIT HUP INT TERM
|
|
263
|
-
archive_path="$temp_dir/$archive_name"
|
|
264
|
-
checksum_path="$temp_dir/checksums.txt"
|
|
265
|
-
source_executable="$temp_dir/etherscan"
|
|
266
|
-
|
|
267
|
-
printf 'Downloading Etherscan CLI %s for %s/%s...\n' "$release_version" "$os" "$arch"
|
|
268
|
-
fetch_file "$download_base" "$archive_name" "$archive_path"
|
|
269
|
-
fetch_file "$download_base" checksums.txt "$checksum_path"
|
|
270
|
-
|
|
271
|
-
expected_hash=$(awk -v name="$archive_name" '$2 == name || $2 == ("*" name) { print tolower($1); exit }' "$checksum_path")
|
|
272
|
-
[ -n "$expected_hash" ] || die "no checksum was published for $archive_name"
|
|
273
|
-
printf '%s\n' "$expected_hash" | grep -Eq '^[0-9a-f]{64}$' || die "invalid checksum published for $archive_name"
|
|
274
|
-
|
|
275
|
-
if command -v sha256sum >/dev/null 2>&1; then
|
|
276
|
-
actual_hash=$(sha256sum "$archive_path" | awk '{ print tolower($1) }')
|
|
277
|
-
elif command -v shasum >/dev/null 2>&1; then
|
|
278
|
-
actual_hash=$(shasum -a 256 "$archive_path" | awk '{ print tolower($1) }')
|
|
279
|
-
else
|
|
280
|
-
die "sha256sum or shasum is required to verify the download"
|
|
281
|
-
fi
|
|
282
|
-
|
|
283
|
-
[ "$actual_hash" = "$expected_hash" ] || die "checksum verification failed for $archive_name"
|
|
284
|
-
|
|
285
|
-
entry_count=$(tar -tzf "$archive_path" | awk '$0 == "etherscan" { count++ } END { print count + 0 }')
|
|
286
|
-
[ "$entry_count" -eq 1 ] || die "$archive_name must contain exactly one root-level etherscan"
|
|
287
|
-
tar -xOzf "$archive_path" etherscan >"$source_executable"
|
|
288
|
-
[ -s "$source_executable" ] || die "$archive_name contains an empty etherscan executable"
|
|
289
|
-
|
|
290
|
-
mkdir -p "$install_dir"
|
|
291
|
-
staged_executable="$install_dir/.etherscan.new.$$"
|
|
292
|
-
cp "$source_executable" "$staged_executable"
|
|
293
|
-
chmod 0755 "$staged_executable"
|
|
294
|
-
mv -f "$staged_executable" "$install_dir/etherscan"
|
|
295
|
-
|
|
296
|
-
path_updated=0
|
|
297
|
-
if [ "$update_path" -eq 1 ]; then
|
|
298
|
-
case ":$PATH:" in
|
|
299
|
-
*:"$install_dir":*) ;;
|
|
300
|
-
*)
|
|
301
|
-
shell_name=${SHELL:-sh}
|
|
302
|
-
shell_name=${shell_name##*/}
|
|
303
|
-
escaped_install_dir=$(printf '%s' "$install_dir" | sed 's/[\\"$`]/\\&/g')
|
|
304
|
-
if [ "$shell_name" = fish ]; then
|
|
305
|
-
profile="$HOME/.config/fish/config.fish"
|
|
306
|
-
mkdir -p "$(dirname "$profile")"
|
|
307
|
-
path_line="fish_add_path \"$escaped_install_dir\""
|
|
308
|
-
else
|
|
309
|
-
case "$shell_name" in
|
|
310
|
-
zsh) profile="$HOME/.zshrc" ;;
|
|
311
|
-
bash) profile="$HOME/.bashrc" ;;
|
|
312
|
-
*) profile="$HOME/.profile" ;;
|
|
313
|
-
esac
|
|
314
|
-
path_line="export PATH=\"$escaped_install_dir:\$PATH\""
|
|
315
|
-
fi
|
|
316
|
-
|
|
317
|
-
# Match the exact line we would write (whole-line, fixed-string) so an
|
|
318
|
-
# unrelated profile line that merely contains the path does not suppress
|
|
319
|
-
# the update, and a genuine duplicate is not appended.
|
|
320
|
-
if ! [ -f "$profile" ] || ! grep -Fx -e "$path_line" "$profile" >/dev/null 2>&1; then
|
|
321
|
-
{
|
|
322
|
-
printf '\n# Etherscan CLI\n'
|
|
323
|
-
printf '%s\n' "$path_line"
|
|
324
|
-
} >>"$profile"
|
|
325
|
-
path_updated=1
|
|
326
|
-
printf '%s' "$marker_content" >"$install_dir/$marker_name"
|
|
327
|
-
fi
|
|
328
|
-
;;
|
|
329
|
-
esac
|
|
330
|
-
fi
|
|
331
|
-
|
|
332
|
-
printf '\nEtherscan CLI %s installed successfully.\n' "$release_version"
|
|
333
|
-
printf 'Installed to: %s\n' "$install_dir/etherscan"
|
|
334
|
-
if [ "$update_path" -eq 0 ]; then
|
|
335
|
-
printf 'Add %s to PATH to run etherscan from any directory.\n' "$install_dir"
|
|
336
|
-
elif [ "$path_updated" -eq 1 ]; then
|
|
337
|
-
printf 'Open a new terminal, then run: etherscan version\n'
|
|
338
|
-
else
|
|
339
|
-
printf 'Run: etherscan version\n'
|
|
340
|
-
fi
|