@quatronic/qore 0.1.3 → 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/dist/cli.js CHANGED
@@ -1,11 +1,18 @@
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";
6
13
 
7
14
  // src/cli/io.ts
8
- import { confirm, select } from "@inquirer/prompts";
15
+ import { confirm, search, select } from "@inquirer/prompts";
9
16
  import { createInterface } from "readline";
10
17
 
11
18
  // src/platform/browser.ts
@@ -129,9 +136,17 @@ function processIo(interactive, stderrTTY = Boolean(process.stderr.isTTY)) {
129
136
  throw e;
130
137
  }
131
138
  },
132
- async confirm(message, signal) {
139
+ async search(message, source) {
140
+ try {
141
+ return await search({ message, source: (term) => source(term ?? "") });
142
+ } catch (e) {
143
+ if (e.name === "ExitPromptError") throw new CancelledError();
144
+ throw e;
145
+ }
146
+ },
147
+ async confirm(message, signal, o) {
133
148
  try {
134
- return await confirm({ message, default: false }, { signal });
149
+ return await confirm({ message, default: o?.default ?? false }, { signal });
135
150
  } catch (e) {
136
151
  if (e.name === "ExitPromptError" || e.name === "AbortPromptError") throw new CancelledError();
137
152
  throw e;
@@ -312,7 +327,7 @@ function renderDoctorJson(io2, report) {
312
327
  return true;
313
328
  }
314
329
  var SYMBOL = { ok: "\u2713", warn: "!", fail: "\u2717" };
315
- var TOOL_IDS = ["qore", "node", "git", "github-ssh", "docker"];
330
+ var TOOL_IDS = ["qore", "node", "git", "github-ssh", "docker", "telemetry"];
316
331
  var ROW_WIDTH = 80;
317
332
  var COL_B = 36;
318
333
  function rawLine(r) {
@@ -395,6 +410,23 @@ function argvBeforeSeparator(argv2) {
395
410
  function jsonFromArgv(argv2) {
396
411
  return argvBeforeSeparator(argv2).some((a) => a === "--json" || a.startsWith("--json="));
397
412
  }
413
+ var DOCTOR_CODES = /* @__PURE__ */ new Set([
414
+ "QORE_DOCKER_MISSING",
415
+ "QORE_DOCKER_NOT_RUNNING",
416
+ "QORE_DOCKER_COMPOSE_MISSING",
417
+ "QORE_NODE_VERSION",
418
+ "QORE_PACKAGE_MANAGER",
419
+ "QORE_INSTALL_FAILED",
420
+ "QORE_PORT_IN_USE",
421
+ "QORE_SERVICE_UNHEALTHY"
422
+ ]);
423
+ var machineFailure = (e) => DOCTOR_CODES.has(e.code) && !(e.code === "QORE_PORT_IN_USE" && e.details?.owner === "qore");
424
+ function needsDoctor(e) {
425
+ if (!(e instanceof QoreError)) return false;
426
+ const problems = e.details?.problems;
427
+ if (Array.isArray(problems)) return problems.some((p) => machineFailure(p));
428
+ return machineFailure(e);
429
+ }
398
430
  function requestLog(ctx) {
399
431
  return ctx.opts().verbose ? (line2) => ctx.io.err(line2) : void 0;
400
432
  }
@@ -410,7 +442,16 @@ function buildProgram(deps2, io2, signal, register, setExitCode) {
410
442
  for (const r of register) r(program, ctx);
411
443
  return program;
412
444
  }
413
- 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 = {}) {
414
455
  const json2 = jsonFromArgv(argv2);
415
456
  const parseIo = json2 ? { ...io2, err: () => {
416
457
  } } : io2;
@@ -418,6 +459,13 @@ async function run(argv2, deps2, io2, signal, register) {
418
459
  const program = buildProgram(deps2, parseIo, signal, register, (code) => {
419
460
  state.exitCode = code;
420
461
  });
462
+ program.hook("preAction", async (_program, action) => {
463
+ state.command = action.name();
464
+ try {
465
+ await hooks.onCommand?.(commandPath(action));
466
+ } catch {
467
+ }
468
+ });
421
469
  try {
422
470
  await program.parseAsync(argv2, { from: "user" });
423
471
  return state.exitCode ?? 0;
@@ -428,8 +476,15 @@ async function run(argv2, deps2, io2, signal, register) {
428
476
  if (json2) io2.out(JSON.stringify({ ok: false, error: { code: "QORE_CONTEXT_INVALID", message: e.message } }));
429
477
  return 2;
430
478
  }
479
+ if (isCrash(e)) {
480
+ try {
481
+ hooks.onCrash?.(e);
482
+ } catch {
483
+ }
484
+ }
431
485
  const opts = program.opts();
432
486
  renderError(io2, e, Boolean(opts.json), pc2.isColorSupported && opts.color !== false);
487
+ if (!opts.json && needsDoctor(e) && state.command !== "doctor") io2.err("Run qore doctor for a full check of your machine.");
433
488
  return exitCodeFor(e);
434
489
  }
435
490
  }
@@ -513,6 +568,9 @@ async function loadMeta(deps2, apiOrigin, refresh) {
513
568
  });
514
569
  return { meta, cached: false };
515
570
  }
571
+ async function metaFor(deps2, apiOrigin) {
572
+ return (await loadMeta(deps2, apiOrigin, false)).meta;
573
+ }
516
574
  async function ensureCapability(deps2, apiOrigin, capability) {
517
575
  const first = await loadMeta(deps2, apiOrigin, false);
518
576
  let meta = first.meta;
@@ -754,8 +812,7 @@ async function openSession(deps2, portal, command, onRequest) {
754
812
  // src/core/workspace/apps.ts
755
813
  var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
756
814
  var tenantCandidates = (tenants) => tenants.map((t) => ({ kind: "tenant", id: t.id, slug: t.slug, name: t.name }));
757
- async function resolveAppRef(deps2, session, arg, tenantFlag) {
758
- if (UUID.test(arg)) return { appId: arg, tenant: null };
815
+ async function resolveTenant(deps2, session, tenantFlag, whatFor) {
759
816
  const me = await session.api.get("/api/cli/v1/me");
760
817
  let tenant;
761
818
  if (tenantFlag) {
@@ -779,11 +836,16 @@ async function resolveAppRef(deps2, session, arg, tenantFlag) {
779
836
  } else if (me.tenants.length === 1) {
780
837
  tenant = { id: me.tenants[0].id, slug: me.tenants[0].slug };
781
838
  } else {
782
- throw new QoreError("QORE_CONTEXT_REQUIRED", `Which tenant is "${arg}" in?`, {
839
+ throw new QoreError("QORE_CONTEXT_REQUIRED", `Which tenant is "${whatFor}" in?`, {
783
840
  hint: "Run `qore tenant use <slug>`, or pass --tenant <slug>.",
784
841
  candidates: tenantCandidates(me.tenants)
785
842
  });
786
843
  }
844
+ return { tenant, me };
845
+ }
846
+ async function resolveAppRef(deps2, session, arg, tenantFlag) {
847
+ if (UUID.test(arg)) return { appId: arg, tenant: null };
848
+ const { tenant } = await resolveTenant(deps2, session, tenantFlag, arg);
787
849
  try {
788
850
  const { id } = await session.api.get(
789
851
  `/api/cli/v1/tenants/${encodeURIComponent(tenant.id)}/apps/resolve?slug=${encodeURIComponent(arg)}`
@@ -806,7 +868,7 @@ async function resolveAppRef(deps2, session, arg, tenantFlag) {
806
868
  }
807
869
 
808
870
  // src/core/workspace/apply.ts
809
- import { join as join4, relative } from "path";
871
+ import { join as join4 } from "path";
810
872
 
811
873
  // src/core/context/cache.ts
812
874
  import { join as join2 } from "path";
@@ -956,7 +1018,7 @@ async function trust(deps2, root, moduleId) {
956
1018
  }
957
1019
 
958
1020
  // src/core/workspace/plan.ts
959
- import { join as join3 } from "path";
1021
+ import { join as join3, relative } from "path";
960
1022
  var SEGMENT = /^[A-Za-z0-9_.-]+$/;
961
1023
  function isSupportedRepoUrl(repoUrl) {
962
1024
  let u;
@@ -981,6 +1043,13 @@ function cloneUrl(repoUrl, protocol) {
981
1043
  const u = new URL(repoUrl);
982
1044
  return `git@${u.hostname}:${u.pathname.replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/, "")}.git`;
983
1045
  }
1046
+ function conflictText(cwd, action) {
1047
+ const dir2 = relative(cwd, action.dir) || ".";
1048
+ const notRepo = action.reason === "not a git repository";
1049
+ const what = notRepo ? "it is not a git repository" : action.origin ? `its origin is ${action.origin}, not ${action.module.repoUrl}` : "it has no origin remote";
1050
+ const message = notRepo ? `${dir2} is not a git repository` : action.origin ? `${dir2} holds another repository (its origin is ${action.origin}, not ${action.module.repoUrl})` : `${dir2} has no origin remote`;
1051
+ return { message, hint: `Move ${dir2} aside (${what}), then run \`qore sync\`.` };
1052
+ }
984
1053
  function planWorkspace(root, modules, dirs, strayRepos2) {
985
1054
  const actions = modules.map((module) => {
986
1055
  const dir2 = join3(root, module.slug);
@@ -997,6 +1066,10 @@ function planWorkspace(root, modules, dirs, strayRepos2) {
997
1066
  const slugs = new Set(modules.map((m) => m.slug));
998
1067
  return { actions, removed: strayRepos2.filter((s) => !slugs.has(s)) };
999
1068
  }
1069
+ function cloneProtocol(dirs) {
1070
+ const origins = Object.values(dirs).flatMap((d) => d.remotes.filter((r) => r.name === "origin").map((r) => r.url));
1071
+ return origins.some((u) => /^(git@|ssh:\/\/)/.test(u)) ? "ssh" : "https";
1072
+ }
1000
1073
 
1001
1074
  // src/core/workspace/diagnose.ts
1002
1075
  var ACCESS_CODES = /* @__PURE__ */ new Set(["QORE_GITHUB_NOT_LINKED", "QORE_REPO_NO_ACCESS", "QORE_REPO_ACCESS_PENDING"]);
@@ -1072,15 +1145,8 @@ function toFailure(slug, e) {
1072
1145
  throw e;
1073
1146
  }
1074
1147
  function conflictResult(deps2, action) {
1075
- const dir2 = relative(deps2.cwd, action.dir) || ".";
1076
- const what = action.reason === "not a git repository" ? "it is not a git repository" : action.origin ? `its origin is ${action.origin}, not ${action.module.repoUrl}` : "it has no origin remote";
1077
- const message = action.reason === "not a git repository" ? `${dir2} is not a git repository` : action.origin ? `${dir2} holds another repository (its origin is ${action.origin}, not ${action.module.repoUrl})` : `${dir2} has no origin remote`;
1078
- return {
1079
- slug: action.module.slug,
1080
- status: "conflict",
1081
- reason: action.reason,
1082
- error: { code: "QORE_WORKSPACE_CONFLICT", message, hint: `Move ${dir2} aside (${what}), then run \`qore sync\`.` }
1083
- };
1148
+ const { message, hint } = conflictText(deps2.cwd, action);
1149
+ return { slug: action.module.slug, status: "conflict", reason: action.reason, error: { code: "QORE_WORKSPACE_CONFLICT", message, hint } };
1084
1150
  }
1085
1151
  async function applyActions(deps2, o) {
1086
1152
  const cache = new ResolutionCache(join4(deps2.config.dir, "cache"), deps2.now);
@@ -1221,8 +1287,8 @@ async function readAt(dir2) {
1221
1287
  async function readWorkspace(dir2) {
1222
1288
  return readAt(dir2);
1223
1289
  }
1224
- async function findWorkspace(start, home) {
1225
- let dir2 = resolve(start);
1290
+ async function findWorkspace(start2, home) {
1291
+ let dir2 = resolve(start2);
1226
1292
  const stop = resolve(home);
1227
1293
  for (; ; ) {
1228
1294
  const ws = await readAt(dir2);
@@ -1382,8 +1448,34 @@ async function resolveApp(deps2, io2, session, appArg, tenantFlag) {
1382
1448
  return resolveAppRef(deps2, session, appArg, slug);
1383
1449
  }
1384
1450
  }
1451
+ async function pickApp(deps2, io2, session, tenantFlag) {
1452
+ if (!io2.interactive) {
1453
+ throw new QoreError("QORE_CONTEXT_REQUIRED", "An app is required", {
1454
+ hint: "Pass an app slug: `qore clone <app>`. Interactively, omitting it opens a picker."
1455
+ });
1456
+ }
1457
+ let tenant, me;
1458
+ try {
1459
+ ({ tenant, me } = await resolveTenant(deps2, session, tenantFlag, "the app you want to clone"));
1460
+ } catch (e) {
1461
+ if (!(e instanceof QoreError) || e.code !== "QORE_CONTEXT_REQUIRED" && e.code !== "QORE_TENANT_ACCESS_LOST" || !e.candidates?.length) throw e;
1462
+ const slug = await io2.select("Select a tenant", e.candidates.map((c) => ({ name: c.name ?? c.slug, value: c.slug })));
1463
+ ({ tenant, me } = await resolveTenant(deps2, session, slug, "the app you want to clone"));
1464
+ }
1465
+ const apps = me.tenants.find((t) => t.id === tenant.id)?.apps ?? [];
1466
+ if (apps.length === 0) {
1467
+ throw new QoreError("QORE_CONTEXT_REQUIRED", `No apps in ${tenant.slug} to clone`, {
1468
+ hint: "Run `qore whoami` to see what you have access to."
1469
+ });
1470
+ }
1471
+ const app = await io2.search("Select an app", (term) => {
1472
+ const q = term.toLowerCase();
1473
+ return apps.filter((a) => !q || a.slug.toLowerCase().includes(q) || a.name.toLowerCase().includes(q)).map((a) => ({ name: `${a.name} (${a.slug})`, value: a.slug }));
1474
+ });
1475
+ return { tenant: tenant.slug, app };
1476
+ }
1385
1477
  function registerClone(program, ctx) {
1386
- program.command("clone").description("Clone an app's modules into a new workspace directory").argument("<app>", "app slug or id").argument("[dir]", "target directory (default: the app's slug)").option("--tenant <slug>", "tenant the app slug belongs to, when it is not your active one").addOption(new Option("--protocol <https|ssh>", "clone URL protocol").choices(["https", "ssh"]).default("https")).action(async (appArg, dirArg, flags) => {
1478
+ program.command("clone").description("Clone an app's modules into a new workspace directory").argument("[app]", "app slug or id (omit to pick one interactively)").argument("[dir]", "target directory (default: the app's slug)").option("--tenant <slug>", "tenant the app slug belongs to, when it is not your active one").addOption(new Option("--protocol <https|ssh>", "clone URL protocol").choices(["https", "ssh"]).default("https")).action(async (appArg, dirArg, flags) => {
1387
1479
  const { deps: deps2, io: io2 } = ctx;
1388
1480
  const g = ctx.opts();
1389
1481
  const cfg = await deps2.config.read();
@@ -1391,7 +1483,9 @@ function registerClone(program, ctx) {
1391
1483
  const onRequest = g.verbose ? (l) => io2.err(l) : void 0;
1392
1484
  const session = await openSession(deps2, portal, "clone", onRequest);
1393
1485
  await ensureCapability(deps2, portal.apiOrigin, "app.modules");
1394
- const ref = await resolveApp(deps2, io2, session, appArg, flags.tenant);
1486
+ const picked = appArg === void 0 ? await pickApp(deps2, io2, session, flags.tenant) : void 0;
1487
+ const appSlug = picked?.app ?? appArg;
1488
+ const ref = await resolveApp(deps2, io2, session, appSlug, picked?.tenant ?? flags.tenant);
1395
1489
  const app = await session.api.get(`/api/cli/v1/apps/${encodeURIComponent(ref.appId)}/modules`);
1396
1490
  if (dirArg === void 0 && !isSafeSlug(app.app.slug)) throw unsafeSlugError(app.app.slug);
1397
1491
  const dirLabel = dirArg ?? app.app.slug;
@@ -1401,7 +1495,7 @@ function registerClone(program, ctx) {
1401
1495
  const ws = await readWorkspace(root);
1402
1496
  if (!ws || ws.appId !== app.app.id || ws.portal !== portal.apiOrigin) {
1403
1497
  throw new QoreError("QORE_WORKSPACE_CONFLICT", `${root} exists and is not the workspace of ${app.app.slug}`, {
1404
- hint: `Pick another directory: qore clone ${appArg} <dir>.`
1498
+ hint: `Pick another directory: qore clone ${appSlug} <dir>.`
1405
1499
  });
1406
1500
  }
1407
1501
  }
@@ -1438,11 +1532,53 @@ function registerClone(program, ctx) {
1438
1532
  });
1439
1533
  }
1440
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
+
1441
1577
  // src/cli/commands/dev.ts
1442
1578
  import { randomBytes as randomBytes5 } from "crypto";
1443
1579
  import { realpath as realpath5 } from "fs/promises";
1444
1580
  import { constants as constants3 } from "os";
1445
- import { relative as relative6 } from "path";
1581
+ import { dirname as dirname6, join as join18, relative as relative6 } from "path";
1446
1582
  import { StringDecoder } from "string_decoder";
1447
1583
 
1448
1584
  // src/core/context/repoConfig.ts
@@ -1515,8 +1651,8 @@ async function readRepoConfig(root) {
1515
1651
  // src/core/context/repoRoot.ts
1516
1652
  import { stat } from "fs/promises";
1517
1653
  import { dirname as dirname2, join as join8, resolve as resolve3 } from "path";
1518
- async function findRepoRoot(start) {
1519
- let dir2 = resolve3(start);
1654
+ async function findRepoRoot(start2) {
1655
+ let dir2 = resolve3(start2);
1520
1656
  for (; ; ) {
1521
1657
  try {
1522
1658
  await stat(join8(dir2, ".git"));
@@ -1801,7 +1937,7 @@ async function resolveModuleContext(deps2, o) {
1801
1937
  });
1802
1938
  }
1803
1939
  var appCandidates = (apps) => apps.map((a) => ({ kind: "app", id: a.id, slug: a.slug, name: a.name }));
1804
- function pickApp(mc, app) {
1940
+ function pickApp2(mc, app) {
1805
1941
  const apps = mc.module.apps;
1806
1942
  if (app) {
1807
1943
  const found = apps.find((a) => a.id === app || a.slug === app);
@@ -1821,14 +1957,14 @@ function pickApp(mc, app) {
1821
1957
 
1822
1958
  // src/core/diagnostics/node.ts
1823
1959
  var KEEP_LINES = 50;
1824
- function earlyExitHint(tail, o) {
1825
- const text = tail.join("\n");
1826
- const missing = text.match(/Cannot find (?:module|package) '([^']+)'/);
1960
+ function exitHint(tail, h) {
1961
+ const text2 = tail.join("\n");
1962
+ const missing = text2.match(/Cannot find (?:module|package) '([^']+)'/);
1827
1963
  if (missing) {
1828
- return { reason: "missing-module", hint: `A dependency is missing (${missing[1]}). Run \`${o.installCommand}\`, then qore dev again.` };
1964
+ return { reason: "missing-module", hint: `A dependency is missing (${missing[1]}). Run \`${h.installCommand}\`, then qore dev again.` };
1829
1965
  }
1830
- if (/ZodError:/.test(text)) {
1831
- const keys = [...text.matchAll(/"path":\s*\[\s*"([^"]+)"/g)].map((m) => m[1]);
1966
+ if (/ZodError:/.test(text2)) {
1967
+ const keys = [...text2.matchAll(/"path":\s*\[\s*"([^"]+)"/g)].map((m) => m[1]);
1832
1968
  if (keys.length > 0) {
1833
1969
  return {
1834
1970
  reason: "invalid-env",
@@ -1837,7 +1973,7 @@ function earlyExitHint(tail, o) {
1837
1973
  };
1838
1974
  }
1839
1975
  }
1840
- const addrInUse = text.match(/listen EADDRINUSE: address already in use (?:\S*:)?(\d+)/);
1976
+ const addrInUse = text2.match(/listen EADDRINUSE: address already in use (?:\S*:)?(\d+)/);
1841
1977
  if (addrInUse) {
1842
1978
  const port = Number(addrInUse[1]);
1843
1979
  return {
@@ -1846,7 +1982,45 @@ function earlyExitHint(tail, o) {
1846
1982
  hint: `Port ${port} was taken while the server started. Run qore dev again; if it repeats, lsof -iTCP:${port} -sTCP:LISTEN names the holder.`
1847
1983
  };
1848
1984
  }
1849
- if (/ENOSPC: System limit for number of file watchers reached/.test(text)) {
1985
+ if (/ECONNREFUSED/.test(text2)) {
1986
+ const m = text2.match(/ECONNREFUSED\s+\S*:(\d+)/) ?? text2.match(/code: 'ECONNREFUSED'[^}]*?port: (\d+)/);
1987
+ if (!m) return { reason: "conn-refused", hint: "A connection was refused (ECONNREFUSED) \u2014 a service this module calls is not running." };
1988
+ const port = Number(m[1]);
1989
+ const role = h.ports.get(port);
1990
+ if (role?.kind === "database") {
1991
+ const up = h.runCommand("db:up");
1992
+ return {
1993
+ reason: "conn-refused",
1994
+ port,
1995
+ hint: `The database on :${port} is not accepting connections (ECONNREFUSED). ` + (up ? `Fix: ${up} (docker compose ps shows its state)` : "docker compose ps shows its state.")
1996
+ };
1997
+ }
1998
+ if (role?.kind === "provider") {
1999
+ return {
2000
+ reason: "conn-refused",
2001
+ port,
2002
+ hint: `${role.key} points at :${port}, where nothing listens (ECONNREFUSED). Start that module \u2014 qore dev in the workspace root starts providers first.`
2003
+ };
2004
+ }
2005
+ return { reason: "conn-refused", port, hint: `Nothing listens on :${port} (ECONNREFUSED) \u2014 a service this module calls is not running.` };
2006
+ }
2007
+ const auth = text2.match(/password authentication failed for user "([^"]+)"/);
2008
+ if (auth) {
2009
+ const reset = h.runCommand("db:reset");
2010
+ return {
2011
+ reason: "db-auth",
2012
+ hint: `The local database was created with other credentials than the portal now provides (user ${auth[1]}).` + (reset ? ` Fix: ${reset} \u2014 this wipes local data.` : "")
2013
+ };
2014
+ }
2015
+ const relation = text2.match(/relation "([^"]+)" does not exist/);
2016
+ if (relation) {
2017
+ const reset = h.runCommand("db:reset");
2018
+ return {
2019
+ reason: "db-schema",
2020
+ hint: `The database schema is behind the code (relation ${relation[1]} does not exist). Migrations run when the app starts; if they failed above, that error is the cause.` + (reset ? ` To start clean: ${reset} (wipes local data).` : "")
2021
+ };
2022
+ }
2023
+ if (/ENOSPC: System limit for number of file watchers reached/.test(text2)) {
1850
2024
  return {
1851
2025
  reason: "watcher-limit",
1852
2026
  hint: "Linux ran out of file watchers. Raise the limit: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p"
@@ -1862,9 +2036,11 @@ import { dirname as dirname5, join as join12 } from "path";
1862
2036
  async function fetchLocalEnv(deps2, mc, app) {
1863
2037
  await ensureCapability(deps2, mc.portal.apiOrigin, "module.local-env");
1864
2038
  try {
1865
- return await mc.session.api.get(
2039
+ const local = await mc.session.api.get(
1866
2040
  `/api/cli/v1/modules/${encodeURIComponent(mc.module.id)}/local-env?app_id=${encodeURIComponent(app.id)}`
1867
2041
  );
2042
+ deps2.registerSecrets(Object.values(local.env));
2043
+ return local;
1868
2044
  } catch (e) {
1869
2045
  if (!(e instanceof QoreError) || e.code !== "QORE_ACCESS_DENIED") throw e;
1870
2046
  if (e.serverCode === "app.access.denied") {
@@ -1947,12 +2123,12 @@ function renderEnv(original, merged) {
1947
2123
  for (const [key, value] of merged) {
1948
2124
  if (!seen.has(key)) appended.push(`${key}=${value}`);
1949
2125
  }
1950
- let text = out.join("\n");
2126
+ let text2 = out.join("\n");
1951
2127
  if (appended.length) {
1952
- if (text.length && !text.endsWith("\n")) text += "\n";
1953
- text += appended.join("\n") + "\n";
2128
+ if (text2.length && !text2.endsWith("\n")) text2 += "\n";
2129
+ text2 += appended.join("\n") + "\n";
1954
2130
  }
1955
- return text;
2131
+ return text2;
1956
2132
  }
1957
2133
 
1958
2134
  // src/core/env/pull.ts
@@ -2034,12 +2210,12 @@ async function readOrNull(p) {
2034
2210
  throw e;
2035
2211
  }
2036
2212
  }
2037
- async function writeSecurely(target, text) {
2213
+ async function writeSecurely(target, text2) {
2038
2214
  const base = basename2(target);
2039
2215
  const tmpName = base.endsWith(".local") ? `${base.slice(0, -".local".length)}.qore-${randomBytes3(6).toString("hex")}.local` : `${base}.qore-${randomBytes3(6).toString("hex")}.tmp`;
2040
2216
  const tmp = join10(dirname4(target), tmpName);
2041
2217
  try {
2042
- await writeFile3(tmp, text, { mode: 384, flag: "wx" });
2218
+ await writeFile3(tmp, text2, { mode: 384, flag: "wx" });
2043
2219
  await chmod2(tmp, 384);
2044
2220
  await rename3(tmp, target);
2045
2221
  } finally {
@@ -2124,16 +2300,117 @@ async function devEnv(deps2, o) {
2124
2300
  cause = e;
2125
2301
  }
2126
2302
  }
2127
- const text = await readOrNull(target);
2128
- if (text === null || !await isTrusted(deps2, o.root, o.moduleId)) {
2303
+ const text2 = await readOrNull(target);
2304
+ if (text2 === null || !await isTrusted(deps2, o.root, o.moduleId)) {
2129
2305
  throw cause ?? new QoreError("QORE_API_UNREACHABLE", "The portal is unreachable", {
2130
2306
  hint: "The portal is unreachable and there is no env file to start from yet."
2131
2307
  });
2132
2308
  }
2133
- const env = Object.fromEntries(parseEnv(text));
2309
+ const env = Object.fromEntries(parseEnv(text2));
2134
2310
  return { env, offline: true, envFile: target, port: envPort(env, "PORT"), dbPort: envPort(env, "DB_PORT"), created: false, kept: [] };
2135
2311
  }
2136
2312
 
2313
+ // src/core/workspace/access.ts
2314
+ function devTeamsOf(am, m) {
2315
+ const own = m.devTeamIds?.length ? am.devTeams.filter((t) => m.devTeamIds.includes(t.id)) : [];
2316
+ return own.length ? own : am.devTeams;
2317
+ }
2318
+ function askMaintainer(am, m) {
2319
+ const teams = devTeamsOf(am, m);
2320
+ const names = teams.map((t) => t.name);
2321
+ const maintainers = [...new Set(teams.flatMap((t) => t.maintainers))];
2322
+ const who = names.length ? names.join(", ") : "the app's dev team";
2323
+ return { teams: names, maintainers, text: `ask a maintainer of ${who}${maintainers.length ? ` (${maintainers.join(", ")})` : ""}` };
2324
+ }
2325
+ function notClonedReason(m, am) {
2326
+ if (am && !m.githubLinked) return `GitHub account not linked (${am.accountUrl})`;
2327
+ if (am && m.repoAccess === "none") {
2328
+ const names = devTeamsOf(am, m).map((t) => t.name);
2329
+ return names.length ? `no repository access (dev team ${names.join(", ")})` : "no repository access (ask your tenant owner)";
2330
+ }
2331
+ return "run qore sync";
2332
+ }
2333
+
2334
+ // src/core/dev/findings.ts
2335
+ var plural = (n, one, many) => n === 1 ? one : many;
2336
+ function modulesMissing(o) {
2337
+ const actionFor = new Map(o.actions.map((a) => [a.module.slug, a]));
2338
+ const findings = [];
2339
+ const fixable = [];
2340
+ const blocked = [];
2341
+ const reasons = {};
2342
+ const own = (m, reason, rest) => {
2343
+ reasons[m.slug] = reason;
2344
+ return { code: "dev.modules-missing", severity: "warn", module: m.slug, modules: [m.slug], message: `${m.slug} is not cloned \u2014 ${reason}`, ...rest };
2345
+ };
2346
+ for (const m of o.missing) {
2347
+ const a = actionFor.get(m.slug);
2348
+ if (a && a.kind !== "clone") {
2349
+ if (a.kind === "conflict") {
2350
+ const c = conflictText(o.cwd, a);
2351
+ blocked.push(own(m, c.message, { action: { command: c.hint } }));
2352
+ } else if (a.kind === "unsupported") {
2353
+ blocked.push(own(m, "its repository URL is not supported", { action: { command: unsupportedRepoUrlError(m.slug, "").hint } }));
2354
+ } else {
2355
+ blocked.push(own(m, a.reason ?? "no repository yet", { why: "It will not run in this session." }));
2356
+ }
2357
+ } else if (o.am !== null && (!m.githubLinked || m.repoAccess === "none")) {
2358
+ const am = o.am;
2359
+ blocked.push(own(m, notClonedReason(m, am), { action: { command: !m.githubLinked ? `Link your GitHub account: ${am.accountUrl}` : askMaintainer(am, m).text } }));
2360
+ } else fixable.push(m);
2361
+ }
2362
+ if (fixable.length > 0) {
2363
+ const slugs = fixable.map((m) => m.slug);
2364
+ const deps2 = slugs.flatMap((s) => (o.consumers[s] ?? []).map((c) => `${c} depends on ${s}`));
2365
+ findings.push({
2366
+ code: "dev.modules-missing",
2367
+ severity: "warn",
2368
+ message: `${fixable.length} ${plural(fixable.length, "module of this app is", "modules of this app are")} not cloned: ${slugs.join(", ")}`,
2369
+ why: deps2.length > 0 ? `${deps2.join("; ")}.` : `${plural(fixable.length, "It", "They")} will not run in this session.`,
2370
+ modules: slugs,
2371
+ action: o.am ? { command: "qore sync", fix: "sync" } : { command: "qore sync" }
2372
+ });
2373
+ }
2374
+ return { findings: [...findings, ...blocked], fixable, reasons };
2375
+ }
2376
+ function strayClone(name, appName) {
2377
+ return { code: "dev.stray-clone", severity: "info", module: name, message: `${name} is no longer part of ${appName} \u2014 you can remove the directory.` };
2378
+ }
2379
+ function repoBehind(slug, s, o) {
2380
+ if (!s.ok || !s.upstream || !s.behind) return null;
2381
+ return {
2382
+ code: "dev.repo-behind",
2383
+ severity: "warn",
2384
+ module: slug,
2385
+ message: `${slug} is ${s.behind} ${plural(s.behind, "commit", "commits")} behind ${s.upstreamRef ?? "its upstream"} (as of your last fetch).`,
2386
+ action: { command: o.workspace ? "qore sync --pull" : "git pull" }
2387
+ };
2388
+ }
2389
+ function servicesMissing(slug, env, recipe, o) {
2390
+ const db = envPort(env, "DB_PORT");
2391
+ if (db === null || recipe.services.length > 0 || !o.servicesFlag || o.configured) return null;
2392
+ return {
2393
+ code: "dev.services-missing",
2394
+ severity: "warn",
2395
+ module: slug,
2396
+ message: `The portal gives ${slug} a database (DB_PORT=${db}), but ${slug} has no db:up script \u2014 no database will be started.`,
2397
+ why: "This repository is probably older than its template.",
2398
+ action: { command: 'add a "db:up" script (see the current app template), or set dev.services in .qore.json' }
2399
+ };
2400
+ }
2401
+ function providerDown(o) {
2402
+ const single = o.providers.length === 1 && o.keys.length === 1;
2403
+ return {
2404
+ code: "dev.provider-down",
2405
+ severity: "warn",
2406
+ module: o.slug,
2407
+ modules: o.providers,
2408
+ message: single ? `${o.providers[0]} is not running \u2014 ${o.keys[0]} points at :${o.ports.get(o.keys[0])}, where nothing listens.` : `${o.keys.join(", ")} ${plural(o.keys.length, "points", "point")} at ports nothing listens on (depends on: ${o.providers.join(", ")}).`,
2409
+ why: single ? `${o.slug} will start, but calls to ${o.providers[0]} will fail. Qore does not fall back to staging.` : `${o.slug} will start, but calls through ${o.keys.join(", ")} will fail. Qore does not fall back to staging.`,
2410
+ action: { command: o.fix }
2411
+ };
2412
+ }
2413
+
2137
2414
  // src/core/dev/project.ts
2138
2415
  import { access, readFile as readFile5 } from "fs/promises";
2139
2416
  import { join as join13 } from "path";
@@ -2252,10 +2529,10 @@ function classifyComposeVersion(r) {
2252
2529
  // src/core/diagnostics/postgres.ts
2253
2530
  var INCOMPATIBLE = /database files are incompatible with server|Counter to that, there appears to be PostgreSQL data in|in 18\+, these Docker images are configured to store database data/;
2254
2531
  function classifyComposeLogs(logs, o) {
2255
- const text = logs.join("\n");
2256
- if (!INCOMPATIBLE.test(text)) return null;
2532
+ const text2 = logs.join("\n");
2533
+ if (!INCOMPATIBLE.test(text2)) return null;
2257
2534
  const details = { reason: "pg-data-version" };
2258
- const m = text.match(/initialized by PostgreSQL version (\d+).*this version (\d+)/);
2535
+ const m = text2.match(/initialized by PostgreSQL version (\d+).*this version (\d+)/);
2259
2536
  if (m) {
2260
2537
  details.found = m[1];
2261
2538
  details.expected = m[2];
@@ -2345,6 +2622,7 @@ function nodeVersionError(found, range, slug) {
2345
2622
  // src/core/dev/preflight.ts
2346
2623
  var pmMajor = (v) => v ? v.split(".")[0] : "latest";
2347
2624
  var GRACE_MS = 1e4;
2625
+ var secs = (ms) => `${Math.round(ms / 1e3)} s`;
2348
2626
  async function runTool(deps2, cmd, args, o) {
2349
2627
  if (o.signal.aborted) throw new CancelledError();
2350
2628
  const tail = [];
@@ -2401,11 +2679,16 @@ async function checkNode(deps2, r, pkg, slug, signal) {
2401
2679
  return found;
2402
2680
  }
2403
2681
  async function installIfNeeded(deps2, r, slug, onLine, report, signal) {
2404
- report({ module: slug, step: "install", status: "start" });
2405
- if (!r.install || !await needsInstall(r.root, r.pm)) {
2406
- report({ module: slug, step: "install", status: "skip" });
2682
+ if (!r.install) {
2683
+ report({ module: slug, step: "install", status: "skip", detail: "disabled" });
2684
+ return;
2685
+ }
2686
+ if (!await needsInstall(r.root, r.pm)) {
2687
+ report({ module: slug, step: "install", status: "skip", detail: "up to date" });
2407
2688
  return;
2408
2689
  }
2690
+ report({ module: slug, step: "install", status: "start" });
2691
+ const began = deps2.now().getTime();
2409
2692
  const { cmd, args } = installArgs(r.pm);
2410
2693
  const res = await runTool(deps2, cmd, args, { cwd: r.root, env: deps2.env, onLine, signal });
2411
2694
  if (res.code !== 0) {
@@ -2414,7 +2697,7 @@ async function installIfNeeded(deps2, r, slug, onLine, report, signal) {
2414
2697
  if (err.details?.reason === "outdated-lockfile") throw err;
2415
2698
  throw new QoreError("QORE_INSTALL_FAILED", `${command} failed in ${slug}`, { hint: err.hint, details: err.details });
2416
2699
  }
2417
- report({ module: slug, step: "install", status: "done" });
2700
+ report({ module: slug, step: "install", status: "done", detail: secs(deps2.now().getTime() - began) });
2418
2701
  }
2419
2702
  async function portInUseError(deps2, o) {
2420
2703
  const owner = await deps2.ports.owner(o.port);
@@ -2477,7 +2760,12 @@ async function composeFailure(deps2, r, slug, env, signal, message, command, std
2477
2760
  });
2478
2761
  }
2479
2762
  async function startServices(deps2, r, slug, env, dbPort, waitMs, signal, onLine, report) {
2480
- report({ module: slug, step: "services", status: "start" });
2763
+ if (r.services.length === 0) {
2764
+ report({ module: slug, step: "services", status: "skip" });
2765
+ return;
2766
+ }
2767
+ report({ module: slug, step: "services", status: "start", ...dbPort !== null ? { detail: `:${dbPort}` } : {} });
2768
+ const began = deps2.now().getTime();
2481
2769
  if (r.dockerServices) {
2482
2770
  const res = await runTool(deps2, "docker", ["info", "--format", "{{.ServerVersion}}"], { cwd: r.root, env, onLine, signal });
2483
2771
  const dockerErr = classifyDockerInfo(res, deps2.platform);
@@ -2497,7 +2785,7 @@ async function startServices(deps2, r, slug, env, dbPort, waitMs, signal, onLine
2497
2785
  throw await composeFailure(deps2, r, slug, env, signal, `${script} failed in ${slug}`, `${cmd} ${args.join(" ")}`, res.tail);
2498
2786
  }
2499
2787
  }
2500
- if (dbPort !== null && r.services.length > 0) {
2788
+ if (dbPort !== null) {
2501
2789
  const ok2 = await deps2.ports.waitFor(dbPort, waitMs, signal);
2502
2790
  if (signal.aborted) throw new CancelledError();
2503
2791
  if (!ok2) {
@@ -2513,7 +2801,8 @@ async function startServices(deps2, r, slug, env, dbPort, waitMs, signal, onLine
2513
2801
  );
2514
2802
  }
2515
2803
  }
2516
- report({ module: slug, step: "services", status: "done" });
2804
+ const took = secs(deps2.now().getTime() - began);
2805
+ report({ module: slug, step: "services", status: "done", detail: dbPort !== null ? `:${dbPort} (${took})` : `(${took})` });
2517
2806
  }
2518
2807
  async function checkPort(deps2, o) {
2519
2808
  if (o.port === null) return;
@@ -2568,6 +2857,44 @@ async function leftoverOwnsPort(deps2, run4, port) {
2568
2857
  return false;
2569
2858
  }
2570
2859
 
2860
+ // src/core/dev/providers.ts
2861
+ var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
2862
+ function connPorts(env) {
2863
+ const out = [];
2864
+ for (const key of Object.keys(env).sort()) {
2865
+ const m = /^CONN_(.+)_URL$/.exec(key);
2866
+ if (!m) continue;
2867
+ let url;
2868
+ try {
2869
+ url = new URL(env[key].trim().replace(/^["']|["']$/g, ""));
2870
+ } catch {
2871
+ continue;
2872
+ }
2873
+ const port = Number(url.port);
2874
+ if (port > 0 && LOCAL_HOSTS.has(url.hostname)) out.push({ key, alias: m[1], port });
2875
+ }
2876
+ return out;
2877
+ }
2878
+ function deadAliases(env, listening) {
2879
+ return connPorts(env).filter((c) => !listening.has(c.port)).map((c) => c.key);
2880
+ }
2881
+ function hintContext(recipe, pkg, env) {
2882
+ const { cmd, args } = installArgs(recipe.pm);
2883
+ const ports = /* @__PURE__ */ new Map();
2884
+ for (const c of connPorts(env)) ports.set(c.port, { kind: "provider", key: c.key });
2885
+ const db = envPort(env, "DB_PORT");
2886
+ if (db !== null) ports.set(db, { kind: "database" });
2887
+ return {
2888
+ installCommand: [cmd, ...args].join(" "),
2889
+ runCommand: (script) => {
2890
+ if (pkg.scripts?.[script] === void 0) return null;
2891
+ const r = runArgs(recipe.pm, script);
2892
+ return [r.cmd, ...r.args].join(" ");
2893
+ },
2894
+ ports
2895
+ };
2896
+ }
2897
+
2571
2898
  // src/core/dev/runFile.ts
2572
2899
  import { unlink as unlink2 } from "fs/promises";
2573
2900
  import { join as join15 } from "path";
@@ -2621,11 +2948,11 @@ async function moduleContextFor(ctx, flags, command) {
2621
2948
  }
2622
2949
  async function appFor(ctx, mc, app) {
2623
2950
  try {
2624
- return pickApp(mc, app);
2951
+ return pickApp2(mc, app);
2625
2952
  } catch (e) {
2626
2953
  if (!(e instanceof QoreError) || e.code !== "QORE_APP_AMBIGUOUS" || !ctx.io.interactive) throw e;
2627
2954
  const id = await ctx.io.select("Select an app", mc.module.apps.map((a) => ({ name: a.name, value: a.id })));
2628
- return pickApp(mc, id);
2955
+ return pickApp2(mc, id);
2629
2956
  }
2630
2957
  }
2631
2958
  function sourceText(mc) {
@@ -2649,13 +2976,32 @@ function contextJson(mc, app) {
2649
2976
  };
2650
2977
  }
2651
2978
 
2652
- // src/cli/devRecover.ts
2653
- var DOCKER_WAIT = { timeoutMs: 12e4, pollMs: 2e3 };
2654
- var STOP_WAIT_MS = 1e4;
2655
- function canRecover(ctx) {
2979
+ // src/cli/findings.ts
2980
+ function canOffer(ctx) {
2656
2981
  const g = ctx.opts();
2657
2982
  return ctx.io.interactive && !g.json && g.input !== false;
2658
2983
  }
2984
+ function findingEvent(f) {
2985
+ const { severity, ...rest } = f;
2986
+ return { type: severity === "info" ? "info" : "warning", ...rest };
2987
+ }
2988
+ function renderFinding(ctx, f) {
2989
+ if (ctx.opts().json) {
2990
+ ctx.io.out(JSON.stringify(findingEvent(f)));
2991
+ return;
2992
+ }
2993
+ ctx.io.err(`${f.severity === "info" ? "\u2192" : "!"} ${f.message}`);
2994
+ if (f.why) ctx.io.err(` ${f.why}`);
2995
+ if (f.action) ctx.io.err(` Fix: ${f.action.command}`);
2996
+ }
2997
+ async function offerFix(ctx, question, o) {
2998
+ if (!canOffer(ctx)) return false;
2999
+ return ctx.io.confirm(question, o.signal, { default: o.default });
3000
+ }
3001
+
3002
+ // src/cli/devRecover.ts
3003
+ var DOCKER_WAIT = { timeoutMs: 12e4, pollMs: 2e3 };
3004
+ var STOP_WAIT_MS = 1e4;
2659
3005
  var daemonDown = (e) => e instanceof QoreError && e.code === "QORE_DOCKER_NOT_RUNNING" && e.details?.reason === "daemon";
2660
3006
  function dockerRecovery(ctx, signal) {
2661
3007
  let answer;
@@ -2663,8 +3009,9 @@ function dockerRecovery(ctx, signal) {
2663
3009
  try {
2664
3010
  await fn();
2665
3011
  } catch (e) {
2666
- if (!daemonDown(e) || !canRecover(ctx)) throw e;
3012
+ if (!daemonDown(e) || !canOffer(ctx)) throw e;
2667
3013
  answer ??= (async () => {
3014
+ if (!await offerFix(ctx, "Docker is not running. Wait for it to start?", { default: true, signal })) return false;
2668
3015
  ctx.io.err("Waiting for Docker\u2026 (Ctrl+C to stop)");
2669
3016
  const ok2 = await waitForDocker(ctx.deps, { ...at, signal, ...DOCKER_WAIT });
2670
3017
  if (ok2) ctx.io.err("\u2713 Docker is running");
@@ -2680,7 +3027,7 @@ async function stopLeftover(ctx, e, runs, signal) {
2680
3027
  const port = e.details?.port;
2681
3028
  const run4 = runs.find((r) => r.qorePid === e.details?.pid);
2682
3029
  if (!run4 || typeof port !== "number" || !await leftoverOwnsPort(deps2, run4, port)) return false;
2683
- if (!await io2.confirm(`Stop the previous qore dev (pid ${run4.qorePid})?`, signal)) return false;
3030
+ if (!await offerFix(ctx, `Stop the previous qore dev (pid ${run4.qorePid})?`, { default: false, signal })) return false;
2684
3031
  if (!deps2.launch.signal(run4.qorePid, "SIGTERM")) {
2685
3032
  io2.err(`! pid ${run4.qorePid} could not be signalled`);
2686
3033
  return false;
@@ -2693,7 +3040,7 @@ async function checkPortOrStopLeftover(ctx, o, signal) {
2693
3040
  try {
2694
3041
  await checkPort(ctx.deps, o);
2695
3042
  } catch (e) {
2696
- if (!(e instanceof QoreError) || e.code !== "QORE_PORT_IN_USE" || e.details?.owner !== "qore" || !canRecover(ctx)) throw e;
3043
+ if (!(e instanceof QoreError) || e.code !== "QORE_PORT_IN_USE" || e.details?.owner !== "qore" || !canOffer(ctx)) throw e;
2697
3044
  const freed = await stopLeftover(ctx, e, o.runs, signal);
2698
3045
  if (signal.aborted) throw new CancelledError();
2699
3046
  if (!freed) throw e;
@@ -2701,6 +3048,42 @@ async function checkPortOrStopLeftover(ctx, o, signal) {
2701
3048
  }
2702
3049
  }
2703
3050
 
3051
+ // src/cli/progress.ts
3052
+ function text(e, o) {
3053
+ switch (`${e.step}:${e.status}`) {
3054
+ case "pm:done":
3055
+ return `\u2713 ${e.detail}`;
3056
+ case "install:skip":
3057
+ return e.detail === "up to date" ? "\u2713 dependencies up to date" : `- install skipped (${o.installFlag ? "dev.install is false in .qore.json" : "--no-install"})`;
3058
+ case "install:start":
3059
+ return "\u22EF installing dependencies\u2026";
3060
+ case "install:done":
3061
+ return `\u2713 dependencies installed (${e.detail})`;
3062
+ case "env:done":
3063
+ return `\u2713 env ${e.detail}`;
3064
+ case "services:skip":
3065
+ return o.servicesFlag ? null : "- services skipped (--no-services)";
3066
+ case "services:start":
3067
+ return e.detail ? `\u22EF database waiting for ${e.detail}\u2026` : "\u22EF starting services\u2026";
3068
+ case "services:done":
3069
+ return e.detail?.startsWith(":") ? `\u2713 database ready on ${e.detail}` : `\u2713 services up ${e.detail}`;
3070
+ case "ports:done":
3071
+ return `\u2713 port ${e.detail} free`;
3072
+ default:
3073
+ return null;
3074
+ }
3075
+ }
3076
+ function progressReporter(ctx, o) {
3077
+ return (e) => {
3078
+ if (ctx.opts().json) {
3079
+ ctx.io.out(JSON.stringify({ type: "step", ...e }));
3080
+ return;
3081
+ }
3082
+ const line2 = text(e, o);
3083
+ if (line2 !== null) ctx.io.err(o.prefix ? o.prefix(e.module, line2) : line2);
3084
+ };
3085
+ }
3086
+
2704
3087
  // src/cli/termination.ts
2705
3088
  var preflightHook = {};
2706
3089
  function preflightTermination(abort, raise) {
@@ -2764,7 +3147,7 @@ async function ensureTrusted(ctx, mc, app, o) {
2764
3147
  // src/cli/commands/devWorkspace.ts
2765
3148
  import { randomBytes as randomBytes4 } from "crypto";
2766
3149
  import { realpath as realpath4 } from "fs/promises";
2767
- import { join as join16, relative as relative5 } from "path";
3150
+ import { join as join17, relative as relative5 } from "path";
2768
3151
 
2769
3152
  // src/core/dev/order.ts
2770
3153
  function selectSkips(modules, sel) {
@@ -2869,8 +3252,8 @@ function planDev(modules, sel) {
2869
3252
  }
2870
3253
  const order = sccs.map((_, i) => i).sort((a, b) => minSlug[a].localeCompare(minSlug[b]));
2871
3254
  for (const i of order) visit(i);
2872
- const start = [];
2873
- for (const i of orderedSccIndices) start.push(...sccSlugs[i]);
3255
+ const start2 = [];
3256
+ for (const i of orderedSccIndices) start2.push(...sccSlugs[i]);
2874
3257
  const waitFor = {};
2875
3258
  for (const m of starting) {
2876
3259
  const mySccId = sccOf.get(m.id);
@@ -2885,7 +3268,7 @@ function planDev(modules, sel) {
2885
3268
  missingProviders[m.slug] = [...new Set(missing)].sort();
2886
3269
  }
2887
3270
  }
2888
- return { start, waitFor, cycles, skipped, missingProviders };
3271
+ return { start: start2, waitFor, cycles, skipped, missingProviders };
2889
3272
  }
2890
3273
 
2891
3274
  // src/core/dev/supervisor.ts
@@ -2986,14 +3369,14 @@ function supervise(o) {
2986
3369
  const early = !!e.spawnError || now() - startedAt < EARLY_MS;
2987
3370
  const rawCode = e.spawnError ? null : e.code;
2988
3371
  const rawSignal = e.spawnError ? null : e.signal;
2989
- const hint = early && rawSignal === null && rawCode !== null && rawCode !== 0 ? earlyExitHint(tails.get(m.slug) ?? [], { installCommand: m.installCmd }) ?? void 0 : void 0;
3372
+ const hint = stopCode === null && rawSignal === null && rawCode !== null && rawCode !== 0 ? exitHint(tails.get(m.slug) ?? [], m.hint) ?? void 0 : void 0;
2990
3373
  emit({ type: "exit", module: m.slug, code: rawCode, signal: rawSignal, early, hint });
2991
3374
  if (code !== 0 && firstFailure === null) firstFailure = code;
2992
3375
  if (o.killOthers) beginStop("kill-others", code);
2993
3376
  checkDone();
2994
3377
  });
2995
3378
  };
2996
- const start = async (m) => {
3379
+ const start2 = async (m) => {
2997
3380
  const providers = m.waitFor.filter((p) => readiness.has(p));
2998
3381
  const results = await Promise.all(providers.map((p) => readiness.get(p).promise));
2999
3382
  if (stopCode !== null) return;
@@ -3003,7 +3386,7 @@ function supervise(o) {
3003
3386
  });
3004
3387
  launch(m);
3005
3388
  };
3006
- for (const m of o.modules) void start(m);
3389
+ for (const m of o.modules) void start2(m);
3007
3390
  checkDone();
3008
3391
  return {
3009
3392
  done,
@@ -3019,25 +3402,26 @@ function supervise(o) {
3019
3402
  };
3020
3403
  }
3021
3404
 
3022
- // src/core/workspace/access.ts
3023
- function devTeamsOf(am, m) {
3024
- const own = m.devTeamIds?.length ? am.devTeams.filter((t) => m.devTeamIds.includes(t.id)) : [];
3025
- return own.length ? own : am.devTeams;
3026
- }
3027
- function askMaintainer(am, m) {
3028
- const teams = devTeamsOf(am, m);
3029
- const names = teams.map((t) => t.name);
3030
- const maintainers = [...new Set(teams.flatMap((t) => t.maintainers))];
3031
- const who = names.length ? names.join(", ") : "the app's dev team";
3032
- return { teams: names, maintainers, text: `ask a maintainer of ${who}${maintainers.length ? ` (${maintainers.join(", ")})` : ""}` };
3033
- }
3034
- function notClonedReason(m, am) {
3035
- if (am && !m.githubLinked) return `GitHub account not linked (${am.accountUrl})`;
3036
- if (am && m.repoAccess === "none") {
3037
- const names = devTeamsOf(am, m).map((t) => t.name);
3038
- return names.length ? `no repository access (dev team ${names.join(", ")})` : "no repository access (ask your tenant owner)";
3405
+ // src/core/workspace/stray.ts
3406
+ import { lstat as lstat2, readdir } from "fs/promises";
3407
+ import { join as join16 } from "path";
3408
+ async function strayRepos(root, slugs) {
3409
+ let entries;
3410
+ try {
3411
+ entries = (await readdir(root, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
3412
+ } catch {
3413
+ return [];
3039
3414
  }
3040
- return "run qore sync";
3415
+ const names = [];
3416
+ for (const name of entries) {
3417
+ if (slugs.has(name)) continue;
3418
+ try {
3419
+ await lstat2(join16(root, name, ".git"));
3420
+ names.push(name);
3421
+ } catch {
3422
+ }
3423
+ }
3424
+ return names.sort();
3041
3425
  }
3042
3426
 
3043
3427
  // src/cli/devOutput.ts
@@ -3135,18 +3519,6 @@ async function workspaceContext(ctx, ws) {
3135
3519
  return { am: { app: last.app, accountUrl: "", devTeams: [], modules }, online: null, offlineCause: e };
3136
3520
  }
3137
3521
  }
3138
- function deadAliases(env, ports) {
3139
- return Object.keys(env).filter((k) => {
3140
- if (!/^CONN_(.+)_URL$/.test(k)) return false;
3141
- let port;
3142
- try {
3143
- port = Number(new URL(env[k].trim().replace(/^["']|["']$/g, "")).port);
3144
- } catch {
3145
- return false;
3146
- }
3147
- return port > 0 && !ports.has(port);
3148
- }).sort();
3149
- }
3150
3522
  async function pool(items, n, fn) {
3151
3523
  let next = 0;
3152
3524
  const results = await Promise.allSettled(Array.from({ length: Math.min(n, items.length) }, async () => {
@@ -3157,7 +3529,7 @@ async function pool(items, n, fn) {
3157
3529
  }
3158
3530
  async function workspaceDev(ctx, ws, f, waitTimeout) {
3159
3531
  const ready = await inPreflight(ctx.deps.signals, ctx.signal, (p) => prepareWorkspace(ctx, ws, f, waitTimeout, p));
3160
- await superviseAll(ctx, ws, ready.units, ready.waitFor, ready.o);
3532
+ await superviseAll(ctx, ws, ready.units, ready.waitFor, ready.o, ready.notRunning);
3161
3533
  }
3162
3534
  async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
3163
3535
  const { deps: deps2, io: io2 } = ctx;
@@ -3202,22 +3574,48 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
3202
3574
  continue;
3203
3575
  }
3204
3576
  try {
3205
- dirs[m.slug] = await deps2.git.inspect(join16(ws.root, m.slug));
3577
+ dirs[m.slug] = await deps2.git.inspect(join17(ws.root, m.slug));
3206
3578
  safe.push(m);
3207
3579
  } catch (e) {
3208
3580
  if (!(e instanceof QoreError) || e.code === "QORE_GIT_UNAVAILABLE") throw e;
3209
3581
  problems.push(withDetails(e, { module: m.slug }));
3210
3582
  }
3211
3583
  }
3212
- const present = new Map(planWorkspace(ws.root, safe, dirs, []).actions.filter((a) => a.kind === "present").map((a) => [a.module.slug, a.dir]));
3213
3584
  const bySlug = new Map(safe.map((m) => [m.slug, m]));
3214
- const plan = planDev(safe.map((m) => ({
3215
- id: m.id,
3216
- slug: m.slug,
3217
- present: present.has(m.slug),
3218
- canWrite: m.access === "write" || m.access === "maintain",
3219
- dependsOn: m.dependsOn
3220
- })), { only: slugList(f.only), skip: slugList(f.skip) });
3585
+ const computePlan = () => {
3586
+ const present2 = new Map(planWorkspace(ws.root, safe, dirs, []).actions.filter((a) => a.kind === "present").map((a) => [a.module.slug, a.dir]));
3587
+ const plan2 = planDev(safe.map((m) => ({
3588
+ id: m.id,
3589
+ slug: m.slug,
3590
+ present: present2.has(m.slug),
3591
+ canWrite: m.access === "write" || m.access === "maintain",
3592
+ dependsOn: m.dependsOn
3593
+ })), { only: slugList(f.only), skip: slugList(f.skip) });
3594
+ return { present: present2, plan: plan2 };
3595
+ };
3596
+ let { present, plan } = computePlan();
3597
+ const missingFindings = () => {
3598
+ const missing2 = plan.skipped.filter((s) => s.reason === "not-cloned").map((s) => bySlug.get(s.slug));
3599
+ const consumers = {};
3600
+ for (const [consumer, providers] of Object.entries(plan.missingProviders)) {
3601
+ for (const p2 of providers) (consumers[p2] ??= []).push(consumer);
3602
+ }
3603
+ const actions = planWorkspace(ws.root, missing2, dirs, []).actions;
3604
+ return modulesMissing({ missing: missing2, actions, cwd: deps2.cwd, consumers, am: online ? am : null });
3605
+ };
3606
+ const missing = missingFindings();
3607
+ for (const finding of missing.findings) renderFinding(ctx, finding);
3608
+ const canSync = online !== null && missing.findings.some((x) => x.action?.fix === "sync");
3609
+ if (canSync && await offerFix(ctx, `Clone ${missing.fixable.map((m) => m.slug).join(", ")} now with qore sync?`, { default: true, signal: ctx.signal })) {
3610
+ const actions = planWorkspace(ws.root, missing.fixable, dirs, []).actions;
3611
+ const results = await applyActions(deps2, { portal: online.portal, app: am, actions, protocol: cloneProtocol(dirs), onResult: () => {
3612
+ } });
3613
+ renderResults(io2, results, false);
3614
+ for (const r of results) if (r.status === "cloned") dirs[r.slug] = await deps2.git.inspect(join17(ws.root, r.slug));
3615
+ ({ present, plan } = computePlan());
3616
+ }
3617
+ for (const name of await strayRepos(ws.root, new Set(am.modules.map((m) => m.slug)))) renderFinding(ctx, strayClone(name, am.app.name));
3618
+ if (ctx.signal.aborted) throw new CancelledError();
3221
3619
  for (const s of plan.skipped) {
3222
3620
  const m = bySlug.get(s.slug);
3223
3621
  if (s.reason === "no-write") {
@@ -3226,8 +3624,10 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
3226
3624
  `- ${s.slug} no write access \u2014 local secrets need write; ${ask.text}`,
3227
3625
  { type: "skip", module: s.slug, reason: s.reason, devTeams: ask.teams, maintainers: ask.maintainers }
3228
3626
  );
3627
+ } else if (s.reason === "not-cloned") {
3628
+ if (json2) io2.out(JSON.stringify({ type: "skip", module: s.slug, reason: s.reason }));
3229
3629
  } else {
3230
- note(`- ${s.slug} ${s.reason === "not-cloned" ? `not cloned \u2014 ${notClonedReason(m, online ? am : void 0)}` : "excluded"}`, { type: "skip", module: s.slug, reason: s.reason });
3630
+ note(`- ${s.slug} excluded`, { type: "skip", module: s.slug, reason: s.reason });
3231
3631
  }
3232
3632
  }
3233
3633
  for (const c of plan.cycles) warn(`${c.join(", ")} depend on each other \u2014 they start together, without waiting`, { modules: c });
@@ -3262,8 +3662,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
3262
3662
  else if (l.stream === "stdout") io2.out(prefix(slug, l.line));
3263
3663
  else io2.err(prefix(slug, l.line));
3264
3664
  };
3265
- const report = () => {
3266
- };
3665
+ const report = progressReporter(ctx, { prefix: json2 ? void 0 : prefix, installFlag: f.install, servicesFlag: f.services });
3267
3666
  for (const u of units) {
3268
3667
  await attempt(u.m.slug, async () => {
3269
3668
  u.cfg = await readRepoConfig(u.dir);
@@ -3285,13 +3684,27 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
3285
3684
  const withRecipe = units.filter((u) => u.recipe && u.trusted);
3286
3685
  for (const name of [...new Set(withRecipe.map((u) => u.recipe.pm.name))]) {
3287
3686
  const users = withRecipe.filter((u) => u.recipe.pm.name === name);
3288
- const pmOk = await attempt(users[0].m.slug, () => checkPackageManager(deps2, users[0].recipe, () => {
3289
- }, p.signal));
3290
- for (const u of users) u.pmOk = pmOk;
3687
+ let version = null;
3688
+ const pmOk = await attempt(users[0].m.slug, async () => {
3689
+ version = await checkPackageManager(deps2, users[0].recipe, () => {
3690
+ }, p.signal);
3691
+ });
3692
+ for (const u of users) {
3693
+ u.pmOk = pmOk;
3694
+ u.pmVersion = version;
3695
+ }
3291
3696
  }
3292
3697
  for (const u of withRecipe) {
3293
- const nodeOk = await attempt(u.m.slug, () => checkNode(deps2, u.recipe, u.pkg, u.m.slug, p.signal));
3698
+ const nodeOk = await attempt(u.m.slug, async () => {
3699
+ u.node = await checkNode(deps2, u.recipe, u.pkg, u.m.slug, p.signal);
3700
+ });
3294
3701
  u.pmOk = u.pmOk && nodeOk;
3702
+ if (u.pmOk) report({
3703
+ module: u.m.slug,
3704
+ step: "pm",
3705
+ status: "done",
3706
+ detail: `${u.recipe.pm.name}${u.pmVersion ? ` ${u.pmVersion}` : ""}${u.node ? ` \xB7 node ${u.node}` : ""}`
3707
+ });
3295
3708
  }
3296
3709
  await pool(units.filter((u) => u.pmOk && u.trusted), INSTALL_PARALLELISM, async (u) => {
3297
3710
  u.installed = await attempt(u.m.slug, () => installIfNeeded(deps2, u.recipe, u.m.slug, onLine(u.m.slug), report, p.signal));
@@ -3337,11 +3750,21 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
3337
3750
  { module: slug, keys: dev.kept }
3338
3751
  );
3339
3752
  }
3753
+ report({ module: slug, step: "env", status: "done", detail: relative5(rootReal, dev.envFile) });
3754
+ const services = servicesMissing(slug, dev.env, u.recipe, { servicesFlag: f.services, configured: u.cfg.dev?.services !== void 0 });
3755
+ if (services) renderFinding(ctx, services);
3756
+ try {
3757
+ const behind = repoBehind(slug, await deps2.git.status(u.dir), { workspace: true });
3758
+ if (behind) renderFinding(ctx, behind);
3759
+ } catch {
3760
+ }
3340
3761
  if (u.installed) {
3341
3762
  const env = { ...deps2.env, ...dev.env };
3342
3763
  await attempt(slug, () => withDocker({ cwd: u.dir, env }, () => startServices(deps2, u.recipe, slug, env, dev.dbPort, waitTimeout * 1e3, p.signal, onLine(slug), report)));
3343
3764
  }
3344
- await attempt(slug, () => checkPortOrStopLeftover(ctx, { slug, port: dev.port, key: "PORT", runs }, p.signal));
3765
+ if (await attempt(slug, () => checkPortOrStopLeftover(ctx, { slug, port: dev.port, key: "PORT", runs }, p.signal)) && dev.port !== null) {
3766
+ report({ module: slug, step: "ports", status: "done", detail: String(dev.port) });
3767
+ }
3345
3768
  }
3346
3769
  if (p.signal.aborted) throw new CancelledError();
3347
3770
  const byPort = /* @__PURE__ */ new Map();
@@ -3369,7 +3792,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
3369
3792
  if (online && units.every((u) => !u.dev.offline)) {
3370
3793
  const envFile = (m) => {
3371
3794
  const u = units.find((x) => x.m.id === m.id);
3372
- return u ? relative5(rootReal, u.dev.envFile) : join16(m.slug, moduleDefaults(m.type).envFile);
3795
+ return u ? relative5(rootReal, u.dev.envFile) : join17(m.slug, moduleDefaults(m.type).envFile);
3373
3796
  };
3374
3797
  await saveLastStart(deps2, ws.root, {
3375
3798
  schemaVersion: 1,
@@ -3390,9 +3813,14 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
3390
3813
  });
3391
3814
  }
3392
3815
  if (p.signal.aborted) throw new CancelledError();
3393
- return { units, waitFor: plan.waitFor, o: { waitTimeout, killOthers: Boolean(f.killOthers), json: json2, color, prefix } };
3816
+ const { reasons } = missingFindings();
3817
+ const notRunning = plan.skipped.filter((s) => s.reason !== "excluded").map((s) => {
3818
+ const m = bySlug.get(s.slug);
3819
+ return { slug: s.slug, reason: s.reason === "no-write" ? `no write access \u2014 ${askMaintainer(am, m).text}` : `not cloned \u2014 ${reasons[s.slug] ?? "Fix: qore sync"}` };
3820
+ });
3821
+ return { units, waitFor: plan.waitFor, notRunning, o: { waitTimeout, killOthers: Boolean(f.killOthers), json: json2, color, prefix } };
3394
3822
  }
3395
- async function superviseAll(ctx, ws, units, waitFor, o) {
3823
+ async function superviseAll(ctx, ws, units, waitFor, o, notRunning) {
3396
3824
  const { deps: deps2, io: io2 } = ctx;
3397
3825
  const startedAt = deps2.now().toISOString();
3398
3826
  const began = /* @__PURE__ */ new Map();
@@ -3414,9 +3842,9 @@ async function superviseAll(ctx, ws, units, waitFor, o) {
3414
3842
  case "line":
3415
3843
  return e.stream === "stdout" ? io2.out(o.prefix(e.module, e.line)) : io2.err(o.prefix(e.module, e.line));
3416
3844
  case "start":
3417
- return io2.err(`\u25B6 ${e.module}${e.pid === null ? "" : ` (pid ${e.pid})`}`);
3845
+ return io2.err(`\u25B6 starting ${e.module}${e.pid === null ? "" : ` (pid ${e.pid})`}`);
3418
3846
  case "ready":
3419
- return io2.err(`\u2713 ${e.module} is listening on ${e.port}`);
3847
+ return io2.err(`\u2713 ${e.module} ready`);
3420
3848
  case "provider-missing":
3421
3849
  return io2.err(`! ${e.provider} did not come up within ${o.waitTimeout} s (or exited); starting ${e.module} anyway \u2014 see its output above`);
3422
3850
  case "stopping":
@@ -3428,23 +3856,45 @@ async function superviseAll(ctx, ws, units, waitFor, o) {
3428
3856
  const how = e.signal ? `was ended by ${e.signal}` : `exited with code ${e.code}`;
3429
3857
  io2.err(`${e.code === 0 ? "\u25A0" : "\u2717"} ${e.module} ${how}${after}`);
3430
3858
  if (e.hint) io2.err(` \u2192 ${e.hint.hint}`);
3859
+ if (e.code !== 0 && !o.killOthers && (ref.sup?.children().length ?? 0) > 0) {
3860
+ io2.err(` Restart it on its own: qore dev --only ${e.module} (in another terminal; the others keep running)`);
3861
+ }
3431
3862
  return;
3432
3863
  }
3433
3864
  }
3434
3865
  };
3866
+ const withPort = new Set(units.filter((u) => u.dev.port !== null).map((u) => u.m.slug));
3867
+ const readyPorts = /* @__PURE__ */ new Map();
3868
+ let summarized = false;
3869
+ const summarize = () => {
3870
+ if (summarized || o.json || readyPorts.size < withPort.size) return;
3871
+ summarized = true;
3872
+ const width = Math.max(0, ...[...readyPorts.keys(), ...notRunning.map((n) => n.slug)].map((s2) => s2.length));
3873
+ io2.err(notRunning.length === 0 ? "\u2714 All modules running:" : "\u2714 Running:");
3874
+ for (const slug of [...readyPorts.keys()].sort()) io2.err(` ${slug.padEnd(width)} http://localhost:${readyPorts.get(slug)}`);
3875
+ if (notRunning.length > 0) {
3876
+ io2.err("! Not running:");
3877
+ for (const n of notRunning) io2.err(` ${n.slug.padEnd(width)} ${n.reason}`);
3878
+ }
3879
+ };
3880
+ const crashed = /* @__PURE__ */ new Map();
3435
3881
  const onEvent = (e) => {
3436
3882
  if (e.type === "start") {
3437
3883
  began.set(e.module, deps2.now().getTime());
3438
3884
  writeRun();
3439
3885
  }
3440
3886
  if (e.type === "stopping") stopping = true;
3887
+ if (e.type === "ready") readyPorts.set(e.module, e.port);
3888
+ if (e.type === "exit" && !stopping && (e.code !== 0 || e.signal !== null)) {
3889
+ crashed.set(e.module, { code: e.code, signal: e.signal, ms: deps2.now().getTime() - (began.get(e.module) ?? deps2.now().getTime()) });
3890
+ }
3441
3891
  if (o.json) io2.out(JSON.stringify(e));
3442
3892
  else render(e);
3893
+ if (e.type === "ready") summarize();
3443
3894
  };
3444
3895
  const s = ref.sup = supervise({
3445
3896
  modules: units.map((u) => {
3446
3897
  const { cmd, args } = runArgs(u.recipe.pm, u.recipe.script);
3447
- const { cmd: installCmd, args: installArgv } = installArgs(u.recipe.pm);
3448
3898
  return {
3449
3899
  slug: u.m.slug,
3450
3900
  cmd,
@@ -3452,7 +3902,7 @@ async function superviseAll(ctx, ws, units, waitFor, o) {
3452
3902
  cwd: u.dir,
3453
3903
  port: u.dev.port,
3454
3904
  waitFor: waitFor[u.m.slug] ?? [],
3455
- installCmd: [installCmd, ...installArgv].join(" "),
3905
+ hint: hintContext(u.recipe, u.pkg, u.dev.env),
3456
3906
  env: { ...deps2.env, ...u.dev.env, ...o.color ? { FORCE_COLOR: "1" } : {} }
3457
3907
  };
3458
3908
  }),
@@ -3474,6 +3924,11 @@ async function superviseAll(ctx, ws, units, waitFor, o) {
3474
3924
  if (ctx.signal.aborted) onAbort();
3475
3925
  writeRun();
3476
3926
  ctx.setExitCode(await s.done);
3927
+ if (!o.json && crashed.size > 0) {
3928
+ const stopped = units.map((u) => u.m.slug).filter((s2) => !crashed.has(s2)).sort();
3929
+ const what = [...crashed].map(([s2, c]) => `${s2} (${c.code === null && c.signal === null ? "could not be started" : `${c.signal ?? `code ${c.code}`}, after ${humanAge(Math.round(c.ms / 1e3))}`})`).join(", ");
3930
+ io2.err(`${stopped.length ? `\u25A0 stopped: ${stopped.join(", ")} ` : ""}\u2717 crashed: ${what}`);
3931
+ }
3477
3932
  } finally {
3478
3933
  deps2.signals.off("SIGTERM", onTerm);
3479
3934
  deps2.signals.off("SIGHUP", onHup);
@@ -3543,6 +3998,48 @@ function parseWaitTimeout(raw) {
3543
3998
  return n;
3544
3999
  }
3545
4000
  var str2 = (v) => typeof v === "string" && v.length > 0;
4001
+ var SHELL_SAFE = /^[A-Za-z0-9_./,:@%+=-]*$/;
4002
+ var shellQuote = (s) => SHELL_SAFE.test(s) ? s : `'${s.replace(/'/g, "'\\''")}'`;
4003
+ function cdAndDev(home, dir2) {
4004
+ if (/^[A-Za-z]:[\\/]|^\\\\/.test(dir2)) return `cd ${/\s/.test(dir2) ? `"${dir2}"` : dir2} && qore dev`;
4005
+ const h = home.length > 1 ? home.replace(/\/+$/, "") : home;
4006
+ let target = shellQuote(dir2);
4007
+ if (h && dir2 === h) target = "~";
4008
+ else if (h && dir2.startsWith(h + "/")) target = `~/${shellQuote(dir2.slice(h.length + 1))}`;
4009
+ return `cd ${target} && qore dev`;
4010
+ }
4011
+ async function providerFindings(ctx, root, mc, app, env) {
4012
+ const { deps: deps2 } = ctx;
4013
+ try {
4014
+ if (!(await metaFor(deps2, mc.portal.apiOrigin)).capabilities.includes("app.modules")) return null;
4015
+ const am = await mc.session.api.get(`/api/cli/v1/apps/${encodeURIComponent(app.id)}/modules`);
4016
+ const self = am.modules.find((m) => m.id === mc.module.id);
4017
+ const providers = am.modules.filter((m) => self?.dependsOn.includes(m.id));
4018
+ if (providers.length === 0) return null;
4019
+ const conns = connPorts(env);
4020
+ const listening = /* @__PURE__ */ new Set();
4021
+ for (const c of conns) if (await deps2.ports.inUse(c.port)) listening.add(c.port);
4022
+ const keys = deadAliases(env, listening);
4023
+ if (keys.length === 0) return null;
4024
+ const ws = await findWorkspace(root, deps2.home);
4025
+ let fix = `qore clone ${app.slug}`;
4026
+ if (ws && ws.appId === app.id) fix = cdAndDev(deps2.home, ws.root);
4027
+ else if (providers.length === 1) {
4028
+ const sibling = join18(dirname6(root), providers[0].slug);
4029
+ if ((await deps2.git.inspect(sibling)).isRepo) fix = cdAndDev(deps2.home, sibling);
4030
+ }
4031
+ return providerDown({
4032
+ slug: mc.module.slug,
4033
+ providers: providers.map((p) => p.slug),
4034
+ keys,
4035
+ ports: new Map(conns.map((c) => [c.key, c.port])),
4036
+ fix
4037
+ });
4038
+ } catch (e) {
4039
+ if (e instanceof QoreError) return null;
4040
+ throw e;
4041
+ }
4042
+ }
3546
4043
  function usableLast2(last) {
3547
4044
  if (!last || last.kind !== "module") return false;
3548
4045
  const m = last.module;
@@ -3567,8 +4064,9 @@ async function devContext(ctx, f, root) {
3567
4064
  }
3568
4065
  async function moduleDev(ctx, f, waitTimeout) {
3569
4066
  const { deps: deps2, io: io2 } = ctx;
3570
- const { root, slug, recipe, env, port } = await inPreflight(deps2.signals, ctx.signal, (p) => prepareModule(ctx, f, waitTimeout, p));
4067
+ const { root, slug, recipe, env, port, pkg, devEnv: devEnv2 } = await inPreflight(deps2.signals, ctx.signal, (p) => prepareModule(ctx, f, waitTimeout, p));
3571
4068
  const { cmd, args } = runArgs(recipe.pm, recipe.script);
4069
+ io2.err(`\u25B6 starting ${slug}${port === null ? "" : ` \u2014 http://localhost:${port}`}`);
3572
4070
  const startedAt = deps2.now();
3573
4071
  await writeRunFile(deps2, root, {
3574
4072
  schemaVersion: 1,
@@ -3589,10 +4087,9 @@ async function moduleDev(ctx, f, waitTimeout) {
3589
4087
  await removeRunFile(deps2, root);
3590
4088
  }
3591
4089
  const ms = deps2.now().getTime() - startedAt.getTime();
3592
- if (exit.code !== 0 && exit.signal === null && ms < EARLY_EXIT_MS) {
3593
- io2.err(`\u2717 ${slug} exited after ${Math.round(ms / 1e3)} s with code ${exit.code ?? 1} \u2014 its output above says why.`);
3594
- const { cmd: installCmd, args: installArgv } = installArgs(recipe.pm);
3595
- const hint = earlyExitHint(tee.lines(), { installCommand: [installCmd, ...installArgv].join(" ") });
4090
+ if (exit.code !== 0 && exit.signal === null) {
4091
+ io2.err(ms < EARLY_EXIT_MS ? `\u2717 ${slug} exited after ${Math.round(ms / 1e3)} s with code ${exit.code ?? 1} \u2014 its output above says why.` : `\u2717 ${slug} exited with code ${exit.code ?? 1}.`);
4092
+ const hint = exitHint(tee.lines(), hintContext(recipe, pkg, devEnv2));
3596
4093
  if (hint) io2.err(` \u2192 ${hint.hint}`);
3597
4094
  }
3598
4095
  if (exit.signal) {
@@ -3637,11 +4134,16 @@ async function prepareModule(ctx, f, waitTimeout, p) {
3637
4134
  });
3638
4135
  const runs = [await leftoverRun(deps2, root)].filter((r) => r !== null);
3639
4136
  const onLine = (l) => l.stream === "stdout" ? io2.out(l.line) : io2.err(l.line);
3640
- const report = () => {
3641
- };
3642
- await checkPackageManager(deps2, recipe, () => {
4137
+ const report = progressReporter(ctx, { installFlag: f.install, servicesFlag: f.services });
4138
+ const pmVersion = await checkPackageManager(deps2, recipe, () => {
3643
4139
  }, p.signal);
3644
- await checkNode(deps2, recipe, pkg, slug, p.signal);
4140
+ const nodeVersion = await checkNode(deps2, recipe, pkg, slug, p.signal);
4141
+ report({
4142
+ module: slug,
4143
+ step: "pm",
4144
+ status: "done",
4145
+ detail: `${recipe.pm.name}${pmVersion ? ` ${pmVersion}` : ""}${nodeVersion ? ` \xB7 node ${nodeVersion}` : ""}`
4146
+ });
3645
4147
  await installIfNeeded(deps2, recipe, slug, onLine, report, p.signal);
3646
4148
  let dev;
3647
4149
  try {
@@ -3663,9 +4165,22 @@ async function prepareModule(ctx, f, waitTimeout, p) {
3663
4165
  if (dev.kept.length) {
3664
4166
  io2.err(`! ${dev.kept.length} key(s) differ locally and were kept: ${dev.kept.join(", ")} \u2014 qore env pull --use-portal takes the portal's values`);
3665
4167
  }
4168
+ report({ module: slug, step: "env", status: "done", detail: dev.offline ? `${envFile} (as last written \u2014 offline)` : envFile });
3666
4169
  const env = { ...deps2.env, ...dev.env };
3667
4170
  await dockerRecovery(ctx, p.signal)({ cwd: root, env }, () => startServices(deps2, recipe, slug, env, dev.dbPort, waitTimeout * 1e3, p.signal, onLine, report));
3668
4171
  await checkPortOrStopLeftover(ctx, { slug, port: dev.port, key: "PORT", runs }, p.signal);
4172
+ if (dev.port !== null) report({ module: slug, step: "ports", status: "done", detail: String(dev.port) });
4173
+ if (c.online && !dev.offline) {
4174
+ const provider = await providerFindings(ctx, root, c.online.mc, c.online.app, dev.env);
4175
+ if (provider) renderFinding(ctx, provider);
4176
+ }
4177
+ const services = servicesMissing(slug, dev.env, recipe, { servicesFlag: f.services, configured: cfg.dev?.services !== void 0 });
4178
+ if (services) renderFinding(ctx, services);
4179
+ try {
4180
+ const behind = repoBehind(slug, await deps2.git.status(root), { workspace: false });
4181
+ if (behind) renderFinding(ctx, behind);
4182
+ } catch {
4183
+ }
3669
4184
  if (c.online && !dev.offline) {
3670
4185
  const { mc, app } = c.online;
3671
4186
  const { id, name, type, access: access2, tenant, apps } = mc.module;
@@ -3688,7 +4203,15 @@ async function prepareModule(ctx, f, waitTimeout, p) {
3688
4203
  });
3689
4204
  }
3690
4205
  if (p.signal.aborted) throw new CancelledError();
3691
- return { root, slug, recipe, env, port: dev.port };
4206
+ return { root, slug, recipe, env, port: dev.port, pkg, devEnv: dev.env };
4207
+ }
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
+ });
3692
4215
  }
3693
4216
 
3694
4217
  // src/cli/commands/doctor.ts
@@ -3696,14 +4219,14 @@ import pc3 from "picocolors";
3696
4219
 
3697
4220
  // src/core/doctor/checks.ts
3698
4221
  import { realpath as realpath6 } from "fs/promises";
3699
- import { basename as basename3, join as join17 } from "path";
4222
+ import { basename as basename3, join as join19 } from "path";
3700
4223
 
3701
4224
  // src/core/diagnostics/ssh.ts
3702
4225
  var SSH_TEST_ARGS = ["-T", "-o", "BatchMode=yes", "-o", "ConnectTimeout=5", "git@github.com"];
3703
4226
  var COMMAND = "ssh -T git@github.com";
3704
4227
  function classifySshTest(r) {
3705
- const text = r.tail.join("\n");
3706
- const hi = /Hi ([^!\s]+)! You've successfully authenticated/.exec(text);
4228
+ const text2 = r.tail.join("\n");
4229
+ const hi = /Hi ([^!\s]+)! You've successfully authenticated/.exec(text2);
3707
4230
  if (hi) return { ok: true, user: hi[1] };
3708
4231
  const details = { protocol: "ssh" };
3709
4232
  if (r.spawnError) {
@@ -3712,13 +4235,13 @@ function classifySshTest(r) {
3712
4235
  details: { ...details, reason: "ssh-missing" }
3713
4236
  }) };
3714
4237
  }
3715
- if (/Permission denied \(publickey\)/.test(text)) {
4238
+ if (/Permission denied \(publickey\)/.test(text2)) {
3716
4239
  return { ok: false, error: new QoreError("QORE_GIT_AUTH", "GitHub refused your SSH key", {
3717
4240
  hint: "Add your key to GitHub (https://github.com/settings/keys) and your ssh agent, or keep cloning over https.",
3718
4241
  details
3719
4242
  }) };
3720
4243
  }
3721
- if (/Host key verification failed/.test(text)) {
4244
+ if (/Host key verification failed/.test(text2)) {
3722
4245
  return { ok: false, error: new QoreError("QORE_GIT_AUTH", "ssh could not verify github.com's host key", {
3723
4246
  hint: `Run \`${COMMAND}\` once to accept its host key (or to see why it changed).`,
3724
4247
  details: { ...details, reason: "host-key" }
@@ -3893,9 +4416,9 @@ async function moduleBody(deps2, m, runs, signal) {
3893
4416
  let env = null;
3894
4417
  await part(async () => {
3895
4418
  const target = await resolveTarget(m.root, cfg.envFile ?? moduleDefaults(m.type).envFile);
3896
- const text = await readOrNull(target);
3897
- if (text === null) return { status: "warn", label: "port unknown (run qore env pull)" };
3898
- env = Object.fromEntries(parseEnv(text));
4419
+ const text2 = await readOrNull(target);
4420
+ if (text2 === null) return { status: "warn", label: "port unknown (run qore env pull)" };
4421
+ env = Object.fromEntries(parseEnv(text2));
3899
4422
  const port = envPort(env, "PORT");
3900
4423
  if (port === null) return { status: "warn", label: `port unknown (no PORT in ${basename3(target)})` };
3901
4424
  if (!await deps2.ports.inUse(port)) return { status: "ok", label: `port ${port} free` };
@@ -3976,6 +4499,12 @@ async function resolveRepoModule(deps2, session, portal, meta, root) {
3976
4499
  }
3977
4500
  return session.api.get(`/api/cli/v1/modules/${encodeURIComponent(ids[0])}`);
3978
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
+ }
3979
4508
  async function runDoctor(deps2, o = {}) {
3980
4509
  const signal = o.signal ?? new AbortController().signal;
3981
4510
  const toolMs = o.toolTimeoutMs ?? TOOL_TIMEOUT_MS;
@@ -3995,12 +4524,14 @@ async function runDoctor(deps2, o = {}) {
3995
4524
  return r;
3996
4525
  };
3997
4526
  let cfg;
4527
+ let cfgReadable = true;
3998
4528
  try {
3999
4529
  cfg = await deps2.config.read();
4000
4530
  } catch (e) {
4001
4531
  if (isCancel(e, signal)) throw e;
4002
4532
  await push("config", "tools", () => withError("fail", errorLabel(e), e));
4003
4533
  cfg = { schemaVersion: 1, profiles: {} };
4534
+ cfgReadable = false;
4004
4535
  }
4005
4536
  let ws = null;
4006
4537
  let wsError = null;
@@ -4022,6 +4553,7 @@ async function runDoctor(deps2, o = {}) {
4022
4553
  await push("git", "tools", () => checkGit(deps2, signal, toolMs));
4023
4554
  await push("github-ssh", "tools", () => checkGithubSsh(deps2, signal, toolMs));
4024
4555
  await push("docker", "tools", () => checkDocker(deps2, signal, toolMs));
4556
+ await push("telemetry", "tools", () => checkTelemetry(deps2, cfgReadable ? cfg : null));
4025
4557
  const sessions = /* @__PURE__ */ new Map();
4026
4558
  let loggedIn = [];
4027
4559
  try {
@@ -4114,7 +4646,7 @@ async function workspaceModule(deps2, ws, am, m, runs, signal, push, perModule)
4114
4646
  await push(`module:${m.slug}`, "workspace", () => ({ status: "warn", label: "not a safe directory name \u2014 skipped" }), m.slug);
4115
4647
  return;
4116
4648
  }
4117
- const mod = { root: join17(ws.root, m.slug), slug: m.slug, type: m.type, id: m.id, access: m.access };
4649
+ const mod = { root: join19(ws.root, m.slug), slug: m.slug, type: m.type, id: m.id, access: m.access };
4118
4650
  let prepared = null;
4119
4651
  await push(`module:${m.slug}`, "workspace", async () => {
4120
4652
  const info = await deps2.git.inspect(mod.root);
@@ -4146,7 +4678,7 @@ function registerDoctor(program, ctx) {
4146
4678
  // src/cli/commands/env.ts
4147
4679
  import { randomBytes as randomBytes6 } from "crypto";
4148
4680
  import { chmod as chmod3, realpath as realpath7 } from "fs/promises";
4149
- import { dirname as dirname6, join as join18, relative as relative7 } from "path";
4681
+ import { dirname as dirname7, join as join20, relative as relative7 } from "path";
4150
4682
  var maskLen = (v) => v === void 0 ? "(unset)" : `\u2022\u2022\u2022\u2022 (${v.length} chars)`;
4151
4683
  function registerEnv(program, ctx) {
4152
4684
  const env = program.command("env").description("Local environment");
@@ -4163,7 +4695,7 @@ function registerEnv(program, ctx) {
4163
4695
  const target = f.target ? await resolveTarget(mc.repoRoot, f.target, deps2.cwd) : await resolveTarget(mc.repoRoot, cfg.envFile ?? defaults.envFile);
4164
4696
  await ensureTrusted(ctx, mc, app, { yes: f.yes });
4165
4697
  const existing = await readOrNull(target);
4166
- const example = existing === null ? await readOrNull(join18(dirname6(target), ".env.example")) : null;
4698
+ const example = existing === null ? await readOrNull(join20(dirname7(target), ".env.example")) : null;
4167
4699
  const local = await fetchLocalEnv(deps2, mc, app);
4168
4700
  const inputs = {
4169
4701
  existing,
@@ -4300,9 +4832,9 @@ async function createOidc(opts) {
4300
4832
  throw mapOidcError(e, host);
4301
4833
  }
4302
4834
  },
4303
- async pollDevice(start, signal) {
4835
+ async pollDevice(start2, signal) {
4304
4836
  try {
4305
- const t = await oc.pollDeviceAuthorizationGrant(config, start.handle, void 0, { signal });
4837
+ const t = await oc.pollDeviceAuthorizationGrant(config, start2.handle, void 0, { signal });
4306
4838
  return toTokenSet(t);
4307
4839
  } catch (e) {
4308
4840
  throw mapOidcError(e, host);
@@ -4329,14 +4861,14 @@ async function createOidc(opts) {
4329
4861
  // src/core/auth/login.ts
4330
4862
  async function deviceLogin(opts) {
4331
4863
  await opts.store.getRefresh();
4332
- const start = await opts.oidc.startDevice(LOGIN_SCOPE);
4864
+ const start2 = await opts.oidc.startDevice(LOGIN_SCOPE);
4333
4865
  const stopPoll = new AbortController();
4334
4866
  const settled = new AbortController();
4335
- 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());
4336
4868
  poll.catch(() => {
4337
4869
  });
4338
4870
  try {
4339
- await opts.onCode(start, settled.signal);
4871
+ await opts.onCode(start2, settled.signal);
4340
4872
  } catch (e) {
4341
4873
  stopPoll.abort();
4342
4874
  const late = await poll.catch(() => null);
@@ -4562,8 +5094,7 @@ function registerStatus(program, ctx) {
4562
5094
  }
4563
5095
 
4564
5096
  // src/cli/commands/sync.ts
4565
- import { lstat as lstat2, readdir } from "fs/promises";
4566
- import { join as join19, relative as relative8 } from "path";
5097
+ import { join as join21, relative as relative8 } from "path";
4567
5098
  import { Option as Option2 } from "commander";
4568
5099
  function asFailure2(slug, e) {
4569
5100
  if (e instanceof QoreError && e.code !== "QORE_GIT_UNAVAILABLE") {
@@ -4571,24 +5102,6 @@ function asFailure2(slug, e) {
4571
5102
  }
4572
5103
  throw e;
4573
5104
  }
4574
- async function strayRepos(root, slugs) {
4575
- let entries;
4576
- try {
4577
- entries = (await readdir(root, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
4578
- } catch {
4579
- return [];
4580
- }
4581
- const names = [];
4582
- for (const name of entries) {
4583
- if (slugs.has(name)) continue;
4584
- try {
4585
- await lstat2(join19(root, name, ".git"));
4586
- names.push(name);
4587
- } catch {
4588
- }
4589
- }
4590
- return names.sort();
4591
- }
4592
5105
  function registerSync(program, ctx) {
4593
5106
  program.command("sync").description("Clone new modules, fetch present ones, report removed and re-pointed ones").option("--pull", "fast-forward every clean present repository with an upstream").addOption(new Option2("--protocol <https|ssh>", "clone URL protocol").choices(["https", "ssh"]).default("https")).action(async (flags) => {
4594
5107
  const { deps: deps2, io: io2 } = ctx;
@@ -4613,7 +5126,7 @@ function registerSync(program, ctx) {
4613
5126
  continue;
4614
5127
  }
4615
5128
  try {
4616
- dirs[m.slug] = await deps2.git.inspect(join19(ws.root, m.slug));
5129
+ dirs[m.slug] = await deps2.git.inspect(join21(ws.root, m.slug));
4617
5130
  modulesForPlan.push(m);
4618
5131
  } catch (e) {
4619
5132
  earlyFailures.push(asFailure2(m.slug, e));
@@ -4753,9 +5266,111 @@ var COMMANDS = [
4753
5266
  registerClone,
4754
5267
  registerSync,
4755
5268
  registerVersion,
4756
- registerDoctor
5269
+ registerConfig,
5270
+ registerDoctor,
5271
+ registerDebugTelemetry
4757
5272
  ];
4758
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."
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
+ }
5373
+
4759
5374
  // src/cli/updateCheck.ts
4760
5375
  var REGISTRY_URL = "https://registry.npmjs.org/@quatronic%2Fqore/latest";
4761
5376
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
@@ -4789,7 +5404,7 @@ async function maybeNotifyUpdate(deps2, io2, o, timeoutMs = DEFAULT_TIMEOUT_MS)
4789
5404
 
4790
5405
  // src/core/config.ts
4791
5406
  import { readFile as readFile6 } from "fs/promises";
4792
- import { join as join20 } from "path";
5407
+ import { join as join22 } from "path";
4793
5408
  var empty = () => ({ schemaVersion: 1, profiles: {} });
4794
5409
  var ConfigStore = class {
4795
5410
  constructor(dir2) {
@@ -4797,7 +5412,7 @@ var ConfigStore = class {
4797
5412
  }
4798
5413
  dir;
4799
5414
  get file() {
4800
- return join20(this.dir, "config.json");
5415
+ return join22(this.dir, "config.json");
4801
5416
  }
4802
5417
  async read() {
4803
5418
  let raw;
@@ -4830,7 +5445,7 @@ var ConfigStore = class {
4830
5445
  import { execFile, execFileSync } from "child_process";
4831
5446
  import { mkdir as mkdir3, readdir as readdir2, stat as stat3 } from "fs/promises";
4832
5447
  import { homedir } from "os";
4833
- import { dirname as dirname7, join as join21 } from "path";
5448
+ import { dirname as dirname8, join as join23 } from "path";
4834
5449
  function parseRemotes(stdout) {
4835
5450
  const seen = /* @__PURE__ */ new Set();
4836
5451
  const out = [];
@@ -4842,6 +5457,30 @@ function parseRemotes(stdout) {
4842
5457
  }
4843
5458
  return out;
4844
5459
  }
5460
+ function parseStatus(stdout) {
5461
+ const s = { ok: true, dirty: false, branch: null, upstream: false };
5462
+ for (const raw of stdout.split("\n")) {
5463
+ const line2 = raw.replace(/\r$/, "");
5464
+ if (line2.startsWith("# branch.head ")) {
5465
+ const name = line2.slice("# branch.head ".length).trim();
5466
+ s.branch = name === "(detached)" ? null : name;
5467
+ } else if (line2.startsWith("# branch.upstream ")) {
5468
+ s.upstream = true;
5469
+ s.upstreamRef = line2.slice("# branch.upstream ".length).trim();
5470
+ } else if (line2.startsWith("# branch.ab ")) {
5471
+ const m = /^# branch\.ab \+(\d+) -(\d+)$/.exec(line2.trim());
5472
+ if (m) {
5473
+ s.ahead = Number(m[1]);
5474
+ s.behind = Number(m[2]);
5475
+ }
5476
+ } else if (line2.startsWith("#")) {
5477
+ continue;
5478
+ } else if (line2.trim().length > 0) {
5479
+ s.dirty = true;
5480
+ }
5481
+ }
5482
+ return s;
5483
+ }
4845
5484
  var configuredSshCommand = /* @__PURE__ */ new Map();
4846
5485
  function hasConfiguredSshCommand(repoDir) {
4847
5486
  const key = repoDir ?? "";
@@ -4900,7 +5539,7 @@ async function remotes(repoRoot) {
4900
5539
  return parseRemotes(r.stdout);
4901
5540
  }
4902
5541
  async function clone(url, dir2) {
4903
- await mkdir3(dirname7(dir2), { recursive: true });
5542
+ await mkdir3(dirname8(dir2), { recursive: true });
4904
5543
  const r = await runGit(["clone", "--", url, dir2]);
4905
5544
  return toResult(r, "git clone");
4906
5545
  }
@@ -4916,22 +5555,7 @@ async function status(repoRoot) {
4916
5555
  const command = "git status --porcelain=v2 --branch";
4917
5556
  const r = await runGit(["--no-optional-locks", "-C", repoRoot, "status", "--porcelain=v2", "--branch"], repoRoot);
4918
5557
  if (r.code !== 0) return { ok: false, stderr: r.stderr, command };
4919
- let branch = null;
4920
- let upstream = false;
4921
- let dirty = false;
4922
- for (const line2 of r.stdout.split(/\r?\n/)) {
4923
- if (line2.startsWith("# branch.head ")) {
4924
- const name = line2.slice("# branch.head ".length).trim();
4925
- branch = name === "(detached)" ? null : name;
4926
- } else if (line2.startsWith("# branch.upstream ")) {
4927
- upstream = true;
4928
- } else if (line2.startsWith("#")) {
4929
- continue;
4930
- } else if (line2.trim().length > 0) {
4931
- dirty = true;
4932
- }
4933
- }
4934
- return { ok: true, dirty, branch, upstream };
5558
+ return parseStatus(r.stdout);
4935
5559
  }
4936
5560
  async function statOrThrow(path) {
4937
5561
  try {
@@ -4952,7 +5576,7 @@ async function inspect(dir2) {
4952
5576
  throw new QoreError("QORE_INTERNAL", `could not read ${dir2}`, { cause: err });
4953
5577
  }
4954
5578
  const empty2 = entries.length === 0;
4955
- const isRepo = await statOrThrow(join21(dir2, ".git")) !== void 0;
5579
+ const isRepo = await statOrThrow(join23(dir2, ".git")) !== void 0;
4956
5580
  const dirRemotes = isRepo ? await remotes(dir2) : [];
4957
5581
  return { exists: true, empty: empty2, isRepo, remotes: dirRemotes };
4958
5582
  }
@@ -5014,13 +5638,13 @@ var OsKeychain = class {
5014
5638
 
5015
5639
  // src/platform/lock.ts
5016
5640
  import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
5017
- import { join as join22 } from "path";
5641
+ import { join as join24 } from "path";
5018
5642
  import lockfile from "proper-lockfile";
5019
5643
  function fileLock(dir2) {
5020
5644
  return async (name, fn) => {
5021
- const locks = join22(dir2, "locks");
5645
+ const locks = join24(dir2, "locks");
5022
5646
  await mkdir4(locks, { recursive: true, mode: 448 });
5023
- const target = join22(locks, `${name}.lock`);
5647
+ const target = join24(locks, `${name}.lock`);
5024
5648
  await writeFile4(target, "", { flag: "a" });
5025
5649
  let release;
5026
5650
  let compromised;
@@ -5121,6 +5745,10 @@ var crossSpawner = makeSpawner(spawn, process);
5121
5745
 
5122
5746
  // src/platform/launch.ts
5123
5747
  import spawn2 from "cross-spawn";
5748
+ var RESET_OR_CLEAR = /\x1Bc|\x1B\[[23]J/g;
5749
+ function stripScreenClears(line2) {
5750
+ return line2.includes("\x1B") ? line2.replace(RESET_OR_CLEAR, "") : line2;
5751
+ }
5124
5752
  function lineSplitter(stream, onLine) {
5125
5753
  let buf = "";
5126
5754
  return {
@@ -5128,13 +5756,13 @@ function lineSplitter(stream, onLine) {
5128
5756
  buf += chunk;
5129
5757
  let i;
5130
5758
  while ((i = buf.indexOf("\n")) >= 0) {
5131
- onLine({ stream, line: buf.slice(0, i).replace(/\r$/, "") });
5759
+ onLine({ stream, line: stripScreenClears(buf.slice(0, i).replace(/\r$/, "")) });
5132
5760
  buf = buf.slice(i + 1);
5133
5761
  }
5134
5762
  },
5135
5763
  flush() {
5136
5764
  if (buf) {
5137
- onLine({ stream, line: buf.replace(/\r$/, "") });
5765
+ onLine({ stream, line: stripScreenClears(buf.replace(/\r$/, "")) });
5138
5766
  buf = "";
5139
5767
  }
5140
5768
  }
@@ -5289,6 +5917,10 @@ var systemPorts = {
5289
5917
  }
5290
5918
  };
5291
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
+
5292
5924
  // src/platform/proxy.ts
5293
5925
  import { execFile as execFile3 } from "child_process";
5294
5926
 
@@ -5351,7 +5983,7 @@ function systemProxy(platform = process.platform) {
5351
5983
  }
5352
5984
 
5353
5985
  // src/version.ts
5354
- var VERSION = true ? "0.1.3" : "0.0.0-dev";
5986
+ var VERSION = true ? "0.1.5" : "0.0.0-dev";
5355
5987
 
5356
5988
  // src/cli.ts
5357
5989
  if (process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY) setGlobalDispatcher(new EnvHttpProxyAgent());
@@ -5394,6 +6026,11 @@ var deps = {
5394
6026
  process.kill(process.pid, signal);
5395
6027
  },
5396
6028
  pid: process.pid,
6029
+ registerSecrets: (values) => {
6030
+ telemetry.registerSecrets(values);
6031
+ },
6032
+ sentryDsn: SENTRY_DSN,
6033
+ sentryEnvironment: SENTRY_ENVIRONMENT,
5397
6034
  systemProxy: () => systemProxy(process.platform)
5398
6035
  };
5399
6036
  var termination = preflightTermination(() => ac.abort(), deps.raise);
@@ -5404,16 +6041,24 @@ var io = processIo(isInteractive({
5404
6041
  noInput: preSeparatorArgv.includes("--no-input"),
5405
6042
  env: process.env
5406
6043
  }));
5407
- process.exitCode = await run(argv, deps, io, ac.signal, COMMANDS);
5408
- await termination.reRaise(async () => {
6044
+ var flushStd = async () => {
5409
6045
  await Promise.all([process.stdout, process.stderr].map((s) => new Promise((r) => {
5410
6046
  s.write("", () => r());
5411
6047
  })));
5412
- });
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);
5413
6053
  if (!ac.signal.aborted) {
5414
6054
  try {
5415
6055
  await maybeNotifyUpdate(deps, io, { json });
5416
6056
  } catch {
5417
6057
  }
5418
6058
  }
5419
- //# 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