@general-liquidity/gordon-cli 0.8.21 → 0.8.24

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 CHANGED
@@ -19,12 +19,34 @@
19
19
  npm install -g @general-liquidity/gordon-cli
20
20
  ```
21
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
+
22
30
  `bun`:
23
31
 
24
32
  ```bash
25
33
  bun add -g @general-liquidity/gordon-cli
26
34
  ```
27
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
+
28
50
  Standalone install script:
29
51
 
30
52
  ```bash
@@ -39,6 +61,26 @@ irm https://raw.githubusercontent.com/general-liquidity/gordon-cli-dist/main/ins
39
61
 
40
62
  The npm package is a thin wrapper. It downloads the matching prebuilt binary for your platform during install.
41
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
+
42
84
  ## Supported binaries
43
85
 
44
86
  - macOS arm64
package/bin/gordon.cjs CHANGED
@@ -4,8 +4,21 @@ const fs = require("fs");
4
4
  const path = require("path");
5
5
  const { spawn } = require("child_process");
6
6
  const { getInstalledBinaryPath } = require("../lib/platform.cjs");
7
+ const { runSelfInstall } = require("../lib/self-install.cjs");
7
8
 
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
+ }
9
22
 
10
23
  let binaryPath;
11
24
  try {
@@ -17,12 +30,12 @@ try {
17
30
 
18
31
  if (!fs.existsSync(binaryPath)) {
19
32
  console.error(
20
- "[gordon] The Gordon binary is missing. Reinstall with `npm install -g @general-liquidity/gordon-cli`."
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."
21
34
  );
22
35
  process.exit(1);
23
36
  }
24
37
 
25
- const child = spawn(binaryPath, process.argv.slice(2), { stdio: "inherit" });
38
+ const child = spawn(binaryPath, args, { stdio: "inherit" });
26
39
 
27
40
  child.on("error", (error) => {
28
41
  console.error(`[gordon] Failed to launch ${path.basename(binaryPath)}: ${error.message}`);
@@ -0,0 +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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@general-liquidity/gordon-cli",
3
- "version": "0.8.21",
3
+ "version": "0.8.24",
4
4
  "description": "The Frontier Trading Agent",
5
5
  "author": "General Liquidity, Inc.",
6
6
  "license": "MIT",