@quatronic/qore 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -511,6 +511,38 @@ and never in an environment variable.
511
511
  - `NODE_EXTRA_CA_CERTS` can point at an additional CA bundle, for portals or
512
512
  identity providers behind a corporate proxy with a private root CA.
513
513
 
514
+ ## Privacy
515
+
516
+ `qore` sends crash reports to Quatronic so we can find and fix bugs. It is on by
517
+ default; the first interactive run says so once, on stderr.
518
+
519
+ **What is sent**, to Sentry (EU region, project `qore-cli`):
520
+
521
+ - errors qore did not anticipate — a bug in qore, not an expected error such as
522
+ "not logged in" or "port in use";
523
+ - one health record per run: whether it crashed or not;
524
+ - with each: the qore version, the Node version, the OS, whether it ran in CI, the
525
+ command name (e.g. `env pull`), the stack trace, and the error message;
526
+ - the method, host and status code of the HTTP requests qore made before a crash.
527
+
528
+ **Removed before sending:** tokens, keys and other secrets, environment variable
529
+ values (including the local env qore fetched), argument values, e-mail addresses,
530
+ URL query strings, and your home directory (paths show as `~`). This is pattern-based:
531
+ an error message about a file qore failed to parse can still quote a fragment of it.
532
+
533
+ **Never attached:** your hostname, your user account, your tenant, or an IP address.
534
+ qore sends no field that carries an IP; the address a report arrives from is dropped
535
+ by the Sentry project's "Prevent Storing of IP Addresses" setting, part of its infra setup.
536
+
537
+ **Turning it off** (the first match wins):
538
+
539
+ 1. `DO_NOT_TRACK=1`
540
+ 2. `QORE_TELEMETRY=0` (or `QORE_TELEMETRY=1` to turn it on)
541
+ 3. `qore config telemetry off` (or `on`)
542
+
543
+ `qore config telemetry` and `qore doctor` show the current state and where it
544
+ comes from. A build from source or a fork carries no DSN and sends nothing.
545
+
514
546
  ## Releasing
515
547
 
