@openhoo/hoopilot 0.2.3 → 0.3.0
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 +70 -16
- package/dist/cli.js +533 -47
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +21 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +21 -33
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -5,17 +5,14 @@ import { execFileSync } from "child_process";
|
|
|
5
5
|
var DEFAULT_COPILOT_API_BASE_URL = "https://api.individual.githubcopilot.com";
|
|
6
6
|
var DEFAULT_TOKEN_EXCHANGE_URL = "https://api.github.com/copilot_internal/v2/token";
|
|
7
7
|
var REFRESH_SKEW_MS = 6e4;
|
|
8
|
-
var defaultLogger = {
|
|
9
|
-
info: () => void 0,
|
|
10
|
-
warn: () => void 0,
|
|
11
|
-
error: () => void 0
|
|
12
|
-
};
|
|
13
8
|
var CopilotAuthError = class extends Error {
|
|
14
9
|
constructor(message) {
|
|
15
10
|
super(message);
|
|
16
11
|
this.name = "CopilotAuthError";
|
|
17
12
|
}
|
|
18
13
|
};
|
|
14
|
+
var CopilotTokenExchangeHttpError = class extends CopilotAuthError {
|
|
15
|
+
};
|
|
19
16
|
var CopilotAuth = class {
|
|
20
17
|
#authMode;
|
|
21
18
|
#copilotApiBaseUrl;
|
|
@@ -37,7 +34,7 @@ var CopilotAuth = class {
|
|
|
37
34
|
this.#fetch = options.fetch ?? fetch;
|
|
38
35
|
this.#githubToken = options.githubToken;
|
|
39
36
|
this.#githubTokenCommand = options.githubTokenCommand ?? "gh auth token";
|
|
40
|
-
this.#logger = options.logger
|
|
37
|
+
this.#logger = options.logger;
|
|
41
38
|
this.#tokenExchangeUrl = options.tokenExchangeUrl ?? options.env?.COPILOT_TOKEN_EXCHANGE_URL ?? DEFAULT_TOKEN_EXCHANGE_URL;
|
|
42
39
|
}
|
|
43
40
|
async getAccess() {
|
|
@@ -45,17 +42,6 @@ var CopilotAuth = class {
|
|
|
45
42
|
return this.#cachedAccess;
|
|
46
43
|
}
|
|
47
44
|
const directCopilotToken = this.#resolveDirectCopilotToken();
|
|
48
|
-
if (this.#authMode === "copilot-token") {
|
|
49
|
-
if (!directCopilotToken) {
|
|
50
|
-
throw new CopilotAuthError("COPILOT_API_TOKEN or GITHUB_COPILOT_API_TOKEN is required.");
|
|
51
|
-
}
|
|
52
|
-
return this.#cacheAccess({
|
|
53
|
-
apiBaseUrl: this.#copilotApiBaseUrl,
|
|
54
|
-
expiresAtMs: Date.now() + 10 * 6e4,
|
|
55
|
-
source: "copilot-token",
|
|
56
|
-
token: directCopilotToken
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
45
|
if (directCopilotToken) {
|
|
60
46
|
return this.#cacheAccess({
|
|
61
47
|
apiBaseUrl: this.#copilotApiBaseUrl,
|
|
@@ -64,29 +50,30 @@ var CopilotAuth = class {
|
|
|
64
50
|
token: directCopilotToken
|
|
65
51
|
});
|
|
66
52
|
}
|
|
53
|
+
if (this.#authMode === "copilot-token") {
|
|
54
|
+
throw new CopilotAuthError("COPILOT_API_TOKEN or GITHUB_COPILOT_API_TOKEN is required.");
|
|
55
|
+
}
|
|
67
56
|
const githubToken = this.#resolveGithubToken();
|
|
68
57
|
if (!githubToken) {
|
|
69
58
|
throw new CopilotAuthError(
|
|
70
|
-
"No Copilot credential found. Set
|
|
59
|
+
"No Copilot credential found. Set COPILOT_API_TOKEN, set COPILOT_GITHUB_TOKEN from gh auth token, or sign in with gh auth login."
|
|
71
60
|
);
|
|
72
61
|
}
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
source: "direct-github-token",
|
|
78
|
-
token: githubToken
|
|
79
|
-
});
|
|
62
|
+
if (isPersonalAccessToken(githubToken)) {
|
|
63
|
+
throw new CopilotAuthError(
|
|
64
|
+
"GitHub personal access tokens are not supported for Copilot authentication. Use gh auth login or COPILOT_API_TOKEN."
|
|
65
|
+
);
|
|
80
66
|
}
|
|
81
67
|
try {
|
|
82
|
-
|
|
83
|
-
return this.#cacheAccess(exchanged);
|
|
68
|
+
return this.#cacheAccess(await this.#exchangeGithubToken(githubToken));
|
|
84
69
|
} catch (error) {
|
|
85
|
-
if (
|
|
70
|
+
if (!(error instanceof CopilotTokenExchangeHttpError)) {
|
|
86
71
|
throw error;
|
|
87
72
|
}
|
|
88
|
-
this.#logger
|
|
89
|
-
`Copilot token exchange failed; falling back to
|
|
73
|
+
this.#logger?.warn(
|
|
74
|
+
`Copilot token exchange failed; falling back to GitHub CLI token mode: ${errorMessage(
|
|
75
|
+
error
|
|
76
|
+
)}`
|
|
90
77
|
);
|
|
91
78
|
return this.#cacheAccess({
|
|
92
79
|
apiBaseUrl: this.#copilotApiBaseUrl,
|
|
@@ -112,7 +99,7 @@ var CopilotAuth = class {
|
|
|
112
99
|
method: "GET"
|
|
113
100
|
});
|
|
114
101
|
if (!response.ok) {
|
|
115
|
-
throw new
|
|
102
|
+
throw new CopilotTokenExchangeHttpError(
|
|
116
103
|
`GitHub Copilot token exchange failed with ${response.status}: ${await safeResponseText(
|
|
117
104
|
response
|
|
118
105
|
)}`
|
|
@@ -143,8 +130,6 @@ var CopilotAuth = class {
|
|
|
143
130
|
this.#githubToken,
|
|
144
131
|
this.#env.COPILOT_GITHUB_TOKEN,
|
|
145
132
|
this.#env.GITHUB_COPILOT_GITHUB_TOKEN,
|
|
146
|
-
this.#env.GH_TOKEN,
|
|
147
|
-
this.#env.GITHUB_TOKEN,
|
|
148
133
|
this.#readGithubTokenCommand()
|
|
149
134
|
);
|
|
150
135
|
}
|
|
@@ -259,6 +244,9 @@ async function safeResponseText(response) {
|
|
|
259
244
|
const text = await response.text();
|
|
260
245
|
return text.slice(0, 500);
|
|
261
246
|
}
|
|
247
|
+
function isPersonalAccessToken(token) {
|
|
248
|
+
return token.startsWith("github_pat_") || token.startsWith("ghp_");
|
|
249
|
+
}
|
|
262
250
|
function errorMessage(error) {
|
|
263
251
|
return error instanceof Error ? error.message : String(error);
|
|
264
252
|
}
|
|
@@ -1022,21 +1010,516 @@ function errorMessage2(error) {
|
|
|
1022
1010
|
return error instanceof Error ? error.message : String(error);
|
|
1023
1011
|
}
|
|
1024
1012
|
|
|
1013
|
+
// src/update.ts
|
|
1014
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
1015
|
+
import {
|
|
1016
|
+
chmodSync,
|
|
1017
|
+
copyFileSync,
|
|
1018
|
+
existsSync,
|
|
1019
|
+
mkdirSync,
|
|
1020
|
+
realpathSync,
|
|
1021
|
+
renameSync,
|
|
1022
|
+
rmSync
|
|
1023
|
+
} from "fs";
|
|
1024
|
+
import { readFile, writeFile } from "fs/promises";
|
|
1025
|
+
import { homedir } from "os";
|
|
1026
|
+
import { dirname, join } from "path";
|
|
1027
|
+
|
|
1028
|
+
// src/update-core.ts
|
|
1029
|
+
var REPO_OWNER = "openhoo";
|
|
1030
|
+
var REPO_NAME = "hoopilot";
|
|
1031
|
+
var REPO = `${REPO_OWNER}/${REPO_NAME}`;
|
|
1032
|
+
var NPM_PACKAGE = "@openhoo/hoopilot";
|
|
1033
|
+
var UPDATE_CHECK_INTERVAL_MS = 1e3 * 60 * 60 * 24;
|
|
1034
|
+
function parseSemver(input) {
|
|
1035
|
+
const value = String(input).trim().replace(/^[v=]+/, "");
|
|
1036
|
+
const match = value.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/);
|
|
1037
|
+
if (!match) {
|
|
1038
|
+
return null;
|
|
1039
|
+
}
|
|
1040
|
+
return {
|
|
1041
|
+
major: Number(match[1]),
|
|
1042
|
+
minor: Number(match[2]),
|
|
1043
|
+
patch: Number(match[3]),
|
|
1044
|
+
// Build metadata (everything after "+") is intentionally dropped.
|
|
1045
|
+
prerelease: match[4] ? match[4].split(".") : []
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
function comparePrerelease(a, b) {
|
|
1049
|
+
if (a.length === 0 && b.length === 0) {
|
|
1050
|
+
return 0;
|
|
1051
|
+
}
|
|
1052
|
+
if (a.length === 0) {
|
|
1053
|
+
return 1;
|
|
1054
|
+
}
|
|
1055
|
+
if (b.length === 0) {
|
|
1056
|
+
return -1;
|
|
1057
|
+
}
|
|
1058
|
+
const len = Math.max(a.length, b.length);
|
|
1059
|
+
for (let i = 0; i < len; i++) {
|
|
1060
|
+
const x = a[i];
|
|
1061
|
+
const y = b[i];
|
|
1062
|
+
if (x === void 0) {
|
|
1063
|
+
return -1;
|
|
1064
|
+
}
|
|
1065
|
+
if (y === void 0) {
|
|
1066
|
+
return 1;
|
|
1067
|
+
}
|
|
1068
|
+
const xNumeric = /^\d+$/.test(x);
|
|
1069
|
+
const yNumeric = /^\d+$/.test(y);
|
|
1070
|
+
if (xNumeric && yNumeric) {
|
|
1071
|
+
const diff = Number(x) - Number(y);
|
|
1072
|
+
if (diff !== 0) {
|
|
1073
|
+
return diff < 0 ? -1 : 1;
|
|
1074
|
+
}
|
|
1075
|
+
} else if (xNumeric) {
|
|
1076
|
+
return -1;
|
|
1077
|
+
} else if (yNumeric) {
|
|
1078
|
+
return 1;
|
|
1079
|
+
} else if (x !== y) {
|
|
1080
|
+
return x < y ? -1 : 1;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
return 0;
|
|
1084
|
+
}
|
|
1085
|
+
function compareSemver(a, b) {
|
|
1086
|
+
const pa = parseSemver(a);
|
|
1087
|
+
const pb = parseSemver(b);
|
|
1088
|
+
if (!pa || !pb) {
|
|
1089
|
+
if (!pa && !pb) {
|
|
1090
|
+
return 0;
|
|
1091
|
+
}
|
|
1092
|
+
return pa ? 1 : -1;
|
|
1093
|
+
}
|
|
1094
|
+
if (pa.major !== pb.major) {
|
|
1095
|
+
return pa.major < pb.major ? -1 : 1;
|
|
1096
|
+
}
|
|
1097
|
+
if (pa.minor !== pb.minor) {
|
|
1098
|
+
return pa.minor < pb.minor ? -1 : 1;
|
|
1099
|
+
}
|
|
1100
|
+
if (pa.patch !== pb.patch) {
|
|
1101
|
+
return pa.patch < pb.patch ? -1 : 1;
|
|
1102
|
+
}
|
|
1103
|
+
return comparePrerelease(pa.prerelease, pb.prerelease);
|
|
1104
|
+
}
|
|
1105
|
+
function isOutdated(current, latest) {
|
|
1106
|
+
return compareSemver(current, latest) < 0;
|
|
1107
|
+
}
|
|
1108
|
+
function versionFromTag(tag) {
|
|
1109
|
+
return tag.trim().replace(/^v/, "");
|
|
1110
|
+
}
|
|
1111
|
+
function assetSuffixFor(platform, arch, isMusl) {
|
|
1112
|
+
const os = platform === "win32" ? "windows" : platform === "darwin" ? "darwin" : "linux";
|
|
1113
|
+
const cpu = arch === "arm64" || arch === "aarch64" ? "arm64" : "x64";
|
|
1114
|
+
const libc = os === "linux" && isMusl ? "-musl" : "";
|
|
1115
|
+
return `${os}-${cpu}${libc}`;
|
|
1116
|
+
}
|
|
1117
|
+
function assetNameFor(suffix) {
|
|
1118
|
+
const name = `hoopilot-${suffix}`;
|
|
1119
|
+
return suffix.startsWith("windows-") ? `${name}.exe` : name;
|
|
1120
|
+
}
|
|
1121
|
+
function isUpdateCheckDisabled(env, isTty) {
|
|
1122
|
+
if (env.HOOPILOT_NO_UPDATE_CHECK || env.NO_UPDATE_NOTIFIER) {
|
|
1123
|
+
return true;
|
|
1124
|
+
}
|
|
1125
|
+
if (env.NODE_ENV === "test") {
|
|
1126
|
+
return true;
|
|
1127
|
+
}
|
|
1128
|
+
if (!isTty) {
|
|
1129
|
+
return true;
|
|
1130
|
+
}
|
|
1131
|
+
if (env.CI && env.CI !== "false" || env.CONTINUOUS_INTEGRATION || env.GITHUB_ACTIONS || env.BUILD_NUMBER || env.RUN_ID) {
|
|
1132
|
+
return true;
|
|
1133
|
+
}
|
|
1134
|
+
return false;
|
|
1135
|
+
}
|
|
1136
|
+
function shouldRefresh(lastCheck, now, intervalMs = UPDATE_CHECK_INTERVAL_MS) {
|
|
1137
|
+
return now - lastCheck >= intervalMs;
|
|
1138
|
+
}
|
|
1139
|
+
function upgradeCommandFor(kind) {
|
|
1140
|
+
return kind === "binary" ? "hoopilot update" : `npm install -g ${NPM_PACKAGE}@latest (or: bun add -g ${NPM_PACKAGE})`;
|
|
1141
|
+
}
|
|
1142
|
+
function shouldCleanupOldBinary(platform, isStandaloneBinary) {
|
|
1143
|
+
return platform === "win32" && isStandaloneBinary;
|
|
1144
|
+
}
|
|
1145
|
+
function formatUpdateNotice(current, latest, kind) {
|
|
1146
|
+
return `
|
|
1147
|
+
Update available for hoopilot: ${current} \u2192 ${latest}
|
|
1148
|
+
Run: ${upgradeCommandFor(kind)}
|
|
1149
|
+
|
|
1150
|
+
`;
|
|
1151
|
+
}
|
|
1152
|
+
function parseState(text) {
|
|
1153
|
+
try {
|
|
1154
|
+
const data = JSON.parse(text);
|
|
1155
|
+
return {
|
|
1156
|
+
lastCheck: typeof data.lastCheck === "number" ? data.lastCheck : 0,
|
|
1157
|
+
latestVersion: typeof data.latestVersion === "string" ? data.latestVersion : null,
|
|
1158
|
+
etag: typeof data.etag === "string" ? data.etag : null
|
|
1159
|
+
};
|
|
1160
|
+
} catch {
|
|
1161
|
+
return { lastCheck: 0, latestVersion: null, etag: null };
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
function parseLatestRelease(json) {
|
|
1165
|
+
if (!json || typeof json !== "object") {
|
|
1166
|
+
return null;
|
|
1167
|
+
}
|
|
1168
|
+
const record = json;
|
|
1169
|
+
const tag = typeof record.tag_name === "string" ? record.tag_name : void 0;
|
|
1170
|
+
if (!tag) {
|
|
1171
|
+
return null;
|
|
1172
|
+
}
|
|
1173
|
+
const assets = [];
|
|
1174
|
+
if (Array.isArray(record.assets)) {
|
|
1175
|
+
for (const item of record.assets) {
|
|
1176
|
+
if (item && typeof item === "object") {
|
|
1177
|
+
const asset = item;
|
|
1178
|
+
if (typeof asset.name === "string" && typeof asset.browser_download_url === "string") {
|
|
1179
|
+
assets.push({ name: asset.name, url: asset.browser_download_url });
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
return { version: versionFromTag(tag), tag, assets };
|
|
1185
|
+
}
|
|
1186
|
+
function checksumFor(sumsText, fileName) {
|
|
1187
|
+
for (const line of sumsText.split(/\r?\n/)) {
|
|
1188
|
+
const match = line.trim().match(/^([0-9a-fA-F]{64})\s+\*?(.+)$/);
|
|
1189
|
+
if (match?.[1] && match[2]?.trim() === fileName) {
|
|
1190
|
+
return match[1].toLowerCase();
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
return void 0;
|
|
1194
|
+
}
|
|
1195
|
+
function resolveCacheDir(env, platform, homedir2, join2) {
|
|
1196
|
+
if (platform === "win32") {
|
|
1197
|
+
const base2 = env.LOCALAPPDATA || join2(homedir2, "AppData", "Local");
|
|
1198
|
+
return join2(base2, "hoopilot");
|
|
1199
|
+
}
|
|
1200
|
+
if (platform === "darwin") {
|
|
1201
|
+
return join2(homedir2, "Library", "Caches", "hoopilot");
|
|
1202
|
+
}
|
|
1203
|
+
const base = env.XDG_CACHE_HOME || join2(homedir2, ".cache");
|
|
1204
|
+
return join2(base, "hoopilot");
|
|
1205
|
+
}
|
|
1206
|
+
function latestReleaseApiUrl() {
|
|
1207
|
+
return `https://api.github.com/repos/${REPO}/releases/latest`;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// src/version.ts
|
|
1211
|
+
var BAKED_VERSION = typeof HOOPILOT_VERSION !== "undefined" ? HOOPILOT_VERSION : void 0;
|
|
1212
|
+
var BAKED_TARGET = typeof HOOPILOT_TARGET !== "undefined" ? HOOPILOT_TARGET : void 0;
|
|
1213
|
+
var IS_STANDALONE_BINARY = BAKED_VERSION !== void 0;
|
|
1214
|
+
var cachedVersion;
|
|
1215
|
+
async function getVersion() {
|
|
1216
|
+
if (cachedVersion !== void 0) {
|
|
1217
|
+
return cachedVersion;
|
|
1218
|
+
}
|
|
1219
|
+
let resolved;
|
|
1220
|
+
if (BAKED_VERSION) {
|
|
1221
|
+
resolved = BAKED_VERSION;
|
|
1222
|
+
} else {
|
|
1223
|
+
try {
|
|
1224
|
+
const manifest = await Bun.file(new URL("../package.json", import.meta.url)).json();
|
|
1225
|
+
resolved = typeof manifest.version === "string" ? manifest.version : "0.0.0";
|
|
1226
|
+
} catch {
|
|
1227
|
+
resolved = "0.0.0";
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
cachedVersion = resolved;
|
|
1231
|
+
return resolved;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// src/update.ts
|
|
1235
|
+
var REQUEST_TIMEOUT_MS = 8e3;
|
|
1236
|
+
var SHA256SUMS = "SHA256SUMS";
|
|
1237
|
+
function userAgent(version) {
|
|
1238
|
+
return `hoopilot/${version}`;
|
|
1239
|
+
}
|
|
1240
|
+
function cacheDir() {
|
|
1241
|
+
return resolveCacheDir(process.env, process.platform, homedir(), join);
|
|
1242
|
+
}
|
|
1243
|
+
function stateFilePath() {
|
|
1244
|
+
return join(cacheDir(), "update-check.json");
|
|
1245
|
+
}
|
|
1246
|
+
async function readStateSafe() {
|
|
1247
|
+
try {
|
|
1248
|
+
return parseState(await readFile(stateFilePath(), "utf8"));
|
|
1249
|
+
} catch {
|
|
1250
|
+
return { lastCheck: 0, latestVersion: null, etag: null };
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
async function writeStateSafe(state) {
|
|
1254
|
+
try {
|
|
1255
|
+
mkdirSync(cacheDir(), { recursive: true });
|
|
1256
|
+
await writeFile(stateFilePath(), JSON.stringify(state), "utf8");
|
|
1257
|
+
} catch {
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
async function fetchLatest(version, etag) {
|
|
1261
|
+
try {
|
|
1262
|
+
const headers = {
|
|
1263
|
+
Accept: "application/vnd.github+json",
|
|
1264
|
+
"User-Agent": userAgent(version),
|
|
1265
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
1266
|
+
};
|
|
1267
|
+
if (etag) {
|
|
1268
|
+
headers["If-None-Match"] = etag;
|
|
1269
|
+
}
|
|
1270
|
+
const response = await fetch(latestReleaseApiUrl(), {
|
|
1271
|
+
headers,
|
|
1272
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
1273
|
+
});
|
|
1274
|
+
if (response.status === 304) {
|
|
1275
|
+
return { status: 304, etag: etag ?? null, release: null };
|
|
1276
|
+
}
|
|
1277
|
+
if (!response.ok) {
|
|
1278
|
+
return { status: response.status, etag: null, release: null };
|
|
1279
|
+
}
|
|
1280
|
+
return {
|
|
1281
|
+
status: response.status,
|
|
1282
|
+
etag: response.headers.get("etag"),
|
|
1283
|
+
release: parseLatestRelease(await response.json())
|
|
1284
|
+
};
|
|
1285
|
+
} catch {
|
|
1286
|
+
return null;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
async function maybeNotifyUpdate(currentVersion, kind) {
|
|
1290
|
+
if (isUpdateCheckDisabled(process.env, Boolean(process.stderr.isTTY))) {
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
const state = await readStateSafe();
|
|
1294
|
+
if (state.latestVersion && isOutdated(currentVersion, state.latestVersion)) {
|
|
1295
|
+
process.stderr.write(formatUpdateNotice(currentVersion, state.latestVersion, kind));
|
|
1296
|
+
}
|
|
1297
|
+
if (shouldRefresh(state.lastCheck, Date.now())) {
|
|
1298
|
+
void refreshState(currentVersion, state.etag ?? null).catch(() => {
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
async function refreshState(currentVersion, etag) {
|
|
1303
|
+
const result = await fetchLatest(currentVersion, etag);
|
|
1304
|
+
if (!result) {
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
if (result.status === 304) {
|
|
1308
|
+
const prev = await readStateSafe();
|
|
1309
|
+
await writeStateSafe({ ...prev, lastCheck: Date.now() });
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
if (result.release) {
|
|
1313
|
+
await writeStateSafe({
|
|
1314
|
+
lastCheck: Date.now(),
|
|
1315
|
+
latestVersion: result.release.version,
|
|
1316
|
+
etag: result.etag
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
function detectInstallKind() {
|
|
1321
|
+
return IS_STANDALONE_BINARY ? "binary" : "npm";
|
|
1322
|
+
}
|
|
1323
|
+
function detectMusl() {
|
|
1324
|
+
if (process.platform !== "linux") {
|
|
1325
|
+
return false;
|
|
1326
|
+
}
|
|
1327
|
+
try {
|
|
1328
|
+
const report = process.report?.getReport?.();
|
|
1329
|
+
if (report?.header && "glibcVersionRuntime" in report.header) {
|
|
1330
|
+
return !report.header.glibcVersionRuntime;
|
|
1331
|
+
}
|
|
1332
|
+
} catch {
|
|
1333
|
+
}
|
|
1334
|
+
try {
|
|
1335
|
+
if (existsSync("/etc/alpine-release")) {
|
|
1336
|
+
return true;
|
|
1337
|
+
}
|
|
1338
|
+
const ldd = execFileSync2("ldd", ["--version"], {
|
|
1339
|
+
encoding: "utf8",
|
|
1340
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1341
|
+
timeout: 2e3
|
|
1342
|
+
});
|
|
1343
|
+
return /musl/i.test(ldd);
|
|
1344
|
+
} catch {
|
|
1345
|
+
return false;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
async function downloadToFile(url, dest, version) {
|
|
1349
|
+
const response = await fetch(url, {
|
|
1350
|
+
headers: { "User-Agent": userAgent(version) },
|
|
1351
|
+
redirect: "follow",
|
|
1352
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS * 10)
|
|
1353
|
+
});
|
|
1354
|
+
if (!response.ok || !response.body) {
|
|
1355
|
+
throw new Error(`Download failed (${response.status}) for ${url}`);
|
|
1356
|
+
}
|
|
1357
|
+
await Bun.write(dest, response);
|
|
1358
|
+
}
|
|
1359
|
+
async function sha256File(path) {
|
|
1360
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
1361
|
+
hasher.update(await Bun.file(path).arrayBuffer());
|
|
1362
|
+
return hasher.digest("hex");
|
|
1363
|
+
}
|
|
1364
|
+
async function verifyChecksum(release, assetName, file, version) {
|
|
1365
|
+
const sums = release.assets.find((asset) => asset.name === SHA256SUMS);
|
|
1366
|
+
if (!sums) {
|
|
1367
|
+
throw new Error(
|
|
1368
|
+
`Release ${release.tag} has no ${SHA256SUMS}; refusing to install an unverified binary.`
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
const response = await fetch(sums.url, {
|
|
1372
|
+
headers: { "User-Agent": userAgent(version) },
|
|
1373
|
+
redirect: "follow",
|
|
1374
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
1375
|
+
});
|
|
1376
|
+
if (!response.ok) {
|
|
1377
|
+
throw new Error(`Could not download ${SHA256SUMS} (${response.status}).`);
|
|
1378
|
+
}
|
|
1379
|
+
const expected = checksumFor(await response.text(), assetName);
|
|
1380
|
+
if (!expected) {
|
|
1381
|
+
throw new Error(`No checksum for ${assetName} in ${SHA256SUMS}.`);
|
|
1382
|
+
}
|
|
1383
|
+
const actual = await sha256File(file);
|
|
1384
|
+
if (actual.toLowerCase() !== expected) {
|
|
1385
|
+
throw new Error(`Checksum mismatch for ${assetName}: expected ${expected}, got ${actual}.`);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
function swapBinary(tmpFile, exePath) {
|
|
1389
|
+
if (process.platform === "win32") {
|
|
1390
|
+
const oldExe = `${exePath}.old`;
|
|
1391
|
+
try {
|
|
1392
|
+
rmSync(oldExe, { force: true });
|
|
1393
|
+
} catch {
|
|
1394
|
+
}
|
|
1395
|
+
renameSync(exePath, oldExe);
|
|
1396
|
+
const restore = () => {
|
|
1397
|
+
try {
|
|
1398
|
+
renameSync(oldExe, exePath);
|
|
1399
|
+
} catch {
|
|
1400
|
+
}
|
|
1401
|
+
};
|
|
1402
|
+
try {
|
|
1403
|
+
renameSync(tmpFile, exePath);
|
|
1404
|
+
} catch (error) {
|
|
1405
|
+
if (error.code === "EXDEV") {
|
|
1406
|
+
try {
|
|
1407
|
+
copyFileSync(tmpFile, exePath);
|
|
1408
|
+
} catch (copyError) {
|
|
1409
|
+
restore();
|
|
1410
|
+
throw copyError;
|
|
1411
|
+
}
|
|
1412
|
+
} else {
|
|
1413
|
+
restore();
|
|
1414
|
+
throw error;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
try {
|
|
1420
|
+
renameSync(tmpFile, exePath);
|
|
1421
|
+
} catch (error) {
|
|
1422
|
+
const code = error.code;
|
|
1423
|
+
if (code === "EXDEV") {
|
|
1424
|
+
copyFileSync(tmpFile, exePath);
|
|
1425
|
+
chmodSync(exePath, 493);
|
|
1426
|
+
} else if (code === "EACCES" || code === "EPERM") {
|
|
1427
|
+
throw new Error(
|
|
1428
|
+
`No permission to update ${exePath}. Re-run with sudo, or reinstall to a writable directory.`
|
|
1429
|
+
);
|
|
1430
|
+
} else {
|
|
1431
|
+
throw error;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
function cleanupOldBinary() {
|
|
1436
|
+
if (!shouldCleanupOldBinary(process.platform, IS_STANDALONE_BINARY)) {
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
try {
|
|
1440
|
+
rmSync(`${realpathSync(process.execPath)}.old`, { force: true });
|
|
1441
|
+
} catch {
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
async function runUpdate(currentVersion) {
|
|
1445
|
+
cleanupOldBinary();
|
|
1446
|
+
const kind = detectInstallKind();
|
|
1447
|
+
if (kind !== "binary") {
|
|
1448
|
+
console.log(`hoopilot ${currentVersion} was installed via npm.`);
|
|
1449
|
+
console.log(`Update with: ${upgradeCommandFor("npm")}`);
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
console.log(`hoopilot ${currentVersion} \u2014 checking for updates...`);
|
|
1453
|
+
const result = await fetchLatest(currentVersion);
|
|
1454
|
+
const release = result?.release ?? null;
|
|
1455
|
+
if (!release) {
|
|
1456
|
+
throw new Error("Could not reach GitHub to check for the latest release.");
|
|
1457
|
+
}
|
|
1458
|
+
if (!isOutdated(currentVersion, release.version)) {
|
|
1459
|
+
console.log(`Already up to date (latest: ${release.version}).`);
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
const suffix = BAKED_TARGET ?? assetSuffixFor(process.platform, process.arch, detectMusl());
|
|
1463
|
+
const assetName = assetNameFor(suffix);
|
|
1464
|
+
const asset = release.assets.find((entry) => entry.name === assetName);
|
|
1465
|
+
if (!asset) {
|
|
1466
|
+
const available = release.assets.map((entry) => entry.name).join(", ") || "none";
|
|
1467
|
+
throw new Error(`Release ${release.tag} has no asset "${assetName}". Available: ${available}.`);
|
|
1468
|
+
}
|
|
1469
|
+
console.log(`Updating ${currentVersion} \u2192 ${release.version} (${assetName})...`);
|
|
1470
|
+
const exePath = realpathSync(process.execPath);
|
|
1471
|
+
const tmpFile = join(dirname(exePath), `.hoopilot-update-${process.pid}.tmp`);
|
|
1472
|
+
try {
|
|
1473
|
+
await downloadToFile(asset.url, tmpFile, currentVersion);
|
|
1474
|
+
await verifyChecksum(release, assetName, tmpFile, currentVersion);
|
|
1475
|
+
if (process.platform !== "win32") {
|
|
1476
|
+
chmodSync(tmpFile, 493);
|
|
1477
|
+
}
|
|
1478
|
+
swapBinary(tmpFile, exePath);
|
|
1479
|
+
} catch (error) {
|
|
1480
|
+
const code = error.code;
|
|
1481
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
1482
|
+
throw new Error(
|
|
1483
|
+
`No permission to update ${exePath}. Re-run with sudo, or reinstall to a writable directory (e.g. set HOOPILOT_INSTALL_DIR).`
|
|
1484
|
+
);
|
|
1485
|
+
}
|
|
1486
|
+
throw error;
|
|
1487
|
+
} finally {
|
|
1488
|
+
try {
|
|
1489
|
+
rmSync(tmpFile, { force: true });
|
|
1490
|
+
} catch {
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
console.log(`Updated hoopilot to ${release.version}.`);
|
|
1494
|
+
if (process.platform === "win32") {
|
|
1495
|
+
console.log("Restart hoopilot to run the new version.");
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1025
1499
|
// src/cli.ts
|
|
1026
1500
|
async function main(argv = Bun.argv.slice(2)) {
|
|
1501
|
+
cleanupOldBinary();
|
|
1502
|
+
const command = argv[0];
|
|
1503
|
+
if (command === "update" || command === "upgrade") {
|
|
1504
|
+
await runUpdate(await getVersion());
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1027
1507
|
const args = parseArgs(argv);
|
|
1028
1508
|
if (args.help) {
|
|
1029
|
-
console.log(helpText(await
|
|
1509
|
+
console.log(helpText(await getVersion()));
|
|
1030
1510
|
return;
|
|
1031
1511
|
}
|
|
1032
1512
|
if (args.version) {
|
|
1033
|
-
console.log(await
|
|
1513
|
+
console.log(await getVersion());
|
|
1034
1514
|
return;
|
|
1035
1515
|
}
|
|
1036
1516
|
const started = startHoopilotServer(args);
|
|
1037
1517
|
console.log(`hoopilot listening on ${started.url}`);
|
|
1038
1518
|
console.log(`OpenAI base URL: ${started.url}/v1`);
|
|
1039
1519
|
console.log("Use Ctrl+C to stop.");
|
|
1520
|
+
if (!args.noUpdateCheck) {
|
|
1521
|
+
void maybeNotifyUpdate(await getVersion(), IS_STANDALONE_BINARY ? "binary" : "npm");
|
|
1522
|
+
}
|
|
1040
1523
|
}
|
|
1041
1524
|
function parseArgs(argv) {
|
|
1042
1525
|
const args = {};
|
|
@@ -1065,6 +1548,10 @@ function parseArgs(argv) {
|
|
|
1065
1548
|
args.githubTokenCommand = false;
|
|
1066
1549
|
continue;
|
|
1067
1550
|
}
|
|
1551
|
+
if (arg === "--no-update-check") {
|
|
1552
|
+
args.noUpdateCheck = true;
|
|
1553
|
+
continue;
|
|
1554
|
+
}
|
|
1068
1555
|
const [name, inlineValue] = arg.split("=", 2);
|
|
1069
1556
|
const value = inlineValue ?? rest.shift();
|
|
1070
1557
|
if (!value) {
|
|
@@ -1106,19 +1593,11 @@ function parseArgs(argv) {
|
|
|
1106
1593
|
return args;
|
|
1107
1594
|
}
|
|
1108
1595
|
function parseAuthMode(value) {
|
|
1109
|
-
if (value === "auto" || value === "copilot-token"
|
|
1596
|
+
if (value === "auto" || value === "copilot-token") {
|
|
1110
1597
|
return value;
|
|
1111
1598
|
}
|
|
1112
1599
|
throw new Error(`Invalid auth mode: ${value}.`);
|
|
1113
1600
|
}
|
|
1114
|
-
async function packageVersion() {
|
|
1115
|
-
try {
|
|
1116
|
-
const manifest = await Bun.file(new URL("../package.json", import.meta.url)).json();
|
|
1117
|
-
return typeof manifest.version === "string" ? manifest.version : "0.0.0";
|
|
1118
|
-
} catch {
|
|
1119
|
-
return "0.0.0";
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
1601
|
function helpText(version) {
|
|
1123
1602
|
return `hoopilot ${version}
|
|
1124
1603
|
|
|
@@ -1126,27 +1605,34 @@ OpenAI-compatible proxy for GitHub Copilot.
|
|
|
1126
1605
|
|
|
1127
1606
|
Usage:
|
|
1128
1607
|
hoopilot [serve] [options]
|
|
1608
|
+
hoopilot update
|
|
1129
1609
|
npx @openhoo/hoopilot [options]
|
|
1130
1610
|
|
|
1611
|
+
Commands:
|
|
1612
|
+
serve Start the proxy server (default)
|
|
1613
|
+
update, upgrade Update hoopilot to the latest release
|
|
1614
|
+
|
|
1131
1615
|
Options:
|
|
1132
1616
|
-p, --port <port> Port to listen on. Default: 4141
|
|
1133
1617
|
--host <host> Host to listen on. Default: 127.0.0.1
|
|
1134
1618
|
--api-key <key> Require clients to send Authorization: Bearer <key>
|
|
1135
|
-
--auth-mode <mode> auto,
|
|
1136
|
-
--github-token <token> GitHub OAuth token for a Copilot account
|
|
1619
|
+
--auth-mode <mode> auto, copilot-token
|
|
1620
|
+
--github-token <token> GitHub CLI OAuth token for a Copilot account. PATs are rejected.
|
|
1137
1621
|
--github-token-command <cmd> Command used to read a GitHub token. Default: gh auth token
|
|
1138
1622
|
--copilot-token <token> Short-lived Copilot API bearer token
|
|
1139
1623
|
--copilot-api-base-url <url> Copilot API base URL override
|
|
1140
1624
|
--no-gh Do not try gh auth token
|
|
1625
|
+
--no-update-check Do not check GitHub for a newer release
|
|
1141
1626
|
--allow-unauthenticated Allow non-loopback bind without --api-key
|
|
1142
1627
|
-h, --help Show help
|
|
1143
1628
|
-v, --version Show version
|
|
1144
1629
|
|
|
1145
1630
|
Environment:
|
|
1146
1631
|
HOOPILOT_API_KEY
|
|
1147
|
-
COPILOT_GITHUB_TOKEN
|
|
1632
|
+
COPILOT_GITHUB_TOKEN
|
|
1148
1633
|
COPILOT_API_TOKEN, GITHUB_COPILOT_API_TOKEN
|
|
1149
1634
|
COPILOT_API_BASE_URL
|
|
1635
|
+
HOOPILOT_NO_UPDATE_CHECK Set to disable update checks (also NO_UPDATE_NOTIFIER)
|
|
1150
1636
|
`;
|
|
1151
1637
|
}
|
|
1152
1638
|
if (import.meta.main) {
|