@ggcode-cli/ggcode 1.1.2 → 1.1.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 +17 -6
- package/bin/{ggcode.js → ggcode-bootstrap.js} +7 -3
- package/lib/install.js +211 -20
- package/package.json +2 -2
- package/scripts/postinstall.js +13 -4
package/README.md
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
# @ggcode-cli/ggcode
|
|
2
2
|
|
|
3
|
-
`@ggcode-cli/ggcode`
|
|
3
|
+
`@ggcode-cli/ggcode` installs the native `ggcode` binary from GitHub Releases.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
`ggcode`
|
|
7
|
-
|
|
5
|
+
During package installation it downloads the matching release archive, verifies it, and places the
|
|
6
|
+
real `ggcode` executable in a stable CLI location:
|
|
7
|
+
|
|
8
|
+
- macOS / Linux: prefers `/usr/local/bin`, falls back to `~/.local/bin`
|
|
9
|
+
- Windows: prefers `%USERPROFILE%\\AppData\\Local\\Programs\\ggcode\\bin`, falls back to `%USERPROFILE%\\.local\\bin`
|
|
10
|
+
|
|
11
|
+
If that directory is not on `PATH`, the installer updates your shell/user PATH and asks you to
|
|
12
|
+
reopen the terminal. The npm package keeps a separate `ggcode-bootstrap` helper command for manual
|
|
13
|
+
repair, but normal usage should be the real `ggcode` binary.
|
|
8
14
|
|
|
9
15
|
## Install
|
|
10
16
|
|
|
@@ -20,14 +26,19 @@ Then run:
|
|
|
20
26
|
ggcode
|
|
21
27
|
```
|
|
22
28
|
|
|
23
|
-
If
|
|
29
|
+
If the native install needs to be retried manually, run:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
ggcode-bootstrap
|
|
33
|
+
```
|
|
24
34
|
|
|
25
35
|
## What it does
|
|
26
36
|
|
|
27
37
|
- Detects your platform and architecture
|
|
28
38
|
- Downloads the latest matching `ggcode` binary from GitHub Releases
|
|
29
39
|
- Verifies the downloaded archive with `checksums.txt`
|
|
30
|
-
-
|
|
40
|
+
- Installs the real binary into a stable PATH location
|
|
41
|
+
- Updates `PATH` when needed so future `ggcode` launches bypass the wrapper
|
|
31
42
|
|
|
32
43
|
## Pin a specific ggcode release
|
|
33
44
|
|
|
@@ -4,8 +4,12 @@ const { spawnSync } = require("child_process");
|
|
|
4
4
|
const { ensureInstalled } = require("../lib/install");
|
|
5
5
|
|
|
6
6
|
async function main() {
|
|
7
|
-
const
|
|
8
|
-
|
|
7
|
+
const install = await ensureInstalled(process.env.GGCODE_INSTALL_VERSION, true);
|
|
8
|
+
if (install.needsRestart) {
|
|
9
|
+
console.error(`ggcode was installed to ${install.installDir}.`);
|
|
10
|
+
console.error("Reopen your terminal, then run `ggcode` directly.");
|
|
11
|
+
}
|
|
12
|
+
const result = spawnSync(install.binaryPath, process.argv.slice(2), {
|
|
9
13
|
stdio: "inherit",
|
|
10
14
|
env: { ...process.env, GGCODE_WRAPPER_KIND: "npm" },
|
|
11
15
|
});
|
|
@@ -16,6 +20,6 @@ async function main() {
|
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
main().catch((err) => {
|
|
19
|
-
console.error(`ggcode npm
|
|
23
|
+
console.error(`ggcode npm bootstrap failed: ${err.message}`);
|
|
20
24
|
process.exit(1);
|
|
21
25
|
});
|
package/lib/install.js
CHANGED
|
@@ -8,6 +8,9 @@ const { execFileSync } = require("child_process");
|
|
|
8
8
|
const OWNER = "topcheer";
|
|
9
9
|
const REPO = "ggcode";
|
|
10
10
|
const BINARY = process.platform === "win32" ? "ggcode.exe" : "ggcode";
|
|
11
|
+
const MARKER_START = "# >>> ggcode PATH >>>";
|
|
12
|
+
const MARKER_END = "# <<< ggcode PATH <<<";
|
|
13
|
+
const METADATA = ".ggcode-wrapper.json";
|
|
11
14
|
|
|
12
15
|
function normalizeVersion(version) {
|
|
13
16
|
const selected = (version || "").trim();
|
|
@@ -50,30 +53,195 @@ function releaseBase(version) {
|
|
|
50
53
|
return `https://github.com/${OWNER}/${REPO}/releases/download/${version}`;
|
|
51
54
|
}
|
|
52
55
|
|
|
53
|
-
function
|
|
56
|
+
function preferredInstallDirs() {
|
|
57
|
+
const home = os.homedir();
|
|
54
58
|
if (process.platform === "win32") {
|
|
55
|
-
return
|
|
59
|
+
return [
|
|
60
|
+
path.join(home, "AppData", "Local", "Programs", "ggcode", "bin"),
|
|
61
|
+
path.join(home, ".local", "bin"),
|
|
62
|
+
];
|
|
56
63
|
}
|
|
57
|
-
return path.join(
|
|
64
|
+
return ["/usr/local/bin", path.join(home, ".local", "bin")];
|
|
58
65
|
}
|
|
59
66
|
|
|
60
|
-
function
|
|
61
|
-
return path.join(
|
|
67
|
+
function metadataPath(dir) {
|
|
68
|
+
return path.join(dir, METADATA);
|
|
62
69
|
}
|
|
63
70
|
|
|
64
|
-
function
|
|
65
|
-
return
|
|
71
|
+
function pathEntries(value) {
|
|
72
|
+
return (value || "")
|
|
73
|
+
.split(path.delimiter)
|
|
74
|
+
.map((entry) => entry.trim())
|
|
75
|
+
.filter(Boolean);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function samePath(a, b) {
|
|
79
|
+
const normalize = (value) => {
|
|
80
|
+
let result = path.resolve(value);
|
|
81
|
+
if (process.platform === "win32") {
|
|
82
|
+
result = result.replace(/[\\/]+$/, "").toLowerCase();
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
};
|
|
86
|
+
return normalize(a) === normalize(b);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isPermissionError(err) {
|
|
90
|
+
return Boolean(err && ["EACCES", "EPERM"].includes(err.code));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function readMetadata(dir) {
|
|
94
|
+
try {
|
|
95
|
+
return JSON.parse(fs.readFileSync(metadataPath(dir), "utf8"));
|
|
96
|
+
} catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function writeMetadata(dir, version) {
|
|
102
|
+
fs.writeFileSync(metadataPath(dir), JSON.stringify({ version }, null, 2) + "\n", "utf8");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function findInstalledBinary(requestedVersion, target) {
|
|
106
|
+
for (const dir of preferredInstallDirs()) {
|
|
107
|
+
const binary = path.join(dir, target.binaryName);
|
|
108
|
+
if (!fs.existsSync(binary)) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (requestedVersion === "latest") {
|
|
112
|
+
return { dir, binary, version: readMetadata(dir)?.version || "unknown" };
|
|
113
|
+
}
|
|
114
|
+
const metadata = readMetadata(dir);
|
|
115
|
+
if (metadata && metadata.version === requestedVersion) {
|
|
116
|
+
return { dir, binary, version: metadata.version };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function installBinary(dir, target, extracted, version) {
|
|
123
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
124
|
+
const dest = path.join(dir, target.binaryName);
|
|
125
|
+
const temp = `${dest}.tmp-${process.pid}`;
|
|
126
|
+
fs.copyFileSync(extracted, temp);
|
|
127
|
+
if (process.platform !== "win32") {
|
|
128
|
+
fs.chmodSync(temp, 0o755);
|
|
129
|
+
}
|
|
130
|
+
fs.rmSync(dest, { force: true });
|
|
131
|
+
fs.renameSync(temp, dest);
|
|
132
|
+
writeMetadata(dir, version);
|
|
133
|
+
return dest;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function ensureInstalledPath(dir) {
|
|
137
|
+
const currentEntries = pathEntries(process.env.PATH);
|
|
138
|
+
if (currentEntries.some((entry) => samePath(entry, dir))) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
if (process.platform === "win32") {
|
|
142
|
+
return ensureWindowsUserPath(dir);
|
|
143
|
+
}
|
|
144
|
+
return ensureUnixPath(dir);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function ensureWindowsUserPath(dir) {
|
|
148
|
+
const script = [
|
|
149
|
+
`$dir = ${powershellString(dir)}`,
|
|
150
|
+
"$current = [Environment]::GetEnvironmentVariable('Path', 'User')",
|
|
151
|
+
"$parts = @()",
|
|
152
|
+
"if ($current) { $parts = $current -split ';' | Where-Object { $_ -and $_.Trim() -ne '' } }",
|
|
153
|
+
"$exists = $parts | Where-Object { $_.TrimEnd('\\\\') -ieq $dir.TrimEnd('\\\\') }",
|
|
154
|
+
"if (-not $exists) {",
|
|
155
|
+
" $new = @($dir) + $parts",
|
|
156
|
+
" [Environment]::SetEnvironmentVariable('Path', ($new -join ';'), 'User')",
|
|
157
|
+
" Write-Output 'updated'",
|
|
158
|
+
"} else {",
|
|
159
|
+
" Write-Output 'unchanged'",
|
|
160
|
+
"}",
|
|
161
|
+
].join("; ");
|
|
162
|
+
const output = execFileSync("powershell", ["-NoProfile", "-Command", script], {
|
|
163
|
+
encoding: "utf8",
|
|
164
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
165
|
+
}).trim();
|
|
166
|
+
process.env.PATH = [dir, ...pathEntries(process.env.PATH)].join(path.delimiter);
|
|
167
|
+
return output === "updated";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function ensureUnixPath(dir) {
|
|
171
|
+
const files = profileTargets();
|
|
172
|
+
let changed = false;
|
|
173
|
+
for (const file of files) {
|
|
174
|
+
const before = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
175
|
+
const after = upsertPathBlock(before, dir);
|
|
176
|
+
if (after !== before) {
|
|
177
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
178
|
+
fs.writeFileSync(file, after, "utf8");
|
|
179
|
+
changed = true;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
process.env.PATH = [dir, ...pathEntries(process.env.PATH)].join(path.delimiter);
|
|
183
|
+
return changed;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function profileTargets() {
|
|
187
|
+
const home = os.homedir();
|
|
188
|
+
const shell = path.basename(process.env.SHELL || "");
|
|
189
|
+
const preferred = [];
|
|
190
|
+
if (shell === "zsh") {
|
|
191
|
+
preferred.push(".zshrc", ".zprofile");
|
|
192
|
+
} else if (shell === "bash") {
|
|
193
|
+
preferred.push(".bashrc", ".bash_profile");
|
|
194
|
+
}
|
|
195
|
+
preferred.push(".profile");
|
|
196
|
+
|
|
197
|
+
const existing = [".zshrc", ".zprofile", ".bashrc", ".bash_profile", ".profile"].filter((name) =>
|
|
198
|
+
fs.existsSync(path.join(home, name)),
|
|
199
|
+
);
|
|
200
|
+
const targets = [];
|
|
201
|
+
for (const name of [...preferred, ...existing]) {
|
|
202
|
+
const file = path.join(home, name);
|
|
203
|
+
if (!targets.some((entry) => samePath(entry, file))) {
|
|
204
|
+
targets.push(file);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return targets;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function upsertPathBlock(content, dir) {
|
|
211
|
+
const block = `${MARKER_START}\nexport PATH="${dir}:$PATH"\n${MARKER_END}\n`;
|
|
212
|
+
const pattern = new RegExp(`${escapeRegExp(MARKER_START)}[\\s\\S]*?${escapeRegExp(MARKER_END)}\\n?`, "m");
|
|
213
|
+
if (pattern.test(content)) {
|
|
214
|
+
return content.replace(pattern, block);
|
|
215
|
+
}
|
|
216
|
+
const suffix = content && !content.endsWith("\n") ? "\n" : "";
|
|
217
|
+
return `${content}${suffix}${block}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function escapeRegExp(value) {
|
|
221
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function powershellString(value) {
|
|
225
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
66
226
|
}
|
|
67
227
|
|
|
68
228
|
async function ensureInstalled(version, quiet) {
|
|
69
229
|
const requestedVersion = normalizeVersion(version);
|
|
70
|
-
const resolvedVersion = await resolveReleaseVersion(requestedVersion);
|
|
71
230
|
const target = resolveTarget();
|
|
72
|
-
const
|
|
73
|
-
if (
|
|
74
|
-
|
|
231
|
+
const existing = findInstalledBinary(requestedVersion, target);
|
|
232
|
+
if (existing) {
|
|
233
|
+
const pathUpdated = ensureInstalledPath(existing.dir);
|
|
234
|
+
return {
|
|
235
|
+
binaryPath: existing.binary,
|
|
236
|
+
installDir: existing.dir,
|
|
237
|
+
version: existing.version,
|
|
238
|
+
pathUpdated,
|
|
239
|
+
installedNow: false,
|
|
240
|
+
needsRestart: pathUpdated,
|
|
241
|
+
};
|
|
75
242
|
}
|
|
76
243
|
|
|
244
|
+
const resolvedVersion = await resolveReleaseVersion(requestedVersion);
|
|
77
245
|
const base = releaseBase(resolvedVersion);
|
|
78
246
|
const archiveURL = `${base}/${target.archiveName}`;
|
|
79
247
|
const checksumsURL = `${base}/checksums.txt`;
|
|
@@ -88,7 +256,7 @@ async function ensureInstalled(version, quiet) {
|
|
|
88
256
|
fs.writeFileSync(archivePath, archive);
|
|
89
257
|
|
|
90
258
|
if (!quiet) {
|
|
91
|
-
process.stderr.write(`
|
|
259
|
+
process.stderr.write(`Installing ggcode ${resolvedVersion}...\n`);
|
|
92
260
|
}
|
|
93
261
|
|
|
94
262
|
extractArchive(target, archivePath, extractDir);
|
|
@@ -97,13 +265,34 @@ async function ensureInstalled(version, quiet) {
|
|
|
97
265
|
throw new Error(`Could not find ${target.binaryName} inside ${target.archiveName}`);
|
|
98
266
|
}
|
|
99
267
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
268
|
+
let installDir = null;
|
|
269
|
+
let usedFallback = false;
|
|
270
|
+
let lastErr = null;
|
|
271
|
+
const installDirs = preferredInstallDirs();
|
|
272
|
+
for (const [index, dir] of installDirs.entries()) {
|
|
273
|
+
try {
|
|
274
|
+
const binaryPath = installBinary(dir, target, extracted, resolvedVersion);
|
|
275
|
+
installDir = dir;
|
|
276
|
+
usedFallback = index > 0;
|
|
277
|
+
const pathUpdated = ensureInstalledPath(dir);
|
|
278
|
+
return {
|
|
279
|
+
binaryPath,
|
|
280
|
+
installDir,
|
|
281
|
+
version: resolvedVersion,
|
|
282
|
+
pathUpdated,
|
|
283
|
+
installedNow: true,
|
|
284
|
+
needsRestart: pathUpdated,
|
|
285
|
+
usedFallback,
|
|
286
|
+
};
|
|
287
|
+
} catch (err) {
|
|
288
|
+
if (!isPermissionError(err) || index === installDirs.length - 1) {
|
|
289
|
+
lastErr = err;
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
105
293
|
}
|
|
106
|
-
|
|
294
|
+
|
|
295
|
+
throw lastErr || new Error("Failed to install ggcode");
|
|
107
296
|
}
|
|
108
297
|
|
|
109
298
|
async function resolveReleaseVersion(version) {
|
|
@@ -140,7 +329,7 @@ function verifyChecksum(assetName, archive, checksumsText) {
|
|
|
140
329
|
|
|
141
330
|
function extractArchive(target, archivePath, extractDir) {
|
|
142
331
|
if (target.archiveExt === ".zip") {
|
|
143
|
-
const command = `Expand-Archive -LiteralPath
|
|
332
|
+
const command = `Expand-Archive -LiteralPath ${powershellString(archivePath)} -DestinationPath ${powershellString(extractDir)} -Force`;
|
|
144
333
|
execFileSync("powershell", ["-NoProfile", "-Command", command], { stdio: "ignore" });
|
|
145
334
|
return;
|
|
146
335
|
}
|
|
@@ -175,7 +364,7 @@ function downloadBuffer(url) {
|
|
|
175
364
|
https
|
|
176
365
|
.get(target, (res) => {
|
|
177
366
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
178
|
-
get(res.headers.location);
|
|
367
|
+
get(new URL(res.headers.location, target).toString());
|
|
179
368
|
return;
|
|
180
369
|
}
|
|
181
370
|
if (res.statusCode !== 200) {
|
|
@@ -220,4 +409,6 @@ module.exports = {
|
|
|
220
409
|
normalizeVersion,
|
|
221
410
|
resolveReleaseVersion,
|
|
222
411
|
resolveTarget,
|
|
412
|
+
upsertPathBlock,
|
|
413
|
+
preferredInstallDirs,
|
|
223
414
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ggcode-cli/ggcode",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"description": "Thin npm wrapper that installs the ggcode GitHub Release binary",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"provenance": true
|
|
17
17
|
},
|
|
18
18
|
"bin": {
|
|
19
|
-
"ggcode": "bin/ggcode.js"
|
|
19
|
+
"ggcode-bootstrap": "bin/ggcode-bootstrap.js"
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
22
|
"bin",
|
package/scripts/postinstall.js
CHANGED
|
@@ -2,7 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
const { ensureInstalled } = require("../lib/install");
|
|
4
4
|
|
|
5
|
-
ensureInstalled(process.env.GGCODE_INSTALL_VERSION, false)
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
});
|
|
5
|
+
ensureInstalled(process.env.GGCODE_INSTALL_VERSION, false)
|
|
6
|
+
.then((install) => {
|
|
7
|
+
const action = install.installedNow ? "installed" : "already available";
|
|
8
|
+
console.warn(`ggcode ${install.version} ${action} at ${install.binaryPath}`);
|
|
9
|
+
if (install.needsRestart) {
|
|
10
|
+
console.warn("Reopen your terminal, then run `ggcode` directly.");
|
|
11
|
+
}
|
|
12
|
+
console.warn("If you ever need to repair the bootstrap flow, run `ggcode-bootstrap`.");
|
|
13
|
+
})
|
|
14
|
+
.catch((err) => {
|
|
15
|
+
console.warn(`ggcode postinstall warning: ${err.message}`);
|
|
16
|
+
console.warn("Run `ggcode-bootstrap` to retry the native binary installation.");
|
|
17
|
+
});
|