516
548
  See [RELEASING.md](./RELEASING.md).
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env node
2
+
3
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="a8210538-dc3e-507d-ab33-bdf6a49d6d16")}catch(e){}}();
4
+ import { createRequire as __qoreCreateRequire } from "node:module"; const require = __qoreCreateRequire(import.meta.url);
5
+
6
+ // src/core/telemetry/scrub.ts
7
+ var SECRET_ENV_KEY = /TOKEN|SECRET|PASSWORD|PASSWD|PRIVATE|API_?KEY|ACCESS_?KEY|DSN|AUTH|CREDENTIAL|COOKIE|SESSION/i;
8
+ var KEEP_CONTEXTS = /* @__PURE__ */ new Set(["runtime", "os"]);
9
+ var PATTERNS = [
10
+ // an unterminated block runs to the end, so a truncated key is still dropped
11
+ [/-----BEGIN [A-Z0-9 ]{1,40}-----[\s\S]*?(?:-----END [A-Z0-9 ]{1,40}-----|$)/g, "[pem]"],
12
+ // query strings and fragments carry OAuth codes, states and signed-URL tokens
13
+ [/\b([a-z][a-z0-9+.-]{0,30}:\/\/[^\s?#"'<>]*)[?#][^\s"'<>]*/gi, "$1"],
14
+ [/\b(authorization["']?\s*[=:]\s*)(\[redacted\]|"[^"]*"|'[^']*'|[A-Za-z][A-Za-z0-9_-]{0,30}\s+[^\s,;&]+|[^\s,;&]+)/gi, "$1[redacted]"],
15
+ [/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [redacted]"],
16
+ [/\bBasic\s+[A-Za-z0-9+/=]{8,}/gi, "Basic [redacted]"],
17
+ [/\beyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]*/g, "[jwt]"],
18
+ [/\b(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|sntr[ysu]_[A-Za-z0-9+/=_-]{20,}|npm_[A-Za-z0-9]{20,}|sk-[A-Za-z0-9_-]{20,}|xox[abpr]-[A-Za-z0-9-]{10,}|[sr]k_(?:live|test)_[A-Za-z0-9]{10,}|(?:AKIA|ASIA)[0-9A-Z]{16})/g, "[token]"],
19
+ // URL credentials, which is also what a DSN's public key looks like
20
+ [/\b([a-z][a-z0-9+.-]{0,30}:\/\/)[^\s/:@]+(?::[^\s/@]*)?@/gi, "$1[redacted]@"],
21
+ [/[A-Za-z0-9._%+-]{1,64}@[A-Za-z0-9-]{1,63}(?:\.[A-Za-z0-9-]{1,63}){0,8}\.[A-Za-z]{2,24}\b/g, "[email]"],
22
+ // affixes are bounded so a long run of key-like characters cannot backtrack quadratically
23
+ [/\b([A-Za-z0-9_.-]{0,40}(?:(?:token|secret|password|passwd|pwd|api[_-]?key|access[_-]?key|private[_-]?key|auth|credential|dsn|session|cookie)[A-Za-z0-9_.-]{0,40}|key))(["']?\s*[=:]\s*)(\[redacted\]|"[^"]*"|'[^']*'|[^\s&,;]+)/gi, "$1$2[redacted]"]
24
+ ];
25
+ var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
26
+ var longestFirst = (xs, min) => [...new Set(xs.filter((x) => x.length >= min))].sort((a, b) => b.length - a.length);
27
+ function buildScrubContext(o) {
28
+ const words = new Set(o.commandWords);
29
+ const argvValues = o.argv.flatMap((a) => {
30
+ if (a.startsWith("-")) {
31
+ const i = a.indexOf("=");
32
+ return i > 0 ? [a.slice(i + 1)] : [];
33
+ }
34
+ return words.has(a) ? [] : [a];
35
+ });
36
+ const secretEnvValues = Object.entries(o.env).flatMap(([k, v]) => v && SECRET_ENV_KEY.test(k) ? [v] : []);
37
+ return { home: o.home, platform: o.platform, argvValues: longestFirst(argvValues, 3), secretEnvValues: longestFirst(secretEnvValues, 8) };
38
+ }
39
+ function addSecretValues(ctx, values) {
40
+ ctx.secretEnvValues = longestFirst([...ctx.secretEnvValues, ...values], 8);
41
+ }
42
+ function replaceLiteral(s, needle, repl, flags) {
43
+ return s.replace(new RegExp(escapeRe(needle), flags), () => repl);
44
+ }
45
+ function replaceHome(s, ctx) {
46
+ const win = ctx.platform === "win32";
47
+ const plain = [ctx.home, ...win ? [ctx.home.replace(/\\/g, "/")] : []];
48
+ const variants = /* @__PURE__ */ new Set([...plain, ...plain.map((h) => encodeURI(h).replace(/[?#]/g, encodeURIComponent))]);
49
+ let out = s;
50
+ for (const h of [...variants].filter((v) => v.length > 1).sort((a, b) => b.length - a.length)) {
51
+ out = out.replace(new RegExp(`${escapeRe(h)}(?![A-Za-z0-9._-])`, win ? "gi" : "g"), () => "~");
52
+ }
53
+ return out;
54
+ }
55
+ function scrubText(s, ctx) {
56
+ let out = s;
57
+ for (const v of ctx.secretEnvValues) out = replaceLiteral(out, v, "[env]", "g");
58
+ for (const v of ctx.argvValues) out = replaceLiteral(out, v, "[arg]", "g");
59
+ out = replaceHome(out, ctx);
60
+ for (const [re, repl] of PATTERNS) out = out.replace(re, repl);
61
+ return out;
62
+ }
63
+ function hostOf(url) {
64
+ if (typeof url !== "string") return void 0;
65
+ try {
66
+ return new URL(url).host;
67
+ } catch {
68
+ return void 0;
69
+ }
70
+ }
71
+ function scrubBreadcrumb(b, ctx) {
72
+ if (b.category === "console") return null;
73
+ if (b.type === "http" || b.category === "http" || b.category === "fetch") {
74
+ const d = b.data ?? {};
75
+ return {
76
+ type: b.type,
77
+ category: b.category,
78
+ level: b.level,
79
+ timestamp: b.timestamp,
80
+ data: { method: d.method ?? d["http.method"], host: hostOf(d.url) ?? d.host, status_code: d.status_code }
81
+ };
82
+ }
83
+ return { ...b, message: b.message === void 0 ? void 0 : scrubText(b.message, ctx), data: void 0 };
84
+ }
85
+ function scrubEvent(e, ctx) {
86
+ delete e.server_name;
87
+ delete e.user;
88
+ delete e.request;
89
+ delete e.extra;
90
+ delete e.modules;
91
+ for (const k of Object.keys(e.contexts ?? {})) if (!KEEP_CONTEXTS.has(k)) delete e.contexts[k];
92
+ if (e.message !== void 0) e.message = scrubText(e.message, ctx);
93
+ if (e.logentry?.message !== void 0) {
94
+ e.logentry.message = scrubText(e.logentry.message, ctx);
95
+ delete e.logentry.params;
96
+ }
97
+ for (const v of e.exception?.values ?? []) {
98
+ if (v.value !== void 0) v.value = scrubText(v.value, ctx);
99
+ for (const f of v.stacktrace?.frames ?? []) {
100
+ if (f.filename) f.filename = replaceHome(f.filename, ctx);
101
+ if (f.abs_path) f.abs_path = replaceHome(f.abs_path, ctx);
102
+ if (f.module) f.module = replaceHome(f.module, ctx);
103
+ delete f.vars;
104
+ delete f.pre_context;
105
+ delete f.context_line;
106
+ delete f.post_context;
107
+ }
108
+ }
109
+ for (const img of e.debug_meta?.images ?? []) if (img.code_file) img.code_file = replaceHome(img.code_file, ctx);
110
+ if (e.breadcrumbs) e.breadcrumbs = e.breadcrumbs.map((b) => scrubBreadcrumb(b, ctx)).filter((b) => b !== null);
111
+ return e;
112
+ }
113
+
114
+ export {
115
+ buildScrubContext,
116
+ addSecretValues,
117
+ scrubBreadcrumb,
118
+ scrubEvent
119
+ };
120
+ //# sourceMappingURL=chunk-QYBDINUC.js.map
121
+ //# debugId=a8210538-dc3e-507d-ab33-bdf6a49d6d16
package/dist/cli.js CHANGED
@@ -1,5 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="a70250c5-daf4-5ff3-b490-5556ac377549")}catch(e){}}();
4
+ import { createRequire as __qoreCreateRequire } from "node:module"; const require = __qoreCreateRequire(import.meta.url);
5
+ import {
6
+ addSecretValues,
7
+ buildScrubContext
8
+ } from "./chunk-QYBDINUC.js";
9
+
3
10
  // src/cli.ts
4
11
  import { homedir as homedir2 } from "os";
5
12
  import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici";
@@ -320,7 +327,7 @@ function renderDoctorJson(io2, report) {
320
327
  return true;
321
328
  }
322
329
  var SYMBOL = { ok: "\u2713", warn: "!", fail: "\u2717" };
323
- var TOOL_IDS = ["qore", "node", "git", "github-ssh", "docker"];
330
+ var TOOL_IDS = ["qore", "node", "git", "github-ssh", "docker", "telemetry"];
324
331
  var ROW_WIDTH = 80;
325
332
  var COL_B = 36;
326
333
  function rawLine(r) {
@@ -435,7 +442,16 @@ function buildProgram(deps2, io2, signal, register, setExitCode) {
435
442
  for (const r of register) r(program, ctx);
436
443
  return program;
437
444
  }
438
- async function run(argv2, deps2, io2, signal, register) {
445
+ function commandPath(action) {
446
+ const names = [];
447
+ for (let c = action; c?.parent; c = c.parent) names.unshift(c.name());
448
+ return names.join(" ");
449
+ }
450
+ function isCrash(e) {
451
+ if (e instanceof QoreError || e instanceof CancelledError || e instanceof CommanderError) return false;
452
+ return !(e instanceof Error && (e.name === "CancelledError" || e.name === "QoreError"));
453
+ }
454
+ async function run(argv2, deps2, io2, signal, register, hooks = {}) {
439
455
  const json2 = jsonFromArgv(argv2);
440
456
  const parseIo = json2 ? { ...io2, err: () => {
441
457
  } } : io2;
@@ -443,8 +459,12 @@ async function run(argv2, deps2, io2, signal, register) {
443
459
  const program = buildProgram(deps2, parseIo, signal, register, (code) => {
444
460
  state.exitCode = code;
445
461
  });
446
- program.hook("preAction", (_program, action) => {
462
+ program.hook("preAction", async (_program, action) => {
447
463
  state.command = action.name();
464
+ try {
465
+ await hooks.onCommand?.(commandPath(action));
466
+ } catch {
467
+ }
448
468
  });
449
469
  try {
450
470
  await program.parseAsync(argv2, { from: "user" });
@@ -456,6 +476,12 @@ async function run(argv2, deps2, io2, signal, register) {
456
476
  if (json2) io2.out(JSON.stringify({ ok: false, error: { code: "QORE_CONTEXT_INVALID", message: e.message } }));
457
477
  return 2;
458
478
  }
479
+ if (isCrash(e)) {
480
+ try {
481
+ hooks.onCrash?.(e);
482
+ } catch {
483
+ }
484
+ }
459
485
  const opts = program.opts();
460
486
  renderError(io2, e, Boolean(opts.json), pc2.isColorSupported && opts.color !== false);
461
487
  if (!opts.json && needsDoctor(e) && state.command !== "doctor") io2.err("Run qore doctor for a full check of your machine.");
@@ -1261,8 +1287,8 @@ async function readAt(dir2) {
1261
1287
  async function readWorkspace(dir2) {
1262
1288
  return readAt(dir2);
1263
1289
  }
1264
- async function findWorkspace(start, home) {
1265
- let dir2 = resolve(start);
1290
+ async function findWorkspace(start2, home) {
1291
+ let dir2 = resolve(start2);
1266
1292
  const stop = resolve(home);
1267
1293
  for (; ; ) {
1268
1294
  const ws = await readAt(dir2);
@@ -1506,6 +1532,48 @@ function registerClone(program, ctx) {
1506
1532
  });
1507
1533
  }
1508
1534
 
1535
+ // src/core/telemetry/consent.ts
1536
+ var ON = /* @__PURE__ */ new Set(["1", "true", "on", "yes"]);
1537
+ var DO_NOT_TRACK_EXEMPT = /* @__PURE__ */ new Set(["0", "false"]);
1538
+ var norm = (v) => (v ?? "").trim().toLowerCase();
1539
+ function resolveConsent(env, cfg) {
1540
+ const dnt = norm(env.DO_NOT_TRACK);
1541
+ if (dnt !== "" && !DO_NOT_TRACK_EXEMPT.has(dnt)) return { enabled: false, source: "DO_NOT_TRACK" };
1542
+ const q = norm(env.QORE_TELEMETRY);
1543
+ if (q !== "") return { enabled: ON.has(q), source: "QORE_TELEMETRY" };
1544
+ if (typeof cfg.telemetry?.enabled === "boolean") return { enabled: cfg.telemetry.enabled, source: "config" };
1545
+ return { enabled: true, source: "default" };
1546
+ }
1547
+ function needsNotice(cfg, consent) {
1548
+ return consent.enabled && consent.source === "default" && !cfg.telemetry?.noticeShownAt;
1549
+ }
1550
+ function describeConsent(consent, built) {
1551
+ const state = `crash reports ${consent.enabled ? "on" : "off"} (${consent.source})`;
1552
+ return consent.enabled && !built ? `${state} \xB7 this build has no DSN, nothing is sent` : state;
1553
+ }
1554
+
1555
+ // src/cli/commands/config.ts
1556
+ function registerConfig(program, ctx) {
1557
+ const config = program.command("config").description("Change qore's own settings");
1558
+ config.command("telemetry").description("Show or set crash reporting").argument("[state]", "on or off").action(async (state) => {
1559
+ const { deps: deps2, io: io2 } = ctx;
1560
+ if (state !== void 0 && state !== "on" && state !== "off") {
1561
+ throw new QoreError("QORE_CONTEXT_INVALID", `Unknown telemetry state: ${state}`, { hint: "Use: qore config telemetry on|off" });
1562
+ }
1563
+ if (state) await deps2.config.update((c) => {
1564
+ c.telemetry = { ...c.telemetry, enabled: state === "on" };
1565
+ });
1566
+ const consent = resolveConsent(deps2.env, await deps2.config.read());
1567
+ const built = deps2.sentryDsn !== "";
1568
+ if (ctx.opts().json) {
1569
+ renderJson(io2, { telemetry: { ...consent, built } });
1570
+ return;
1571
+ }
1572
+ io2.out(describeConsent(consent, built));
1573
+ if (state && consent.source !== "config") io2.err(`${consent.source} in your environment overrides this setting.`);
1574
+ });
1575
+ }
1576
+
1509
1577
  // src/cli/commands/dev.ts
1510
1578
  import { randomBytes as randomBytes5 } from "crypto";
1511
1579
  import { realpath as realpath5 } from "fs/promises";
@@ -1583,8 +1651,8 @@ async function readRepoConfig(root) {
1583
1651
  // src/core/context/repoRoot.ts
1584
1652
  import { stat } from "fs/promises";
1585
1653
  import { dirname as dirname2, join as join8, resolve as resolve3 } from "path";
1586
- async function findRepoRoot(start) {
1587
- let dir2 = resolve3(start);
1654
+ async function findRepoRoot(start2) {
1655
+ let dir2 = resolve3(start2);
1588
1656
  for (; ; ) {
1589
1657
  try {
1590
1658
  await stat(join8(dir2, ".git"));
@@ -1968,9 +2036,11 @@ import { dirname as dirname5, join as join12 } from "path";
1968
2036
  async function fetchLocalEnv(deps2, mc, app) {
1969
2037
  await ensureCapability(deps2, mc.portal.apiOrigin, "module.local-env");
1970
2038
  try {
1971
- return await mc.session.api.get(
2039
+ const local = await mc.session.api.get(
1972
2040
  `/api/cli/v1/modules/${encodeURIComponent(mc.module.id)}/local-env?app_id=${encodeURIComponent(app.id)}`
1973
2041
  );
2042
+ deps2.registerSecrets(Object.values(local.env));
2043
+ return local;
1974
2044
  } catch (e) {
1975
2045
  if (!(e instanceof QoreError) || e.code !== "QORE_ACCESS_DENIED") throw e;
1976
2046
  if (e.serverCode === "app.access.denied") {
@@ -3182,8 +3252,8 @@ function planDev(modules, sel) {
3182
3252
  }
3183
3253
  const order = sccs.map((_, i) => i).sort((a, b) => minSlug[a].localeCompare(minSlug[b]));
3184
3254
  for (const i of order) visit(i);
3185
- const start = [];
3186
- for (const i of orderedSccIndices) start.push(...sccSlugs[i]);
3255
+ const start2 = [];
3256
+ for (const i of orderedSccIndices) start2.push(...sccSlugs[i]);
3187
3257
  const waitFor = {};
3188
3258
  for (const m of starting) {
3189
3259
  const mySccId = sccOf.get(m.id);
@@ -3198,7 +3268,7 @@ function planDev(modules, sel) {
3198
3268
  missingProviders[m.slug] = [...new Set(missing)].sort();
3199
3269
  }
3200
3270
  }
3201
- return { start, waitFor, cycles, skipped, missingProviders };
3271
+ return { start: start2, waitFor, cycles, skipped, missingProviders };
3202
3272
  }
3203
3273
 
3204
3274
  // src/core/dev/supervisor.ts
@@ -3306,7 +3376,7 @@ function supervise(o) {
3306
3376
  checkDone();
3307
3377
  });
3308
3378
  };
3309
- const start = async (m) => {
3379
+ const start2 = async (m) => {
3310
3380
  const providers = m.waitFor.filter((p) => readiness.has(p));
3311
3381
  const results = await Promise.all(providers.map((p) => readiness.get(p).promise));
3312
3382
  if (stopCode !== null) return;
@@ -3316,7 +3386,7 @@ function supervise(o) {
3316
3386
  });
3317
3387
  launch(m);
3318
3388
  };
3319
- for (const m of o.modules) void start(m);
3389
+ for (const m of o.modules) void start2(m);
3320
3390
  checkDone();
3321
3391
  return {
3322
3392
  done,
@@ -4136,6 +4206,14 @@ async function prepareModule(ctx, f, waitTimeout, p) {
4136
4206
  return { root, slug, recipe, env, port: dev.port, pkg, devEnv: dev.env };
4137
4207
  }
4138
4208
 
4209
+ // src/cli/commands/debugTelemetry.ts
4210
+ function registerDebugTelemetry(program, ctx) {
4211
+ program.command("debug-telemetry", { hidden: true }).description("Send a test crash report").option("--crash", "throw an unexpected error").action((o) => {
4212
+ if (o.crash) throw new Error(`qore debug-telemetry crash in ${ctx.deps.home} token=debug-telemetry-secret`);
4213
+ if (ctx.opts().json) renderJson(ctx.io, { crash: false });
4214
+ });
4215
+ }
4216
+
4139
4217
  // src/cli/commands/doctor.ts
4140
4218
  import pc3 from "picocolors";
4141
4219
 
@@ -4421,6 +4499,12 @@ async function resolveRepoModule(deps2, session, portal, meta, root) {
4421
4499
  }
4422
4500
  return session.api.get(`/api/cli/v1/modules/${encodeURIComponent(ids[0])}`);
4423
4501
  }
4502
+ function checkTelemetry(deps2, cfg) {
4503
+ const built = deps2.sentryDsn !== "";
4504
+ if (!cfg) return ok("crash reports off (config unreadable)", { enabled: false, source: "config", reason: "unreadable", built });
4505
+ const consent = resolveConsent(deps2.env, cfg);
4506
+ return ok(describeConsent(consent, built), { enabled: consent.enabled, source: consent.source, built });
4507
+ }
4424
4508
  async function runDoctor(deps2, o = {}) {
4425
4509
  const signal = o.signal ?? new AbortController().signal;
4426
4510
  const toolMs = o.toolTimeoutMs ?? TOOL_TIMEOUT_MS;
@@ -4440,12 +4524,14 @@ async function runDoctor(deps2, o = {}) {
4440
4524
  return r;
4441
4525
  };
4442
4526
  let cfg;
4527
+ let cfgReadable = true;
4443
4528
  try {
4444
4529
  cfg = await deps2.config.read();
4445
4530
  } catch (e) {
4446
4531
  if (isCancel(e, signal)) throw e;
4447
4532
  await push("config", "tools", () => withError("fail", errorLabel(e), e));
4448
4533
  cfg = { schemaVersion: 1, profiles: {} };
4534
+ cfgReadable = false;
4449
4535
  }
4450
4536
  let ws = null;
4451
4537
  let wsError = null;
@@ -4467,6 +4553,7 @@ async function runDoctor(deps2, o = {}) {
4467
4553
  await push("git", "tools", () => checkGit(deps2, signal, toolMs));
4468
4554
  await push("github-ssh", "tools", () => checkGithubSsh(deps2, signal, toolMs));
4469
4555
  await push("docker", "tools", () => checkDocker(deps2, signal, toolMs));
4556
+ await push("telemetry", "tools", () => checkTelemetry(deps2, cfgReadable ? cfg : null));
4470
4557
  const sessions = /* @__PURE__ */ new Map();
4471
4558
  let loggedIn = [];
4472
4559
  try {
@@ -4745,9 +4832,9 @@ async function createOidc(opts) {
4745
4832
  throw mapOidcError(e, host);
4746
4833
  }
4747
4834
  },
4748
- async pollDevice(start, signal) {
4835
+ async pollDevice(start2, signal) {
4749
4836
  try {
4750
- const t = await oc.pollDeviceAuthorizationGrant(config, start.handle, void 0, { signal });
4837
+ const t = await oc.pollDeviceAuthorizationGrant(config, start2.handle, void 0, { signal });
4751
4838
  return toTokenSet(t);
4752
4839
  } catch (e) {
4753
4840
  throw mapOidcError(e, host);
@@ -4774,14 +4861,14 @@ async function createOidc(opts) {
4774
4861
  // src/core/auth/login.ts
4775
4862
  async function deviceLogin(opts) {
4776
4863
  await opts.store.getRefresh();
4777
- const start = await opts.oidc.startDevice(LOGIN_SCOPE);
4864
+ const start2 = await opts.oidc.startDevice(LOGIN_SCOPE);
4778
4865
  const stopPoll = new AbortController();
4779
4866
  const settled = new AbortController();
4780
- const poll = opts.oidc.pollDevice(start, AbortSignal.any([opts.signal, stopPoll.signal])).finally(() => settled.abort());
4867
+ const poll = opts.oidc.pollDevice(start2, AbortSignal.any([opts.signal, stopPoll.signal])).finally(() => settled.abort());
4781
4868
  poll.catch(() => {
4782
4869
  });
4783
4870
  try {
4784
- await opts.onCode(start, settled.signal);
4871
+ await opts.onCode(start2, settled.signal);
4785
4872
  } catch (e) {
4786
4873
  stopPoll.abort();
4787
4874
  const late = await poll.catch(() => null);
@@ -5179,8 +5266,110 @@ var COMMANDS = [
5179
5266
  registerClone,
5180
5267
  registerSync,
5181
5268
  registerVersion,
5182
- registerDoctor
5269
+ registerConfig,
5270
+ registerDoctor,
5271
+ registerDebugTelemetry
5272
+ ];
5273
+
5274
+ // src/cli/telemetry/notice.ts
5275
+ var NOTICE = [
5276
+ "qore sends anonymous crash reports and a per-run health record to Quatronic, to find and fix bugs.",
5277
+ "Tokens, env values and your home path are removed; no user or tenant is attached.",
5278
+ "Turn it off with `qore config telemetry off`, QORE_TELEMETRY=0 or DO_NOT_TRACK=1. See the Privacy section of the README."
5183
5279
  ];
5280
+ async function showNoticeOnce(deps2, io2, o) {
5281
+ if (o.json || !io2.interactive || !deps2.sentryDsn) return;
5282
+ try {
5283
+ const cfg = await deps2.config.read();
5284
+ if (!needsNotice(cfg, resolveConsent(deps2.env, cfg))) return;
5285
+ await deps2.config.update((c) => {
5286
+ c.telemetry = { ...c.telemetry, noticeShownAt: deps2.now().toISOString() };
5287
+ });
5288
+ } catch {
5289
+ return;
5290
+ }
5291
+ for (const line2 of NOTICE) io2.err(line2);
5292
+ }
5293
+
5294
+ // src/cli/telemetry/index.ts
5295
+ var CRASH_FLUSH_MS = 2e3;
5296
+ var CLEAN_FLUSH_MS = 200;
5297
+ var loadSentry = async () => (await import("./sentry-B3MDCAOI.js")).createSdk;
5298
+ function createTelemetry(deps2, o) {
5299
+ const load = o.load ?? loadSentry;
5300
+ const flushMs = o.flushMs ?? { crash: CRASH_FLUSH_MS, clean: CLEAN_FLUSH_MS };
5301
+ let sdk = null;
5302
+ let crashed = null;
5303
+ const secrets = [];
5304
+ let scrub = null;
5305
+ return {
5306
+ begin(command) {
5307
+ if (sdk || !deps2.sentryDsn) return;
5308
+ sdk = start(deps2, o.argv, command, load, (ctx) => {
5309
+ scrub = ctx;
5310
+ addSecretValues(ctx, secrets);
5311
+ }).catch(() => null);
5312
+ },
5313
+ crash(e) {
5314
+ crashed ??= { e };
5315
+ },
5316
+ registerSecrets(values) {
5317
+ secrets.push(...values);
5318
+ if (scrub) addSecretValues(scrub, values);
5319
+ },
5320
+ async end(signal) {
5321
+ if (!sdk) return { drained: true };
5322
+ const ms = crashed ? flushMs.crash : flushMs.clean;
5323
+ const work = sdk.then(async (s) => {
5324
+ if (!s) return true;
5325
+ if (crashed) s.crash(crashed.e);
5326
+ return s.end(ms);
5327
+ }).catch(() => false);
5328
+ return { drained: await withDeadline(work, ms, signal) };
5329
+ }
5330
+ };
5331
+ }
5332
+ async function start(deps2, argv2, command, load, onScrub) {
5333
+ const cfg = await deps2.config.read().catch(() => null);
5334
+ if (!cfg) return null;
5335
+ if (!resolveConsent(deps2.env, cfg).enabled) return null;
5336
+ const create = await load();
5337
+ const scrub = buildScrubContext({ home: deps2.home, platform: deps2.platform, env: deps2.env, argv: argv2, commandWords: command.split(" ") });
5338
+ onScrub(scrub);
5339
+ const sdk = create({
5340
+ dsn: deps2.sentryDsn,
5341
+ environment: deps2.sentryEnvironment,
5342
+ release: deps2.version,
5343
+ tags: { command, "cli.version": deps2.version, "node.version": deps2.nodeVersion, os: deps2.platform, ci: deps2.env.CI ? "true" : "false" },
5344
+ scrub
5345
+ });
5346
+ sdk.start();
5347
+ return sdk;
5348
+ }
5349
+ function withDeadline(work, ms, signal) {
5350
+ if (signal?.aborted) return Promise.resolve(false);
5351
+ return new Promise((resolve5) => {
5352
+ const done = (v) => {
5353
+ clearTimeout(timer);
5354
+ signal?.removeEventListener("abort", onAbort);
5355
+ resolve5(v);
5356
+ };
5357
+ const onAbort = () => done(false);
5358
+ const timer = setTimeout(() => done(false), ms);
5359
+ signal?.addEventListener("abort", onAbort, { once: true });
5360
+ work.then(done, () => done(false));
5361
+ });
5362
+ }
5363
+ function telemetryHooks(deps2, io2, t, o) {
5364
+ return {
5365
+ onCommand: async (command) => {
5366
+ if (command === "config telemetry") return;
5367
+ await showNoticeOnce(deps2, io2, o);
5368
+ t.begin(command);
5369
+ },
5370
+ onCrash: (e) => t.crash(e)
5371
+ };
5372
+ }
5184
5373
 
5185
5374
  // src/cli/updateCheck.ts
5186
5375
  var REGISTRY_URL = "https://registry.npmjs.org/@quatronic%2Fqore/latest";
@@ -5728,6 +5917,10 @@ var systemPorts = {
5728
5917
  }
5729
5918
  };
5730
5919
 
5920
+ // src/buildInfo.ts
5921
+ var SENTRY_DSN = true ? "https://50b592efb53dbc1a31cb10d3945c6d52@o4512129906049024.ingest.de.sentry.io/4512174341750864" : "";
5922
+ var SENTRY_ENVIRONMENT = true ? "production" : "development";
5923
+
5731
5924
  // src/platform/proxy.ts
5732
5925
  import { execFile as execFile3 } from "child_process";
5733
5926
 
@@ -5790,7 +5983,7 @@ function systemProxy(platform = process.platform) {
5790
5983
  }
5791
5984
 
5792
5985
  // src/version.ts
5793
- var VERSION = true ? "0.1.4" : "0.0.0-dev";
5986
+ var VERSION = true ? "0.1.5" : "0.0.0-dev";
5794
5987
 
5795
5988
  // src/cli.ts
5796
5989
  if (process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY) setGlobalDispatcher(new EnvHttpProxyAgent());
@@ -5833,6 +6026,11 @@ var deps = {
5833
6026
  process.kill(process.pid, signal);
5834
6027
  },
5835
6028
  pid: process.pid,
6029
+ registerSecrets: (values) => {
6030
+ telemetry.registerSecrets(values);
6031
+ },
6032
+ sentryDsn: SENTRY_DSN,
6033
+ sentryEnvironment: SENTRY_ENVIRONMENT,
5836
6034
  systemProxy: () => systemProxy(process.platform)
5837
6035
  };
5838
6036
  var termination = preflightTermination(() => ac.abort(), deps.raise);
@@ -5843,16 +6041,24 @@ var io = processIo(isInteractive({
5843
6041
  noInput: preSeparatorArgv.includes("--no-input"),
5844
6042
  env: process.env
5845
6043
  }));
5846
- process.exitCode = await run(argv, deps, io, ac.signal, COMMANDS);
5847
- await termination.reRaise(async () => {
6044
+ var flushStd = async () => {
5848
6045
  await Promise.all([process.stdout, process.stderr].map((s) => new Promise((r) => {
5849
6046
  s.write("", () => r());
5850
6047
  })));
5851
- });
6048
+ };
6049
+ var telemetry = createTelemetry(deps, { argv });
6050
+ process.exitCode = await run(argv, deps, io, ac.signal, COMMANDS, telemetryHooks(deps, io, telemetry, { json }));
6051
+ var { drained } = await telemetry.end(ac.signal);
6052
+ await termination.reRaise(flushStd);
5852
6053
  if (!ac.signal.aborted) {
5853
6054
  try {
5854
6055
  await maybeNotifyUpdate(deps, io, { json });
5855
6056
  } catch {
5856
6057
  }
5857
6058
  }
5858
- //# sourceMappingURL=cli.js.map
6059
+ if (!drained) {
6060
+ await flushStd();
6061
+ process.exit();
6062
+ }
6063
+ //# sourceMappingURL=cli.js.map
6064
+ //# debugId=a70250c5-daf4-5ff3-b490-5556ac377549