@quatronic/qore 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/dist/cli.js ADDED
@@ -0,0 +1,1084 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { homedir } from "os";
5
+ import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici";
6
+
7
+ // src/cli/io.ts
8
+ import { select } from "@inquirer/prompts";
9
+ import { createInterface } from "readline";
10
+
11
+ // src/platform/browser.ts
12
+ import open from "open";
13
+ async function openBrowser(url) {
14
+ if (process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return false;
15
+ try {
16
+ await open(url);
17
+ return true;
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
22
+
23
+ // src/core/errors.ts
24
+ var EXIT = {
25
+ QORE_INTERNAL: 1,
26
+ QORE_KEYCHAIN_UNAVAILABLE: 1,
27
+ QORE_KEYCHAIN_DENIED: 1,
28
+ QORE_CONTEXT_REQUIRED: 2,
29
+ QORE_CONTEXT_INVALID: 2,
30
+ QORE_NOT_LOGGED_IN: 4,
31
+ QORE_SESSION_EXPIRED: 4,
32
+ QORE_LOGIN_EXPIRED: 4,
33
+ QORE_LOGIN_DENIED: 4,
34
+ QORE_CLOCK_SKEW: 4,
35
+ QORE_TENANT_ACCESS_LOST: 5,
36
+ QORE_RESOURCE_NOT_FOUND: 5,
37
+ QORE_ACCESS_DENIED: 5,
38
+ QORE_NO_DEVELOPER_ACCESS: 5,
39
+ QORE_AUTH_UNAVAILABLE: 6,
40
+ QORE_AUTH_BUSY: 6,
41
+ QORE_API_UNREACHABLE: 6,
42
+ QORE_API_UNAVAILABLE: 6,
43
+ QORE_INCOMPATIBLE_SERVER: 7,
44
+ QORE_UNSUPPORTED_BY_SERVER: 7,
45
+ QORE_UPGRADE_REQUIRED: 7,
46
+ QORE_PORTAL_UNTRUSTED: 7,
47
+ QORE_RATE_LIMITED: 8
48
+ };
49
+ var QoreError = class extends Error {
50
+ code;
51
+ hint;
52
+ status;
53
+ requestId;
54
+ candidates;
55
+ serverCode;
56
+ details;
57
+ constructor(code, message, opts = {}) {
58
+ super(message, opts.cause === void 0 ? void 0 : { cause: opts.cause });
59
+ this.name = "QoreError";
60
+ this.code = code;
61
+ this.hint = opts.hint;
62
+ this.status = opts.status;
63
+ this.requestId = opts.requestId;
64
+ this.candidates = opts.candidates;
65
+ this.serverCode = opts.serverCode;
66
+ this.details = opts.details;
67
+ }
68
+ };
69
+ var CancelledError = class extends Error {
70
+ constructor() {
71
+ super("Cancelled");
72
+ this.name = "CancelledError";
73
+ }
74
+ };
75
+ function exitCodeFor(err) {
76
+ if (err instanceof CancelledError || err instanceof Error && err.name === "CancelledError") return 130;
77
+ if (err instanceof QoreError) return EXIT[err.code];
78
+ return 1;
79
+ }
80
+ function toJsonError(err) {
81
+ if (!(err instanceof QoreError)) {
82
+ return { code: "QORE_INTERNAL", message: err instanceof Error ? err.message : String(err) };
83
+ }
84
+ const out = { code: err.code, message: err.message };
85
+ for (const k of ["hint", "status", "requestId", "candidates", "serverCode", "details"]) {
86
+ if (err[k] !== void 0) out[k] = err[k];
87
+ }
88
+ return out;
89
+ }
90
+
91
+ // src/cli/io.ts
92
+ function isInteractive(o) {
93
+ return o.stdinTTY && o.stdoutTTY && !o.json && !o.noInput && !o.env.CI;
94
+ }
95
+ function processIo(interactive) {
96
+ return {
97
+ out: (s) => {
98
+ process.stdout.write(s + "\n");
99
+ },
100
+ err: (s) => {
101
+ process.stderr.write(s + "\n");
102
+ },
103
+ interactive,
104
+ async select(message, choices) {
105
+ try {
106
+ return await select({ message, choices });
107
+ } catch (e) {
108
+ if (e.name === "ExitPromptError") throw new CancelledError();
109
+ throw e;
110
+ }
111
+ },
112
+ waitForEnter: createWaitForEnter(process.stdin, process.stderr),
113
+ openUrl: openBrowser
114
+ };
115
+ }
116
+ function createWaitForEnter(input, output) {
117
+ return (message, signal) => new Promise((resolve, reject) => {
118
+ const rl = createInterface({ input, output, terminal: true });
119
+ const onAbort = () => {
120
+ rl.close();
121
+ reject(new CancelledError());
122
+ };
123
+ const done = () => {
124
+ signal.removeEventListener("abort", onAbort);
125
+ rl.close();
126
+ resolve();
127
+ };
128
+ signal.addEventListener("abort", onAbort, { once: true });
129
+ rl.on("SIGINT", () => {
130
+ signal.removeEventListener("abort", onAbort);
131
+ rl.close();
132
+ reject(new CancelledError());
133
+ });
134
+ rl.question(message, done);
135
+ });
136
+ }
137
+
138
+ // src/cli/program.ts
139
+ import { Command, CommanderError } from "commander";
140
+
141
+ // src/core/api/http.ts
142
+ function abortableFetch(fetch2, signal) {
143
+ return (input, init) => fetch2(input, { ...init, signal: init?.signal ? AbortSignal.any([init.signal, signal]) : signal });
144
+ }
145
+ var TLS = /* @__PURE__ */ new Set([
146
+ "SELF_SIGNED_CERT_IN_CHAIN",
147
+ "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
148
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
149
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
150
+ "CERT_HAS_EXPIRED"
151
+ ]);
152
+ function causeCode(e) {
153
+ const c = e?.cause;
154
+ const code = c?.code ?? e?.code;
155
+ return typeof code === "string" ? code : void 0;
156
+ }
157
+ function isAbortError(e) {
158
+ return e?.name === "AbortError";
159
+ }
160
+ function fetchError(e, host) {
161
+ return isAbortError(e) ? new CancelledError() : networkError(e, host);
162
+ }
163
+ function networkError(e, host) {
164
+ const code = causeCode(e) ?? "";
165
+ let reason = "connect";
166
+ let hint = "Check your network or VPN.";
167
+ if (code === "ENOTFOUND" || code === "EAI_AGAIN") {
168
+ reason = "dns";
169
+ hint = `${host} could not be resolved. Check your network or VPN.`;
170
+ } else if (TLS.has(code)) {
171
+ reason = "tls";
172
+ hint = "A proxy is re-signing TLS. Point NODE_EXTRA_CA_CERTS at your organisation's CA certificate.";
173
+ } else if (code.includes("TIMEOUT") || code === "ETIMEDOUT") {
174
+ reason = "timeout";
175
+ } else if (!process.env.HTTPS_PROXY && !process.env.https_proxy) {
176
+ hint = "Check your network. Behind a corporate proxy, set HTTPS_PROXY.";
177
+ }
178
+ return new QoreError("QORE_API_UNREACHABLE", `Could not reach ${host}`, { hint, details: { reason, host, errno: code || void 0 }, cause: e });
179
+ }
180
+ var BY_SERVER_CODE = {
181
+ "app.access.denied": "QORE_ACCESS_DENIED",
182
+ "auth.scope.insufficient": "QORE_ACCESS_DENIED",
183
+ "cli.upgrade_required": "QORE_UPGRADE_REQUIRED",
184
+ "rate_limited": "QORE_RATE_LIMITED"
185
+ };
186
+ function byStatus(status) {
187
+ if (status === 403) return "QORE_ACCESS_DENIED";
188
+ if (status === 404) return "QORE_RESOURCE_NOT_FOUND";
189
+ if (status === 426) return "QORE_UPGRADE_REQUIRED";
190
+ if (status === 429) return "QORE_RATE_LIMITED";
191
+ if (status === 502 || status === 503 || status === 504) return "QORE_API_UNAVAILABLE";
192
+ return "QORE_INTERNAL";
193
+ }
194
+ async function problemToError(res) {
195
+ let body = {};
196
+ try {
197
+ body = await res.json();
198
+ } catch {
199
+ }
200
+ const serverCode = body.code;
201
+ const code = serverCode && BY_SERVER_CODE[serverCode] || byStatus(res.status);
202
+ const requestId = res.headers.get("x-request-id") ?? void 0;
203
+ const details = {};
204
+ if (body.detail && typeof body.detail === "object") Object.assign(details, body.detail);
205
+ const detail = typeof body.detail === "string" && body.detail ? body.detail : void 0;
206
+ if (detail) details.detail = detail;
207
+ const retry = res.headers.get("retry-after");
208
+ if (retry && /^\d+$/.test(retry)) details.retryAfter = Number(retry);
209
+ const hint = code === "QORE_UPGRADE_REQUIRED" ? "Run: npm i -g @quatronic/qore" : code === "QORE_RATE_LIMITED" ? `Wait ${details.retryAfter ?? "a moment"} s and retry.` : code === "QORE_API_UNAVAILABLE" ? "Qore is temporarily unavailable; retry in a minute." : void 0;
210
+ const genericTitle = !body.title || body.title === "HTTP error" || body.title === serverCode;
211
+ const message = genericTitle && detail || body.title || `Request failed (${res.status})`;
212
+ return new QoreError(code, message, {
213
+ status: res.status,
214
+ requestId,
215
+ serverCode,
216
+ hint,
217
+ details: Object.keys(details).length ? details : void 0
218
+ });
219
+ }
220
+
221
+ // src/cli/program.ts
222
+ import pc2 from "picocolors";
223
+
224
+ // src/cli/render.ts
225
+ import pc, { createColors } from "picocolors";
226
+ function renderJson(io2, data) {
227
+ io2.out(JSON.stringify({ ok: true, data }));
228
+ }
229
+ function renderError(io2, err, json2, colorEnabled = pc.isColorSupported) {
230
+ if (json2) {
231
+ io2.out(JSON.stringify({ ok: false, error: toJsonError(err) }));
232
+ return;
233
+ }
234
+ if (err instanceof Error && err.name === "CancelledError") {
235
+ io2.err("Cancelled");
236
+ return;
237
+ }
238
+ const c = createColors(colorEnabled);
239
+ const e = err instanceof QoreError ? err : new QoreError("QORE_INTERNAL", err instanceof Error ? err.message : String(err));
240
+ io2.err(c.red(`Error: ${e.message}`));
241
+ if (e.hint) io2.err(e.hint);
242
+ io2.err(c.dim(`Code: ${e.code}${e.requestId ? ` (request ${e.requestId})` : ""}`));
243
+ }
244
+
245
+ // src/cli/program.ts
246
+ function jsonFromArgv(argv2) {
247
+ return argv2.some((a) => a === "--json" || a.startsWith("--json="));
248
+ }
249
+ function requestLog(ctx) {
250
+ return ctx.opts().verbose ? (line) => ctx.io.err(line) : void 0;
251
+ }
252
+ function buildProgram(deps2, io2, signal, register) {
253
+ const program = new Command("qore").description("Qore developer CLI").option("--portal <name|url>", "prod, staging, or a portal URL (default: the portal you last logged in to, else prod)").option("--json", "print exactly one JSON document on stdout").option("--no-input", "never prompt").option("--verbose", "log request lines (never secrets)").option("--no-color", "disable colours").exitOverride().configureOutput({ writeOut: (s) => io2.out(s.trimEnd()), writeErr: (s) => io2.err(s.trimEnd()) });
254
+ const ctx = { deps: deps2, io: io2, signal, opts: () => program.opts() };
255
+ for (const r of register) r(program, ctx);
256
+ return program;
257
+ }
258
+ async function run(argv2, deps2, io2, signal, register) {
259
+ const json2 = jsonFromArgv(argv2);
260
+ const parseIo = json2 ? { ...io2, err: () => {
261
+ } } : io2;
262
+ const program = buildProgram(deps2, parseIo, signal, register);
263
+ try {
264
+ await program.parseAsync(argv2, { from: "user" });
265
+ return 0;
266
+ } catch (caught) {
267
+ const e = signal.aborted && isAbortError(caught) ? new CancelledError() : caught;
268
+ if (e instanceof CommanderError) {
269
+ if (e.exitCode === 0) return 0;
270
+ if (json2) io2.out(JSON.stringify({ ok: false, error: { code: "QORE_CONTEXT_INVALID", message: e.message } }));
271
+ return 2;
272
+ }
273
+ const opts = program.opts();
274
+ renderError(io2, e, Boolean(opts.json), pc2.isColorSupported && opts.color !== false);
275
+ return exitCodeFor(e);
276
+ }
277
+ }
278
+
279
+ // src/core/api/meta.ts
280
+ var SUPPORTED = ["v1"];
281
+ var SEMVER = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/;
282
+ function compareVersions(a, b) {
283
+ const pa = SEMVER.exec(a), pb = SEMVER.exec(b);
284
+ if (!pa || !pb) return 0;
285
+ for (let i = 1; i <= 3; i++) {
286
+ const d = Number(pa[i]) - Number(pb[i]);
287
+ if (d !== 0) return d;
288
+ }
289
+ if (pa[4] && !pb[4]) return -1;
290
+ if (!pa[4] && pb[4]) return 1;
291
+ return 0;
292
+ }
293
+ async function fetchMeta(fetch2, apiOrigin) {
294
+ let res;
295
+ try {
296
+ res = await fetch2(`${apiOrigin}/api/cli/meta`, { headers: { accept: "application/json" } });
297
+ } catch (e) {
298
+ throw fetchError(e, new URL(apiOrigin).host);
299
+ }
300
+ if (res.status === 404) {
301
+ throw new QoreError("QORE_INCOMPATIBLE_SERVER", "This portal does not support the Qore CLI yet", {
302
+ status: 404,
303
+ requestId: res.headers.get("x-request-id") ?? void 0,
304
+ hint: `Check the portal URL (${apiOrigin}); if it is right, the portal must be upgraded before qore can use it.`
305
+ });
306
+ }
307
+ if (!res.ok) throw await problemToError(res);
308
+ return await res.json();
309
+ }
310
+ function negotiate(meta, portal, cliVersion) {
311
+ if (portal.pinnedIssuer && meta.auth.issuer !== portal.pinnedIssuer) {
312
+ throw new QoreError("QORE_PORTAL_UNTRUSTED", `${portal.apiOrigin} names an unexpected identity provider`, {
313
+ details: { expected: portal.pinnedIssuer, got: meta.auth.issuer },
314
+ hint: "Refusing to log in. Check the portal URL and your network."
315
+ });
316
+ }
317
+ const apiVersion = SUPPORTED.find((v) => meta.apiVersions.includes(v));
318
+ if (!apiVersion) {
319
+ throw new QoreError("QORE_INCOMPATIBLE_SERVER", "This portal speaks no API version this qore knows", {
320
+ details: { server: meta.apiVersions, client: [...SUPPORTED] },
321
+ hint: "Run: npm i -g @quatronic/qore"
322
+ });
323
+ }
324
+ if (!cliVersion.endsWith("-dev") && compareVersions(cliVersion, meta.minCliVersion) < 0) {
325
+ throw new QoreError("QORE_UPGRADE_REQUIRED", `qore ${cliVersion} is too old for this portal (needs ${meta.minCliVersion})`, {
326
+ hint: "Run: npm i -g @quatronic/qore"
327
+ });
328
+ }
329
+ return { apiVersion };
330
+ }
331
+
332
+ // src/core/auth/oidc.ts
333
+ import * as oc from "openid-client";
334
+ var LOGIN_SCOPE = "openid profile email offline_access qore:read qore:local-env";
335
+ function decodeSub(accessToken) {
336
+ const part = accessToken.split(".")[1] ?? "";
337
+ const claims = JSON.parse(Buffer.from(part, "base64url").toString("utf8"));
338
+ return { sub: claims.sub, email: claims.email };
339
+ }
340
+ function toTokenSet(t, fallbackRt) {
341
+ const rt = t.refresh_token ?? fallbackRt;
342
+ if (!rt) throw new QoreError("QORE_INTERNAL", "Keycloak returned no refresh token");
343
+ return {
344
+ accessToken: t.access_token,
345
+ refreshToken: rt,
346
+ expiresIn: t.expires_in ?? 600,
347
+ scope: t.scope ?? "",
348
+ ...decodeSub(t.access_token)
349
+ };
350
+ }
351
+ function unreachable(e, host) {
352
+ const n = networkError(e, host);
353
+ return new QoreError("QORE_AUTH_UNAVAILABLE", n.message, { hint: n.hint, details: n.details, cause: e });
354
+ }
355
+ function mapOidcError(e, host) {
356
+ if (e instanceof QoreError || e instanceof CancelledError) return e;
357
+ if (isAbortError(e) || e?.code === "OAUTH_ABORT" || isAbortError(e?.cause)) {
358
+ return new CancelledError();
359
+ }
360
+ const err = e.error;
361
+ if (err === "access_denied") return new QoreError("QORE_LOGIN_DENIED", "The login was denied in the browser");
362
+ if (err === "expired_token") return new QoreError("QORE_LOGIN_EXPIRED", "The login code expired", { hint: "Run `qore login` again." });
363
+ if (err === "invalid_grant") return new QoreError("QORE_SESSION_EXPIRED", "Your session has ended", { hint: "Run `qore login`." });
364
+ const status = e.status;
365
+ if (status && status >= 500) return new QoreError("QORE_AUTH_UNAVAILABLE", "Keycloak is unavailable", { cause: e });
366
+ if (e instanceof TypeError) return unreachable(e, host);
367
+ const cause = e?.cause;
368
+ if (cause instanceof TypeError) return unreachable(cause, host);
369
+ return new QoreError("QORE_AUTH_UNAVAILABLE", "Login failed", { cause: e });
370
+ }
371
+ async function createOidc(opts) {
372
+ const host = new URL(opts.issuer).host;
373
+ const execute = new URL(opts.issuer).protocol === "http:" ? [oc.allowInsecureRequests] : [];
374
+ let config;
375
+ try {
376
+ config = await oc.discovery(new URL(opts.issuer), opts.clientId, void 0, oc.None(), {
377
+ execute,
378
+ [oc.customFetch]: opts.fetch
379
+ });
380
+ } catch (e) {
381
+ throw mapOidcError(e, host);
382
+ }
383
+ return {
384
+ async startDevice(scope) {
385
+ try {
386
+ const r = await oc.initiateDeviceAuthorization(config, { scope });
387
+ return {
388
+ userCode: r.user_code,
389
+ verificationUri: r.verification_uri,
390
+ verificationUriComplete: r.verification_uri_complete,
391
+ expiresIn: r.expires_in,
392
+ handle: r
393
+ };
394
+ } catch (e) {
395
+ throw mapOidcError(e, host);
396
+ }
397
+ },
398
+ async pollDevice(start, signal) {
399
+ try {
400
+ const t = await oc.pollDeviceAuthorizationGrant(config, start.handle, void 0, { signal });
401
+ return toTokenSet(t);
402
+ } catch (e) {
403
+ throw mapOidcError(e, host);
404
+ }
405
+ },
406
+ async refresh(rt, scope) {
407
+ try {
408
+ const t = await oc.refreshTokenGrant(config, rt, scope ? { scope } : void 0);
409
+ return toTokenSet(t, rt);
410
+ } catch (e) {
411
+ throw mapOidcError(e, host);
412
+ }
413
+ },
414
+ async revoke(rt) {
415
+ try {
416
+ await oc.tokenRevocation(config, rt, { token_type_hint: "refresh_token" });
417
+ } catch (e) {
418
+ throw mapOidcError(e, host);
419
+ }
420
+ }
421
+ };
422
+ }
423
+
424
+ // src/core/auth/login.ts
425
+ async function deviceLogin(opts) {
426
+ await opts.store.getRefresh();
427
+ const start = await opts.oidc.startDevice(LOGIN_SCOPE);
428
+ const stopPoll = new AbortController();
429
+ const settled = new AbortController();
430
+ const poll = opts.oidc.pollDevice(start, AbortSignal.any([opts.signal, stopPoll.signal])).finally(() => settled.abort());
431
+ poll.catch(() => {
432
+ });
433
+ try {
434
+ await opts.onCode(start, settled.signal);
435
+ } catch (e) {
436
+ stopPoll.abort();
437
+ const late = await poll.catch(() => null);
438
+ if (late) await opts.oidc.revoke(late.refreshToken).catch(() => {
439
+ });
440
+ throw e;
441
+ }
442
+ const t = await poll;
443
+ const at = opts.now();
444
+ const write = async () => {
445
+ await opts.store.setRefresh({ rt: t.refreshToken, sub: t.sub, scope: t.scope, obtainedAt: at.toISOString() });
446
+ await opts.store.setAccess({ at: t.accessToken, expiresAt: new Date(at.getTime() + t.expiresIn * 1e3).toISOString() });
447
+ };
448
+ try {
449
+ if (opts.withLock && opts.lockName) await opts.withLock(opts.lockName, write);
450
+ else await write();
451
+ } catch (e) {
452
+ await opts.oidc.revoke(t.refreshToken).catch(() => {
453
+ });
454
+ await opts.store.clear().catch(() => {
455
+ });
456
+ throw e;
457
+ }
458
+ return t;
459
+ }
460
+
461
+ // src/core/auth/tokenStore.ts
462
+ var TokenStore = class {
463
+ constructor(kc, hash) {
464
+ this.kc = kc;
465
+ this.hash = hash;
466
+ }
467
+ kc;
468
+ hash;
469
+ async read(suffix) {
470
+ const raw = await this.kc.get(`${this.hash}:${suffix}`);
471
+ if (raw === null) return null;
472
+ try {
473
+ return JSON.parse(raw);
474
+ } catch {
475
+ return null;
476
+ }
477
+ }
478
+ getRefresh() {
479
+ return this.read("refresh");
480
+ }
481
+ getAccess() {
482
+ return this.read("access");
483
+ }
484
+ setRefresh(v) {
485
+ return this.kc.set(`${this.hash}:refresh`, JSON.stringify(v));
486
+ }
487
+ setAccess(v) {
488
+ return this.kc.set(`${this.hash}:access`, JSON.stringify(v));
489
+ }
490
+ async clear() {
491
+ await this.kc.delete(`${this.hash}:access`);
492
+ await this.kc.delete(`${this.hash}:refresh`);
493
+ }
494
+ };
495
+
496
+ // src/core/profiles.ts
497
+ import { createHash } from "crypto";
498
+ var BUILTIN_PORTALS = {
499
+ prod: {
500
+ apiOrigin: "https://api.quatronic.app",
501
+ pinnedIssuer: "https://auth.quatronic.app/realms/platform-prod"
502
+ },
503
+ staging: {
504
+ apiOrigin: "https://p01--portal-api-staging--hcywmtb2vksv.code.run",
505
+ pinnedIssuer: "https://auth-staging.quatronic.app/realms/platform-staging"
506
+ }
507
+ };
508
+ function builtinFor(origin) {
509
+ for (const [name, p] of Object.entries(BUILTIN_PORTALS)) {
510
+ if (p.apiOrigin === origin) return { ...p, builtin: name };
511
+ }
512
+ return null;
513
+ }
514
+ function toOrigin(raw) {
515
+ let url;
516
+ try {
517
+ url = new URL(raw);
518
+ } catch {
519
+ throw new QoreError("QORE_CONTEXT_INVALID", `Not a portal name or URL: ${raw}`, {
520
+ hint: "Use --portal prod, --portal staging or --portal https://\u2026"
521
+ });
522
+ }
523
+ const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1";
524
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
525
+ throw new QoreError("QORE_CONTEXT_INVALID", "A portal URL must use https (http only for localhost)");
526
+ }
527
+ return url.origin.toLowerCase();
528
+ }
529
+ function resolvePortal(arg, defaultProfile) {
530
+ const chosen = arg ?? defaultProfile;
531
+ if (chosen === void 0 || chosen === "prod") return { ...BUILTIN_PORTALS.prod, builtin: "prod" };
532
+ if (chosen === "staging") return { ...BUILTIN_PORTALS.staging, builtin: "staging" };
533
+ const origin = toOrigin(chosen);
534
+ return builtinFor(origin) ?? { apiOrigin: origin, pinnedIssuer: null, builtin: null };
535
+ }
536
+ function profileHash(apiOrigin, issuer) {
537
+ return createHash("sha256").update(`${apiOrigin}|${issuer}`).digest("hex").slice(0, 16);
538
+ }
539
+
540
+ // src/core/api/client.ts
541
+ var QoreApiClient = class {
542
+ constructor(o) {
543
+ this.o = o;
544
+ }
545
+ o;
546
+ async once(path, force) {
547
+ const token = await this.o.token(force);
548
+ let res;
549
+ try {
550
+ res = await this.o.fetch(`${this.o.apiOrigin}${path}`, {
551
+ headers: {
552
+ authorization: `Bearer ${token}`,
553
+ accept: "application/json",
554
+ "qore-cli-version": this.o.cliVersion,
555
+ "qore-command": this.o.command,
556
+ "user-agent": `qore/${this.o.cliVersion} (${this.o.platform})`
557
+ }
558
+ });
559
+ } catch (e) {
560
+ throw fetchError(e, new URL(this.o.apiOrigin).host);
561
+ }
562
+ this.o.onRequest?.(`GET ${path} \u2192 ${res.status}${res.headers.get("x-request-id") ? ` (${res.headers.get("x-request-id")})` : ""}`);
563
+ return res;
564
+ }
565
+ async get(path) {
566
+ let res = await this.once(path, false);
567
+ if (res.status === 401) {
568
+ res = await this.once(path, true);
569
+ if (res.status === 401) {
570
+ throw new QoreError("QORE_SESSION_EXPIRED", "The portal no longer accepts your session", {
571
+ hint: "Run `qore login`.",
572
+ status: 401,
573
+ requestId: res.headers.get("x-request-id") ?? void 0
574
+ });
575
+ }
576
+ }
577
+ if (!res.ok) throw await problemToError(res);
578
+ return await res.json();
579
+ }
580
+ };
581
+
582
+ // src/core/auth/refresh.ts
583
+ var SKEW_MS = 6e4;
584
+ var RefreshCoordinator = class {
585
+ constructor(o) {
586
+ this.o = o;
587
+ }
588
+ o;
589
+ fresh(expiresAt) {
590
+ return new Date(expiresAt).getTime() - this.o.now().getTime() > SKEW_MS;
591
+ }
592
+ async rotate(scope) {
593
+ let stored = await this.o.store.getRefresh();
594
+ if (!stored) throw new QoreError("QORE_NOT_LOGGED_IN", "Not logged in", { hint: "Run `qore login`." });
595
+ let t;
596
+ for (let attempt = 0; ; attempt++) {
597
+ try {
598
+ t = await (await this.o.oidc()).refresh(stored.rt, scope);
599
+ break;
600
+ } catch (e) {
601
+ if (!(e instanceof QoreError && e.code === "QORE_SESSION_EXPIRED")) throw e;
602
+ const now = await this.o.store.getRefresh();
603
+ if (now && now.rt !== stored.rt) {
604
+ if (attempt === 0) {
605
+ stored = now;
606
+ continue;
607
+ }
608
+ throw e;
609
+ }
610
+ await this.o.store.clear();
611
+ throw e;
612
+ }
613
+ }
614
+ await this.o.store.setRefresh({ rt: t.refreshToken, sub: t.sub, scope: t.scope || (scope ?? stored.scope), obtainedAt: this.o.now().toISOString() });
615
+ return t;
616
+ }
617
+ async accessToken(opts = {}) {
618
+ const seen = await this.o.store.getAccess();
619
+ if (!opts.force && seen && this.fresh(seen.expiresAt)) return seen.at;
620
+ return this.o.withLock(this.o.lockName, async () => {
621
+ const again = await this.o.store.getAccess();
622
+ if (again && again.at !== seen?.at && this.fresh(again.expiresAt)) return again.at;
623
+ const t = await this.rotate();
624
+ await this.o.store.setAccess({ at: t.accessToken, expiresAt: new Date(this.o.now().getTime() + t.expiresIn * 1e3).toISOString() });
625
+ return t.accessToken;
626
+ });
627
+ }
628
+ /** A token carrying only `scopes`, held in memory by the caller (qore mcp). */
629
+ async downscoped(scopes) {
630
+ return this.o.withLock(this.o.lockName, async () => (await this.rotate(scopes.join(" "))).accessToken);
631
+ }
632
+ };
633
+
634
+ // src/core/session.ts
635
+ function loginHint(portal) {
636
+ return portal.builtin === "prod" ? "Run `qore login`." : `Run \`qore login --portal ${portal.builtin ?? portal.apiOrigin}\`.`;
637
+ }
638
+ async function openSession(deps2, portal, command, onRequest) {
639
+ const profile = (await deps2.config.read()).profiles[portal.apiOrigin];
640
+ if (!profile) throw new QoreError("QORE_NOT_LOGGED_IN", `Not logged in to ${portal.apiOrigin}`, { hint: loginHint(portal) });
641
+ const hash = profileHash(profile.apiOrigin, profile.issuer);
642
+ const tokens = new TokenStore(deps2.keychain, hash);
643
+ if (!await tokens.getRefresh()) throw new QoreError("QORE_NOT_LOGGED_IN", `Not logged in to ${portal.apiOrigin}`, { hint: loginHint(portal) });
644
+ const refresh = new RefreshCoordinator({ store: tokens, oidc: () => deps2.oidcFor(profile.issuer), withLock: deps2.withLock, lockName: hash, now: deps2.now });
645
+ const api = new QoreApiClient({
646
+ apiOrigin: profile.apiOrigin,
647
+ fetch: deps2.fetch,
648
+ token: (force) => refresh.accessToken({ force }),
649
+ cliVersion: deps2.version,
650
+ command,
651
+ platform: `${deps2.platform} ${process.arch}; node ${process.versions.node}`,
652
+ onRequest
653
+ });
654
+ return { profile, api, tokens, refresh };
655
+ }
656
+
657
+ // src/cli/commands/login.ts
658
+ function registerLogin(program, ctx) {
659
+ program.command("login").description("Log in with your Qore account").option("--no-browser", "only print the URL").action(async (cmd) => {
660
+ const { deps: deps2, io: io2, signal } = ctx;
661
+ const g = ctx.opts();
662
+ const cfg = await deps2.config.read();
663
+ const portal = resolvePortal(g.portal, cfg.defaultProfile);
664
+ const meta = await fetchMeta(deps2.fetch, portal.apiOrigin);
665
+ const known = cfg.profiles[portal.apiOrigin]?.issuer;
666
+ const pinned = { ...portal, pinnedIssuer: portal.pinnedIssuer ?? known ?? null };
667
+ if (!pinned.pinnedIssuer && portal.builtin === null) {
668
+ io2.err(`! Logging in to ${portal.apiOrigin}, identity provider ${meta.auth.issuer}`);
669
+ }
670
+ negotiate(meta, pinned, deps2.version);
671
+ const issuer = pinned.pinnedIssuer ?? meta.auth.issuer;
672
+ const hash = profileHash(portal.apiOrigin, issuer);
673
+ const store = new TokenStore(deps2.keychain, hash);
674
+ const t = await deviceLogin({
675
+ oidc: await deps2.oidcFor(issuer),
676
+ store,
677
+ now: deps2.now,
678
+ signal,
679
+ withLock: deps2.withLock,
680
+ lockName: hash,
681
+ onCode: async (s, settled) => {
682
+ const url = s.verificationUriComplete ?? s.verificationUri;
683
+ io2.err(`! First copy your one-time code: ${s.userCode}`);
684
+ if (io2.interactive && cmd.browser) {
685
+ try {
686
+ await io2.waitForEnter(` Press Enter to open ${url} (or open it yourself)`, AbortSignal.any([signal, settled]));
687
+ } catch (e) {
688
+ if (settled.aborted && !signal.aborted) return;
689
+ throw e;
690
+ }
691
+ if (settled.aborted) return;
692
+ if (!await io2.openUrl(url)) io2.err(` Open ${url} in a browser`);
693
+ } else {
694
+ io2.err(` Open ${url} in a browser`);
695
+ }
696
+ }
697
+ });
698
+ await deps2.config.update((c) => {
699
+ c.profiles[portal.apiOrigin] = {
700
+ ...c.profiles[portal.apiOrigin],
701
+ apiOrigin: portal.apiOrigin,
702
+ issuer,
703
+ sub: t.sub,
704
+ email: t.email,
705
+ meta: { fetchedAt: deps2.now().toISOString(), doc: meta }
706
+ };
707
+ if (g.portal || !c.defaultProfile) c.defaultProfile = portal.apiOrigin;
708
+ });
709
+ const { api } = await openSession(deps2, { ...portal, pinnedIssuer: issuer }, "login", requestLog(ctx));
710
+ const me = await api.get("/api/cli/v1/me");
711
+ const prev = cfg.profiles[portal.apiOrigin];
712
+ let active = prev?.sub === t.sub && prev.activeTenant && me.tenants.some((x) => x.id === prev.activeTenant.id) ? prev.activeTenant : void 0;
713
+ if (active) {
714
+ } else if (me.tenants.length === 0) {
715
+ io2.err("! Your account is not a developer in any organisation. Ask an admin to invite you as developer.");
716
+ } else if (me.tenants.length === 1) {
717
+ active = { id: me.tenants[0].id, slug: me.tenants[0].slug };
718
+ io2.err(`\u2713 Using tenant ${me.tenants[0].name}`);
719
+ } else if (io2.interactive) {
720
+ active = await io2.select("Select a tenant", me.tenants.map((x) => ({ name: `${x.name} ${x.role}`, value: { id: x.id, slug: x.slug } })));
721
+ } else {
722
+ io2.err(`! You belong to ${me.tenants.length} tenants; choose one with \`qore tenant use <slug>\``);
723
+ }
724
+ await deps2.config.update((c) => {
725
+ c.profiles[portal.apiOrigin].activeTenant = active;
726
+ });
727
+ const label = portal.builtin ?? portal.apiOrigin;
728
+ if (g.json) renderJson(io2, { portal: label, email: me.email, activeTenant: active ?? null });
729
+ else io2.out(`\u2713 Logged in as ${me.email} (${label})`);
730
+ });
731
+ }
732
+
733
+ // src/cli/commands/logout.ts
734
+ function registerLogout(program, ctx) {
735
+ program.command("logout").description("End the CLI session").option("--all", "log out of every portal").action(async (cmd) => {
736
+ const { deps: deps2, io: io2 } = ctx;
737
+ const g = ctx.opts();
738
+ const cfg = await deps2.config.read();
739
+ const targets = cmd.all ? Object.values(cfg.profiles) : [cfg.profiles[resolvePortal(g.portal, cfg.defaultProfile).apiOrigin]].filter((p) => p !== void 0);
740
+ const done = [];
741
+ for (const p of targets) {
742
+ const hash = profileHash(p.apiOrigin, p.issuer);
743
+ const store = new TokenStore(deps2.keychain, hash);
744
+ const host = new URL(p.issuer).host;
745
+ await deps2.withLock(hash, async () => {
746
+ const rt = await store.getRefresh();
747
+ if (rt) {
748
+ try {
749
+ await (await deps2.oidcFor(p.issuer)).revoke(rt.rt);
750
+ } catch (e) {
751
+ const network = e instanceof QoreError && e.details?.reason !== void 0;
752
+ io2.err(network ? `! Could not reach ${host}; the server session may persist until it expires.` : `! Could not revoke the session at ${host} (${e instanceof Error ? e.message : String(e)}); the server session may persist until it expires.`);
753
+ }
754
+ }
755
+ await store.clear();
756
+ });
757
+ done.push(p.apiOrigin);
758
+ }
759
+ if (g.json) renderJson(io2, { loggedOut: done });
760
+ else if (done.length === 0) io2.out("Not logged in");
761
+ else io2.out(`\u2713 Logged out of ${done.map((o) => resolvePortal(o, void 0).builtin ?? o).join(", ")}`);
762
+ });
763
+ }
764
+
765
+ // src/cli/commands/tenant.ts
766
+ function registerTenant(program, ctx) {
767
+ const tenant = program.command("tenant").description("Manage the active tenant");
768
+ tenant.command("list").description("List tenants you have access to").action(async () => {
769
+ const { deps: deps2, io: io2 } = ctx;
770
+ const g = ctx.opts();
771
+ const cfg = await deps2.config.read();
772
+ const portal = resolvePortal(g.portal, cfg.defaultProfile);
773
+ const { profile, api } = await openSession(deps2, portal, "tenant list", requestLog(ctx));
774
+ const me = await api.get("/api/cli/v1/me");
775
+ const activeId = profile.activeTenant?.id;
776
+ if (g.json) {
777
+ renderJson(io2, { tenants: me.tenants.map((t) => ({ ...t, active: t.id === activeId })) });
778
+ return;
779
+ }
780
+ for (const t of me.tenants) {
781
+ const mark = t.id === activeId ? "*" : " ";
782
+ io2.out(`${mark} ${t.name} ${t.role}`);
783
+ }
784
+ });
785
+ tenant.command("use").description("Set the active tenant").argument("[slug]", "tenant slug").action(async (slug) => {
786
+ const { deps: deps2, io: io2 } = ctx;
787
+ const g = ctx.opts();
788
+ const cfg = await deps2.config.read();
789
+ const portal = resolvePortal(g.portal, cfg.defaultProfile);
790
+ const { api } = await openSession(deps2, portal, "tenant use", requestLog(ctx));
791
+ const me = await api.get("/api/cli/v1/me");
792
+ const candidates = me.tenants.map((t) => t.slug);
793
+ let chosen;
794
+ if (slug) {
795
+ const found = me.tenants.find((t) => t.slug === slug);
796
+ if (!found) throw new QoreError("QORE_RESOURCE_NOT_FOUND", `No tenant "${slug}" for this account`, { candidates });
797
+ chosen = { id: found.id, slug: found.slug };
798
+ } else if (io2.interactive) {
799
+ chosen = await io2.select("Select a tenant", me.tenants.map((t) => ({ name: `${t.name} ${t.role}`, value: { id: t.id, slug: t.slug } })));
800
+ } else {
801
+ throw new QoreError("QORE_CONTEXT_REQUIRED", "Which tenant? Pass a slug or run interactively.", { candidates });
802
+ }
803
+ await deps2.config.update((c) => {
804
+ c.profiles[portal.apiOrigin].activeTenant = chosen;
805
+ });
806
+ if (g.json) renderJson(io2, { activeTenant: chosen });
807
+ else io2.out(`\u2713 Using tenant ${chosen.slug}`);
808
+ });
809
+ }
810
+
811
+ // src/cli/commands/version.ts
812
+ function registerVersion(program, ctx) {
813
+ program.command("version").description("Show the CLI and portal version").action(async () => {
814
+ const { deps: deps2, io: io2 } = ctx;
815
+ const g = ctx.opts();
816
+ const cfg = await deps2.config.read();
817
+ const portal = resolvePortal(g.portal, cfg.defaultProfile);
818
+ let portalInfo = null;
819
+ try {
820
+ const meta = await fetchMeta(deps2.fetch, portal.apiOrigin);
821
+ portalInfo = { apiVersions: meta.apiVersions, minCliVersion: meta.minCliVersion, latestCliVersion: meta.latestCliVersion, capabilities: meta.capabilities };
822
+ } catch (e) {
823
+ if (e instanceof CancelledError) throw e;
824
+ }
825
+ if (g.json) {
826
+ renderJson(io2, { cli: deps2.version, portal: portalInfo });
827
+ return;
828
+ }
829
+ io2.out(`qore ${deps2.version}`);
830
+ const label = portal.builtin ?? portal.apiOrigin;
831
+ if (portalInfo) {
832
+ io2.out(`portal ${label}: api ${portalInfo.apiVersions.join(",")}, min ${portalInfo.minCliVersion}, latest ${portalInfo.latestCliVersion}`);
833
+ } else {
834
+ io2.out(`portal ${label}: unreachable`);
835
+ }
836
+ });
837
+ }
838
+
839
+ // src/cli/commands/whoami.ts
840
+ function registerWhoami(program, ctx) {
841
+ program.command("whoami").description("Show who you're logged in as").action(async () => {
842
+ const { deps: deps2, io: io2 } = ctx;
843
+ const g = ctx.opts();
844
+ const cfg = await deps2.config.read();
845
+ const portal = resolvePortal(g.portal, cfg.defaultProfile);
846
+ const { profile, api } = await openSession(deps2, portal, "whoami", requestLog(ctx));
847
+ const me = await api.get("/api/cli/v1/me");
848
+ const activeTenant = profile.activeTenant;
849
+ if (activeTenant && !me.tenants.some((t) => t.id === activeTenant.id)) {
850
+ io2.err(`! You no longer have developer access to ${activeTenant.slug}`);
851
+ }
852
+ const label = portal.builtin ?? portal.apiOrigin;
853
+ if (g.json) {
854
+ renderJson(io2, { portal: label, activeTenant: activeTenant ?? null, me });
855
+ return;
856
+ }
857
+ io2.out(`${me.email} (${label})`);
858
+ for (const t of me.tenants) {
859
+ const mark = activeTenant?.id === t.id ? "*" : " ";
860
+ io2.out(`${mark} ${t.name} ${t.role}`);
861
+ for (const a of t.apps) {
862
+ io2.out(` ${a.slug} ${a.access}`);
863
+ for (const m of a.modules) {
864
+ io2.out(` ${m.slug} ${m.type} ${m.access}`);
865
+ }
866
+ }
867
+ }
868
+ });
869
+ }
870
+
871
+ // src/cli/commands/index.ts
872
+ var COMMANDS = [registerLogin, registerLogout, registerWhoami, registerTenant, registerVersion];
873
+
874
+ // src/cli/updateCheck.ts
875
+ var REGISTRY_URL = "https://registry.npmjs.org/@quatronic%2Fqore/latest";
876
+ var DAY_MS = 24 * 60 * 60 * 1e3;
877
+ var DEFAULT_TIMEOUT_MS = 1500;
878
+ async function maybeNotifyUpdate(deps2, io2, o, timeoutMs = DEFAULT_TIMEOUT_MS) {
879
+ if (o.json || deps2.env.CI || deps2.env.QORE_NO_UPDATE_CHECK === "1" || !io2.interactive) return;
880
+ const cfg = await deps2.config.read();
881
+ const last = cfg.updateCheck?.lastCheckedAt;
882
+ if (last && deps2.now().getTime() - new Date(last).getTime() < DAY_MS) return;
883
+ const controller = new AbortController();
884
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
885
+ let latest;
886
+ try {
887
+ const res = await deps2.fetch(REGISTRY_URL, { signal: controller.signal });
888
+ if (res.ok) latest = (await res.json()).version;
889
+ } catch {
890
+ } finally {
891
+ clearTimeout(timer);
892
+ }
893
+ try {
894
+ await deps2.config.update((c) => {
895
+ c.updateCheck = { lastCheckedAt: deps2.now().toISOString(), latest: latest ?? c.updateCheck?.latest ?? deps2.version };
896
+ });
897
+ } catch {
898
+ return;
899
+ }
900
+ if (latest && compareVersions(latest, deps2.version) > 0) {
901
+ io2.err(`A new qore is available: ${deps2.version} \u2192 ${latest} npm i -g @quatronic/qore`);
902
+ }
903
+ }
904
+
905
+ // src/core/config.ts
906
+ import { mkdir, readFile, rename, writeFile, chmod } from "fs/promises";
907
+ import { join } from "path";
908
+ import { randomBytes } from "crypto";
909
+ var empty = () => ({ schemaVersion: 1, profiles: {} });
910
+ var ConfigStore = class {
911
+ constructor(dir2) {
912
+ this.dir = dir2;
913
+ }
914
+ dir;
915
+ get file() {
916
+ return join(this.dir, "config.json");
917
+ }
918
+ async read() {
919
+ let raw;
920
+ try {
921
+ raw = await readFile(this.file, "utf8");
922
+ } catch (e) {
923
+ if (e.code === "ENOENT") return empty();
924
+ throw e;
925
+ }
926
+ try {
927
+ const parsed = JSON.parse(raw);
928
+ if (parsed.schemaVersion !== 1 || typeof parsed.profiles !== "object") throw new Error("shape");
929
+ return parsed;
930
+ } catch (cause) {
931
+ throw new QoreError("QORE_CONTEXT_INVALID", `Qore config is malformed: ${this.file}`, {
932
+ hint: "Fix or delete the file, then run `qore login` again",
933
+ cause
934
+ });
935
+ }
936
+ }
937
+ async update(fn) {
938
+ const current = await this.read();
939
+ const next = fn(current) ?? current;
940
+ await mkdir(this.dir, { recursive: true, mode: 448 });
941
+ const tmp = join(this.dir, `.config.${randomBytes(6).toString("hex")}.tmp`);
942
+ await writeFile(tmp, JSON.stringify(next, null, 2) + "\n", { mode: 384 });
943
+ await chmod(tmp, 384);
944
+ await rename(tmp, this.file);
945
+ return next;
946
+ }
947
+ };
948
+
949
+ // src/platform/keychain.ts
950
+ import { Entry } from "@napi-rs/keyring";
951
+ var SERVICE = "qore";
952
+ var HINTS = {
953
+ linux: "Install and unlock a Secret Service provider (gnome-keyring or KWallet); headless machines and containers have none.",
954
+ darwin: "Unlock your login keychain in Keychain Access.",
955
+ win32: "Windows Credential Manager must be available for your user."
956
+ };
957
+ function classifyKeychainError(e, platform) {
958
+ const msg = e instanceof Error ? e.message : String(e);
959
+ if (/TooLong|longer than the platform limit/i.test(msg)) {
960
+ return new QoreError("QORE_KEYCHAIN_UNAVAILABLE", "The session token is too large for the OS keychain", {
961
+ hint: platform === "win32" ? "The token exceeds the Windows Credential Manager size limit. Report this to the Qore team." : "The token exceeds the keychain's size limit. Report this to the Qore team.",
962
+ cause: e
963
+ });
964
+ }
965
+ if (/cancel|denied|errSecAuthFailed|user interaction/i.test(msg)) {
966
+ return new QoreError("QORE_KEYCHAIN_DENIED", "Access to the OS keychain was denied", {
967
+ hint: "Allow qore to use the keychain when your OS asks, then retry.",
968
+ cause: e
969
+ });
970
+ }
971
+ return new QoreError("QORE_KEYCHAIN_UNAVAILABLE", "No usable OS keychain", {
972
+ hint: HINTS[platform] ?? "qore stores tokens only in the OS keychain.",
973
+ cause: e
974
+ });
975
+ }
976
+ var OsKeychain = class {
977
+ constructor(platform = process.platform) {
978
+ this.platform = platform;
979
+ }
980
+ platform;
981
+ wrap(fn) {
982
+ try {
983
+ return Promise.resolve(fn());
984
+ } catch (e) {
985
+ return Promise.reject(classifyKeychainError(e, this.platform));
986
+ }
987
+ }
988
+ get(account) {
989
+ return this.wrap(() => new Entry(SERVICE, account).getPassword() ?? null);
990
+ }
991
+ set(account, value) {
992
+ return this.wrap(() => {
993
+ new Entry(SERVICE, account).setPassword(value);
994
+ });
995
+ }
996
+ delete(account) {
997
+ return this.wrap(() => {
998
+ new Entry(SERVICE, account).deletePassword();
999
+ });
1000
+ }
1001
+ };
1002
+
1003
+ // src/platform/lock.ts
1004
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
1005
+ import { join as join2 } from "path";
1006
+ import lockfile from "proper-lockfile";
1007
+ function fileLock(dir2) {
1008
+ return async (name, fn) => {
1009
+ const locks = join2(dir2, "locks");
1010
+ await mkdir2(locks, { recursive: true, mode: 448 });
1011
+ const target = join2(locks, `${name}.lock`);
1012
+ await writeFile2(target, "", { flag: "a" });
1013
+ let release;
1014
+ let compromised;
1015
+ try {
1016
+ release = await lockfile.lock(target, {
1017
+ stale: 3e4,
1018
+ retries: { retries: 30, minTimeout: 100, maxTimeout: 1e3, randomize: true },
1019
+ onCompromised: (err) => {
1020
+ compromised = err;
1021
+ }
1022
+ });
1023
+ } catch (cause) {
1024
+ throw new QoreError("QORE_AUTH_BUSY", "Another qore process is refreshing the session", { hint: "Retry in a moment.", cause });
1025
+ }
1026
+ try {
1027
+ return await fn();
1028
+ } finally {
1029
+ await release().catch((e) => {
1030
+ if (!compromised) throw e;
1031
+ });
1032
+ }
1033
+ };
1034
+ }
1035
+
1036
+ // src/platform/paths.ts
1037
+ import { posix, win32 } from "path";
1038
+ function configDir(env, platform, home) {
1039
+ if (platform === "win32") return win32.join(env.APPDATA ?? win32.join(home, "AppData", "Roaming"), "qore");
1040
+ if (platform === "darwin") return posix.join(home, "Library", "Application Support", "qore");
1041
+ const xdg = env.XDG_CONFIG_HOME && posix.isAbsolute(env.XDG_CONFIG_HOME) ? env.XDG_CONFIG_HOME : void 0;
1042
+ return posix.join(xdg ?? posix.join(home, ".config"), "qore");
1043
+ }
1044
+
1045
+ // src/version.ts
1046
+ var VERSION = true ? "0.1.0" : "0.0.0-dev";
1047
+
1048
+ // src/cli.ts
1049
+ if (process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY) setGlobalDispatcher(new EnvHttpProxyAgent());
1050
+ var dir = configDir(process.env, process.platform, homedir());
1051
+ var argv = process.argv.slice(2);
1052
+ var json = argv.includes("--json");
1053
+ var ac = new AbortController();
1054
+ process.on("SIGINT", () => {
1055
+ if (ac.signal.aborted) process.exit(130);
1056
+ ac.abort();
1057
+ });
1058
+ var fetch = abortableFetch(globalThis.fetch, ac.signal);
1059
+ var deps = {
1060
+ fetch,
1061
+ keychain: new OsKeychain(),
1062
+ config: new ConfigStore(dir),
1063
+ withLock: fileLock(dir),
1064
+ now: () => /* @__PURE__ */ new Date(),
1065
+ oidcFor: (issuer) => createOidc({ issuer, clientId: "qore-cli", fetch }),
1066
+ env: process.env,
1067
+ platform: process.platform,
1068
+ version: VERSION
1069
+ };
1070
+ var io = processIo(isInteractive({
1071
+ stdinTTY: Boolean(process.stdin.isTTY),
1072
+ stdoutTTY: Boolean(process.stdout.isTTY),
1073
+ json,
1074
+ noInput: argv.includes("--no-input"),
1075
+ env: process.env
1076
+ }));
1077
+ process.exitCode = await run(argv, deps, io, ac.signal, COMMANDS);
1078
+ if (!ac.signal.aborted) {
1079
+ try {
1080
+ await maybeNotifyUpdate(deps, io, { json });
1081
+ } catch {
1082
+ }
1083
+ }
1084
+ //# sourceMappingURL=cli.js.map