@mutmutco/installer-launcher 0.1.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 +187 -0
- package/config/product.template.json +9 -0
- package/dist/launcher.js +825 -0
- package/dist/launcher.sea.cjs +855 -0
- package/dist/product.json +9 -0
- package/package.json +36 -0
|
@@ -0,0 +1,855 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
LAUNCHER_VERSION: () => LAUNCHER_VERSION,
|
|
24
|
+
defaultRunEntry: () => defaultRunEntry,
|
|
25
|
+
ensureFreshToken: () => ensureFreshToken,
|
|
26
|
+
readPayloadEntry: () => readPayloadEntry,
|
|
27
|
+
resolveEntry: () => resolveEntry,
|
|
28
|
+
run: () => run,
|
|
29
|
+
runFile: () => runFile
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(index_exports);
|
|
32
|
+
var import_node_child_process2 = require("node:child_process");
|
|
33
|
+
var import_node_fs4 = require("node:fs");
|
|
34
|
+
var import_node_path3 = require("node:path");
|
|
35
|
+
var import_node_url2 = require("node:url");
|
|
36
|
+
|
|
37
|
+
// src/config.ts
|
|
38
|
+
var import_node_fs = require("node:fs");
|
|
39
|
+
var import_node_sea = require("node:sea");
|
|
40
|
+
|
|
41
|
+
// src/module-url.ts
|
|
42
|
+
var import_node_url = require("node:url");
|
|
43
|
+
var import_meta = {};
|
|
44
|
+
function moduleUrl() {
|
|
45
|
+
if (typeof __filename === "string") return (0, import_node_url.pathToFileURL)(__filename).href;
|
|
46
|
+
return import_meta.url;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/config.ts
|
|
50
|
+
function publicKeyBytes(config) {
|
|
51
|
+
const raw = Buffer.from(config.publicKey, "base64");
|
|
52
|
+
if (raw.length !== 32) {
|
|
53
|
+
throw new Error(`product config for "${config.product}" has a bad publicKey (want 32 raw bytes)`);
|
|
54
|
+
}
|
|
55
|
+
return raw;
|
|
56
|
+
}
|
|
57
|
+
function loadProductConfig(options = {}) {
|
|
58
|
+
const explicit = options.configPath ?? process.env.LAUNCHER_CONFIG;
|
|
59
|
+
const devFallback = new URL("../config/product.template.json", moduleUrl());
|
|
60
|
+
if (explicit) {
|
|
61
|
+
return parseProductConfig((0, import_node_fs.readFileSync)(explicit, "utf8"));
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
return parseProductConfig((0, import_node_fs.readFileSync)(devFallback, "utf8"));
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const asset = (0, import_node_sea.getAsset)("product.json", "utf8");
|
|
69
|
+
if (typeof asset === "string") return parseProductConfig(asset);
|
|
70
|
+
} catch {
|
|
71
|
+
}
|
|
72
|
+
throw new Error(
|
|
73
|
+
"no product config found (pass --config <path>, set LAUNCHER_CONFIG, or bake product.json into the SEA binary)"
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
function parseProductConfig(text) {
|
|
77
|
+
let data;
|
|
78
|
+
try {
|
|
79
|
+
data = JSON.parse(text);
|
|
80
|
+
} catch {
|
|
81
|
+
throw new Error("product config is not valid JSON");
|
|
82
|
+
}
|
|
83
|
+
if (typeof data !== "object" || data === null) throw new Error("product config must be an object");
|
|
84
|
+
const record = data;
|
|
85
|
+
const product = field(record, "product");
|
|
86
|
+
const host = field(record, "host").replace(/\/+$/, "");
|
|
87
|
+
const loginKind = field(record, "loginKind");
|
|
88
|
+
const binName = field(record, "binName");
|
|
89
|
+
if (!product || !host || !binName) throw new Error("product config needs product, host and binName");
|
|
90
|
+
if (loginKind !== "github" && loginKind !== "google") {
|
|
91
|
+
throw new Error('product config loginKind must be "github" or "google"');
|
|
92
|
+
}
|
|
93
|
+
const config = { product, host, loginKind, publicKey: field(record, "publicKey"), binName };
|
|
94
|
+
if (!config.publicKey) throw new Error("product config needs publicKey (base64 raw Ed25519)");
|
|
95
|
+
publicKeyBytes(config);
|
|
96
|
+
if (loginKind === "github") {
|
|
97
|
+
const clientId = record.githubClientId;
|
|
98
|
+
if (typeof clientId !== "string" || clientId.length === 0) {
|
|
99
|
+
throw new Error("product config needs githubClientId for the github loginKind");
|
|
100
|
+
}
|
|
101
|
+
config.githubClientId = clientId;
|
|
102
|
+
} else if (typeof record.githubClientId === "string") {
|
|
103
|
+
config.githubClientId = record.githubClientId;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const url = new URL(host);
|
|
107
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") throw new Error();
|
|
108
|
+
} catch {
|
|
109
|
+
throw new Error("product config host must be an http(s) URL");
|
|
110
|
+
}
|
|
111
|
+
return config;
|
|
112
|
+
}
|
|
113
|
+
function field(record, key) {
|
|
114
|
+
const value = record[key];
|
|
115
|
+
return typeof value === "string" ? value : "";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/login-github.ts
|
|
119
|
+
var import_node_child_process = require("node:child_process");
|
|
120
|
+
var realSleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
121
|
+
function openBrowser(url) {
|
|
122
|
+
if (process.env.LAUNCHER_NO_OPEN === "1") return;
|
|
123
|
+
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
124
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : process.platform === "darwin" ? [url] : [url];
|
|
125
|
+
try {
|
|
126
|
+
const child = (0, import_node_child_process.spawn)(opener, args, { detached: true, stdio: "ignore", windowsHide: true });
|
|
127
|
+
child.on("error", () => {
|
|
128
|
+
});
|
|
129
|
+
child.unref();
|
|
130
|
+
} catch {
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function postJson(url, body, fetchImpl) {
|
|
134
|
+
return fetchImpl(url, {
|
|
135
|
+
method: "POST",
|
|
136
|
+
headers: { "content-type": "application/json" },
|
|
137
|
+
body: JSON.stringify(body)
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
async function requestDeviceCode(config, fetchImpl) {
|
|
141
|
+
if (!config.githubClientId) throw new Error("this product has no githubClientId configured");
|
|
142
|
+
const response = await postJson(`${config.host}/gate/device/code`, { client_id: config.githubClientId }, fetchImpl);
|
|
143
|
+
if (!response.ok) throw new Error(`the sign-in server refused the request (${response.status})`);
|
|
144
|
+
const data = await response.json();
|
|
145
|
+
if (typeof data.device_code !== "string" || typeof data.user_code !== "string" || typeof data.verification_uri !== "string" || typeof data.expires_in !== "number" || typeof data.interval !== "number") {
|
|
146
|
+
throw new Error("the sign-in server returned a malformed device code");
|
|
147
|
+
}
|
|
148
|
+
const issued = {
|
|
149
|
+
device_code: data.device_code,
|
|
150
|
+
user_code: data.user_code,
|
|
151
|
+
verification_uri: data.verification_uri,
|
|
152
|
+
expires_in: data.expires_in,
|
|
153
|
+
interval: data.interval
|
|
154
|
+
};
|
|
155
|
+
if (typeof data.verification_uri_complete === "string") {
|
|
156
|
+
issued.verification_uri_complete = data.verification_uri_complete;
|
|
157
|
+
}
|
|
158
|
+
return issued;
|
|
159
|
+
}
|
|
160
|
+
async function loginGithub(config, options = {}) {
|
|
161
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
162
|
+
const open = options.open ?? openBrowser;
|
|
163
|
+
const print = options.print ?? ((message) => process.stdout.write(`${message}
|
|
164
|
+
`));
|
|
165
|
+
const now = options.now ?? (() => Date.now());
|
|
166
|
+
const sleep = options.sleep ?? realSleep;
|
|
167
|
+
const issued = await requestDeviceCode(config, fetchImpl);
|
|
168
|
+
const openUrl = issued.verification_uri_complete ?? issued.verification_uri;
|
|
169
|
+
print(`To sign in, open ${issued.verification_uri} and enter the code ${issued.user_code}.`);
|
|
170
|
+
open(openUrl);
|
|
171
|
+
const deadline = now() + issued.expires_in * 1e3;
|
|
172
|
+
let intervalMs = Math.max(1, issued.interval) * 1e3;
|
|
173
|
+
for (; ; ) {
|
|
174
|
+
if (now() >= deadline) throw new Error("the sign-in code expired before you approved it \u2014 run login again.");
|
|
175
|
+
await sleep(intervalMs);
|
|
176
|
+
const response = await postJson(
|
|
177
|
+
`${config.host}/gate/device/token`,
|
|
178
|
+
{ client_id: config.githubClientId, device_code: issued.device_code },
|
|
179
|
+
fetchImpl
|
|
180
|
+
);
|
|
181
|
+
if (response.ok) {
|
|
182
|
+
const data = await response.json();
|
|
183
|
+
if (typeof data.access_token !== "string" || typeof data.refresh_token !== "string" || typeof data.expires_in !== "number") {
|
|
184
|
+
throw new Error("the sign-in server returned malformed tokens");
|
|
185
|
+
}
|
|
186
|
+
return { accessToken: data.access_token, refreshToken: data.refresh_token, expiresIn: data.expires_in };
|
|
187
|
+
}
|
|
188
|
+
let error = "";
|
|
189
|
+
try {
|
|
190
|
+
error = (await response.json()).error ?? "";
|
|
191
|
+
} catch {
|
|
192
|
+
error = "";
|
|
193
|
+
}
|
|
194
|
+
if (error === "authorization_pending") continue;
|
|
195
|
+
if (error === "slow_down") {
|
|
196
|
+
intervalMs += 5e3;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (error === "expired_token") {
|
|
200
|
+
throw new Error("the sign-in code expired before you approved it \u2014 run login again.");
|
|
201
|
+
}
|
|
202
|
+
if (error === "denied") {
|
|
203
|
+
throw new Error("you declined the sign-in request \u2014 nothing was installed.");
|
|
204
|
+
}
|
|
205
|
+
throw new Error(
|
|
206
|
+
`the sign-in server refused the request (${response.status}${error ? `: ${error}` : ""})`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async function refreshAccessToken(config, refreshToken, fetchImpl = fetch) {
|
|
211
|
+
const response = await postJson(`${config.host}/gate/refresh`, { refresh_token: refreshToken }, fetchImpl);
|
|
212
|
+
if (response.status === 403) return null;
|
|
213
|
+
if (!response.ok) throw new Error(`the sign-in server refused the refresh (${response.status})`);
|
|
214
|
+
const data = await response.json();
|
|
215
|
+
if (typeof data.access_token !== "string" || typeof data.expires_in !== "number") {
|
|
216
|
+
throw new Error("the sign-in server returned a malformed refresh");
|
|
217
|
+
}
|
|
218
|
+
return { accessToken: data.access_token, expiresIn: data.expires_in };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// src/login-google.ts
|
|
222
|
+
var import_node_crypto = require("node:crypto");
|
|
223
|
+
var import_node_http = require("node:http");
|
|
224
|
+
var b64url = (bytes) => bytes.toString("base64url");
|
|
225
|
+
async function loginGoogle(options) {
|
|
226
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
227
|
+
const server = options.host.replace(/\/+$/, "");
|
|
228
|
+
const timeoutMs = options.timeoutMs ?? 5 * 6e4;
|
|
229
|
+
const listener = (0, import_node_http.createServer)();
|
|
230
|
+
await new Promise((resolve2) => listener.listen(0, "127.0.0.1", resolve2));
|
|
231
|
+
const port = listener.address().port;
|
|
232
|
+
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
233
|
+
try {
|
|
234
|
+
const registration = await fetchImpl(`${server}/oauth/register`, {
|
|
235
|
+
method: "POST",
|
|
236
|
+
headers: { "content-type": "application/json" },
|
|
237
|
+
body: JSON.stringify({ client_name: "launcher", redirect_uris: [redirectUri] })
|
|
238
|
+
});
|
|
239
|
+
if (!registration.ok) throw new Error(`the sign-in server refused to register this app (${registration.status})`);
|
|
240
|
+
const clientId = (await registration.json()).client_id;
|
|
241
|
+
if (typeof clientId !== "string" || !clientId) {
|
|
242
|
+
throw new Error("the sign-in server returned a malformed registration");
|
|
243
|
+
}
|
|
244
|
+
const verifier = b64url((0, import_node_crypto.randomBytes)(32));
|
|
245
|
+
const challenge = b64url((0, import_node_crypto.createHash)("sha256").update(verifier).digest());
|
|
246
|
+
const state = b64url((0, import_node_crypto.randomBytes)(16));
|
|
247
|
+
const authorize = new URL(`${server}/oauth/authorize`);
|
|
248
|
+
authorize.search = new URLSearchParams({
|
|
249
|
+
response_type: "code",
|
|
250
|
+
client_id: clientId,
|
|
251
|
+
redirect_uri: redirectUri,
|
|
252
|
+
scope: "installer",
|
|
253
|
+
state,
|
|
254
|
+
code_challenge: challenge,
|
|
255
|
+
code_challenge_method: "S256"
|
|
256
|
+
}).toString();
|
|
257
|
+
const code = await new Promise((resolve2, reject) => {
|
|
258
|
+
const timer = setTimeout(() => reject(new Error("sign-in timed out \u2014 run login again.")), timeoutMs);
|
|
259
|
+
listener.on("request", (req, res) => {
|
|
260
|
+
const url = new URL(req.url ?? "/", redirectUri);
|
|
261
|
+
if (url.pathname !== "/callback") {
|
|
262
|
+
res.writeHead(404).end();
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const error = url.searchParams.get("error");
|
|
266
|
+
const received = url.searchParams.get("code");
|
|
267
|
+
if (url.searchParams.get("state") !== state || error || !received) {
|
|
268
|
+
res.writeHead(400, { "content-type": "text/html; charset=utf-8" }).end(page("Sign-in did not complete.", error ?? "no code"));
|
|
269
|
+
clearTimeout(timer);
|
|
270
|
+
reject(new Error(error ?? "sign-in rejected"));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" }).end(page("Signed in.", "You can close this tab and go back to the terminal."));
|
|
274
|
+
clearTimeout(timer);
|
|
275
|
+
resolve2(received);
|
|
276
|
+
});
|
|
277
|
+
options.open(authorize.toString());
|
|
278
|
+
});
|
|
279
|
+
const exchange = await fetchImpl(`${server}/oauth/token`, {
|
|
280
|
+
method: "POST",
|
|
281
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
282
|
+
body: new URLSearchParams({
|
|
283
|
+
grant_type: "authorization_code",
|
|
284
|
+
code,
|
|
285
|
+
redirect_uri: redirectUri,
|
|
286
|
+
client_id: clientId,
|
|
287
|
+
code_verifier: verifier
|
|
288
|
+
}).toString()
|
|
289
|
+
});
|
|
290
|
+
if (!exchange.ok) throw new Error(`token exchange failed (${exchange.status})`);
|
|
291
|
+
const tokens = await exchange.json();
|
|
292
|
+
if (typeof tokens.access_token !== "string" || typeof tokens.refresh_token !== "string" || typeof tokens.expires_in !== "number") {
|
|
293
|
+
throw new Error("the sign-in server returned malformed tokens");
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
accessToken: tokens.access_token,
|
|
297
|
+
refreshToken: tokens.refresh_token,
|
|
298
|
+
expiresIn: tokens.expires_in,
|
|
299
|
+
clientId
|
|
300
|
+
};
|
|
301
|
+
} finally {
|
|
302
|
+
listener.close();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
async function refreshGoogleToken(host, clientId, refreshToken, fetchImpl = fetch) {
|
|
306
|
+
const server = host.replace(/\/+$/, "");
|
|
307
|
+
const response = await fetchImpl(`${server}/oauth/token`, {
|
|
308
|
+
method: "POST",
|
|
309
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
310
|
+
body: new URLSearchParams({
|
|
311
|
+
grant_type: "refresh_token",
|
|
312
|
+
refresh_token: refreshToken,
|
|
313
|
+
client_id: clientId
|
|
314
|
+
}).toString()
|
|
315
|
+
});
|
|
316
|
+
if (!response.ok) return null;
|
|
317
|
+
const tokens = await response.json();
|
|
318
|
+
if (typeof tokens.access_token !== "string" || typeof tokens.expires_in !== "number") return null;
|
|
319
|
+
return {
|
|
320
|
+
accessToken: tokens.access_token,
|
|
321
|
+
refreshToken: typeof tokens.refresh_token === "string" ? tokens.refresh_token : refreshToken,
|
|
322
|
+
expiresIn: tokens.expires_in
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function page(title, body) {
|
|
326
|
+
return `<!doctype html><meta charset="utf-8"><title>${title}</title><style>body{font-family:system-ui;margin:0;padding:12vh 1.5rem}main{max-width:32rem;margin:auto}h1{font-size:1.4rem}</style><main><h1>${title}</h1><p>${body}</p></main>`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/payload.ts
|
|
330
|
+
var import_node_crypto2 = require("node:crypto");
|
|
331
|
+
var import_node_fs2 = require("node:fs");
|
|
332
|
+
var import_node_os = require("node:os");
|
|
333
|
+
var import_node_path = require("node:path");
|
|
334
|
+
|
|
335
|
+
// src/canonical.ts
|
|
336
|
+
function canonicalJson(value) {
|
|
337
|
+
return encode(value);
|
|
338
|
+
}
|
|
339
|
+
function encode(value) {
|
|
340
|
+
if (value === null) return "null";
|
|
341
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
342
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
343
|
+
if (typeof value === "number") {
|
|
344
|
+
if (!Number.isFinite(value)) {
|
|
345
|
+
throw new TypeError("canonicalJson: cannot encode a non-finite number");
|
|
346
|
+
}
|
|
347
|
+
return JSON.stringify(value);
|
|
348
|
+
}
|
|
349
|
+
if (Array.isArray(value)) {
|
|
350
|
+
return `[${value.map((entry) => encode(entry)).join(",")}]`;
|
|
351
|
+
}
|
|
352
|
+
if (typeof value === "object") {
|
|
353
|
+
const record = value;
|
|
354
|
+
const keys = Object.keys(record).filter((key) => record[key] !== void 0).sort();
|
|
355
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${encode(record[key])}`).join(",")}}`;
|
|
356
|
+
}
|
|
357
|
+
throw new TypeError(`canonicalJson: cannot encode a value of type ${typeof value}`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// src/payload.ts
|
|
361
|
+
var NeedsLoginError = class extends Error {
|
|
362
|
+
constructor() {
|
|
363
|
+
super("signed out \u2014 run login first");
|
|
364
|
+
this.name = "NeedsLoginError";
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
var ForbiddenError = class extends Error {
|
|
368
|
+
constructor() {
|
|
369
|
+
super("this install is not allowed for your account \u2014 access was revoked or never granted.");
|
|
370
|
+
this.name = "ForbiddenError";
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
function canonicalManifestBytes(manifest) {
|
|
374
|
+
return Buffer.from(
|
|
375
|
+
canonicalJson({
|
|
376
|
+
created: manifest.created,
|
|
377
|
+
files: manifest.files.map((file) => ({ path: file.path, sha256: file.sha256, size: file.size })),
|
|
378
|
+
version: manifest.version
|
|
379
|
+
}),
|
|
380
|
+
"utf8"
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
function ed25519PublicKey(config) {
|
|
384
|
+
const raw = publicKeyBytes(config);
|
|
385
|
+
return (0, import_node_crypto2.createPublicKey)({
|
|
386
|
+
key: { kty: "OKP", crv: "Ed25519", x: raw.toString("base64url") },
|
|
387
|
+
format: "jwk"
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
function verifyManifest(manifest, signature, config) {
|
|
391
|
+
try {
|
|
392
|
+
const signatureBytes = Buffer.from(signature, "base64");
|
|
393
|
+
if (signatureBytes.length === 0) return false;
|
|
394
|
+
return (0, import_node_crypto2.verify)(null, canonicalManifestBytes(manifest), ed25519PublicKey(config), signatureBytes);
|
|
395
|
+
} catch {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function verifyFileBytes(entry, bytes) {
|
|
400
|
+
if (entry.size !== bytes.length) return false;
|
|
401
|
+
return (0, import_node_crypto2.createHash)("sha256").update(bytes).digest("hex") === entry.sha256.toLowerCase();
|
|
402
|
+
}
|
|
403
|
+
async function readErrorCode(response) {
|
|
404
|
+
try {
|
|
405
|
+
const data = await response.json();
|
|
406
|
+
return typeof data.error === "string" ? data.error : "";
|
|
407
|
+
} catch {
|
|
408
|
+
return "";
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
function throwForStatus(status, errorCode) {
|
|
412
|
+
if (status === 401) throw new NeedsLoginError();
|
|
413
|
+
if (status === 403) throw new ForbiddenError();
|
|
414
|
+
throw new Error(
|
|
415
|
+
errorCode ? `the release server refused the request (${status}: ${errorCode})` : `the release server refused the request (${status})`
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
async function getJson(url, accessToken, fetchImpl) {
|
|
419
|
+
const response = await fetchImpl(url, { headers: { authorization: `Bearer ${accessToken}` } });
|
|
420
|
+
if (!response.ok) throwForStatus(response.status, await readErrorCode(response));
|
|
421
|
+
return { status: response.status, json: await response.json() };
|
|
422
|
+
}
|
|
423
|
+
function parseManifest(json) {
|
|
424
|
+
if (typeof json !== "object" || json === null) throw new Error("the release manifest is not an object");
|
|
425
|
+
const record = json;
|
|
426
|
+
if (typeof record.version !== "string" || !record.version) throw new Error("the release manifest has no version");
|
|
427
|
+
if (typeof record.created !== "string" || !record.created) throw new Error("the release manifest has no created stamp");
|
|
428
|
+
if (!Array.isArray(record.files)) throw new Error("the release manifest has no files list");
|
|
429
|
+
if (typeof record.signature !== "string" || !record.signature) {
|
|
430
|
+
throw new Error("the release manifest is unsigned");
|
|
431
|
+
}
|
|
432
|
+
const files = record.files.map((entry) => {
|
|
433
|
+
const file = entry;
|
|
434
|
+
if (typeof file.path !== "string" || typeof file.sha256 !== "string" || typeof file.size !== "number") {
|
|
435
|
+
throw new Error("the release manifest lists a malformed file");
|
|
436
|
+
}
|
|
437
|
+
const safe = safeManifestPath(file.path);
|
|
438
|
+
if (safe === null) throw new Error(`the release manifest lists an unsafe path: ${file.path}`);
|
|
439
|
+
return { path: safe, sha256: file.sha256, size: file.size };
|
|
440
|
+
});
|
|
441
|
+
return { version: record.version, created: record.created, files, signature: record.signature };
|
|
442
|
+
}
|
|
443
|
+
function safeManifestPath(rawPath) {
|
|
444
|
+
if (rawPath.length === 0 || rawPath.includes("\0") || rawPath.includes("\\")) return null;
|
|
445
|
+
if (rawPath.startsWith("/")) return null;
|
|
446
|
+
const segments = rawPath.split("/");
|
|
447
|
+
for (const segment of segments) {
|
|
448
|
+
if (segment === "" || segment === "." || segment === "..") return null;
|
|
449
|
+
}
|
|
450
|
+
return segments.join("/");
|
|
451
|
+
}
|
|
452
|
+
async function fetchVerifiedManifest(config, accessToken, fetchImpl = fetch) {
|
|
453
|
+
const { json } = await getJson(`${config.host}/release/manifest`, accessToken, fetchImpl);
|
|
454
|
+
const manifest = parseManifest(json);
|
|
455
|
+
if (!verifyManifest(manifest, manifest.signature, config)) {
|
|
456
|
+
throw new Error("the release manifest signature is invalid \u2014 refusing to install anything.");
|
|
457
|
+
}
|
|
458
|
+
return manifest;
|
|
459
|
+
}
|
|
460
|
+
async function fetchFileBytes(config, accessToken, path, fetchImpl) {
|
|
461
|
+
const encoded = path.split("/").map(encodeURIComponent).join("/");
|
|
462
|
+
const response = await fetchImpl(`${config.host}/release/${encoded}`, {
|
|
463
|
+
headers: { authorization: `Bearer ${accessToken}` }
|
|
464
|
+
});
|
|
465
|
+
if (!response.ok) {
|
|
466
|
+
if (response.status === 404) throw new Error(`the release server has no file at ${path}`);
|
|
467
|
+
throwForStatus(response.status, await readErrorCode(response));
|
|
468
|
+
}
|
|
469
|
+
return Buffer.from(await response.arrayBuffer());
|
|
470
|
+
}
|
|
471
|
+
async function downloadAndUnpack(config, dir, manifest, accessToken, options = {}) {
|
|
472
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
473
|
+
const staging = (0, import_node_path.join)((0, import_node_os.tmpdir)(), `launcher-${config.product}-${Date.now()}-${Math.floor(Math.random() * 1e6)}`);
|
|
474
|
+
(0, import_node_fs2.mkdirSync)(staging, { recursive: true });
|
|
475
|
+
try {
|
|
476
|
+
for (const entry of manifest.files) {
|
|
477
|
+
const bytes = await fetchFileBytes(config, accessToken, entry.path, fetchImpl);
|
|
478
|
+
if (!verifyFileBytes(entry, bytes)) {
|
|
479
|
+
throw new Error(`file ${entry.path} failed its checksum \u2014 refusing to install anything.`);
|
|
480
|
+
}
|
|
481
|
+
const dest = (0, import_node_path.join)(staging, entry.path);
|
|
482
|
+
(0, import_node_fs2.mkdirSync)((0, import_node_path.dirname)(dest), { recursive: true });
|
|
483
|
+
(0, import_node_fs2.writeFileSync)(dest, bytes);
|
|
484
|
+
}
|
|
485
|
+
const target = (0, import_node_path.join)(dir, "payload");
|
|
486
|
+
(0, import_node_fs2.mkdirSync)(dir, { recursive: true });
|
|
487
|
+
(0, import_node_fs2.rmSync)(target, { force: true, recursive: true });
|
|
488
|
+
(0, import_node_fs2.renameSync)(staging, target);
|
|
489
|
+
} catch (error) {
|
|
490
|
+
(0, import_node_fs2.rmSync)(staging, { force: true, recursive: true });
|
|
491
|
+
throw error;
|
|
492
|
+
}
|
|
493
|
+
return manifest.version;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// src/store.ts
|
|
497
|
+
var import_node_fs3 = require("node:fs");
|
|
498
|
+
var import_node_os2 = require("node:os");
|
|
499
|
+
var import_node_path2 = require("node:path");
|
|
500
|
+
function defaultProductDir(product) {
|
|
501
|
+
if (process.platform === "win32") {
|
|
502
|
+
const base = process.env.LOCALAPPDATA ?? (0, import_node_path2.join)((0, import_node_os2.tmpdir)(), "launcher-fallback");
|
|
503
|
+
return (0, import_node_path2.join)(base, product);
|
|
504
|
+
}
|
|
505
|
+
const home = process.env.HOME ?? (0, import_node_os2.tmpdir)();
|
|
506
|
+
return (0, import_node_path2.join)(home, `.${product}`);
|
|
507
|
+
}
|
|
508
|
+
function resolveProductDir(product, explicit) {
|
|
509
|
+
return explicit ?? process.env.LAUNCHER_DIR ?? defaultProductDir(product);
|
|
510
|
+
}
|
|
511
|
+
function tokensPath(dir) {
|
|
512
|
+
return (0, import_node_path2.join)(dir, "tokens.json");
|
|
513
|
+
}
|
|
514
|
+
function statePath(dir) {
|
|
515
|
+
return (0, import_node_path2.join)(dir, "state.json");
|
|
516
|
+
}
|
|
517
|
+
function payloadDir(dir) {
|
|
518
|
+
return (0, import_node_path2.join)(dir, "payload");
|
|
519
|
+
}
|
|
520
|
+
function readTokens(dir) {
|
|
521
|
+
try {
|
|
522
|
+
const data = JSON.parse((0, import_node_fs3.readFileSync)(tokensPath(dir), "utf8"));
|
|
523
|
+
if (typeof data.accessToken !== "string" || !data.accessToken) return null;
|
|
524
|
+
if (typeof data.expiresAt !== "number" || !Number.isFinite(data.expiresAt)) return null;
|
|
525
|
+
const tokens = { accessToken: data.accessToken, expiresAt: data.expiresAt };
|
|
526
|
+
if (typeof data.refreshToken === "string" && data.refreshToken) tokens.refreshToken = data.refreshToken;
|
|
527
|
+
if (typeof data.clientId === "string" && data.clientId) tokens.clientId = data.clientId;
|
|
528
|
+
return tokens;
|
|
529
|
+
} catch {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
function writeTokens(dir, tokens) {
|
|
534
|
+
(0, import_node_fs3.mkdirSync)(dir, { recursive: true });
|
|
535
|
+
try {
|
|
536
|
+
(0, import_node_fs3.writeFileSync)(tokensPath(dir), `${JSON.stringify(tokens)}
|
|
537
|
+
`, { mode: 384 });
|
|
538
|
+
} catch {
|
|
539
|
+
(0, import_node_fs3.writeFileSync)(tokensPath(dir), `${JSON.stringify(tokens)}
|
|
540
|
+
`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
function clearTokens(dir) {
|
|
544
|
+
(0, import_node_fs3.rmSync)(tokensPath(dir), { force: true });
|
|
545
|
+
}
|
|
546
|
+
function readState(dir) {
|
|
547
|
+
try {
|
|
548
|
+
const data = JSON.parse((0, import_node_fs3.readFileSync)(statePath(dir), "utf8"));
|
|
549
|
+
if (typeof data.version !== "string" || !data.version) return null;
|
|
550
|
+
return { version: data.version, updatedAt: typeof data.updatedAt === "string" ? data.updatedAt : "" };
|
|
551
|
+
} catch {
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
function writeState(dir, state) {
|
|
556
|
+
(0, import_node_fs3.mkdirSync)(dir, { recursive: true });
|
|
557
|
+
(0, import_node_fs3.writeFileSync)(statePath(dir), `${JSON.stringify(state, null, 2)}
|
|
558
|
+
`);
|
|
559
|
+
}
|
|
560
|
+
function wipeProductDir(dir) {
|
|
561
|
+
(0, import_node_fs3.rmSync)(tokensPath(dir), { force: true });
|
|
562
|
+
(0, import_node_fs3.rmSync)(statePath(dir), { force: true });
|
|
563
|
+
(0, import_node_fs3.rmSync)(payloadDir(dir), { force: true, recursive: true });
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// src/index.ts
|
|
567
|
+
var LAUNCHER_VERSION = "0.1.0";
|
|
568
|
+
function defaultPrint(message) {
|
|
569
|
+
process.stdout.write(`${message}
|
|
570
|
+
`);
|
|
571
|
+
}
|
|
572
|
+
function defaultPrintErr(message) {
|
|
573
|
+
process.stderr.write(`${message}
|
|
574
|
+
`);
|
|
575
|
+
}
|
|
576
|
+
async function run(rawOptions = {}) {
|
|
577
|
+
const print = rawOptions.print ?? defaultPrint;
|
|
578
|
+
const printErr = rawOptions.printErr ?? defaultPrintErr;
|
|
579
|
+
const argv = rawOptions.argv ?? process.argv.slice(2);
|
|
580
|
+
const runAt = argv.indexOf("--run");
|
|
581
|
+
if (runAt >= 0) return runFile(argv[runAt + 1], argv.slice(runAt + 2), printErr);
|
|
582
|
+
let config;
|
|
583
|
+
try {
|
|
584
|
+
config = loadProductConfig({ configPath: rawOptions.configPath ?? flagValue(argv, "--config") });
|
|
585
|
+
} catch (error) {
|
|
586
|
+
print(`cannot start: ${error.message}`);
|
|
587
|
+
return 2;
|
|
588
|
+
}
|
|
589
|
+
const dir = resolveProductDir(config.product, rawOptions.dir ?? flagValue(argv, "--dir"));
|
|
590
|
+
const positional = argv.filter((arg) => !arg.startsWith("-"));
|
|
591
|
+
if (argv.includes("--version") || argv.includes("-v")) {
|
|
592
|
+
print(`${config.binName} launcher ${LAUNCHER_VERSION}`);
|
|
593
|
+
return 0;
|
|
594
|
+
}
|
|
595
|
+
if (argv.includes("--help") || argv.includes("-h") || positional.length === 0) {
|
|
596
|
+
printUsage(config, print);
|
|
597
|
+
return positional.length === 0 ? 2 : 0;
|
|
598
|
+
}
|
|
599
|
+
const command = positional[0];
|
|
600
|
+
try {
|
|
601
|
+
switch (command) {
|
|
602
|
+
case "login":
|
|
603
|
+
await doLogin(config, dir, rawOptions, print);
|
|
604
|
+
return 0;
|
|
605
|
+
case "logout":
|
|
606
|
+
wipeProductDir(dir);
|
|
607
|
+
print(`signed out of ${config.product} and removed the downloaded payload.`);
|
|
608
|
+
return 0;
|
|
609
|
+
case "install":
|
|
610
|
+
return await doInstall(config, dir, rawOptions, print);
|
|
611
|
+
case "update":
|
|
612
|
+
return await doUpdate(config, dir, rawOptions, print);
|
|
613
|
+
case "doctor":
|
|
614
|
+
doDoctor(config, dir, print);
|
|
615
|
+
return 0;
|
|
616
|
+
default:
|
|
617
|
+
print(`unknown command: ${command}`);
|
|
618
|
+
printUsage(config, print);
|
|
619
|
+
return 2;
|
|
620
|
+
}
|
|
621
|
+
} catch (error) {
|
|
622
|
+
if (error instanceof NeedsLoginError) {
|
|
623
|
+
print("you are signed out \u2014 run login first.");
|
|
624
|
+
return 3;
|
|
625
|
+
}
|
|
626
|
+
print(`failed: ${error.message}`);
|
|
627
|
+
return 1;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
function flagValue(argv, flag) {
|
|
631
|
+
const index = argv.indexOf(flag);
|
|
632
|
+
if (index < 0) return void 0;
|
|
633
|
+
const value = argv[index + 1];
|
|
634
|
+
return value && !value.startsWith("-") ? value : void 0;
|
|
635
|
+
}
|
|
636
|
+
function printUsage(config, print) {
|
|
637
|
+
print(`${config.binName} launcher ${LAUNCHER_VERSION} \u2014 sign in and install ${config.product}.`);
|
|
638
|
+
print("usage: launcher <login|logout|install|update|doctor> [--config <path>] [--dir <path>]");
|
|
639
|
+
print(" launcher --run <file> [args\u2026] (run a payload file with the embedded runtime)");
|
|
640
|
+
}
|
|
641
|
+
async function runFile(file, args, printErr) {
|
|
642
|
+
if (!file) {
|
|
643
|
+
printErr("launcher --run needs a file to run.");
|
|
644
|
+
return 1;
|
|
645
|
+
}
|
|
646
|
+
const abs = (0, import_node_path3.resolve)(process.cwd(), file);
|
|
647
|
+
if (!(0, import_node_fs4.existsSync)(abs)) {
|
|
648
|
+
printErr(`cannot run ${file}: no such file.`);
|
|
649
|
+
return 1;
|
|
650
|
+
}
|
|
651
|
+
const previousArgv = process.argv;
|
|
652
|
+
const previousExitCode = process.exitCode;
|
|
653
|
+
let code;
|
|
654
|
+
try {
|
|
655
|
+
process.argv = [process.execPath, abs, ...args];
|
|
656
|
+
process.exitCode = void 0;
|
|
657
|
+
await import((0, import_node_url2.pathToFileURL)(abs).href);
|
|
658
|
+
code = typeof process.exitCode === "number" ? process.exitCode : 0;
|
|
659
|
+
} catch (error) {
|
|
660
|
+
printErr(`cannot run ${file}: ${error.message}`);
|
|
661
|
+
code = 1;
|
|
662
|
+
} finally {
|
|
663
|
+
process.argv = previousArgv;
|
|
664
|
+
process.exitCode = previousExitCode;
|
|
665
|
+
}
|
|
666
|
+
return code;
|
|
667
|
+
}
|
|
668
|
+
async function ensureFreshToken(config, dir, fetchImpl = fetch) {
|
|
669
|
+
const stored = readTokens(dir);
|
|
670
|
+
if (!stored) return null;
|
|
671
|
+
if (stored.expiresAt - Date.now() > 6e4) return stored.accessToken;
|
|
672
|
+
if (!stored.refreshToken) return null;
|
|
673
|
+
if (config.loginKind === "github") {
|
|
674
|
+
const refreshed2 = await refreshAccessToken(config, stored.refreshToken, fetchImpl);
|
|
675
|
+
if (!refreshed2) {
|
|
676
|
+
clearTokens(dir);
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
writeTokens(dir, {
|
|
680
|
+
accessToken: refreshed2.accessToken,
|
|
681
|
+
refreshToken: stored.refreshToken,
|
|
682
|
+
expiresAt: Date.now() + refreshed2.expiresIn * 1e3
|
|
683
|
+
});
|
|
684
|
+
return refreshed2.accessToken;
|
|
685
|
+
}
|
|
686
|
+
if (!stored.clientId) {
|
|
687
|
+
clearTokens(dir);
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
const refreshed = await refreshGoogleToken(config.host, stored.clientId, stored.refreshToken, fetchImpl);
|
|
691
|
+
if (!refreshed) {
|
|
692
|
+
clearTokens(dir);
|
|
693
|
+
return null;
|
|
694
|
+
}
|
|
695
|
+
writeTokens(dir, {
|
|
696
|
+
accessToken: refreshed.accessToken,
|
|
697
|
+
refreshToken: refreshed.refreshToken,
|
|
698
|
+
expiresAt: Date.now() + refreshed.expiresIn * 1e3,
|
|
699
|
+
clientId: stored.clientId
|
|
700
|
+
});
|
|
701
|
+
return refreshed.accessToken;
|
|
702
|
+
}
|
|
703
|
+
async function doLogin(config, dir, options, print) {
|
|
704
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
705
|
+
const open = options.open ?? openBrowser;
|
|
706
|
+
if (config.loginKind === "github") {
|
|
707
|
+
const tokens = await loginGithub(config, { fetchImpl, open, print });
|
|
708
|
+
writeTokens(dir, {
|
|
709
|
+
accessToken: tokens.accessToken,
|
|
710
|
+
refreshToken: tokens.refreshToken,
|
|
711
|
+
expiresAt: Date.now() + tokens.expiresIn * 1e3
|
|
712
|
+
});
|
|
713
|
+
} else {
|
|
714
|
+
const tokens = await loginGoogle({ host: config.host, open, fetchImpl });
|
|
715
|
+
writeTokens(dir, {
|
|
716
|
+
accessToken: tokens.accessToken,
|
|
717
|
+
refreshToken: tokens.refreshToken,
|
|
718
|
+
expiresAt: Date.now() + tokens.expiresIn * 1e3,
|
|
719
|
+
clientId: tokens.clientId
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
print(`signed in to ${config.product}.`);
|
|
723
|
+
}
|
|
724
|
+
async function fetchAccessTokenOrLogin(config, dir, options, print) {
|
|
725
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
726
|
+
const fresh = await ensureFreshToken(config, dir, fetchImpl);
|
|
727
|
+
if (fresh) return fresh;
|
|
728
|
+
await doLogin(config, dir, options, print);
|
|
729
|
+
const after = readTokens(dir);
|
|
730
|
+
if (!after) throw new Error("sign-in did not produce a token");
|
|
731
|
+
return after.accessToken;
|
|
732
|
+
}
|
|
733
|
+
function readPayloadEntry(dir) {
|
|
734
|
+
try {
|
|
735
|
+
const parsed = JSON.parse((0, import_node_fs4.readFileSync)((0, import_node_path3.join)(payloadDir(dir), "payload.json"), "utf8"));
|
|
736
|
+
const entry = parsed.entry;
|
|
737
|
+
if (typeof entry === "string") {
|
|
738
|
+
const parts = entry.trim().split(/\s+/).filter(Boolean);
|
|
739
|
+
return parts.length > 0 ? parts : null;
|
|
740
|
+
}
|
|
741
|
+
if (Array.isArray(entry) && entry.every((part) => typeof part === "string" && part.length > 0)) {
|
|
742
|
+
return entry;
|
|
743
|
+
}
|
|
744
|
+
return null;
|
|
745
|
+
} catch {
|
|
746
|
+
return null;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
function resolveEntry(entry) {
|
|
750
|
+
return entry[0] === "$self" ? [process.execPath, ...entry.slice(1)] : entry;
|
|
751
|
+
}
|
|
752
|
+
function needsShell(command) {
|
|
753
|
+
if (process.platform !== "win32") return false;
|
|
754
|
+
if (/\.(cmd|bat)$/i.test(command)) return true;
|
|
755
|
+
return !(0, import_node_fs4.existsSync)(command);
|
|
756
|
+
}
|
|
757
|
+
function defaultRunEntry(entry, cwd) {
|
|
758
|
+
const [command, ...args] = entry;
|
|
759
|
+
const result = (0, import_node_child_process2.spawnSync)(command, args, {
|
|
760
|
+
cwd,
|
|
761
|
+
stdio: "inherit",
|
|
762
|
+
shell: needsShell(command),
|
|
763
|
+
windowsHide: true
|
|
764
|
+
});
|
|
765
|
+
if (result.error) return { ok: false, error: result.error.message };
|
|
766
|
+
return { ok: result.status === 0, error: result.status === 0 ? void 0 : `exit code ${result.status}` };
|
|
767
|
+
}
|
|
768
|
+
async function doInstall(config, dir, options, print) {
|
|
769
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
770
|
+
const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print);
|
|
771
|
+
const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
|
|
772
|
+
await downloadAndUnpack(config, dir, manifest, accessToken, { fetchImpl });
|
|
773
|
+
writeState(dir, { version: manifest.version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
774
|
+
print(`installed ${config.product} ${manifest.version} into ${dir}.`);
|
|
775
|
+
const entry = readPayloadEntry(dir);
|
|
776
|
+
if (!entry) {
|
|
777
|
+
print(`next step: run ${(0, import_node_path3.join)(payloadDir(dir), config.binName)} to start ${config.product}.`);
|
|
778
|
+
return 0;
|
|
779
|
+
}
|
|
780
|
+
const payload = payloadDir(dir);
|
|
781
|
+
const command = resolveEntry(entry);
|
|
782
|
+
const runEntry = options.runEntry ?? defaultRunEntry;
|
|
783
|
+
const result = runEntry(command, payload);
|
|
784
|
+
if (result.ok) {
|
|
785
|
+
print(`${config.product} is ready.`);
|
|
786
|
+
return 0;
|
|
787
|
+
}
|
|
788
|
+
print(`could not finish the last mile automatically${result.error ? ` (${result.error})` : ""}. Run it yourself:`);
|
|
789
|
+
print(` (cd ${payload} && ${command.join(" ")})`);
|
|
790
|
+
return 0;
|
|
791
|
+
}
|
|
792
|
+
async function doUpdate(config, dir, options, print) {
|
|
793
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
794
|
+
const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print);
|
|
795
|
+
const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
|
|
796
|
+
const current = readState(dir);
|
|
797
|
+
if (current && current.version === manifest.version && (0, import_node_fs4.existsSync)(payloadDir(dir))) {
|
|
798
|
+
print(`${config.product} is already up to date at ${manifest.version}.`);
|
|
799
|
+
return 0;
|
|
800
|
+
}
|
|
801
|
+
await downloadAndUnpack(config, dir, manifest, accessToken, { fetchImpl });
|
|
802
|
+
writeState(dir, { version: manifest.version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
803
|
+
print(`updated ${config.product} to ${manifest.version}.`);
|
|
804
|
+
return 0;
|
|
805
|
+
}
|
|
806
|
+
function doDoctor(config, dir, print) {
|
|
807
|
+
const tokens = readTokens(dir);
|
|
808
|
+
const state = readState(dir);
|
|
809
|
+
const payloadPresent = (0, import_node_fs4.existsSync)(payloadDir(dir));
|
|
810
|
+
print(`product: ${config.product}`);
|
|
811
|
+
print(`host: ${config.host}`);
|
|
812
|
+
print(`login: ${config.loginKind}`);
|
|
813
|
+
print(`dir: ${dir}`);
|
|
814
|
+
if (tokens) {
|
|
815
|
+
const remaining = Math.max(0, Math.round((tokens.expiresAt - Date.now()) / 1e3));
|
|
816
|
+
print(`token: present (expires in ${remaining}s)`);
|
|
817
|
+
} else {
|
|
818
|
+
print("token: none \u2014 run login first.");
|
|
819
|
+
}
|
|
820
|
+
print(`payload: ${state ? state.version : "none"}${payloadPresent ? "" : " (not downloaded)"}`);
|
|
821
|
+
print(`paths: tokens ${(0, import_node_path3.join)(dir, "tokens.json")}, state ${(0, import_node_path3.join)(dir, "state.json")}, payload ${payloadDir(dir)}`);
|
|
822
|
+
}
|
|
823
|
+
var invokedAsMain = typeof process.argv[1] === "string" && (() => {
|
|
824
|
+
try {
|
|
825
|
+
if (moduleUrl() === (0, import_node_url2.pathToFileURL)(process.argv[1]).href) return true;
|
|
826
|
+
} catch {
|
|
827
|
+
}
|
|
828
|
+
try {
|
|
829
|
+
return (0, import_node_url2.fileURLToPath)(moduleUrl()).toLowerCase() === process.argv[1].toLowerCase();
|
|
830
|
+
} catch {
|
|
831
|
+
return false;
|
|
832
|
+
}
|
|
833
|
+
})();
|
|
834
|
+
if (invokedAsMain) {
|
|
835
|
+
run().then(
|
|
836
|
+
(code) => {
|
|
837
|
+
process.exitCode = code;
|
|
838
|
+
},
|
|
839
|
+
(error) => {
|
|
840
|
+
process.stderr.write(`failed: ${error.message}
|
|
841
|
+
`);
|
|
842
|
+
process.exitCode = 1;
|
|
843
|
+
}
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
847
|
+
0 && (module.exports = {
|
|
848
|
+
LAUNCHER_VERSION,
|
|
849
|
+
defaultRunEntry,
|
|
850
|
+
ensureFreshToken,
|
|
851
|
+
readPayloadEntry,
|
|
852
|
+
resolveEntry,
|
|
853
|
+
run,
|
|
854
|
+
runFile
|
|
855
|
+
});
|