@general-liquidity/gordon-cli 0.8.25 → 0.9.0-friends.7
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 +126 -126
- package/bin/gordon.cjs +52 -52
- package/lib/platform.cjs +78 -53
- package/lib/self-install.cjs +343 -343
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -1,126 +1,126 @@
|
|
|
1
|
-
<h1 align="center">Gordon CLI</h1>
|
|
2
|
-
|
|
3
|
-
<p align="center">
|
|
4
|
-
The Frontier Trading Agent
|
|
5
|
-
</p>
|
|
6
|
-
|
|
7
|
-
<p align="center">
|
|
8
|
-
<a href="https://www.npmjs.com/package/@general-liquidity/gordon-cli">npm</a> •
|
|
9
|
-
<a href="https://gordoncli.com">Website</a> •
|
|
10
|
-
<a href="https://docs.gordon.trade">Docs</a> •
|
|
11
|
-
<a href="https://github.com/general-liquidity/gordon-cli-dist/releases">Downloads</a>
|
|
12
|
-
</p>
|
|
13
|
-
|
|
14
|
-
## Install
|
|
15
|
-
|
|
16
|
-
`npm`:
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
npm install -g @general-liquidity/gordon-cli
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
If global npm install fails with `EACCES` / permission errors on Linux or macOS, use the user-local npm path instead:
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
npx @general-liquidity/gordon-cli@latest install
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
That installs Gordon into a user-writable bin directory without `sudo`.
|
|
29
|
-
|
|
30
|
-
`bun`:
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
bun add -g @general-liquidity/gordon-cli
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
`Homebrew`:
|
|
37
|
-
|
|
38
|
-
```bash
|
|
39
|
-
brew tap general-liquidity/gordon-cli-dist https://github.com/general-liquidity/gordon-cli-dist
|
|
40
|
-
brew install general-liquidity/gordon-cli-dist/gordon
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
`Scoop`:
|
|
44
|
-
|
|
45
|
-
```powershell
|
|
46
|
-
scoop bucket add gordon https://github.com/general-liquidity/gordon-cli-dist
|
|
47
|
-
scoop install gordon/gordon
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
Standalone install script:
|
|
51
|
-
|
|
52
|
-
```bash
|
|
53
|
-
curl -fsSL https://raw.githubusercontent.com/general-liquidity/gordon-cli-dist/main/install.sh | sh
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
Windows PowerShell:
|
|
57
|
-
|
|
58
|
-
```powershell
|
|
59
|
-
irm https://raw.githubusercontent.com/general-liquidity/gordon-cli-dist/main/install.ps1 | iex
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
The npm package is a thin wrapper. It downloads the matching prebuilt binary for your platform during install.
|
|
63
|
-
|
|
64
|
-
## npm Permission Fallback
|
|
65
|
-
|
|
66
|
-
Global `npm install -g` can fail on Unix machines when the npm global prefix is root-owned. Gordon now supports a universal npm fallback:
|
|
67
|
-
|
|
68
|
-
```bash
|
|
69
|
-
npx @general-liquidity/gordon-cli@latest install
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
If the chosen install directory is not already on `PATH`, Gordon prints the exact command to add it.
|
|
73
|
-
|
|
74
|
-
## Upgrades
|
|
75
|
-
|
|
76
|
-
Once installed, Gordon can upgrade itself with:
|
|
77
|
-
|
|
78
|
-
```bash
|
|
79
|
-
gordon --upgrade
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
That now resolves through the active install channel for npm, the user-local `npx` installer, Homebrew, Scoop, and the standalone install scripts.
|
|
83
|
-
|
|
84
|
-
## Supported binaries
|
|
85
|
-
|
|
86
|
-
- macOS arm64
|
|
87
|
-
- macOS x64
|
|
88
|
-
- Linux arm64
|
|
89
|
-
- Linux x64
|
|
90
|
-
- Windows x64
|
|
91
|
-
|
|
92
|
-
Release binaries and package manager manifests are published at:
|
|
93
|
-
|
|
94
|
-
- `https://github.com/general-liquidity/gordon-cli-dist/releases`
|
|
95
|
-
|
|
96
|
-
## Setup
|
|
97
|
-
|
|
98
|
-
Set one LLM provider key before first launch:
|
|
99
|
-
|
|
100
|
-
```bash
|
|
101
|
-
export OPENAI_API_KEY="sk-..."
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
or
|
|
105
|
-
|
|
106
|
-
```bash
|
|
107
|
-
export DEDALUS_API_KEY="dd-..."
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
or
|
|
111
|
-
|
|
112
|
-
```bash
|
|
113
|
-
export INCEPTION_API_KEY="..."
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
Then run:
|
|
117
|
-
|
|
118
|
-
```bash
|
|
119
|
-
gordon
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
## Docs
|
|
123
|
-
|
|
124
|
-
- Website: `https://gordoncli.com`
|
|
125
|
-
- Docs: `https://docs.gordon.trade`
|
|
126
|
-
- Public distribution repo: `https://github.com/general-liquidity/gordon-cli-dist`
|
|
1
|
+
<h1 align="center">Gordon CLI</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
The Frontier Trading Agent
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/@general-liquidity/gordon-cli">npm</a> •
|
|
9
|
+
<a href="https://gordoncli.com">Website</a> •
|
|
10
|
+
<a href="https://docs.gordon.trade">Docs</a> •
|
|
11
|
+
<a href="https://github.com/general-liquidity/gordon-cli-dist/releases">Downloads</a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
`npm`:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g @general-liquidity/gordon-cli
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
If global npm install fails with `EACCES` / permission errors on Linux or macOS, use the user-local npm path instead:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx @general-liquidity/gordon-cli@latest install
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
That installs Gordon into a user-writable bin directory without `sudo`.
|
|
29
|
+
|
|
30
|
+
`bun`:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
bun add -g @general-liquidity/gordon-cli
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`Homebrew`:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
brew tap general-liquidity/gordon-cli-dist https://github.com/general-liquidity/gordon-cli-dist
|
|
40
|
+
brew install general-liquidity/gordon-cli-dist/gordon
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`Scoop`:
|
|
44
|
+
|
|
45
|
+
```powershell
|
|
46
|
+
scoop bucket add gordon https://github.com/general-liquidity/gordon-cli-dist
|
|
47
|
+
scoop install gordon/gordon
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Standalone install script:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
curl -fsSL https://raw.githubusercontent.com/general-liquidity/gordon-cli-dist/main/install.sh | sh
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Windows PowerShell:
|
|
57
|
+
|
|
58
|
+
```powershell
|
|
59
|
+
irm https://raw.githubusercontent.com/general-liquidity/gordon-cli-dist/main/install.ps1 | iex
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The npm package is a thin wrapper. It downloads the matching prebuilt binary for your platform during install.
|
|
63
|
+
|
|
64
|
+
## npm Permission Fallback
|
|
65
|
+
|
|
66
|
+
Global `npm install -g` can fail on Unix machines when the npm global prefix is root-owned. Gordon now supports a universal npm fallback:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npx @general-liquidity/gordon-cli@latest install
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
If the chosen install directory is not already on `PATH`, Gordon prints the exact command to add it.
|
|
73
|
+
|
|
74
|
+
## Upgrades
|
|
75
|
+
|
|
76
|
+
Once installed, Gordon can upgrade itself with:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
gordon --upgrade
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
That now resolves through the active install channel for npm, the user-local `npx` installer, Homebrew, Scoop, and the standalone install scripts.
|
|
83
|
+
|
|
84
|
+
## Supported binaries
|
|
85
|
+
|
|
86
|
+
- macOS arm64
|
|
87
|
+
- macOS x64
|
|
88
|
+
- Linux arm64
|
|
89
|
+
- Linux x64
|
|
90
|
+
- Windows x64
|
|
91
|
+
|
|
92
|
+
Release binaries and package manager manifests are published at:
|
|
93
|
+
|
|
94
|
+
- `https://github.com/general-liquidity/gordon-cli-dist/releases`
|
|
95
|
+
|
|
96
|
+
## Setup
|
|
97
|
+
|
|
98
|
+
Set one LLM provider key before first launch:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
export OPENAI_API_KEY="sk-..."
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
or
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
export DEDALUS_API_KEY="dd-..."
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
or
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
export INCEPTION_API_KEY="..."
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Then run:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
gordon
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Docs
|
|
123
|
+
|
|
124
|
+
- Website: `https://gordoncli.com`
|
|
125
|
+
- Docs: `https://docs.gordon.trade`
|
|
126
|
+
- Public distribution repo: `https://github.com/general-liquidity/gordon-cli-dist`
|
package/bin/gordon.cjs
CHANGED
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const fs = require("fs");
|
|
4
|
-
const path = require("path");
|
|
5
|
-
const { spawn } = require("child_process");
|
|
6
|
-
const { getInstalledBinaryPath } = require("../lib/platform.cjs");
|
|
7
|
-
const { runSelfInstall } = require("../lib/self-install.cjs");
|
|
8
|
-
|
|
9
|
-
const packageRoot = path.resolve(__dirname, "..");
|
|
10
|
-
const args = process.argv.slice(2);
|
|
11
|
-
|
|
12
|
-
if (args[0] === "install") {
|
|
13
|
-
runSelfInstall(args.slice(1), { packageRoot }).then(
|
|
14
|
-
(code) => process.exit(code || 0),
|
|
15
|
-
(error) => {
|
|
16
|
-
console.error(`[gordon] ${error.message}`);
|
|
17
|
-
process.exit(1);
|
|
18
|
-
}
|
|
19
|
-
);
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
let binaryPath;
|
|
24
|
-
try {
|
|
25
|
-
binaryPath = getInstalledBinaryPath(packageRoot);
|
|
26
|
-
} catch (error) {
|
|
27
|
-
console.error(`[gordon] ${error.message}`);
|
|
28
|
-
process.exit(1);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (!fs.existsSync(binaryPath)) {
|
|
32
|
-
console.error(
|
|
33
|
-
"[gordon] The Gordon binary is missing. Reinstall with `npm install -g @general-liquidity/gordon-cli` or run `npx @general-liquidity/gordon-cli@latest install` for a user-local install."
|
|
34
|
-
);
|
|
35
|
-
process.exit(1);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const child = spawn(binaryPath, args, { stdio: "inherit" });
|
|
39
|
-
|
|
40
|
-
child.on("error", (error) => {
|
|
41
|
-
console.error(`[gordon] Failed to launch ${path.basename(binaryPath)}: ${error.message}`);
|
|
42
|
-
process.exit(1);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
child.on("exit", (code, signal) => {
|
|
46
|
-
if (signal) {
|
|
47
|
-
process.kill(process.pid, signal);
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
process.exit(code ?? 1);
|
|
52
|
-
});
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { spawn } = require("child_process");
|
|
6
|
+
const { getInstalledBinaryPath } = require("../lib/platform.cjs");
|
|
7
|
+
const { runSelfInstall } = require("../lib/self-install.cjs");
|
|
8
|
+
|
|
9
|
+
const packageRoot = path.resolve(__dirname, "..");
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
|
|
12
|
+
if (args[0] === "install") {
|
|
13
|
+
runSelfInstall(args.slice(1), { packageRoot }).then(
|
|
14
|
+
(code) => process.exit(code || 0),
|
|
15
|
+
(error) => {
|
|
16
|
+
console.error(`[gordon] ${error.message}`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let binaryPath;
|
|
24
|
+
try {
|
|
25
|
+
binaryPath = getInstalledBinaryPath(packageRoot);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error(`[gordon] ${error.message}`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!fs.existsSync(binaryPath)) {
|
|
32
|
+
console.error(
|
|
33
|
+
"[gordon] The Gordon binary is missing. Reinstall with `npm install -g @general-liquidity/gordon-cli` or run `npx @general-liquidity/gordon-cli@latest install` for a user-local install."
|
|
34
|
+
);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const child = spawn(binaryPath, args, { stdio: "inherit" });
|
|
39
|
+
|
|
40
|
+
child.on("error", (error) => {
|
|
41
|
+
console.error(`[gordon] Failed to launch ${path.basename(binaryPath)}: ${error.message}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
child.on("exit", (code, signal) => {
|
|
46
|
+
if (signal) {
|
|
47
|
+
process.kill(process.pid, signal);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
process.exit(code ?? 1);
|
|
52
|
+
});
|
package/lib/platform.cjs
CHANGED
|
@@ -1,53 +1,78 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
SUPPORTED_TARGETS
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Detect whether we're running on a musl libc Linux distro (Alpine,
|
|
6
|
+
* Void, etc). musl binaries are ABI-incompatible with glibc binaries —
|
|
7
|
+
* shipping the wrong one crashes immediately at startup.
|
|
8
|
+
*/
|
|
9
|
+
function isMuslLinux() {
|
|
10
|
+
if (process.platform !== "linux") return false;
|
|
11
|
+
// Heuristic 1: Alpine and Void leave a marker file
|
|
12
|
+
if (fs.existsSync("/etc/alpine-release")) return true;
|
|
13
|
+
// Heuristic 2: ldd prints the libc path
|
|
14
|
+
try {
|
|
15
|
+
const { execFileSync } = require("child_process");
|
|
16
|
+
const out = execFileSync("ldd", ["--version"], { stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" });
|
|
17
|
+
if (/musl/i.test(out)) return true;
|
|
18
|
+
} catch {
|
|
19
|
+
// ldd missing — fall through
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const SUPPORTED_TARGETS = {
|
|
25
|
+
"darwin:arm64": { assetName: "gordon-darwin-arm64", binaryName: "gordon" },
|
|
26
|
+
"darwin:x64": { assetName: "gordon-darwin-x64", binaryName: "gordon" },
|
|
27
|
+
"linux:arm64": { assetName: "gordon-linux-arm64", binaryName: "gordon" },
|
|
28
|
+
"linux:x64": { assetName: "gordon-linux-x64", binaryName: "gordon" },
|
|
29
|
+
"linux:arm64:musl": { assetName: "gordon-linux-arm64-musl", binaryName: "gordon" },
|
|
30
|
+
"linux:x64:musl": { assetName: "gordon-linux-x64-musl", binaryName: "gordon" },
|
|
31
|
+
"win32:arm64": { assetName: "gordon-windows-arm64.exe", binaryName: "gordon.exe" },
|
|
32
|
+
"win32:x64": { assetName: "gordon-windows-x64.exe", binaryName: "gordon.exe" }
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function getTarget(platform = process.platform, arch = process.arch) {
|
|
36
|
+
// Linux gets a musl variant — pick the right one based on the host libc
|
|
37
|
+
let key = `${platform}:${arch}`;
|
|
38
|
+
if (platform === "linux" && isMuslLinux()) {
|
|
39
|
+
const muslKey = `${platform}:${arch}:musl`;
|
|
40
|
+
if (SUPPORTED_TARGETS[muslKey]) key = muslKey;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const target = SUPPORTED_TARGETS[key];
|
|
44
|
+
if (target) {
|
|
45
|
+
return target;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const supported = Object.keys(SUPPORTED_TARGETS)
|
|
49
|
+
.sort()
|
|
50
|
+
.join(", ");
|
|
51
|
+
throw new Error(`Unsupported platform ${platform}/${arch}. Supported targets: ${supported}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getInstallDirectory(packageRoot = path.resolve(__dirname, "..")) {
|
|
55
|
+
return path.join(packageRoot, "vendor");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getInstalledBinaryPath(packageRoot = path.resolve(__dirname, ".."), platform, arch) {
|
|
59
|
+
const { binaryName } = getTarget(platform, arch);
|
|
60
|
+
return path.join(getInstallDirectory(packageRoot), binaryName);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getDownloadUrl(version, platform = process.platform, arch = process.arch) {
|
|
64
|
+
const { assetName } = getTarget(platform, arch);
|
|
65
|
+
const cleanVersion = String(version).replace(/^v/, "");
|
|
66
|
+
const distRepo = process.env.GORDON_BINARY_DIST_REPO || "general-liquidity/gordon-cli-dist";
|
|
67
|
+
const baseUrl =
|
|
68
|
+
process.env.GORDON_BINARY_BASE_URL || `https://github.com/${distRepo}/releases/download/v${cleanVersion}`;
|
|
69
|
+
return `${baseUrl.replace(/\/$/, "")}/${assetName}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = {
|
|
73
|
+
SUPPORTED_TARGETS,
|
|
74
|
+
getDownloadUrl,
|
|
75
|
+
getInstallDirectory,
|
|
76
|
+
getInstalledBinaryPath,
|
|
77
|
+
getTarget
|
|
78
|
+
};
|
package/lib/self-install.cjs
CHANGED
|
@@ -1,343 +1,343 @@
|
|
|
1
|
-
const fs = require("fs");
|
|
2
|
-
const os = require("os");
|
|
3
|
-
const path = require("path");
|
|
4
|
-
const https = require("https");
|
|
5
|
-
const { pipeline } = require("stream/promises");
|
|
6
|
-
const { getDownloadUrl, getInstalledBinaryPath, getTarget } = require("./platform.cjs");
|
|
7
|
-
const pkg = require("../package.json");
|
|
8
|
-
const INSTALL_METADATA_FILENAME = "gordon-install.json";
|
|
9
|
-
|
|
10
|
-
function log(message) {
|
|
11
|
-
console.log(`[gordon] ${message}`);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function parseArgs(args) {
|
|
15
|
-
const options = {
|
|
16
|
-
help: false,
|
|
17
|
-
targetDir: null
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
21
|
-
const arg = args[index];
|
|
22
|
-
if (arg === "--help" || arg === "-h") {
|
|
23
|
-
options.help = true;
|
|
24
|
-
continue;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if ((arg === "--target-dir" || arg === "--dir") && args[index + 1]) {
|
|
28
|
-
options.targetDir = path.resolve(args[index + 1]);
|
|
29
|
-
index += 1;
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
throw new Error(`Unknown install option: ${arg}`);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return options;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function printHelp() {
|
|
40
|
-
console.log(`gordon install
|
|
41
|
-
|
|
42
|
-
Install Gordon into a user-writable bin directory without requiring \`npm install -g\`.
|
|
43
|
-
|
|
44
|
-
Usage:
|
|
45
|
-
npx @general-liquidity/gordon-cli@latest install
|
|
46
|
-
gordon install --target-dir <directory>
|
|
47
|
-
|
|
48
|
-
Options:
|
|
49
|
-
--target-dir <dir> Override the install directory
|
|
50
|
-
-h, --help Show this help
|
|
51
|
-
`);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function normalizeForCompare(value) {
|
|
55
|
-
const resolved = path.resolve(value);
|
|
56
|
-
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function splitPathEntries(envPath = process.env.PATH || "") {
|
|
60
|
-
return envPath.split(path.delimiter).filter(Boolean);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function pathContainsDirectory(directory, env = process.env) {
|
|
64
|
-
const target = normalizeForCompare(directory);
|
|
65
|
-
return splitPathEntries(env.PATH).some((entry) => {
|
|
66
|
-
try {
|
|
67
|
-
return normalizeForCompare(entry) === target;
|
|
68
|
-
} catch {
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function uniqueDirectories(values) {
|
|
75
|
-
const seen = new Set();
|
|
76
|
-
const result = [];
|
|
77
|
-
for (const value of values) {
|
|
78
|
-
if (!value) {
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const normalized = normalizeForCompare(value);
|
|
83
|
-
if (seen.has(normalized)) {
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
seen.add(normalized);
|
|
88
|
-
result.push(path.resolve(value));
|
|
89
|
-
}
|
|
90
|
-
return result;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async function isWritableDirectory(directory, { create } = { create: false }) {
|
|
94
|
-
try {
|
|
95
|
-
if (create) {
|
|
96
|
-
await fs.promises.mkdir(directory, { recursive: true });
|
|
97
|
-
} else {
|
|
98
|
-
const stats = await fs.promises.stat(directory);
|
|
99
|
-
if (!stats.isDirectory()) {
|
|
100
|
-
return false;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const probePath = path.join(directory, `.gordon-write-test-${process.pid}-${Date.now()}`);
|
|
105
|
-
await fs.promises.writeFile(probePath, "");
|
|
106
|
-
await fs.promises.rm(probePath, { force: true });
|
|
107
|
-
return true;
|
|
108
|
-
} catch {
|
|
109
|
-
return false;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async function resolveInstallDirectory(explicitTargetDir = null, env = process.env) {
|
|
114
|
-
if (explicitTargetDir) {
|
|
115
|
-
return {
|
|
116
|
-
directory: explicitTargetDir,
|
|
117
|
-
inPath: pathContainsDirectory(explicitTargetDir, env)
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const homeDirectory = os.homedir();
|
|
122
|
-
const pathEntries = splitPathEntries(env.PATH);
|
|
123
|
-
const standardCandidates = [];
|
|
124
|
-
const pathCandidates = [];
|
|
125
|
-
|
|
126
|
-
if (process.platform === "win32") {
|
|
127
|
-
if (env.APPDATA) {
|
|
128
|
-
standardCandidates.push(path.join(env.APPDATA, "npm"));
|
|
129
|
-
}
|
|
130
|
-
if (env.LOCALAPPDATA) {
|
|
131
|
-
standardCandidates.push(path.join(env.LOCALAPPDATA, "Microsoft", "WinGet", "Links"));
|
|
132
|
-
standardCandidates.push(path.join(env.LOCALAPPDATA, "gordon"));
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
for (const entry of pathEntries) {
|
|
136
|
-
const resolvedEntry = path.resolve(entry);
|
|
137
|
-
const normalizedEntry = normalizeForCompare(resolvedEntry);
|
|
138
|
-
const userRoots = uniqueDirectories([
|
|
139
|
-
env.USERPROFILE,
|
|
140
|
-
env.LOCALAPPDATA,
|
|
141
|
-
env.APPDATA,
|
|
142
|
-
homeDirectory
|
|
143
|
-
]);
|
|
144
|
-
const withinUserRoot = userRoots.some((root) => {
|
|
145
|
-
const normalizedRoot = normalizeForCompare(root);
|
|
146
|
-
return normalizedEntry === normalizedRoot || normalizedEntry.startsWith(`${normalizedRoot}${path.sep}`);
|
|
147
|
-
});
|
|
148
|
-
if (
|
|
149
|
-
withinUserRoot
|
|
150
|
-
&& !normalizedEntry.includes(`${path.sep}node_modules${path.sep}`)
|
|
151
|
-
) {
|
|
152
|
-
pathCandidates.push(resolvedEntry);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
} else {
|
|
156
|
-
standardCandidates.push(path.join(homeDirectory, ".local", "bin"));
|
|
157
|
-
standardCandidates.push(path.join(homeDirectory, "bin"));
|
|
158
|
-
|
|
159
|
-
for (const entry of pathEntries) {
|
|
160
|
-
const resolvedEntry = path.resolve(entry);
|
|
161
|
-
const normalizedEntry = normalizeForCompare(resolvedEntry);
|
|
162
|
-
const normalizedHome = `${normalizeForCompare(homeDirectory)}${path.sep}`;
|
|
163
|
-
if (
|
|
164
|
-
normalizedEntry.startsWith(normalizedHome)
|
|
165
|
-
&& !normalizedEntry.includes(`${path.sep}node_modules${path.sep}`)
|
|
166
|
-
) {
|
|
167
|
-
pathCandidates.push(resolvedEntry);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const orderedCandidates = uniqueDirectories([
|
|
173
|
-
...standardCandidates.filter((candidate) => pathContainsDirectory(candidate, env)),
|
|
174
|
-
...pathCandidates,
|
|
175
|
-
...standardCandidates
|
|
176
|
-
]);
|
|
177
|
-
|
|
178
|
-
for (const candidate of orderedCandidates) {
|
|
179
|
-
const create = standardCandidates.some((standardCandidate) => normalizeForCompare(standardCandidate) === normalizeForCompare(candidate));
|
|
180
|
-
if (await isWritableDirectory(candidate, { create })) {
|
|
181
|
-
return {
|
|
182
|
-
directory: candidate,
|
|
183
|
-
inPath: pathContainsDirectory(candidate, env)
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
throw new Error("Could not find a writable user install directory.");
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async function downloadBinary(url, destinationPath, redirectCount = 0) {
|
|
192
|
-
if (redirectCount > 5) {
|
|
193
|
-
throw new Error(`Too many redirects while downloading ${url}`);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
await new Promise((resolve, reject) => {
|
|
197
|
-
const request = https.get(
|
|
198
|
-
url,
|
|
199
|
-
{ headers: { "User-Agent": "gordon-npm-installer" } },
|
|
200
|
-
async (response) => {
|
|
201
|
-
if (
|
|
202
|
-
response.statusCode &&
|
|
203
|
-
response.statusCode >= 300 &&
|
|
204
|
-
response.statusCode < 400 &&
|
|
205
|
-
response.headers.location
|
|
206
|
-
) {
|
|
207
|
-
response.resume();
|
|
208
|
-
try {
|
|
209
|
-
await downloadBinary(response.headers.location, destinationPath, redirectCount + 1);
|
|
210
|
-
resolve();
|
|
211
|
-
} catch (error) {
|
|
212
|
-
reject(error);
|
|
213
|
-
}
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (response.statusCode !== 200) {
|
|
218
|
-
response.resume();
|
|
219
|
-
reject(new Error(`Download failed with status ${response.statusCode} for ${url}`));
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const fileStream = fs.createWriteStream(destinationPath);
|
|
224
|
-
pipeline(response, fileStream).then(resolve, reject);
|
|
225
|
-
}
|
|
226
|
-
);
|
|
227
|
-
|
|
228
|
-
request.on("error", reject);
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
async function finalizeBinary(tempPath, targetPath) {
|
|
233
|
-
if (process.platform !== "win32") {
|
|
234
|
-
await fs.promises.chmod(tempPath, 0o755);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
await fs.promises.rm(targetPath, { force: true });
|
|
238
|
-
await fs.promises.rename(tempPath, targetPath);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
async function writeInstallMetadata(targetDirectory, version, channel) {
|
|
242
|
-
const metadataPath = path.join(targetDirectory, INSTALL_METADATA_FILENAME);
|
|
243
|
-
const payload = {
|
|
244
|
-
channel,
|
|
245
|
-
installDir: targetDirectory,
|
|
246
|
-
version,
|
|
247
|
-
installedAt: new Date().toISOString()
|
|
248
|
-
};
|
|
249
|
-
await fs.promises.writeFile(metadataPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function getUnixPathHint(directory, env = process.env) {
|
|
253
|
-
const shell = env.SHELL || "";
|
|
254
|
-
if (shell.includes("fish")) {
|
|
255
|
-
return {
|
|
256
|
-
profile: "~/.config/fish/config.fish",
|
|
257
|
-
command: `fish_add_path ${directory.replace(os.homedir(), "$HOME")}`
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
if (shell.includes("zsh")) {
|
|
262
|
-
return {
|
|
263
|
-
profile: "~/.zshrc",
|
|
264
|
-
command: `echo 'export PATH=\"${directory.replace(os.homedir(), "$HOME")}:$PATH\"' >> ~/.zshrc`
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
return {
|
|
269
|
-
profile: "~/.bashrc",
|
|
270
|
-
command: `echo 'export PATH=\"${directory.replace(os.homedir(), "$HOME")}:$PATH\"' >> ~/.bashrc`
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function printPathGuidance(directory) {
|
|
275
|
-
if (process.platform === "win32") {
|
|
276
|
-
log(`Add ${directory} to your user PATH, then open a new terminal.`);
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const hint = getUnixPathHint(directory);
|
|
281
|
-
log(`Add ${directory} to PATH if your shell cannot find \`gordon\` yet.`);
|
|
282
|
-
console.log(` ${hint.command}`);
|
|
283
|
-
console.log(` # then restart your shell or source ${hint.profile}`);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
async function installBinary({ targetDirectory, sourceBinaryPath, version }) {
|
|
287
|
-
const { binaryName, assetName } = getTarget();
|
|
288
|
-
await fs.promises.mkdir(targetDirectory, { recursive: true });
|
|
289
|
-
|
|
290
|
-
const installPath = path.join(targetDirectory, binaryName);
|
|
291
|
-
const tempPath = `${installPath}.tmp`;
|
|
292
|
-
await fs.promises.rm(tempPath, { force: true });
|
|
293
|
-
|
|
294
|
-
if (sourceBinaryPath && fs.existsSync(sourceBinaryPath)) {
|
|
295
|
-
log(`Copying ${path.basename(sourceBinaryPath)} to ${installPath}`);
|
|
296
|
-
await fs.promises.copyFile(sourceBinaryPath, tempPath);
|
|
297
|
-
} else {
|
|
298
|
-
const downloadUrl = getDownloadUrl(version);
|
|
299
|
-
log(`Downloading ${assetName} from ${downloadUrl}`);
|
|
300
|
-
await downloadBinary(downloadUrl, tempPath);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
await finalizeBinary(tempPath, installPath);
|
|
304
|
-
return installPath;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
async function runSelfInstall(args, options = {}) {
|
|
308
|
-
const parsed = parseArgs(args);
|
|
309
|
-
if (parsed.help) {
|
|
310
|
-
printHelp();
|
|
311
|
-
return 0;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const packageRoot = options.packageRoot || path.resolve(__dirname, "..");
|
|
315
|
-
const version = String(options.version || pkg.version).replace(/^v/, "");
|
|
316
|
-
const bundledBinaryPath = options.sourceBinaryPath || getInstalledBinaryPath(packageRoot);
|
|
317
|
-
const sourceBinaryPath = fs.existsSync(bundledBinaryPath) ? bundledBinaryPath : null;
|
|
318
|
-
const { directory, inPath } = await resolveInstallDirectory(parsed.targetDir);
|
|
319
|
-
const installPath = await installBinary({
|
|
320
|
-
targetDirectory: directory,
|
|
321
|
-
sourceBinaryPath,
|
|
322
|
-
version
|
|
323
|
-
});
|
|
324
|
-
await writeInstallMetadata(directory, version, options.channel || "npx");
|
|
325
|
-
|
|
326
|
-
console.log("");
|
|
327
|
-
log(`Installed Gordon v${version} to ${installPath}`);
|
|
328
|
-
if (!inPath) {
|
|
329
|
-
printPathGuidance(directory);
|
|
330
|
-
}
|
|
331
|
-
console.log("");
|
|
332
|
-
console.log("Next steps:");
|
|
333
|
-
console.log(" gordon --help");
|
|
334
|
-
console.log(" gordon");
|
|
335
|
-
|
|
336
|
-
return 0;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
module.exports = {
|
|
340
|
-
parseArgs,
|
|
341
|
-
resolveInstallDirectory,
|
|
342
|
-
runSelfInstall
|
|
343
|
-
};
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const os = require("os");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const https = require("https");
|
|
5
|
+
const { pipeline } = require("stream/promises");
|
|
6
|
+
const { getDownloadUrl, getInstalledBinaryPath, getTarget } = require("./platform.cjs");
|
|
7
|
+
const pkg = require("../package.json");
|
|
8
|
+
const INSTALL_METADATA_FILENAME = "gordon-install.json";
|
|
9
|
+
|
|
10
|
+
function log(message) {
|
|
11
|
+
console.log(`[gordon] ${message}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parseArgs(args) {
|
|
15
|
+
const options = {
|
|
16
|
+
help: false,
|
|
17
|
+
targetDir: null
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
21
|
+
const arg = args[index];
|
|
22
|
+
if (arg === "--help" || arg === "-h") {
|
|
23
|
+
options.help = true;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if ((arg === "--target-dir" || arg === "--dir") && args[index + 1]) {
|
|
28
|
+
options.targetDir = path.resolve(args[index + 1]);
|
|
29
|
+
index += 1;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
throw new Error(`Unknown install option: ${arg}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return options;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function printHelp() {
|
|
40
|
+
console.log(`gordon install
|
|
41
|
+
|
|
42
|
+
Install Gordon into a user-writable bin directory without requiring \`npm install -g\`.
|
|
43
|
+
|
|
44
|
+
Usage:
|
|
45
|
+
npx @general-liquidity/gordon-cli@latest install
|
|
46
|
+
gordon install --target-dir <directory>
|
|
47
|
+
|
|
48
|
+
Options:
|
|
49
|
+
--target-dir <dir> Override the install directory
|
|
50
|
+
-h, --help Show this help
|
|
51
|
+
`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeForCompare(value) {
|
|
55
|
+
const resolved = path.resolve(value);
|
|
56
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function splitPathEntries(envPath = process.env.PATH || "") {
|
|
60
|
+
return envPath.split(path.delimiter).filter(Boolean);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function pathContainsDirectory(directory, env = process.env) {
|
|
64
|
+
const target = normalizeForCompare(directory);
|
|
65
|
+
return splitPathEntries(env.PATH).some((entry) => {
|
|
66
|
+
try {
|
|
67
|
+
return normalizeForCompare(entry) === target;
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function uniqueDirectories(values) {
|
|
75
|
+
const seen = new Set();
|
|
76
|
+
const result = [];
|
|
77
|
+
for (const value of values) {
|
|
78
|
+
if (!value) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const normalized = normalizeForCompare(value);
|
|
83
|
+
if (seen.has(normalized)) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
seen.add(normalized);
|
|
88
|
+
result.push(path.resolve(value));
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function isWritableDirectory(directory, { create } = { create: false }) {
|
|
94
|
+
try {
|
|
95
|
+
if (create) {
|
|
96
|
+
await fs.promises.mkdir(directory, { recursive: true });
|
|
97
|
+
} else {
|
|
98
|
+
const stats = await fs.promises.stat(directory);
|
|
99
|
+
if (!stats.isDirectory()) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const probePath = path.join(directory, `.gordon-write-test-${process.pid}-${Date.now()}`);
|
|
105
|
+
await fs.promises.writeFile(probePath, "");
|
|
106
|
+
await fs.promises.rm(probePath, { force: true });
|
|
107
|
+
return true;
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function resolveInstallDirectory(explicitTargetDir = null, env = process.env) {
|
|
114
|
+
if (explicitTargetDir) {
|
|
115
|
+
return {
|
|
116
|
+
directory: explicitTargetDir,
|
|
117
|
+
inPath: pathContainsDirectory(explicitTargetDir, env)
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const homeDirectory = os.homedir();
|
|
122
|
+
const pathEntries = splitPathEntries(env.PATH);
|
|
123
|
+
const standardCandidates = [];
|
|
124
|
+
const pathCandidates = [];
|
|
125
|
+
|
|
126
|
+
if (process.platform === "win32") {
|
|
127
|
+
if (env.APPDATA) {
|
|
128
|
+
standardCandidates.push(path.join(env.APPDATA, "npm"));
|
|
129
|
+
}
|
|
130
|
+
if (env.LOCALAPPDATA) {
|
|
131
|
+
standardCandidates.push(path.join(env.LOCALAPPDATA, "Microsoft", "WinGet", "Links"));
|
|
132
|
+
standardCandidates.push(path.join(env.LOCALAPPDATA, "gordon"));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const entry of pathEntries) {
|
|
136
|
+
const resolvedEntry = path.resolve(entry);
|
|
137
|
+
const normalizedEntry = normalizeForCompare(resolvedEntry);
|
|
138
|
+
const userRoots = uniqueDirectories([
|
|
139
|
+
env.USERPROFILE,
|
|
140
|
+
env.LOCALAPPDATA,
|
|
141
|
+
env.APPDATA,
|
|
142
|
+
homeDirectory
|
|
143
|
+
]);
|
|
144
|
+
const withinUserRoot = userRoots.some((root) => {
|
|
145
|
+
const normalizedRoot = normalizeForCompare(root);
|
|
146
|
+
return normalizedEntry === normalizedRoot || normalizedEntry.startsWith(`${normalizedRoot}${path.sep}`);
|
|
147
|
+
});
|
|
148
|
+
if (
|
|
149
|
+
withinUserRoot
|
|
150
|
+
&& !normalizedEntry.includes(`${path.sep}node_modules${path.sep}`)
|
|
151
|
+
) {
|
|
152
|
+
pathCandidates.push(resolvedEntry);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
standardCandidates.push(path.join(homeDirectory, ".local", "bin"));
|
|
157
|
+
standardCandidates.push(path.join(homeDirectory, "bin"));
|
|
158
|
+
|
|
159
|
+
for (const entry of pathEntries) {
|
|
160
|
+
const resolvedEntry = path.resolve(entry);
|
|
161
|
+
const normalizedEntry = normalizeForCompare(resolvedEntry);
|
|
162
|
+
const normalizedHome = `${normalizeForCompare(homeDirectory)}${path.sep}`;
|
|
163
|
+
if (
|
|
164
|
+
normalizedEntry.startsWith(normalizedHome)
|
|
165
|
+
&& !normalizedEntry.includes(`${path.sep}node_modules${path.sep}`)
|
|
166
|
+
) {
|
|
167
|
+
pathCandidates.push(resolvedEntry);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const orderedCandidates = uniqueDirectories([
|
|
173
|
+
...standardCandidates.filter((candidate) => pathContainsDirectory(candidate, env)),
|
|
174
|
+
...pathCandidates,
|
|
175
|
+
...standardCandidates
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
for (const candidate of orderedCandidates) {
|
|
179
|
+
const create = standardCandidates.some((standardCandidate) => normalizeForCompare(standardCandidate) === normalizeForCompare(candidate));
|
|
180
|
+
if (await isWritableDirectory(candidate, { create })) {
|
|
181
|
+
return {
|
|
182
|
+
directory: candidate,
|
|
183
|
+
inPath: pathContainsDirectory(candidate, env)
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
throw new Error("Could not find a writable user install directory.");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function downloadBinary(url, destinationPath, redirectCount = 0) {
|
|
192
|
+
if (redirectCount > 5) {
|
|
193
|
+
throw new Error(`Too many redirects while downloading ${url}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
await new Promise((resolve, reject) => {
|
|
197
|
+
const request = https.get(
|
|
198
|
+
url,
|
|
199
|
+
{ headers: { "User-Agent": "gordon-npm-installer" } },
|
|
200
|
+
async (response) => {
|
|
201
|
+
if (
|
|
202
|
+
response.statusCode &&
|
|
203
|
+
response.statusCode >= 300 &&
|
|
204
|
+
response.statusCode < 400 &&
|
|
205
|
+
response.headers.location
|
|
206
|
+
) {
|
|
207
|
+
response.resume();
|
|
208
|
+
try {
|
|
209
|
+
await downloadBinary(response.headers.location, destinationPath, redirectCount + 1);
|
|
210
|
+
resolve();
|
|
211
|
+
} catch (error) {
|
|
212
|
+
reject(error);
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (response.statusCode !== 200) {
|
|
218
|
+
response.resume();
|
|
219
|
+
reject(new Error(`Download failed with status ${response.statusCode} for ${url}`));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const fileStream = fs.createWriteStream(destinationPath);
|
|
224
|
+
pipeline(response, fileStream).then(resolve, reject);
|
|
225
|
+
}
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
request.on("error", reject);
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function finalizeBinary(tempPath, targetPath) {
|
|
233
|
+
if (process.platform !== "win32") {
|
|
234
|
+
await fs.promises.chmod(tempPath, 0o755);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
await fs.promises.rm(targetPath, { force: true });
|
|
238
|
+
await fs.promises.rename(tempPath, targetPath);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function writeInstallMetadata(targetDirectory, version, channel) {
|
|
242
|
+
const metadataPath = path.join(targetDirectory, INSTALL_METADATA_FILENAME);
|
|
243
|
+
const payload = {
|
|
244
|
+
channel,
|
|
245
|
+
installDir: targetDirectory,
|
|
246
|
+
version,
|
|
247
|
+
installedAt: new Date().toISOString()
|
|
248
|
+
};
|
|
249
|
+
await fs.promises.writeFile(metadataPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function getUnixPathHint(directory, env = process.env) {
|
|
253
|
+
const shell = env.SHELL || "";
|
|
254
|
+
if (shell.includes("fish")) {
|
|
255
|
+
return {
|
|
256
|
+
profile: "~/.config/fish/config.fish",
|
|
257
|
+
command: `fish_add_path ${directory.replace(os.homedir(), "$HOME")}`
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (shell.includes("zsh")) {
|
|
262
|
+
return {
|
|
263
|
+
profile: "~/.zshrc",
|
|
264
|
+
command: `echo 'export PATH=\"${directory.replace(os.homedir(), "$HOME")}:$PATH\"' >> ~/.zshrc`
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
profile: "~/.bashrc",
|
|
270
|
+
command: `echo 'export PATH=\"${directory.replace(os.homedir(), "$HOME")}:$PATH\"' >> ~/.bashrc`
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function printPathGuidance(directory) {
|
|
275
|
+
if (process.platform === "win32") {
|
|
276
|
+
log(`Add ${directory} to your user PATH, then open a new terminal.`);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const hint = getUnixPathHint(directory);
|
|
281
|
+
log(`Add ${directory} to PATH if your shell cannot find \`gordon\` yet.`);
|
|
282
|
+
console.log(` ${hint.command}`);
|
|
283
|
+
console.log(` # then restart your shell or source ${hint.profile}`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function installBinary({ targetDirectory, sourceBinaryPath, version }) {
|
|
287
|
+
const { binaryName, assetName } = getTarget();
|
|
288
|
+
await fs.promises.mkdir(targetDirectory, { recursive: true });
|
|
289
|
+
|
|
290
|
+
const installPath = path.join(targetDirectory, binaryName);
|
|
291
|
+
const tempPath = `${installPath}.tmp`;
|
|
292
|
+
await fs.promises.rm(tempPath, { force: true });
|
|
293
|
+
|
|
294
|
+
if (sourceBinaryPath && fs.existsSync(sourceBinaryPath)) {
|
|
295
|
+
log(`Copying ${path.basename(sourceBinaryPath)} to ${installPath}`);
|
|
296
|
+
await fs.promises.copyFile(sourceBinaryPath, tempPath);
|
|
297
|
+
} else {
|
|
298
|
+
const downloadUrl = getDownloadUrl(version);
|
|
299
|
+
log(`Downloading ${assetName} from ${downloadUrl}`);
|
|
300
|
+
await downloadBinary(downloadUrl, tempPath);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
await finalizeBinary(tempPath, installPath);
|
|
304
|
+
return installPath;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function runSelfInstall(args, options = {}) {
|
|
308
|
+
const parsed = parseArgs(args);
|
|
309
|
+
if (parsed.help) {
|
|
310
|
+
printHelp();
|
|
311
|
+
return 0;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const packageRoot = options.packageRoot || path.resolve(__dirname, "..");
|
|
315
|
+
const version = String(options.version || pkg.version).replace(/^v/, "");
|
|
316
|
+
const bundledBinaryPath = options.sourceBinaryPath || getInstalledBinaryPath(packageRoot);
|
|
317
|
+
const sourceBinaryPath = fs.existsSync(bundledBinaryPath) ? bundledBinaryPath : null;
|
|
318
|
+
const { directory, inPath } = await resolveInstallDirectory(parsed.targetDir);
|
|
319
|
+
const installPath = await installBinary({
|
|
320
|
+
targetDirectory: directory,
|
|
321
|
+
sourceBinaryPath,
|
|
322
|
+
version
|
|
323
|
+
});
|
|
324
|
+
await writeInstallMetadata(directory, version, options.channel || "npx");
|
|
325
|
+
|
|
326
|
+
console.log("");
|
|
327
|
+
log(`Installed Gordon v${version} to ${installPath}`);
|
|
328
|
+
if (!inPath) {
|
|
329
|
+
printPathGuidance(directory);
|
|
330
|
+
}
|
|
331
|
+
console.log("");
|
|
332
|
+
console.log("Next steps:");
|
|
333
|
+
console.log(" gordon --help");
|
|
334
|
+
console.log(" gordon");
|
|
335
|
+
|
|
336
|
+
return 0;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
module.exports = {
|
|
340
|
+
parseArgs,
|
|
341
|
+
resolveInstallDirectory,
|
|
342
|
+
runSelfInstall
|
|
343
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@general-liquidity/gordon-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0-friends.7",
|
|
4
4
|
"description": "The Frontier Trading Agent",
|
|
5
5
|
"author": "General Liquidity, Inc.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,6 +32,10 @@
|
|
|
32
32
|
"engines": {
|
|
33
33
|
"node": ">=18"
|
|
34
34
|
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public",
|
|
37
|
+
"tag": "friends"
|
|
38
|
+
},
|
|
35
39
|
"scripts": {
|
|
36
40
|
"prepack": "node ../scripts/prepare-npm-wrapper.cjs",
|
|
37
41
|
"postinstall": "node scripts/postinstall.cjs"
|