@quatronic/qore 0.1.6 → 0.1.8

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.
Files changed (76) hide show
  1. package/README.md +81 -2
  2. package/dist/cli.js +1990 -746
  3. package/package.json +23 -4
  4. package/viewer/dist/assets/elk.bundled-C8gcDXx2.js +24 -0
  5. package/viewer/dist/assets/index-BRgYtLRN.css +1 -0
  6. package/viewer/dist/assets/index-VDGZO23h.js +15 -0
  7. package/viewer/dist/assets/inter-cyrillic-400-normal-HOLc17fK.woff +0 -0
  8. package/viewer/dist/assets/inter-cyrillic-400-normal-obahsSVq.woff2 +0 -0
  9. package/viewer/dist/assets/inter-cyrillic-500-normal-BasfLYem.woff2 +0 -0
  10. package/viewer/dist/assets/inter-cyrillic-500-normal-CxZf_p3X.woff +0 -0
  11. package/viewer/dist/assets/inter-cyrillic-600-normal-4D_pXhcN.woff +0 -0
  12. package/viewer/dist/assets/inter-cyrillic-600-normal-CWCymEST.woff2 +0 -0
  13. package/viewer/dist/assets/inter-cyrillic-ext-400-normal-BQZuk6qB.woff2 +0 -0
  14. package/viewer/dist/assets/inter-cyrillic-ext-400-normal-DQukG94-.woff +0 -0
  15. package/viewer/dist/assets/inter-cyrillic-ext-500-normal-B0yAr1jD.woff2 +0 -0
  16. package/viewer/dist/assets/inter-cyrillic-ext-500-normal-BmqWE9Dz.woff +0 -0
  17. package/viewer/dist/assets/inter-cyrillic-ext-600-normal-Bcila6Z-.woff +0 -0
  18. package/viewer/dist/assets/inter-cyrillic-ext-600-normal-Dfes3d0z.woff2 +0 -0
  19. package/viewer/dist/assets/inter-greek-400-normal-B4URO6DV.woff2 +0 -0
  20. package/viewer/dist/assets/inter-greek-400-normal-q2sYcFCs.woff +0 -0
  21. package/viewer/dist/assets/inter-greek-500-normal-BIZE56-Y.woff2 +0 -0
  22. package/viewer/dist/assets/inter-greek-500-normal-Xzm54t5V.woff +0 -0
  23. package/viewer/dist/assets/inter-greek-600-normal-BZpKdvQh.woff +0 -0
  24. package/viewer/dist/assets/inter-greek-600-normal-plRanbMR.woff2 +0 -0
  25. package/viewer/dist/assets/inter-greek-ext-400-normal-DGGRlc-M.woff2 +0 -0
  26. package/viewer/dist/assets/inter-greek-ext-400-normal-KugGGMne.woff +0 -0
  27. package/viewer/dist/assets/inter-greek-ext-500-normal-2j5mBUwD.woff +0 -0
  28. package/viewer/dist/assets/inter-greek-ext-500-normal-C4iEst2y.woff2 +0 -0
  29. package/viewer/dist/assets/inter-greek-ext-600-normal-B8X0CLgF.woff +0 -0
  30. package/viewer/dist/assets/inter-greek-ext-600-normal-DRtmH8MT.woff2 +0 -0
  31. package/viewer/dist/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
  32. package/viewer/dist/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
  33. package/viewer/dist/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
  34. package/viewer/dist/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
  35. package/viewer/dist/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
  36. package/viewer/dist/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
  37. package/viewer/dist/assets/inter-latin-ext-400-normal-77YHD8bZ.woff +0 -0
  38. package/viewer/dist/assets/inter-latin-ext-400-normal-C1nco2VV.woff2 +0 -0
  39. package/viewer/dist/assets/inter-latin-ext-500-normal-BxGbmqWO.woff +0 -0
  40. package/viewer/dist/assets/inter-latin-ext-500-normal-CV4jyFjo.woff2 +0 -0
  41. package/viewer/dist/assets/inter-latin-ext-600-normal-CIVaiw4L.woff +0 -0
  42. package/viewer/dist/assets/inter-latin-ext-600-normal-D2bJ5OIk.woff2 +0 -0
  43. package/viewer/dist/assets/inter-vietnamese-400-normal-Bbgyi5SW.woff +0 -0
  44. package/viewer/dist/assets/inter-vietnamese-400-normal-DMkecbls.woff2 +0 -0
  45. package/viewer/dist/assets/inter-vietnamese-500-normal-DOriooB6.woff2 +0 -0
  46. package/viewer/dist/assets/inter-vietnamese-500-normal-mJboJaSs.woff +0 -0
  47. package/viewer/dist/assets/inter-vietnamese-600-normal-BuLX-rYi.woff +0 -0
  48. package/viewer/dist/assets/inter-vietnamese-600-normal-Cc8MFFhd.woff2 +0 -0
  49. package/viewer/dist/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  50. package/viewer/dist/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  51. package/viewer/dist/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
  52. package/viewer/dist/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
  53. package/viewer/dist/assets/jetbrains-mono-cyrillic-600-normal-8K4wrrwR.woff +0 -0
  54. package/viewer/dist/assets/jetbrains-mono-cyrillic-600-normal-EVf6-Yzo.woff2 +0 -0
  55. package/viewer/dist/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  56. package/viewer/dist/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  57. package/viewer/dist/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
  58. package/viewer/dist/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
  59. package/viewer/dist/assets/jetbrains-mono-greek-600-normal-H7WoG9Et.woff2 +0 -0
  60. package/viewer/dist/assets/jetbrains-mono-greek-600-normal-mc2nkWzM.woff +0 -0
  61. package/viewer/dist/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  62. package/viewer/dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  63. package/viewer/dist/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
  64. package/viewer/dist/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
  65. package/viewer/dist/assets/jetbrains-mono-latin-600-normal-BfsvjouI.woff +0 -0
  66. package/viewer/dist/assets/jetbrains-mono-latin-600-normal-C8RAYTDA.woff2 +0 -0
  67. package/viewer/dist/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  68. package/viewer/dist/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  69. package/viewer/dist/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
  70. package/viewer/dist/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
  71. package/viewer/dist/assets/jetbrains-mono-latin-ext-600-normal-BfB_LPfz.woff2 +0 -0
  72. package/viewer/dist/assets/jetbrains-mono-latin-ext-600-normal-DObL3zCW.woff +0 -0
  73. package/viewer/dist/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  74. package/viewer/dist/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
  75. package/viewer/dist/assets/jetbrains-mono-vietnamese-600-normal-OWROknRo.woff +0 -0
  76. package/viewer/dist/index.html +13 -0
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
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]="28c91dc8-dbb9-5c0a-91e3-f368931a4ed3")}catch(e){}}();
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]="c4bda056-b13b-52b5-831f-2afd3f6c6867")}catch(e){}}();
4
4
  import { createRequire as __qoreCreateRequire } from "node:module"; const require = __qoreCreateRequire(import.meta.url);
5
5
  import {
6
6
  addSecretValues,
@@ -9,6 +9,7 @@ import {
9
9
 
10
10
  // src/cli.ts
11
11
  import { homedir as homedir2 } from "os";
12
+ import { fileURLToPath } from "url";
12
13
  import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici";
13
14
 
14
15
  // src/cli/io.ts
@@ -169,7 +170,7 @@ function processIo(interactive, stderrTTY = Boolean(process.stderr.isTTY)) {
169
170
  };
170
171
  }
171
172
  function createWaitForEnter(input2, output) {
172
- return (message, signal) => new Promise((resolve6, reject) => {
173
+ return (message, signal) => new Promise((resolve7, reject) => {
173
174
  const rl = createInterface({ input: input2, output, terminal: true });
174
175
  const onAbort = () => {
175
176
  rl.close();
@@ -178,7 +179,7 @@ function createWaitForEnter(input2, output) {
178
179
  const done = () => {
179
180
  signal.removeEventListener("abort", onAbort);
180
181
  rl.close();
181
- resolve6();
182
+ resolve7();
182
183
  };
183
184
  signal.addEventListener("abort", onAbort, { once: true });
184
185
  rl.on("SIGINT", () => {
@@ -382,16 +383,16 @@ function groupByModule(results) {
382
383
  const order = [];
383
384
  const groups = /* @__PURE__ */ new Map();
384
385
  for (const r of results) {
385
- const key = r.module ?? r.id;
386
- if (!groups.has(key)) {
387
- groups.set(key, []);
388
- order.push(key);
386
+ const key2 = r.module ?? r.id;
387
+ if (!groups.has(key2)) {
388
+ groups.set(key2, []);
389
+ order.push(key2);
389
390
  }
390
- groups.get(key).push(r);
391
+ groups.get(key2).push(r);
391
392
  }
392
393
  const isSub = (r) => r.id.startsWith("pm:") || r.id.startsWith("node:");
393
- return order.flatMap((key) => {
394
- const g = groups.get(key);
394
+ return order.flatMap((key2) => {
395
+ const g = groups.get(key2);
395
396
  return [...g.filter((r) => !isSub(r)), ...g.filter(isSub)];
396
397
  });
397
398
  }
@@ -538,8 +539,8 @@ async function fetchMetaWithDate(fetch3, apiOrigin) {
538
539
  }
539
540
  if (!res.ok) throw await problemToError(res);
540
541
  const header = res.headers.get("date");
541
- const ms = header ? Date.parse(header) : NaN;
542
- return { meta: await res.json(), date: Number.isNaN(ms) ? null : new Date(ms) };
542
+ const ms2 = header ? Date.parse(header) : NaN;
543
+ return { meta: await res.json(), date: Number.isNaN(ms2) ? null : new Date(ms2) };
543
544
  }
544
545
  function negotiate(meta, portal, cliVersion) {
545
546
  if (portal.pinnedIssuer && meta.auth.issuer !== portal.pinnedIssuer) {
@@ -678,7 +679,7 @@ var QoreApiClient = class {
678
679
  this.o.onRequest?.(`${method} ${path} \u2192 ${res.status}${rid ? ` (${rid})` : ""}`);
679
680
  return res;
680
681
  }
681
- async request(method, path, body) {
682
+ async request(method, path, body, parse2 = (r) => r.json()) {
682
683
  let res = await this.once(method, path, body, false);
683
684
  if (res.status === 401) {
684
685
  res = await this.once(method, path, body, true);
@@ -700,11 +701,14 @@ var QoreApiClient = class {
700
701
  }
701
702
  }
702
703
  if (!res.ok) throw await problemToError(res);
703
- return await res.json();
704
+ return parse2(res);
704
705
  }
705
706
  get(path) {
706
707
  return this.request("GET", path);
707
708
  }
709
+ getText(path) {
710
+ return this.request("GET", path, void 0, (r) => r.text());
711
+ }
708
712
  post(path, body) {
709
713
  return this.request("POST", path, body);
710
714
  }
@@ -720,13 +724,13 @@ var RefreshCoordinator = class {
720
724
  fresh(expiresAt) {
721
725
  return new Date(expiresAt).getTime() - this.o.now().getTime() > SKEW_MS;
722
726
  }
723
- async rotate(scope) {
727
+ async rotate(scope2) {
724
728
  let stored = await this.o.store.getRefresh();
725
729
  if (!stored) throw new QoreError("QORE_NOT_LOGGED_IN", "Not logged in", { hint: "Run `qore login`." });
726
730
  let t;
727
731
  for (let attempt = 0; ; attempt++) {
728
732
  try {
729
- t = await (await this.o.oidc()).refresh(stored.rt, scope);
733
+ t = await (await this.o.oidc()).refresh(stored.rt, scope2);
730
734
  break;
731
735
  } catch (e) {
732
736
  if (!(e instanceof QoreError && e.code === "QORE_SESSION_EXPIRED")) throw e;
@@ -742,7 +746,7 @@ var RefreshCoordinator = class {
742
746
  throw e;
743
747
  }
744
748
  }
745
- await this.o.store.setRefresh({ rt: t.refreshToken, sub: t.sub, scope: t.scope || (scope ?? stored.scope), obtainedAt: this.o.now().toISOString() });
749
+ await this.o.store.setRefresh({ rt: t.refreshToken, sub: t.sub, scope: t.scope || (scope2 ?? stored.scope), obtainedAt: this.o.now().toISOString() });
746
750
  return t;
747
751
  }
748
752
  async accessToken(opts = {}) {
@@ -927,23 +931,23 @@ var ResolutionCache = class {
927
931
  fresh(e) {
928
932
  return !!e && this.now().getTime() - new Date(e.resolvedAt).getTime() < TTL_MS2;
929
933
  }
930
- async get(origin, key) {
931
- const e = (await this.load()).entries[`${origin}|${key}`];
934
+ async get(origin, key2) {
935
+ const e = (await this.load()).entries[`${origin}|${key2}`];
932
936
  return this.fresh(e) ? e.moduleId : null;
933
937
  }
934
938
  /** Never throws: a read-only config dir, a full disk or a Windows EPERM on rename
935
939
  * (another qore holding the file) must not fail a command that already resolved. */
936
- async put(origin, key, moduleId) {
940
+ async put(origin, key2, moduleId) {
937
941
  const f = await this.load();
938
- f.entries[`${origin}|${key}`] = { moduleId, resolvedAt: this.now().toISOString() };
942
+ f.entries[`${origin}|${key2}`] = { moduleId, resolvedAt: this.now().toISOString() };
939
943
  for (const [k, e] of Object.entries(f.entries)) if (!this.fresh(e)) delete f.entries[k];
940
944
  await this.save(f);
941
945
  }
942
946
  /** Never throws, like `put`. */
943
- async drop(origin, key) {
947
+ async drop(origin, key2) {
944
948
  const f = await this.load();
945
- if (!(`${origin}|${key}` in f.entries)) return;
946
- delete f.entries[`${origin}|${key}`];
949
+ if (!(`${origin}|${key2}` in f.entries)) return;
950
+ delete f.entries[`${origin}|${key2}`];
947
951
  await this.save(f);
948
952
  }
949
953
  async save(f) {
@@ -956,8 +960,8 @@ var ResolutionCache = class {
956
960
  const f = await this.load();
957
961
  const hits = [];
958
962
  for (const [k, e] of Object.entries(f.entries)) {
959
- const sep3 = k.lastIndexOf("|");
960
- if (keys.includes(k.slice(sep3 + 1)) && this.fresh(e)) hits.push({ origin: k.slice(0, sep3), at: new Date(e.resolvedAt).getTime() });
963
+ const sep4 = k.lastIndexOf("|");
964
+ if (keys.includes(k.slice(sep4 + 1)) && this.fresh(e)) hits.push({ origin: k.slice(0, sep4), at: new Date(e.resolvedAt).getTime() });
961
965
  }
962
966
  hits.sort((a, b) => b.at - a.at);
963
967
  return [...new Set(hits.map((h) => h.origin))];
@@ -1020,12 +1024,12 @@ async function isTrusted(deps2, root, moduleId) {
1020
1024
  return (cfg.trustedRepos?.[await keyOf(deps2, root)] ?? []).includes(moduleId);
1021
1025
  }
1022
1026
  async function trust(deps2, root, moduleId) {
1023
- const key = await keyOf(deps2, root);
1027
+ const key2 = await keyOf(deps2, root);
1024
1028
  await deps2.config.update((c) => {
1025
1029
  const repos = c.trustedRepos ??= {};
1026
- const list = new Set(repos[key] ?? []);
1030
+ const list = new Set(repos[key2] ?? []);
1027
1031
  list.add(moduleId);
1028
- repos[key] = [...list];
1032
+ repos[key2] = [...list];
1029
1033
  });
1030
1034
  }
1031
1035
 
@@ -1186,8 +1190,8 @@ async function applyActions(deps2, o) {
1186
1190
  const r = await deps2.git.clone(url, action.dir);
1187
1191
  if (r.ok) {
1188
1192
  await trust(deps2, action.dir, action.module.id);
1189
- const key = repoKey(action.module.repoUrl);
1190
- if (key) await cache.put(o.portal.apiOrigin, key, action.module.id);
1193
+ const key2 = repoKey(action.module.repoUrl);
1194
+ if (key2) await cache.put(o.portal.apiOrigin, key2, action.module.id);
1191
1195
  result = { slug: action.module.slug, status: "cloned", dir: action.dir };
1192
1196
  } else {
1193
1197
  const err = classifyCloneFailure(r.stderr, action.module, o.app, o.protocol);
@@ -1432,9 +1436,9 @@ function expandPath(deps2, input2) {
1432
1436
  }
1433
1437
  function trimSeparator(p) {
1434
1438
  const root = parse(p).root;
1435
- let end = p.length;
1436
- while (end > root.length && (p[end - 1] === "/" || p[end - 1] === "\\")) end--;
1437
- return p.slice(0, end);
1439
+ let end2 = p.length;
1440
+ while (end2 > root.length && (p[end2 - 1] === "/" || p[end2 - 1] === "\\")) end2--;
1441
+ return p.slice(0, end2);
1438
1442
  }
1439
1443
  async function underAFile(path) {
1440
1444
  for (let p = dirname3(path); ; p = dirname3(p)) {
@@ -1838,82 +1842,13 @@ function registerConfig(program, ctx) {
1838
1842
  });
1839
1843
  }
1840
1844
 
1841
- // src/cli/commands/dev.ts
1842
- import { randomBytes as randomBytes6 } from "crypto";
1843
- import { realpath as realpath8 } from "fs/promises";
1844
- import { constants as constants3 } from "os";
1845
- import { dirname as dirname7, join as join21, relative as relative7 } from "path";
1846
- import { StringDecoder } from "string_decoder";
1847
-
1848
- // src/core/context/repoConfig.ts
1849
- import { readFile as readFile3 } from "fs/promises";
1850
- import { join as join10 } from "path";
1851
- var FORBIDDEN = ["portal", "api", "apiOrigin", "issuer", "tenant", "tenantId", "app", "appId", "module", "moduleId"];
1852
- var KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
1853
- var SCRIPT = /^[A-Za-z0-9:_.-]+$/;
1854
- var invalid3 = (msg, details) => new QoreError("QORE_CONTEXT_INVALID", `.qore.json: ${msg}`, { hint: "Fix .qore.json in the repository root; see the $schema it names.", details });
1855
- function moduleDefaults(type) {
1856
- return type === "frontend" ? { envFile: ".env.local", localSecrets: ["SESSION_COOKIE_SECRET"] } : { envFile: ".env", localSecrets: [] };
1857
- }
1858
- async function readRepoConfig(root) {
1859
- let raw;
1860
- try {
1861
- raw = await readFile3(join10(root, ".qore.json"), "utf8");
1862
- } catch (e) {
1863
- if (e.code === "ENOENT") return {};
1864
- throw e;
1865
- }
1866
- let doc;
1867
- try {
1868
- doc = JSON.parse(raw);
1869
- } catch {
1870
- throw invalid3("not valid JSON");
1871
- }
1872
- if (!doc || typeof doc !== "object" || Array.isArray(doc)) throw invalid3("must be an object");
1873
- const v = doc.schemaVersion === void 0 ? 1 : doc.schemaVersion;
1874
- if (!Number.isInteger(v) || v < 1) throw invalid3("schemaVersion must be a positive integer");
1875
- if (v > 1) {
1876
- throw new QoreError("QORE_CONTEXT_UNSUPPORTED", `.qore.json schemaVersion ${v} is newer than this qore`, { hint: "Run: npm i -g @quatronic/qore" });
1877
- }
1878
- for (const f of FORBIDDEN) if (f in doc) throw invalid3(`"${f}" is not allowed \u2014 .qore.json never selects a portal or a resource`, { field: f });
1879
- const out = {};
1880
- if (doc.envFile !== void 0) {
1881
- if (typeof doc.envFile !== "string" || !doc.envFile) throw invalid3("envFile must be a non-empty string");
1882
- out.envFile = doc.envFile;
1883
- }
1884
- if (doc.localSecrets !== void 0) {
1885
- if (!Array.isArray(doc.localSecrets) || !doc.localSecrets.every((k) => typeof k === "string" && KEY.test(k))) {
1886
- throw invalid3("localSecrets must be a list of env key names");
1887
- }
1888
- if (new Set(doc.localSecrets).size !== doc.localSecrets.length) throw invalid3("localSecrets must not contain duplicates");
1889
- out.localSecrets = doc.localSecrets;
1890
- }
1891
- if (doc.dev !== void 0) {
1892
- const d = doc.dev;
1893
- if (!d || typeof d !== "object" || Array.isArray(d)) throw invalid3("dev must be an object");
1894
- const dev = {};
1895
- if (d.install !== void 0) {
1896
- if (typeof d.install !== "boolean") throw invalid3("dev.install must be true or false");
1897
- dev.install = d.install;
1898
- }
1899
- if (d.services !== void 0) {
1900
- if (!Array.isArray(d.services) || !d.services.every((s) => typeof s === "string" && SCRIPT.test(s))) {
1901
- throw invalid3("dev.services must be a list of script names");
1902
- }
1903
- if (new Set(d.services).size !== d.services.length) throw invalid3("dev.services must not contain duplicates");
1904
- dev.services = d.services;
1905
- }
1906
- if (d.script !== void 0) {
1907
- if (typeof d.script !== "string" || !SCRIPT.test(d.script)) throw invalid3("dev.script must be a script name");
1908
- dev.script = d.script;
1909
- }
1910
- out.dev = dev;
1911
- }
1912
- return out;
1913
- }
1845
+ // src/cli/commands/db.ts
1846
+ import { stat as stat4 } from "fs/promises";
1847
+ import { basename as basename3, join as join15 } from "path";
1848
+ import { InvalidArgumentError } from "commander";
1914
1849
 
1915
1850
  // src/core/context/resolver.ts
1916
- import { isAbsolute as isAbsolute2, join as join11, relative as relative4 } from "path";
1851
+ import { isAbsolute as isAbsolute2, join as join10, relative as relative4 } from "path";
1917
1852
  var UUID3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1918
1853
  var HINT_FLAG = "cd into a module repository, or pass --module <slug|id>.";
1919
1854
  var RESOLVE = "/api/cli/v1/repos/resolve";
@@ -1951,10 +1886,10 @@ async function flagOverrides(deps2, session, portal, cache, moduleId) {
1951
1886
  const keyed = (await repoRemotes(deps2))?.keyed ?? [];
1952
1887
  const derived = /* @__PURE__ */ new Map();
1953
1888
  const uncached = [];
1954
- for (const key of new Set(keyed.map((r) => r.key))) {
1955
- const id = await cache.get(portal.apiOrigin, key);
1956
- if (id) derived.set(key, /* @__PURE__ */ new Set([id]));
1957
- else uncached.push(key);
1889
+ for (const key2 of new Set(keyed.map((r) => r.key))) {
1890
+ const id = await cache.get(portal.apiOrigin, key2);
1891
+ if (id) derived.set(key2, /* @__PURE__ */ new Set([id]));
1892
+ else uncached.push(key2);
1958
1893
  }
1959
1894
  if (uncached.length > 0) {
1960
1895
  try {
@@ -2125,8 +2060,9 @@ async function anyLoggedIn(deps2, cfg) {
2125
2060
  }
2126
2061
  return false;
2127
2062
  }
2063
+ var isForeignRepo = (e) => e instanceof QoreError && e.code === "QORE_CONTEXT_REQUIRED" && e.details?.foreignRepo === true;
2128
2064
  async function resolveModuleContext(deps2, o) {
2129
- const cache = new ResolutionCache(join11(deps2.config.dir, "cache"), deps2.now);
2065
+ const cache = new ResolutionCache(join10(deps2.config.dir, "cache"), deps2.now);
2130
2066
  const ws = await findWorkspace(deps2.cwd, deps2.home);
2131
2067
  if (o.module) return byFlag(deps2, o, cache, ws);
2132
2068
  const cfg = await deps2.config.read();
@@ -2143,7 +2079,7 @@ async function resolveModuleContext(deps2, o) {
2143
2079
  const said = gitError?.hint;
2144
2080
  throw new QoreError("QORE_CONTEXT_REQUIRED", "This repository has no remote Qore can match", {
2145
2081
  hint: said ? `${HINT_FLAG} git said: ${said}` : HINT_FLAG,
2146
- details: gitError ? { git: said ?? gitError.message } : void 0,
2082
+ details: gitError ? { git: said ?? gitError.message } : { foreignRepo: true },
2147
2083
  cause: gitError
2148
2084
  });
2149
2085
  }
@@ -2172,6 +2108,7 @@ async function resolveModuleContext(deps2, o) {
2172
2108
  }
2173
2109
  const details = { triedProfiles: tried };
2174
2110
  if (skipped.length > 0) details.skippedProfiles = skipped;
2111
+ if (answered.length > 0) details.foreignRepo = true;
2175
2112
  if (answered.length === 0) {
2176
2113
  const cause = skipErrors[0] ?? unsupported2;
2177
2114
  if (cause) throw withDetails(cause, details);
@@ -2202,183 +2139,457 @@ function pickApp2(mc, app) {
2202
2139
  throw new QoreError("QORE_APP_AMBIGUOUS", `${mc.module.slug} is part of several apps`, { hint: "Pass --app <slug>.", candidates: appCandidates(apps) });
2203
2140
  }
2204
2141
 
2205
- // src/core/diagnostics/node.ts
2206
- var KEEP_LINES = 50;
2207
- function exitHint(tail, h) {
2208
- const text2 = tail.join("\n");
2209
- const missing = text2.match(/Cannot find (?:module|package) '([^']+)'/);
2210
- if (missing) {
2211
- return { reason: "missing-module", hint: `A dependency is missing (${missing[1]}). Run \`${h.installCommand}\`, then qore dev again.` };
2212
- }
2213
- if (/ZodError:/.test(text2)) {
2214
- const keys = [...text2.matchAll(/"path":\s*\[\s*"([^"]+)"/g)].map((m) => m[1]);
2215
- if (keys.length > 0) {
2216
- return {
2217
- reason: "invalid-env",
2218
- keys,
2219
- hint: `Missing or invalid environment variables: ${keys.join(", ")}. Set them in the portal's local secrets (then qore env pull), or in your env file.`
2220
- };
2221
- }
2222
- }
2223
- const addrInUse = text2.match(/listen EADDRINUSE: address already in use (?:\S*:)?(\d+)/);
2224
- if (addrInUse) {
2225
- const port = Number(addrInUse[1]);
2226
- return {
2227
- reason: "port-in-use",
2228
- port,
2229
- hint: `Port ${port} was taken while the server started. Run qore dev again; if it repeats, lsof -iTCP:${port} -sTCP:LISTEN names the holder.`
2230
- };
2142
+ // src/core/erd/dbErrors.ts
2143
+ function dbPort(url) {
2144
+ try {
2145
+ const u = new URL(url);
2146
+ return u.port ? Number(u.port) : 5432;
2147
+ } catch {
2148
+ return null;
2231
2149
  }
2232
- if (/ECONNREFUSED/.test(text2)) {
2233
- const m = text2.match(/ECONNREFUSED\s+\S*:(\d+)/) ?? text2.match(/code: 'ECONNREFUSED'[^}]*?port: (\d+)/);
2234
- if (!m) return { reason: "conn-refused", hint: "A connection was refused (ECONNREFUSED) \u2014 a service this module calls is not running." };
2235
- const port = Number(m[1]);
2236
- const role = h.ports.get(port);
2237
- if (role?.kind === "database") {
2238
- const up = h.runCommand("db:up");
2239
- return {
2240
- reason: "conn-refused",
2241
- port,
2242
- 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.")
2243
- };
2244
- }
2245
- if (role?.kind === "provider") {
2246
- return {
2247
- reason: "conn-refused",
2248
- port,
2249
- hint: `${role.key} points at :${port}, where nothing listens (ECONNREFUSED). Start that module \u2014 qore dev in the workspace root starts providers first.`
2250
- };
2251
- }
2252
- return { reason: "conn-refused", port, hint: `Nothing listens on :${port} (ECONNREFUSED) \u2014 a service this module calls is not running.` };
2150
+ }
2151
+ function connectionOf(url) {
2152
+ try {
2153
+ const u = new URL(url);
2154
+ return { host: u.hostname, port: u.port ? Number(u.port) : 5432, database: decodeURIComponent(u.pathname.replace(/^\//, "")) };
2155
+ } catch {
2156
+ return { host: "", port: 0, database: "" };
2253
2157
  }
2254
- const auth = text2.match(/password authentication failed for user "([^"]+)"/);
2255
- if (auth) {
2256
- const reset = h.runCommand("db:reset");
2257
- return {
2258
- reason: "db-auth",
2259
- 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.` : "")
2260
- };
2158
+ }
2159
+ function classifyDbError(e, url) {
2160
+ const { code, message } = e;
2161
+ const port = dbPort(url);
2162
+ const where = port === null ? "The database" : `The database on :${port}`;
2163
+ if (code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "ETIMEDOUT" || /timeout expired/i.test(message ?? "")) {
2164
+ return new QoreError("QORE_SERVICE_UNHEALTHY", `${where} is not accepting connections`, {
2165
+ hint: "Start it with `qore dev` (or `pnpm db:up`), then run qore db erd again.",
2166
+ details: { reason: "conn-refused", port }
2167
+ });
2261
2168
  }
2262
- const relation = text2.match(/relation "([^"]+)" does not exist/);
2263
- if (relation) {
2264
- const reset = h.runCommand("db:reset");
2265
- return {
2266
- reason: "db-schema",
2267
- 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).` : "")
2268
- };
2169
+ if (code === "28P01" || code === "28000") {
2170
+ return new QoreError("QORE_SERVICE_UNHEALTHY", `${where} rejected the credentials in DATABASE_URL`, {
2171
+ hint: "The local database was created with other credentials than the portal now provides. `pnpm db:reset` recreates it \u2014 this deletes local data.",
2172
+ details: { reason: "db-auth", port }
2173
+ });
2269
2174
  }
2270
- if (/ENOSPC: System limit for number of file watchers reached/.test(text2)) {
2271
- return {
2272
- reason: "watcher-limit",
2273
- 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"
2274
- };
2175
+ if (code === "3D000") {
2176
+ return new QoreError("QORE_SERVICE_UNHEALTHY", "The database named in DATABASE_URL does not exist", {
2177
+ hint: "Start the app once with `qore dev` so its migrations create it, or run `pnpm db:reset`.",
2178
+ details: { reason: "db-schema", port }
2179
+ });
2275
2180
  }
2276
- return null;
2181
+ return new QoreError("QORE_SERVICE_UNHEALTHY", `Could not read the database: ${message ?? String(e)}`, {
2182
+ hint: "Check DATABASE_URL in the env file, then run qore db erd again.",
2183
+ details: { port }
2184
+ });
2277
2185
  }
2278
2186
 
2279
- // src/core/dev/env.ts
2280
- import { dirname as dirname6, join as join14 } from "path";
2281
-
2282
- // src/core/env/fetch.ts
2283
- async function fetchLocalEnv(deps2, mc, app) {
2284
- await ensureCapability(deps2, mc.portal.apiOrigin, "module.local-env");
2285
- try {
2286
- const local = await mc.session.api.get(
2287
- `/api/cli/v1/modules/${encodeURIComponent(mc.module.id)}/local-env?app_id=${encodeURIComponent(app.id)}`
2288
- );
2289
- deps2.registerSecrets(Object.values(local.env));
2290
- return local;
2291
- } catch (e) {
2292
- if (!(e instanceof QoreError) || e.code !== "QORE_ACCESS_DENIED") throw e;
2293
- if (e.serverCode === "app.access.denied") {
2294
- throw withDetails(
2295
- e,
2296
- { required: "write", have: mc.module.access },
2297
- `You have ${mc.module.access} on ${mc.module.slug}; local secrets need write. Ask a maintainer of the app's dev team for write access.`
2298
- );
2187
+ // src/core/erd/app.ts
2188
+ function orderModules(modules) {
2189
+ const bySlug = new Map(modules.map((m) => [m.slug, m]));
2190
+ const out = [];
2191
+ const seen = /* @__PURE__ */ new Set();
2192
+ const visit = (m, trail) => {
2193
+ if (seen.has(m.slug) || trail.has(m.slug)) return;
2194
+ trail.add(m.slug);
2195
+ for (const dep of m.dependsOn) {
2196
+ const d = bySlug.get(dep);
2197
+ if (d) visit(d, trail);
2299
2198
  }
2300
- if (e.serverCode === "auth.scope.insufficient") throw withDetails(e, {}, "Run `qore login` again.");
2301
- throw e;
2199
+ seen.add(m.slug);
2200
+ out.push(m);
2201
+ };
2202
+ for (const m of modules) visit(m, /* @__PURE__ */ new Set());
2203
+ return out;
2204
+ }
2205
+ function groupDatabases(modules) {
2206
+ const groups = [];
2207
+ for (const m of modules) {
2208
+ if (!m.url) continue;
2209
+ const c = connectionOf(m.url);
2210
+ const same2 = groups.find((g) => g.connection.host === c.host && g.connection.port === c.port && g.connection.database === c.database);
2211
+ if (same2) {
2212
+ same2.modules.push(m.slug);
2213
+ continue;
2214
+ }
2215
+ groups.push({ key: m.slug, modules: [m.slug], url: m.url, connection: c });
2302
2216
  }
2217
+ return groups;
2303
2218
  }
2304
-
2305
- // src/core/env/merge.ts
2306
- function parseEnv(contents) {
2307
- const map = /* @__PURE__ */ new Map();
2308
- for (const line2 of contents.split("\n")) {
2309
- const m = line2.replace(/\r$/, "").match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
2310
- if (m?.[1] !== void 0 && m[2] !== void 0) map.set(m[1], m[2]);
2219
+ var REF = /(?:^|\s)->\s*(?:([A-Za-z0-9_-]+):)?([A-Za-z_][\w]*)\.([A-Za-z_][\w]*)\.([A-Za-z_][\w]*)/;
2220
+ function parseRef(comment) {
2221
+ const m = comment ? REF.exec(comment) : null;
2222
+ return m ? { module: m[1] ?? null, schema: m[2], table: m[3], column: m[4] } : null;
2223
+ }
2224
+ function findEnd(dbs, ref, from) {
2225
+ const has = (d) => d.model?.tables.some((t) => t.schema === ref.schema && t.name === ref.table && t.columns.some((c) => c.name === ref.column)) ?? false;
2226
+ const candidates = ref.module ? dbs.filter((d) => d.modules.includes(ref.module)) : [...dbs.filter((d) => d.key !== from), ...dbs.filter((d) => d.key === from)];
2227
+ const hit2 = candidates.find(has);
2228
+ return hit2 ? { database: hit2.key, schema: ref.schema, table: ref.table, column: ref.column } : null;
2229
+ }
2230
+ function resolveRefs(dbs) {
2231
+ const refs = [];
2232
+ for (const d of dbs) for (const t of d.model?.tables ?? []) for (const c of t.columns) {
2233
+ const ref = parseRef(c.comment);
2234
+ const to = ref && findEnd(dbs, ref, d.key);
2235
+ if (to) refs.push({ from: { database: d.key, schema: t.schema, table: t.name, column: c.name }, to });
2311
2236
  }
2312
- return map;
2237
+ return refs;
2313
2238
  }
2314
- function isManagedKey(key, managed) {
2315
- for (const pattern of managed) {
2316
- if (pattern.endsWith("*")) {
2317
- if (key.startsWith(pattern.slice(0, -1))) return true;
2318
- } else if (key === pattern) return true;
2239
+ function combineModels(dbs) {
2240
+ return {
2241
+ tables: dbs.flatMap((d) => (d.model?.tables ?? []).map((t) => ({ ...t, database: d.key }))),
2242
+ enums: dbs.flatMap((d) => (d.model?.enums ?? []).map((e) => ({ ...e, database: d.key }))),
2243
+ databases: dbs.map((d) => ({
2244
+ key: d.key,
2245
+ modules: d.modules,
2246
+ connection: d.connection,
2247
+ ...d.error ? { error: d.error } : {},
2248
+ ...d.migrations ? { migrations: d.migrations } : {}
2249
+ })),
2250
+ refs: resolveRefs(dbs)
2251
+ };
2252
+ }
2253
+
2254
+ // src/core/erd/introspect.ts
2255
+ var USER_SCHEMAS = `n.nspname not in ('pg_catalog', 'information_schema') and n.nspname not like 'pg_toast%' and n.nspname not like 'pg_temp%'`;
2256
+ var TABLE_KIND = `c.relkind in ('r', 'p') and not c.relispartition`;
2257
+ var TABLES = `
2258
+ select n.nspname as schema, c.relname as name, obj_description(c.oid, 'pg_class') as comment
2259
+ from pg_class c join pg_namespace n on n.oid = c.relnamespace
2260
+ where ${TABLE_KIND} and ${USER_SCHEMAS}
2261
+ order by 1, 2`;
2262
+ var COLUMNS = `
2263
+ select n.nspname as schema, c.relname as table, a.attname as name,
2264
+ format_type(a.atttypid, a.atttypmod) as type, a.attnotnull as not_null,
2265
+ pg_get_expr(d.adbin, d.adrelid) as "default", col_description(c.oid, a.attnum) as comment
2266
+ from pg_attribute a
2267
+ join pg_class c on c.oid = a.attrelid
2268
+ join pg_namespace n on n.oid = c.relnamespace
2269
+ left join pg_attrdef d on d.adrelid = a.attrelid and d.adnum = a.attnum
2270
+ where ${TABLE_KIND} and a.attnum > 0 and not a.attisdropped and ${USER_SCHEMAS}
2271
+ order by n.nspname, c.relname, a.attnum`;
2272
+ var CONSTRAINTS = `
2273
+ select n.nspname as schema, c.relname as table, con.conname as name, con.contype::text as type,
2274
+ (select array_agg(a.attname::text order by k.ord)
2275
+ from unnest(con.conkey) with ordinality as k(attnum, ord)
2276
+ join pg_attribute a on a.attrelid = con.conrelid and a.attnum = k.attnum) as columns,
2277
+ fn.nspname as target_schema, fc.relname as target_table,
2278
+ (select array_agg(a.attname::text order by k.ord)
2279
+ from unnest(con.confkey) with ordinality as k(attnum, ord)
2280
+ join pg_attribute a on a.attrelid = con.confrelid and a.attnum = k.attnum) as target_columns,
2281
+ con.confupdtype::text as on_update, con.confdeltype::text as on_delete,
2282
+ pg_get_constraintdef(con.oid) as definition
2283
+ from pg_constraint con
2284
+ join pg_class c on c.oid = con.conrelid
2285
+ join pg_namespace n on n.oid = c.relnamespace
2286
+ left join pg_class fc on fc.oid = con.confrelid
2287
+ left join pg_namespace fn on fn.oid = fc.relnamespace
2288
+ where con.contype in ('p', 'u', 'f', 'c') and not c.relispartition and ${USER_SCHEMAS}
2289
+ order by n.nspname, c.relname, con.conname`;
2290
+ var INDEXES = `
2291
+ select n.nspname as schema, c.relname as table, ic.relname as name, i.indisunique as "unique", am.amname as method,
2292
+ (select array_agg(pg_get_indexdef(i.indexrelid, k.n, true) order by k.n)
2293
+ from generate_series(1, i.indnkeyatts) as k(n)) as columns
2294
+ from pg_index i
2295
+ join pg_class c on c.oid = i.indrelid
2296
+ join pg_class ic on ic.oid = i.indexrelid
2297
+ join pg_namespace n on n.oid = c.relnamespace
2298
+ join pg_am am on am.oid = ic.relam
2299
+ where ${TABLE_KIND} and not i.indisprimary
2300
+ and not exists (select 1 from pg_constraint x where x.conindid = i.indexrelid and x.contype in ('p', 'u'))
2301
+ and ${USER_SCHEMAS}
2302
+ order by n.nspname, c.relname, ic.relname`;
2303
+ var ENUMS = `
2304
+ select n.nspname as schema, t.typname as name, array_agg(e.enumlabel::text order by e.enumsortorder) as "values"
2305
+ from pg_type t
2306
+ join pg_namespace n on n.oid = t.typnamespace
2307
+ join pg_enum e on e.enumtypid = t.oid
2308
+ where t.typtype = 'e' and ${USER_SCHEMAS}
2309
+ group by 1, 2 order by 1, 2`;
2310
+ var FK_ACTION = { a: "NO_ACTION", r: "RESTRICT", c: "CASCADE", n: "SET_NULL", d: "SET_DEFAULT" };
2311
+ var str = (v) => String(v);
2312
+ var strOrNull = (v) => v === null || v === void 0 ? null : String(v);
2313
+ function strArray(v) {
2314
+ if (Array.isArray(v)) return v.map(str);
2315
+ if (typeof v === "string" && v.startsWith("{") && v.endsWith("}")) {
2316
+ return v.length === 2 ? [] : v.slice(1, -1).split(",").map((s) => s.replace(/^"|"$/g, ""));
2319
2317
  }
2320
- return false;
2318
+ return [];
2321
2319
  }
2322
- function planEnvUpdate({
2323
- current,
2324
- fetched,
2325
- choices = {},
2326
- defaultChoice = "keep",
2327
- managed = []
2328
- }) {
2329
- const merged = new Map(current);
2330
- const conflicts = [];
2331
- const added = [];
2332
- const unchanged = [];
2333
- const overwritten = [];
2334
- for (const [key, portalValue] of Object.entries(fetched)) {
2335
- const hasLocal = current.has(key);
2336
- const localValue = current.get(key);
2337
- const localEmpty = !hasLocal || localValue === "" || localValue === '""' || localValue === "''";
2338
- if (localEmpty) {
2339
- merged.set(key, portalValue);
2340
- added.push(key);
2341
- continue;
2342
- }
2343
- if (localValue === portalValue) {
2344
- unchanged.push(key);
2345
- continue;
2346
- }
2347
- if (isManagedKey(key, managed)) {
2348
- merged.set(key, portalValue);
2349
- overwritten.push(key);
2350
- continue;
2320
+ var key = (r) => `${str(r.schema)}.${str(r.table)}`;
2321
+ var checkExpression = (definition) => definition.replace(/^CHECK \((.*)\)$/s, "$1");
2322
+ async function introspect(query) {
2323
+ const rows = [];
2324
+ for (const sql of [TABLES, COLUMNS, CONSTRAINTS, INDEXES, ENUMS]) rows.push((await query(sql)).rows);
2325
+ const [tables, columns, constraints, indexes, enums] = rows;
2326
+ const byKey = /* @__PURE__ */ new Map();
2327
+ for (const r of tables) {
2328
+ const t = {
2329
+ schema: str(r.schema),
2330
+ name: str(r.name),
2331
+ comment: strOrNull(r.comment),
2332
+ columns: [],
2333
+ primaryKey: null,
2334
+ uniques: [],
2335
+ foreignKeys: [],
2336
+ checks: [],
2337
+ indexes: []
2338
+ };
2339
+ byKey.set(`${t.schema}.${t.name}`, t);
2340
+ }
2341
+ for (const r of columns) {
2342
+ byKey.get(key(r))?.columns.push({
2343
+ name: str(r.name),
2344
+ type: str(r.type),
2345
+ notNull: Boolean(r.not_null),
2346
+ default: strOrNull(r.default),
2347
+ comment: strOrNull(r.comment)
2348
+ });
2349
+ }
2350
+ for (const r of constraints) {
2351
+ const t = byKey.get(key(r));
2352
+ if (!t) continue;
2353
+ const name = str(r.name);
2354
+ const cols = strArray(r.columns);
2355
+ switch (str(r.type)) {
2356
+ case "p":
2357
+ t.primaryKey = { name, columns: cols };
2358
+ break;
2359
+ case "u":
2360
+ t.uniques.push({ name, columns: cols });
2361
+ break;
2362
+ case "c":
2363
+ t.checks.push({ name, expression: checkExpression(str(r.definition)) });
2364
+ break;
2365
+ case "f":
2366
+ t.foreignKeys.push({
2367
+ name,
2368
+ columns: cols,
2369
+ targetSchema: str(r.target_schema),
2370
+ targetTable: str(r.target_table),
2371
+ targetColumns: strArray(r.target_columns),
2372
+ onUpdate: FK_ACTION[str(r.on_update)] ?? "NO_ACTION",
2373
+ onDelete: FK_ACTION[str(r.on_delete)] ?? "NO_ACTION"
2374
+ });
2375
+ break;
2351
2376
  }
2352
- conflicts.push(key);
2353
- const choice = choices[key] ?? defaultChoice;
2354
- merged.set(key, choice === "use-portal" ? portalValue : localValue);
2355
2377
  }
2356
- return { merged, conflicts, added, unchanged, overwritten };
2378
+ for (const r of indexes) {
2379
+ byKey.get(key(r))?.indexes.push({ name: str(r.name), unique: Boolean(r.unique), columns: strArray(r.columns), method: str(r.method) });
2380
+ }
2381
+ return {
2382
+ tables: [...byKey.values()],
2383
+ enums: enums.map((r) => ({ schema: str(r.schema), name: str(r.name), values: strArray(r.values) }))
2384
+ };
2357
2385
  }
2358
- function renderEnv(original, merged) {
2359
- const seen = /* @__PURE__ */ new Set();
2360
- const lines = original.split("\n");
2361
- const out = lines.map((line2) => {
2362
- const m = line2.replace(/\r$/, "").match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
2363
- if (!m?.[1]) return line2;
2364
- const key = m[1];
2365
- if (!merged.has(key)) return line2;
2366
- seen.add(key);
2367
- return `${key}=${merged.get(key) ?? ""}`;
2368
- });
2369
- const appended = [];
2370
- for (const [key, value] of merged) {
2371
- if (!seen.has(key)) appended.push(`${key}=${value}`);
2386
+
2387
+ // src/core/erd/migrations.ts
2388
+ import { readFile as readFile3 } from "fs/promises";
2389
+ import { join as join11 } from "path";
2390
+ var JOURNAL = join11("src", "db", "migrations", "meta", "_journal.json");
2391
+ async function readJournal(repoRoot) {
2392
+ let text2;
2393
+ try {
2394
+ text2 = await readFile3(join11(repoRoot, JOURNAL), "utf8");
2395
+ } catch {
2396
+ return null;
2372
2397
  }
2373
- let text2 = out.join("\n");
2374
- if (appended.length) {
2375
- if (text2.length && !text2.endsWith("\n")) text2 += "\n";
2376
- text2 += appended.join("\n") + "\n";
2398
+ try {
2399
+ return JSON.parse(text2).entries ?? [];
2400
+ } catch {
2401
+ return null;
2377
2402
  }
2378
- return text2;
2403
+ }
2404
+ async function appliedMigrations(query) {
2405
+ const { rows: [ledger] } = await query(`select to_regclass('drizzle.__drizzle_migrations') is not null as present`);
2406
+ if (!ledger?.present) return [];
2407
+ const { rows } = await query(`select created_at::text as at from drizzle.__drizzle_migrations`);
2408
+ return rows.map((r) => Number(r.at));
2409
+ }
2410
+ function compareMigrations(journal, applied) {
2411
+ const latest = applied.reduce((a, b) => Math.max(a, b), 0);
2412
+ const done = new Set(applied);
2413
+ const known = new Set(journal.map((e) => e.when));
2414
+ const missing = journal.filter((e) => !done.has(e.when));
2415
+ return {
2416
+ pending: missing.filter((e) => e.when > latest).map((e) => e.tag),
2417
+ skipped: missing.filter((e) => e.when <= latest).map((e) => e.tag),
2418
+ unknown: applied.filter((a) => !known.has(a)).length
2419
+ };
2420
+ }
2421
+ async function checkMigrations(repoRoot, query) {
2422
+ const journal = repoRoot ? await readJournal(repoRoot) : null;
2423
+ return journal ? compareMigrations(journal, await appliedMigrations(query)) : void 0;
2379
2424
  }
2380
2425
 
2381
- // src/core/env/pull.ts
2426
+ // src/core/context/repoConfig.ts
2427
+ import { readFile as readFile4 } from "fs/promises";
2428
+ import { join as join12 } from "path";
2429
+ var FORBIDDEN = ["portal", "api", "apiOrigin", "issuer", "tenant", "tenantId", "app", "appId", "module", "moduleId"];
2430
+ var KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
2431
+ var SCRIPT = /^[A-Za-z0-9:_.-]+$/;
2432
+ var invalid3 = (msg, details) => new QoreError("QORE_CONTEXT_INVALID", `.qore.json: ${msg}`, { hint: "Fix .qore.json in the repository root; see the $schema it names.", details });
2433
+ function moduleDefaults(type) {
2434
+ return type === "frontend" ? { envFile: ".env.local", localSecrets: ["SESSION_COOKIE_SECRET"] } : { envFile: ".env", localSecrets: [] };
2435
+ }
2436
+ async function readRepoConfig(root) {
2437
+ let raw;
2438
+ try {
2439
+ raw = await readFile4(join12(root, ".qore.json"), "utf8");
2440
+ } catch (e) {
2441
+ if (e.code === "ENOENT") return {};
2442
+ throw e;
2443
+ }
2444
+ let doc;
2445
+ try {
2446
+ doc = JSON.parse(raw);
2447
+ } catch {
2448
+ throw invalid3("not valid JSON");
2449
+ }
2450
+ if (!doc || typeof doc !== "object" || Array.isArray(doc)) throw invalid3("must be an object");
2451
+ const v = doc.schemaVersion === void 0 ? 1 : doc.schemaVersion;
2452
+ if (!Number.isInteger(v) || v < 1) throw invalid3("schemaVersion must be a positive integer");
2453
+ if (v > 1) {
2454
+ throw new QoreError("QORE_CONTEXT_UNSUPPORTED", `.qore.json schemaVersion ${v} is newer than this qore`, { hint: "Run: npm i -g @quatronic/qore" });
2455
+ }
2456
+ for (const f of FORBIDDEN) if (f in doc) throw invalid3(`"${f}" is not allowed \u2014 .qore.json never selects a portal or a resource`, { field: f });
2457
+ const out = {};
2458
+ if (doc.envFile !== void 0) {
2459
+ if (typeof doc.envFile !== "string" || !doc.envFile) throw invalid3("envFile must be a non-empty string");
2460
+ out.envFile = doc.envFile;
2461
+ }
2462
+ if (doc.localSecrets !== void 0) {
2463
+ if (!Array.isArray(doc.localSecrets) || !doc.localSecrets.every((k) => typeof k === "string" && KEY.test(k))) {
2464
+ throw invalid3("localSecrets must be a list of env key names");
2465
+ }
2466
+ if (new Set(doc.localSecrets).size !== doc.localSecrets.length) throw invalid3("localSecrets must not contain duplicates");
2467
+ out.localSecrets = doc.localSecrets;
2468
+ }
2469
+ if (doc.dev !== void 0) {
2470
+ const d = doc.dev;
2471
+ if (!d || typeof d !== "object" || Array.isArray(d)) throw invalid3("dev must be an object");
2472
+ const dev = {};
2473
+ if (d.install !== void 0) {
2474
+ if (typeof d.install !== "boolean") throw invalid3("dev.install must be true or false");
2475
+ dev.install = d.install;
2476
+ }
2477
+ if (d.services !== void 0) {
2478
+ if (!Array.isArray(d.services) || !d.services.every((s) => typeof s === "string" && SCRIPT.test(s))) {
2479
+ throw invalid3("dev.services must be a list of script names");
2480
+ }
2481
+ if (new Set(d.services).size !== d.services.length) throw invalid3("dev.services must not contain duplicates");
2482
+ dev.services = d.services;
2483
+ }
2484
+ if (d.script !== void 0) {
2485
+ if (typeof d.script !== "string" || !SCRIPT.test(d.script)) throw invalid3("dev.script must be a script name");
2486
+ dev.script = d.script;
2487
+ }
2488
+ out.dev = dev;
2489
+ }
2490
+ return out;
2491
+ }
2492
+
2493
+ // src/core/env/fetch.ts
2494
+ async function fetchLocalEnv(deps2, mc, app) {
2495
+ await ensureCapability(deps2, mc.portal.apiOrigin, "module.local-env");
2496
+ try {
2497
+ const local = await mc.session.api.get(
2498
+ `/api/cli/v1/modules/${encodeURIComponent(mc.module.id)}/local-env?app_id=${encodeURIComponent(app.id)}`
2499
+ );
2500
+ deps2.registerSecrets(Object.values(local.env));
2501
+ return local;
2502
+ } catch (e) {
2503
+ if (!(e instanceof QoreError) || e.code !== "QORE_ACCESS_DENIED") throw e;
2504
+ if (e.serverCode === "app.access.denied") {
2505
+ throw withDetails(
2506
+ e,
2507
+ { required: "write", have: mc.module.access },
2508
+ `You have ${mc.module.access} on ${mc.module.slug}; local secrets need write. Ask a maintainer of the app's dev team for write access.`
2509
+ );
2510
+ }
2511
+ if (e.serverCode === "auth.scope.insufficient") throw withDetails(e, {}, "Run `qore login` again.");
2512
+ throw e;
2513
+ }
2514
+ }
2515
+
2516
+ // src/core/env/merge.ts
2517
+ function parseEnv(contents) {
2518
+ const map = /* @__PURE__ */ new Map();
2519
+ for (const line2 of contents.split("\n")) {
2520
+ const m = line2.replace(/\r$/, "").match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
2521
+ if (m?.[1] !== void 0 && m[2] !== void 0) map.set(m[1], m[2]);
2522
+ }
2523
+ return map;
2524
+ }
2525
+ function isManagedKey(key2, managed) {
2526
+ for (const pattern of managed) {
2527
+ if (pattern.endsWith("*")) {
2528
+ if (key2.startsWith(pattern.slice(0, -1))) return true;
2529
+ } else if (key2 === pattern) return true;
2530
+ }
2531
+ return false;
2532
+ }
2533
+ function planEnvUpdate({
2534
+ current,
2535
+ fetched,
2536
+ choices = {},
2537
+ defaultChoice = "keep",
2538
+ managed = []
2539
+ }) {
2540
+ const merged = new Map(current);
2541
+ const conflicts = [];
2542
+ const added = [];
2543
+ const unchanged = [];
2544
+ const overwritten = [];
2545
+ for (const [key2, portalValue] of Object.entries(fetched)) {
2546
+ const hasLocal = current.has(key2);
2547
+ const localValue = current.get(key2);
2548
+ const localEmpty = !hasLocal || localValue === "" || localValue === '""' || localValue === "''";
2549
+ if (localEmpty) {
2550
+ merged.set(key2, portalValue);
2551
+ added.push(key2);
2552
+ continue;
2553
+ }
2554
+ if (localValue === portalValue) {
2555
+ unchanged.push(key2);
2556
+ continue;
2557
+ }
2558
+ if (isManagedKey(key2, managed)) {
2559
+ merged.set(key2, portalValue);
2560
+ overwritten.push(key2);
2561
+ continue;
2562
+ }
2563
+ conflicts.push(key2);
2564
+ const choice = choices[key2] ?? defaultChoice;
2565
+ merged.set(key2, choice === "use-portal" ? portalValue : localValue);
2566
+ }
2567
+ return { merged, conflicts, added, unchanged, overwritten };
2568
+ }
2569
+ function renderEnv(original, merged) {
2570
+ const seen = /* @__PURE__ */ new Set();
2571
+ const lines = original.split("\n");
2572
+ const out = lines.map((line2) => {
2573
+ const m = line2.replace(/\r$/, "").match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
2574
+ if (!m?.[1]) return line2;
2575
+ const key2 = m[1];
2576
+ if (!merged.has(key2)) return line2;
2577
+ seen.add(key2);
2578
+ return `${key2}=${merged.get(key2) ?? ""}`;
2579
+ });
2580
+ const appended = [];
2581
+ for (const [key2, value] of merged) {
2582
+ if (!seen.has(key2)) appended.push(`${key2}=${value}`);
2583
+ }
2584
+ let text2 = out.join("\n");
2585
+ if (appended.length) {
2586
+ if (text2.length && !text2.endsWith("\n")) text2 += "\n";
2587
+ text2 += appended.join("\n") + "\n";
2588
+ }
2589
+ return text2;
2590
+ }
2591
+
2592
+ // src/core/env/pull.ts
2382
2593
  import { lstat, realpath as realpath5 } from "fs/promises";
2383
2594
  import { basename, dirname as dirname4, isAbsolute as isAbsolute3, relative as relative5, resolve as resolve5, sep as sep2 } from "path";
2384
2595
  var outside = (target) => new QoreError("QORE_CONTEXT_INVALID", `The env file must be inside the repository (got ${target})`, {
@@ -2424,11 +2635,11 @@ function planPull(o) {
2424
2635
  const r = planEnvUpdate({ current, fetched: o.fetched, choices: o.choices ?? {}, defaultChoice: o.defaultChoice, managed: o.managed });
2425
2636
  const choiceOf = (k) => o.choices?.[k] ?? o.defaultChoice;
2426
2637
  const generated = [];
2427
- for (const key of o.localSecrets) {
2428
- const v = r.merged.get(key);
2638
+ for (const key2 of o.localSecrets) {
2639
+ const v = r.merged.get(key2);
2429
2640
  if (v === void 0 || v === "" || v === '""' || v === "''") {
2430
- r.merged.set(key, o.random());
2431
- generated.push(key);
2641
+ r.merged.set(key2, o.random());
2642
+ generated.push(key2);
2432
2643
  }
2433
2644
  }
2434
2645
  return {
@@ -2446,50 +2657,720 @@ function planPull(o) {
2446
2657
  }
2447
2658
 
2448
2659
  // src/core/env/write.ts
2449
- import { chmod as chmod2, readFile as readFile4, rename as rename3, unlink, writeFile as writeFile4 } from "fs/promises";
2660
+ import { chmod as chmod2, readFile as readFile5, rename as rename3, unlink, writeFile as writeFile4 } from "fs/promises";
2450
2661
  import { randomBytes as randomBytes4 } from "crypto";
2451
- import { basename as basename2, dirname as dirname5, join as join12 } from "path";
2662
+ import { basename as basename2, dirname as dirname5, join as join13 } from "path";
2452
2663
  async function readOrNull(p) {
2453
2664
  try {
2454
- return await readFile4(p, "utf8");
2665
+ return await readFile5(p, "utf8");
2455
2666
  } catch (e) {
2456
2667
  if (e.code === "ENOENT") return null;
2457
2668
  throw e;
2458
2669
  }
2459
- }
2460
- async function writeSecurely(target, text2) {
2461
- const base = basename2(target);
2462
- const tmpName = base.endsWith(".local") ? `${base.slice(0, -".local".length)}.qore-${randomBytes4(6).toString("hex")}.local` : `${base}.qore-${randomBytes4(6).toString("hex")}.tmp`;
2463
- const tmp = join12(dirname5(target), tmpName);
2464
- try {
2465
- await writeFile4(tmp, text2, { mode: 384, flag: "wx" });
2466
- await chmod2(tmp, 384);
2467
- await rename3(tmp, target);
2468
- } finally {
2469
- await unlink(tmp).catch(() => {
2470
- });
2670
+ }
2671
+ async function writeSecurely(target, text2) {
2672
+ const base = basename2(target);
2673
+ const tmpName = base.endsWith(".local") ? `${base.slice(0, -".local".length)}.qore-${randomBytes4(6).toString("hex")}.local` : `${base}.qore-${randomBytes4(6).toString("hex")}.tmp`;
2674
+ const tmp = join13(dirname5(target), tmpName);
2675
+ try {
2676
+ await writeFile4(tmp, text2, { mode: 384, flag: "wx" });
2677
+ await chmod2(tmp, 384);
2678
+ await rename3(tmp, target);
2679
+ } finally {
2680
+ await unlink(tmp).catch(() => {
2681
+ });
2682
+ }
2683
+ }
2684
+ async function syncEnvFile(o) {
2685
+ const existing = await readOrNull(o.target);
2686
+ const plan = planPull({
2687
+ existing,
2688
+ example: existing === null ? o.example : null,
2689
+ fetched: o.fetched,
2690
+ managed: o.managed,
2691
+ localSecrets: o.localSecrets,
2692
+ defaultChoice: "keep",
2693
+ random: o.random
2694
+ });
2695
+ if (existing !== null && plan.text === existing) await chmod2(o.target, 384);
2696
+ else await writeSecurely(o.target, plan.text);
2697
+ return { created: plan.created, kept: plan.kept };
2698
+ }
2699
+
2700
+ // src/core/erd/source.ts
2701
+ function noDatabase(slug) {
2702
+ return new QoreError("QORE_CONTEXT_UNSUPPORTED", `${slug} has no database`, {
2703
+ hint: "qore db erd shows a module's own Postgres (DATABASE_URL); this module declares none."
2704
+ });
2705
+ }
2706
+ async function resolveDatabaseUrl(deps2, mc, app) {
2707
+ if (mc.repoRoot) {
2708
+ const cfg = await readRepoConfig(mc.repoRoot);
2709
+ const envFile = await resolveTarget(mc.repoRoot, cfg.envFile ?? moduleDefaults(mc.module.type).envFile);
2710
+ const text2 = await readOrNull(envFile);
2711
+ const url2 = text2 === null ? void 0 : parseEnv(text2).get("DATABASE_URL")?.trim().replace(/^["']|["']$/g, "");
2712
+ if (url2) {
2713
+ deps2.registerSecrets([url2]);
2714
+ return { url: url2, source: "env-file", envFile };
2715
+ }
2716
+ }
2717
+ const local = await fetchLocalEnv(deps2, mc, app);
2718
+ const url = local.env.DATABASE_URL?.trim();
2719
+ if (!url) throw noDatabase(mc.module.slug);
2720
+ return { url, source: "portal", envFile: null };
2721
+ }
2722
+
2723
+ // src/platform/httpServe.ts
2724
+ import { readFile as readFile6 } from "fs/promises";
2725
+ import { createServer } from "http";
2726
+ import { extname, resolve as resolve6, sep as sep3 } from "path";
2727
+ var TYPES = {
2728
+ ".html": "text/html; charset=utf-8",
2729
+ ".js": "text/javascript; charset=utf-8",
2730
+ ".css": "text/css; charset=utf-8",
2731
+ ".json": "application/json",
2732
+ ".ico": "image/x-icon",
2733
+ ".png": "image/png",
2734
+ ".svg": "image/svg+xml",
2735
+ ".woff": "font/woff",
2736
+ ".woff2": "font/woff2"
2737
+ };
2738
+ async function serveErd(o) {
2739
+ const root = resolve6(o.dir);
2740
+ let port = 0;
2741
+ const server = createServer((req, res) => {
2742
+ void handle(req, res);
2743
+ });
2744
+ await new Promise((ok2, fail) => {
2745
+ server.once("error", fail);
2746
+ server.listen(o.port, "127.0.0.1", () => {
2747
+ server.off("error", fail);
2748
+ ok2();
2749
+ });
2750
+ });
2751
+ port = server.address().port;
2752
+ async function handle(req, res) {
2753
+ try {
2754
+ await route(req, res);
2755
+ } catch (e) {
2756
+ if (res.headersSent) res.destroy();
2757
+ else end(res, e instanceof TypeError ? 400 : 500);
2758
+ }
2759
+ }
2760
+ async function route(req, res) {
2761
+ const host = req.headers.host;
2762
+ if (host !== `127.0.0.1:${port}` && host !== `localhost:${port}`) return end(res, 403);
2763
+ if (req.method !== "GET" && req.method !== "HEAD") return end(res, 405);
2764
+ const path = new URL(req.url ?? "/", "http://127.0.0.1").pathname;
2765
+ const produce = o.routes[path];
2766
+ if (produce) {
2767
+ try {
2768
+ const body = JSON.stringify(await produce());
2769
+ res.writeHead(200, { "content-type": TYPES[".json"], "cache-control": "no-store" });
2770
+ res.end(body);
2771
+ return;
2772
+ } catch (e) {
2773
+ try {
2774
+ o.onError(e);
2775
+ } catch {
2776
+ }
2777
+ res.writeHead(500, { "content-type": TYPES[".json"] });
2778
+ res.end(JSON.stringify({ error: e instanceof Error ? e.message : String(e) }));
2779
+ return;
2780
+ }
2781
+ }
2782
+ const file = resolve6(root, `.${path === "/" ? "/index.html" : path}`);
2783
+ if (file !== root && !file.startsWith(root + sep3)) return end(res, 404);
2784
+ try {
2785
+ const body = await readFile6(file);
2786
+ res.writeHead(200, { "content-type": TYPES[extname(file)] ?? "application/octet-stream" });
2787
+ res.end(body);
2788
+ } catch {
2789
+ end(res, 404);
2790
+ }
2791
+ }
2792
+ return {
2793
+ port,
2794
+ close: () => new Promise((ok2) => {
2795
+ server.closeAllConnections();
2796
+ server.close(() => ok2());
2797
+ })
2798
+ };
2799
+ }
2800
+ function end(res, status2) {
2801
+ res.writeHead(status2);
2802
+ res.end();
2803
+ }
2804
+
2805
+ // src/cli/context.ts
2806
+ var allOfKind = (c, kind) => !!c && c.length > 0 && c.every((x) => x.kind === kind && typeof x.id === "string");
2807
+ async function pick(ctx, err, message, choices) {
2808
+ if (!ctx.io.interactive) throw err;
2809
+ return ctx.io.select(message, choices);
2810
+ }
2811
+ async function moduleContextFor(ctx, flags, command) {
2812
+ const g = ctx.opts();
2813
+ const onRequest = g.verbose ? (l) => ctx.io.err(l) : void 0;
2814
+ const base = { portal: g.portal, module: flags.module, command, onRequest };
2815
+ try {
2816
+ return await resolveModuleContext(ctx.deps, base);
2817
+ } catch (e) {
2818
+ if (!(e instanceof QoreError) || e.code !== "QORE_CONTEXT_REQUIRED" || !ctx.io.interactive) throw e;
2819
+ if (allOfKind(e.candidates, "module")) {
2820
+ const choose2 = await pick(
2821
+ ctx,
2822
+ e,
2823
+ "Select a module",
2824
+ e.candidates.map((c) => ({ name: `${c.tenant ? `${c.tenant} / ` : ""}${c.slug} (${c.remote})`, value: c.id }))
2825
+ );
2826
+ return resolveModuleContext(ctx.deps, { ...base, choose: choose2 });
2827
+ }
2828
+ if (allOfKind(e.candidates, "tenant")) {
2829
+ const tenant = await pick(ctx, e, "Select a tenant", e.candidates.map((c) => ({ name: c.name ?? c.slug, value: c.id })));
2830
+ return resolveModuleContext(ctx.deps, { ...base, tenant });
2831
+ }
2832
+ throw e;
2833
+ }
2834
+ }
2835
+ async function appFor(ctx, mc, app) {
2836
+ try {
2837
+ return pickApp2(mc, app);
2838
+ } catch (e) {
2839
+ if (!(e instanceof QoreError) || e.code !== "QORE_APP_AMBIGUOUS" || !ctx.io.interactive) throw e;
2840
+ const id = await pick(ctx, e, "Select an app", mc.module.apps.map((a) => ({ name: a.name, value: a.id })));
2841
+ return pickApp2(mc, id);
2842
+ }
2843
+ }
2844
+ function sourceText(mc) {
2845
+ if (mc.source.kind === "remote") return mc.source.remote;
2846
+ if (mc.source.kind === "workspace") return WORKSPACE_FILE;
2847
+ return mc.source.overrides ? `--module, overrides ${mc.source.overrides}` : "--module";
2848
+ }
2849
+ function contextLine(mc, app) {
2850
+ const parts = [mc.module.tenant.name, ...app ? [app.name] : [], mc.module.slug];
2851
+ return `\u2192 ${parts.join(" / ")} (${sourceText(mc)})`;
2852
+ }
2853
+ function contextJson(mc, app) {
2854
+ const { id, slug, name, type, access: access2 } = mc.module;
2855
+ return {
2856
+ portal: mc.portal.builtin ?? mc.portal.apiOrigin,
2857
+ tenant: mc.module.tenant,
2858
+ app: app ?? null,
2859
+ module: { id, slug, name, type, access: access2 },
2860
+ source: mc.source.kind === "remote" ? mc.source.remote : mc.source.kind === "workspace" ? WORKSPACE_FILE : "--module",
2861
+ ...mc.source.kind === "flag" && mc.source.overrides ? { overrides: mc.source.overrides } : {}
2862
+ };
2863
+ }
2864
+
2865
+ // src/cli/project.ts
2866
+ import { readdir as readdir2, stat as stat3 } from "fs/promises";
2867
+ import { join as join14 } from "path";
2868
+
2869
+ // src/core/projects/match.ts
2870
+ function readings(ref) {
2871
+ const s = ref.split("/").map((x) => x.trim());
2872
+ if (s.some((x) => x === "")) return [];
2873
+ if (s.length === 1) return [{ app: s[0] }];
2874
+ if (s.length === 2) return [{ app: s[0], module: s[1] }, { tenant: s[0], app: s[1] }];
2875
+ if (s.length === 3) return [{ tenant: s[0], app: s[1], module: s[2] }];
2876
+ return [];
2877
+ }
2878
+ function hit(p, w) {
2879
+ if (p.appSlug !== w.app || w.tenant !== void 0 && p.tenantSlug !== w.tenant) return null;
2880
+ if (p.kind === "module") return w.module === p.moduleSlug ? { project: p } : null;
2881
+ return w.module ? { project: p, module: w.module } : { project: p };
2882
+ }
2883
+ function matchProjects(projects, ref) {
2884
+ const out = [];
2885
+ for (const w of readings(ref)) {
2886
+ for (const p of projects) {
2887
+ const m = hit(p, w);
2888
+ if (m) out.push(m);
2889
+ }
2890
+ }
2891
+ return out;
2892
+ }
2893
+
2894
+ // src/cli/project.ts
2895
+ async function loggedInAs(ctx) {
2896
+ const cfg = await ctx.deps.config.read();
2897
+ const portal = resolvePortal(ctx.opts().portal, cfg.defaultProfile);
2898
+ const p = cfg.profiles[portal.apiOrigin];
2899
+ if (!p || !await new TokenStore(ctx.deps.keychain, profileHash(p.apiOrigin, p.issuer)).getRefresh()) return null;
2900
+ return p.email ?? p.sub ?? portal.apiOrigin;
2901
+ }
2902
+ var isDir = async (p) => (await stat3(p).catch(() => null))?.isDirectory() ?? false;
2903
+ var label2 = (p) => `${p.tenantSlug} / ${p.appSlug}${p.kind === "module" ? ` / ${p.moduleSlug}` : ""}${p.source === "registry" ? ` (${p.path})` : ""}`;
2904
+ var candidate = (p) => p.kind === "module" ? { kind: "module", id: p.moduleId, slug: p.moduleSlug, tenant: p.tenantSlug, name: p.path } : { kind: "app", slug: p.appSlug, tenant: p.tenantSlug, name: p.path };
2905
+ var withCwd = (ctx, cwd) => ({ ...ctx, deps: { ...ctx.deps, cwd } });
2906
+ async function standingIn(ctx) {
2907
+ if (await findWorkspace(ctx.deps.cwd, ctx.deps.home)) return "workspace";
2908
+ const repo = await findRepoRoot(ctx.deps.cwd);
2909
+ return repo !== null && !isInside(repo, ctx.deps.home) ? { repo } : null;
2910
+ }
2911
+ async function isForeign(ctx, command) {
2912
+ const g = ctx.opts();
2913
+ try {
2914
+ await resolveModuleContext(ctx.deps, { portal: g.portal, command, onRequest: g.verbose ? (l) => ctx.io.err(l) : void 0 });
2915
+ return false;
2916
+ } catch (e) {
2917
+ if (e instanceof CancelledError) throw e;
2918
+ return isForeignRepo(e);
2919
+ }
2920
+ }
2921
+ async function moduleDirs(wsPath) {
2922
+ const out = [];
2923
+ for (const d of await readdir2(wsPath, { withFileTypes: true }).catch(() => [])) {
2924
+ if (d.isDirectory() && !d.name.startsWith(".") && await stat3(join14(wsPath, d.name, ".git")).catch(() => null)) out.push(d.name);
2925
+ }
2926
+ return out.sort();
2927
+ }
2928
+ async function pathFor(ctx, m, needModule) {
2929
+ const p = m.project;
2930
+ if (p.kind === "module") return p.path;
2931
+ if (m.module) {
2932
+ const dir2 = join14(p.path, m.module);
2933
+ if (!await isDir(dir2)) {
2934
+ throw new QoreError("QORE_PROJECT_NOT_FOUND", `${p.appSlug} has no checkout of module ${m.module} in ${p.path}`, {
2935
+ hint: `Run \`qore sync\` in ${p.path} to clone the app's missing modules.`
2936
+ });
2937
+ }
2938
+ return dir2;
2939
+ }
2940
+ if (!needModule) return p.path;
2941
+ const dirs = await moduleDirs(p.path);
2942
+ if (dirs.length === 1) return join14(p.path, dirs[0]);
2943
+ if (dirs.length === 0) {
2944
+ throw new QoreError("QORE_PROJECT_NOT_FOUND", `${p.appSlug} has no module checkouts in ${p.path}`, {
2945
+ hint: `Run \`qore sync\` in ${p.path}.`
2946
+ });
2947
+ }
2948
+ if (!ctx.io.interactive) {
2949
+ throw new QoreError("QORE_CONTEXT_REQUIRED", `${p.appSlug} has several modules \u2014 which one?`, {
2950
+ hint: `Pass ${p.appSlug}/<module>.`,
2951
+ candidates: dirs.map((slug) => ({ kind: "module", slug, tenant: p.tenantSlug, name: join14(p.path, slug) }))
2952
+ });
2953
+ }
2954
+ return join14(p.path, await ctx.io.select("Select a module", dirs.map((d) => ({ name: d, value: d }))));
2955
+ }
2956
+ async function liveMatches(matches) {
2957
+ const live = [];
2958
+ for (const m of matches) {
2959
+ if (m.project.kind === "workspace" && m.module && !await isDir(join14(m.project.path, m.module))) continue;
2960
+ live.push(m);
2961
+ }
2962
+ return live.length > 0 ? live : matches;
2963
+ }
2964
+ async function choose(ctx, matches, what, explicit) {
2965
+ if (explicit && matches.length === 1) return matches[0];
2966
+ if (!ctx.io.interactive) {
2967
+ throw new QoreError("QORE_CONTEXT_REQUIRED", what, {
2968
+ hint: "Pass a more specific reference: tenant/app or app/module.",
2969
+ candidates: matches.map((m) => candidate(m.project))
2970
+ });
2971
+ }
2972
+ const i = await ctx.io.select("Select a project", matches.map((m, idx) => ({
2973
+ name: m.module ? `${label2(m.project)} / ${m.module}` : label2(m.project),
2974
+ value: idx
2975
+ })));
2976
+ return matches[i];
2977
+ }
2978
+ async function projectCtx(ctx, o) {
2979
+ const g = ctx.opts();
2980
+ const cfg = await ctx.deps.config.read();
2981
+ const portal = g.portal ? resolvePortal(g.portal, cfg.defaultProfile).apiOrigin : void 0;
2982
+ if (o.ref) {
2983
+ const matches = matchProjects(await knownProjects(ctx.deps, { portal }), o.ref);
2984
+ if (matches.length === 0) {
2985
+ const parts = o.ref.split("/");
2986
+ const app = parts.length === 1 ? o.ref : parts.length === 3 ? parts[1] : "<app>";
2987
+ throw new QoreError("QORE_PROJECT_NOT_FOUND", `No local project "${o.ref}"`, {
2988
+ hint: cfg.projectsRoot ? `Run \`qore clone ${app}\`.` : `Run \`qore init\`, or \`qore clone ${app}\`.`
2989
+ });
2990
+ }
2991
+ const m2 = await choose(ctx, await liveMatches(matches), `"${o.ref}" matches several projects`, true);
2992
+ return relocate(ctx, await pathFor(ctx, m2, o.needModule));
2993
+ }
2994
+ if (o.module || o.app) return ctx;
2995
+ const here = await standingIn(ctx);
2996
+ if (here === "workspace") return ctx;
2997
+ const known = await knownProjects(ctx.deps, { portal });
2998
+ if (here && (known.length === 0 || known.some((p) => isInside(p.path, here.repo)) || !await isForeign(ctx, o.command ?? "project"))) return ctx;
2999
+ if (known.length === 0) {
3000
+ if (!cfg.projectsRoot) {
3001
+ if (!await loggedInAs(ctx)) return ctx;
3002
+ throw new QoreError("QORE_ROOT_NOT_SET", "Not inside a project, and qore knows no projects yet", {
3003
+ hint: "Run `qore init` to choose a projects folder, or cd into a project."
3004
+ });
3005
+ }
3006
+ throw new QoreError("QORE_PROJECT_NOT_FOUND", `Not inside a project, and ${cfg.projectsRoot} holds none yet`, {
3007
+ hint: "Run `qore clone <app>`."
3008
+ });
3009
+ }
3010
+ const what = here ? "This repository is not a Qore module \u2014 which project?" : "Not inside a project \u2014 which one?";
3011
+ const m = await choose(ctx, known.map((project) => ({ project })), what, false);
3012
+ return relocate(ctx, await pathFor(ctx, m, o.needModule));
3013
+ }
3014
+ function relocate(ctx, path) {
3015
+ if (path === ctx.deps.cwd) return ctx;
3016
+ if (!ctx.opts().json) ctx.io.err(`\u2192 in ${path}`);
3017
+ return withCwd(ctx, path);
3018
+ }
3019
+ async function rememberContext(ctx, mc, app) {
3020
+ const tenantSlug = mc.module.tenant.slug;
3021
+ if (mc.workspace && mc.workspace.appId === app.id) {
3022
+ await remember(ctx.deps, {
3023
+ kind: "workspace",
3024
+ path: mc.workspace.root,
3025
+ portal: mc.workspace.portal,
3026
+ tenantSlug,
3027
+ appId: app.id,
3028
+ appSlug: app.slug
3029
+ });
3030
+ return;
3031
+ }
3032
+ if (mc.repoRoot) {
3033
+ await remember(ctx.deps, {
3034
+ kind: "module",
3035
+ path: mc.repoRoot,
3036
+ portal: mc.portal.apiOrigin,
3037
+ tenantSlug,
3038
+ appId: app.id,
3039
+ appSlug: app.slug,
3040
+ moduleId: mc.module.id,
3041
+ moduleSlug: mc.module.slug
3042
+ });
3043
+ }
3044
+ }
3045
+ async function rememberWorkspace(ctx, ws, app) {
3046
+ if (ws.appId !== app.id) return;
3047
+ await remember(ctx.deps, { kind: "workspace", path: ws.root, portal: ws.portal, tenantSlug: app.tenant.slug, appId: app.id, appSlug: app.slug });
3048
+ }
3049
+
3050
+ // src/cli/trustPrompt.ts
3051
+ async function ensureTrusted(ctx, mc, app, o) {
3052
+ const root = mc.repoRoot ?? ctx.deps.cwd;
3053
+ if (!ctx.opts().json) ctx.io.err(contextLine(mc, app));
3054
+ if (await isTrusted(ctx.deps, root, mc.module.id)) return;
3055
+ if (o.yes) {
3056
+ await trust(ctx.deps, root, mc.module.id);
3057
+ return;
3058
+ }
3059
+ if (!ctx.io.interactive) {
3060
+ throw new QoreError("QORE_CONTEXT_REQUIRED", `This repository is not trusted for ${mc.module.slug} yet`, {
3061
+ hint: "Run interactively once, or pass --yes.",
3062
+ details: { reason: "untrusted", repository: root }
3063
+ });
3064
+ }
3065
+ if (mc.source.kind === "remote") {
3066
+ const key2 = repoKey(mc.source.url);
3067
+ ctx.io.err(` from remote ${mc.source.remote}: ${key2 ?? "(unparsable remote URL)"}`);
3068
+ }
3069
+ if (!await ctx.io.confirm("Pull local secrets for this module into this repository?")) throw new CancelledError();
3070
+ await trust(ctx.deps, root, mc.module.id);
3071
+ }
3072
+
3073
+ // src/cli/commands/db.ts
3074
+ var isDir2 = async (p) => stat4(p).then((s) => s.isDirectory(), () => false);
3075
+ function parsePort(raw) {
3076
+ const n = /^\d+$/.test(raw) ? Number(raw) : NaN;
3077
+ if (!Number.isInteger(n) || n < 1 || n > 65535) throw new InvalidArgumentError("a port is a number from 1 to 65535");
3078
+ return n;
3079
+ }
3080
+ var aborted = (signal) => signal.aborted ? Promise.resolve() : new Promise((r) => signal.addEventListener("abort", () => r(), { once: true }));
3081
+ var plural = (n) => `${n} migration${n === 1 ? "" : "s"}`;
3082
+ function migrationLines(m, who) {
3083
+ if (!m) return [];
3084
+ const lines = [];
3085
+ if (m.pending.length) lines.push(`${who} lacks ${plural(m.pending.length)} of this checkout (${m.pending.join(", ")}); they apply on the next start of \`qore dev\`.`);
3086
+ if (m.skipped.length) lines.push(`${who} lacks ${plural(m.skipped.length)} older than its latest (${m.skipped.join(", ")}); Drizzle never applies those, so reset it with \`pnpm db:reset\`.`);
3087
+ if (m.unknown) lines.push(`${who} has ${plural(m.unknown)} this checkout does not know; applied from another branch?`);
3088
+ return lines;
3089
+ }
3090
+ var portInUse = (port) => new QoreError("QORE_PORT_IN_USE", `Port ${port} is already in use`, { hint: "Pick another --port, or leave it out to get a free one." });
3091
+ function registerDb(program, ctx) {
3092
+ const db = program.command("db").description("Work with the local databases of a module or an app");
3093
+ db.command("erd").description("Show this module's data model in the browser \u2014 or, in a workspace root, the whole app's").argument("[project]", "app or app/module to show, from anywhere (see qore init)").option("--module <slug|id>", "the module (default: from this repository's git remote)").option("--app <slug|id>", "the app, when the module is part of several").option("--project <ref>", "same as the [project] argument").option("--yes", "trust this repository without asking").option("--port <n>", "listen on this port (default: a free one)", parsePort).option("--no-open", "print the URL, do not open a browser").action(async (project, f) => {
3094
+ if (project !== void 0 && f.project !== void 0 && project !== f.project) {
3095
+ throw new QoreError("QORE_CONTEXT_INVALID", `Pass the project once: "${project}" and --project "${f.project}" differ`, {
3096
+ hint: "qore db erd <project> | qore db erd --project <project>"
3097
+ });
3098
+ }
3099
+ await erd(ctx, { ...f, project: project ?? f.project });
3100
+ });
3101
+ }
3102
+ async function erd(outer, f) {
3103
+ const json2 = Boolean(outer.opts().json);
3104
+ if (json2 && (f.port !== void 0 || !f.open)) {
3105
+ throw new QoreError("QORE_CONTEXT_INVALID", "--json writes the model and starts no server", {
3106
+ hint: "Drop --port and --no-open, or drop --json."
3107
+ });
3108
+ }
3109
+ if (f.port !== void 0 && await outer.deps.ports.inUse(f.port)) throw portInUse(f.port);
3110
+ const ctx = await projectCtx(outer, { ref: f.project, module: f.module, app: f.app, needModule: false, command: "db erd" });
3111
+ const { deps: deps2, io: io2 } = ctx;
3112
+ if (!f.module) {
3113
+ const chosen = Boolean(f.project) || ctx !== outer || Boolean(f.app);
3114
+ const ws = await workspaceRoot(deps2) ?? (chosen ? null : await wholeAppWanted(ctx));
3115
+ if (ws) {
3116
+ await erdApp(ctx, f, ws);
3117
+ return;
3118
+ }
3119
+ }
3120
+ const mc = await moduleContextFor(ctx, f, "db erd");
3121
+ const app = await appFor(ctx, mc, f.app);
3122
+ await ensureTrusted(ctx, mc, app, { yes: f.yes });
3123
+ const src = await resolveDatabaseUrl(deps2, mc, app);
3124
+ const model = async () => {
3125
+ const conn = await deps2.postgres(src.url, { timeoutMs: 3e3 });
3126
+ try {
3127
+ const m = await introspect(conn.query);
3128
+ const migrations = await checkMigrations(mc.repoRoot, conn.query);
3129
+ return migrations ? { ...m, migrations } : m;
3130
+ } finally {
3131
+ await conn.close().catch(() => {
3132
+ });
3133
+ }
3134
+ };
3135
+ const fresh = () => model().catch((e) => {
3136
+ throw classifyDbError(e, src.url);
3137
+ });
3138
+ const first = await model().catch((e) => {
3139
+ throw ctx.signal.aborted ? new CancelledError() : classifyDbError(e, src.url);
3140
+ });
3141
+ if (json2) {
3142
+ renderJson(io2, { context: contextJson(mc, app), source: src.source, model: first });
3143
+ return;
3144
+ }
3145
+ if (first.tables.length === 0) io2.err("No tables yet \u2014 have the migrations run? Start the app once with `qore dev`.");
3146
+ for (const line2 of migrationLines(first.migrations, "The database")) io2.err(line2);
3147
+ if (ctx.signal.aborted) throw new CancelledError();
3148
+ const meta = { context: contextJson(mc, app), connection: connectionOf(src.url) };
3149
+ const server = await serveErd({
3150
+ dir: deps2.viewerDir,
3151
+ port: f.port ?? 0,
3152
+ routes: { "/model.json": fresh, "/meta.json": async () => meta },
3153
+ onError: (e) => {
3154
+ if (!ctx.signal.aborted) io2.err(`Could not refresh the model: ${e instanceof Error ? e.message : String(e)}`);
3155
+ }
3156
+ }).catch((e) => {
3157
+ const code = e.code;
3158
+ throw f.port !== void 0 && (code === "EADDRINUSE" || code === "EACCES") ? portInUse(f.port) : e;
3159
+ });
3160
+ await serve(ctx, f, server);
3161
+ }
3162
+ async function wholeAppWanted(ctx) {
3163
+ if (!ctx.io.interactive) return null;
3164
+ const ws = await findWorkspace(ctx.deps.cwd, ctx.deps.home);
3165
+ const repo = ws && await findRepoRoot(ctx.deps.cwd);
3166
+ if (!ws || !repo || repo === ws.root || !isInside(ws.root, repo)) return null;
3167
+ const pick2 = await ctx.io.select("Which data model?", [
3168
+ { name: "the whole app", value: "app" },
3169
+ { name: `only ${basename3(repo)}`, value: "module" }
3170
+ ]);
3171
+ return pick2 === "app" ? ws : null;
3172
+ }
3173
+ async function serve(ctx, f, server) {
3174
+ try {
3175
+ const url = `http://127.0.0.1:${server.port}`;
3176
+ ctx.io.err(`\u2192 ${url} (Ctrl+C stops)`);
3177
+ if (f.open) await ctx.io.openUrl(url);
3178
+ await aborted(ctx.signal);
3179
+ throw new CancelledError();
3180
+ } finally {
3181
+ await server.close();
3182
+ }
3183
+ }
3184
+ async function erdApp(ctx, f, ws) {
3185
+ const { deps: deps2, io: io2 } = ctx;
3186
+ const g = ctx.opts();
3187
+ const json2 = Boolean(g.json);
3188
+ const portal = await flagPortal(deps2, g.portal, ws);
3189
+ const session = await openSession(deps2, portal, "db erd", g.verbose ? (l) => io2.err(l) : void 0);
3190
+ await ensureCapability(deps2, portal.apiOrigin, "app.modules");
3191
+ const am = await workspaceAppModules(session, portal, ws);
3192
+ const { tenant, ...app } = am.app;
3193
+ if (f.app && f.app !== app.id && f.app !== app.slug) {
3194
+ throw new QoreError("QORE_RESOURCE_NOT_FOUND", `This workspace is for ${app.slug}, not "${f.app}"`, { hint: "Pass --module <slug> --app <slug>." });
3195
+ }
3196
+ const modules = [];
3197
+ const sources = /* @__PURE__ */ new Map();
3198
+ const roots = /* @__PURE__ */ new Map();
3199
+ for (const m of orderModules(am.modules)) {
3200
+ const dir2 = join15(ws.root, m.slug);
3201
+ const repoRoot = isSafeSlug(m.slug) && await isDir2(dir2) ? dir2 : null;
3202
+ const mc = { portal, session, module: { ...m, tenant, apps: [app] }, source: { kind: "workspace" }, repoRoot, workspace: ws };
3203
+ roots.set(m.slug, repoRoot);
3204
+ if (repoRoot) await ensureTrusted(ctx, mc, app, { yes: f.yes });
3205
+ try {
3206
+ const src = await resolveDatabaseUrl(deps2, mc, app);
3207
+ modules.push({ slug: m.slug, dependsOn: m.dependsOn, url: src.url });
3208
+ sources.set(m.slug, src.source);
3209
+ } catch (e) {
3210
+ if (e instanceof QoreError && e.code === "QORE_CONTEXT_UNSUPPORTED") {
3211
+ modules.push({ slug: m.slug, dependsOn: m.dependsOn, url: null });
3212
+ continue;
3213
+ }
3214
+ if (!json2 && e instanceof QoreError && e.code === "QORE_ACCESS_DENIED") {
3215
+ io2.err(`${m.slug} is left out: ${e.message}${e.hint ? `
3216
+ ${e.hint}` : ""}`);
3217
+ modules.push({ slug: m.slug, dependsOn: m.dependsOn, url: null });
3218
+ continue;
3219
+ }
3220
+ throw e;
3221
+ }
3222
+ }
3223
+ const groups = groupDatabases(modules);
3224
+ if (groups.length === 0) {
3225
+ throw new QoreError("QORE_CONTEXT_UNSUPPORTED", `${app.slug} has no module with a database`, {
3226
+ hint: "qore db erd shows the Postgres of an app's modules; none of them declares a DATABASE_URL."
3227
+ });
3228
+ }
3229
+ for (const gdb of groups) deps2.registerSecrets([gdb.url]);
3230
+ if (!json2) for (const { modules: mods, connection: c } of groups) {
3231
+ if (mods.length > 1) io2.err(`${mods.join(" and ")} use the same database (${c.host}:${c.port}/${c.database}); each module normally has its own \u2014 check their DATABASE_URL.`);
3232
+ }
3233
+ const readAll = (strict) => Promise.all(groups.map(async (gdb) => {
3234
+ try {
3235
+ const conn = await deps2.postgres(gdb.url, { timeoutMs: 3e3 });
3236
+ try {
3237
+ const model = await introspect(conn.query);
3238
+ const migrations = gdb.modules.length === 1 ? await checkMigrations(roots.get(gdb.modules[0]) ?? null, conn.query) : void 0;
3239
+ return { ...gdb, model, ...migrations ? { migrations } : {} };
3240
+ } finally {
3241
+ await conn.close().catch(() => {
3242
+ });
3243
+ }
3244
+ } catch (e) {
3245
+ const err = classifyDbError(e, gdb.url);
3246
+ if (strict) throw err;
3247
+ return { ...gdb, model: null, error: err.message };
3248
+ }
3249
+ }));
3250
+ const first = await readAll(json2).catch((e) => {
3251
+ throw ctx.signal.aborted ? new CancelledError() : e;
3252
+ });
3253
+ if (first.every((d) => d.model === null)) throw new QoreError("QORE_SERVICE_UNHEALTHY", `No database of ${app.slug} is reachable`, { hint: first.map((d) => `${d.key}: ${d.error}`).join("\n") });
3254
+ const context = {
3255
+ portal: portal.builtin ?? portal.apiOrigin,
3256
+ tenant,
3257
+ app,
3258
+ module: null,
3259
+ source: WORKSPACE_FILE,
3260
+ modules: am.modules.map(({ id, slug, name, type }) => ({ id, slug, name, type, database: groups.find((gdb) => gdb.modules.includes(slug))?.key ?? null }))
3261
+ };
3262
+ const databases = groups.map(({ key: key2, modules: mods, connection }) => ({ key: key2, modules: mods, connection, source: sources.get(mods[0]) }));
3263
+ if (json2) {
3264
+ renderJson(io2, { context, databases, model: combineModels(first) });
3265
+ return;
3266
+ }
3267
+ for (const d of first) if (d.error) io2.err(`${d.key} (${d.modules.join(", ")}): ${d.error}`);
3268
+ for (const d of first) for (const line2 of migrationLines(d.migrations, `The database of ${d.modules.join(", ")}`)) io2.err(line2);
3269
+ if (first.every((d) => !d.model || d.model.tables.length === 0)) io2.err("No tables yet \u2014 have the migrations run? Start the app once with `qore dev`.");
3270
+ if (ctx.signal.aborted) throw new CancelledError();
3271
+ const meta = { context, connection: null, databases: databases.map(({ key: key2, modules: mods, connection }) => ({ key: key2, modules: mods, connection })) };
3272
+ const server = await serveErd({
3273
+ dir: deps2.viewerDir,
3274
+ port: f.port ?? 0,
3275
+ routes: { "/model.json": () => readAll(false).then(combineModels), "/meta.json": async () => meta },
3276
+ onError: (e) => {
3277
+ if (!ctx.signal.aborted) io2.err(`Could not refresh the model: ${e instanceof Error ? e.message : String(e)}`);
3278
+ }
3279
+ }).catch((e) => {
3280
+ const code = e.code;
3281
+ throw f.port !== void 0 && (code === "EADDRINUSE" || code === "EACCES") ? portInUse(f.port) : e;
3282
+ });
3283
+ await serve(ctx, f, server);
3284
+ }
3285
+
3286
+ // src/cli/commands/dev.ts
3287
+ import { randomBytes as randomBytes6 } from "crypto";
3288
+ import { realpath as realpath8 } from "fs/promises";
3289
+ import { constants as constants3 } from "os";
3290
+ import { dirname as dirname7, join as join23, relative as relative7 } from "path";
3291
+ import { StringDecoder } from "string_decoder";
3292
+
3293
+ // src/core/diagnostics/node.ts
3294
+ var KEEP_LINES = 50;
3295
+ function exitHint(tail, h) {
3296
+ const text2 = tail.join("\n");
3297
+ const missing = text2.match(/Cannot find (?:module|package) '([^']+)'/);
3298
+ if (missing) {
3299
+ return { reason: "missing-module", hint: `A dependency is missing (${missing[1]}). Run \`${h.installCommand}\`, then qore dev again.` };
3300
+ }
3301
+ if (/ZodError:/.test(text2)) {
3302
+ const keys = [...text2.matchAll(/"path":\s*\[\s*"([^"]+)"/g)].map((m) => m[1]);
3303
+ if (keys.length > 0) {
3304
+ return {
3305
+ reason: "invalid-env",
3306
+ keys,
3307
+ hint: `Missing or invalid environment variables: ${keys.join(", ")}. Set them in the portal's local secrets (then qore env pull), or in your env file.`
3308
+ };
3309
+ }
3310
+ }
3311
+ const addrInUse = text2.match(/listen EADDRINUSE: address already in use (?:\S*:)?(\d+)/);
3312
+ if (addrInUse) {
3313
+ const port = Number(addrInUse[1]);
3314
+ return {
3315
+ reason: "port-in-use",
3316
+ port,
3317
+ hint: `Port ${port} was taken while the server started. Run qore dev again; if it repeats, lsof -iTCP:${port} -sTCP:LISTEN names the holder.`
3318
+ };
3319
+ }
3320
+ if (/ECONNREFUSED/.test(text2)) {
3321
+ const m = text2.match(/ECONNREFUSED\s+\S*:(\d+)/) ?? text2.match(/code: 'ECONNREFUSED'[^}]*?port: (\d+)/);
3322
+ if (!m) return { reason: "conn-refused", hint: "A connection was refused (ECONNREFUSED) \u2014 a service this module calls is not running." };
3323
+ const port = Number(m[1]);
3324
+ const role = h.ports.get(port);
3325
+ if (role?.kind === "database") {
3326
+ const up = h.runCommand("db:up");
3327
+ return {
3328
+ reason: "conn-refused",
3329
+ port,
3330
+ 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.")
3331
+ };
3332
+ }
3333
+ if (role?.kind === "provider") {
3334
+ return {
3335
+ reason: "conn-refused",
3336
+ port,
3337
+ hint: `${role.key} points at :${port}, where nothing listens (ECONNREFUSED). Start that module \u2014 qore dev in the workspace root starts providers first.`
3338
+ };
3339
+ }
3340
+ return { reason: "conn-refused", port, hint: `Nothing listens on :${port} (ECONNREFUSED) \u2014 a service this module calls is not running.` };
3341
+ }
3342
+ const auth = text2.match(/password authentication failed for user "([^"]+)"/);
3343
+ if (auth) {
3344
+ const reset = h.runCommand("db:reset");
3345
+ return {
3346
+ reason: "db-auth",
3347
+ 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.` : "")
3348
+ };
3349
+ }
3350
+ const relation = text2.match(/relation "([^"]+)" does not exist/);
3351
+ if (relation) {
3352
+ const reset = h.runCommand("db:reset");
3353
+ return {
3354
+ reason: "db-schema",
3355
+ 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).` : "")
3356
+ };
3357
+ }
3358
+ if (/ENOSPC: System limit for number of file watchers reached/.test(text2)) {
3359
+ return {
3360
+ reason: "watcher-limit",
3361
+ 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"
3362
+ };
2471
3363
  }
2472
- }
2473
- async function syncEnvFile(o) {
2474
- const existing = await readOrNull(o.target);
2475
- const plan = planPull({
2476
- existing,
2477
- example: existing === null ? o.example : null,
2478
- fetched: o.fetched,
2479
- managed: o.managed,
2480
- localSecrets: o.localSecrets,
2481
- defaultChoice: "keep",
2482
- random: o.random
2483
- });
2484
- if (existing !== null && plan.text === existing) await chmod2(o.target, 384);
2485
- else await writeSecurely(o.target, plan.text);
2486
- return { created: plan.created, kept: plan.kept };
3364
+ return null;
2487
3365
  }
2488
3366
 
3367
+ // src/core/dev/env.ts
3368
+ import { dirname as dirname6, join as join17 } from "path";
3369
+
2489
3370
  // src/core/dev/lastStart.ts
2490
3371
  import { createHash as createHash2 } from "crypto";
2491
3372
  import { realpath as realpath6 } from "fs/promises";
2492
- import { join as join13 } from "path";
3373
+ import { join as join16 } from "path";
2493
3374
  var NETWORK_REASONS = /* @__PURE__ */ new Set(["connect", "dns", "tls", "timeout"]);
2494
3375
  function isOffline(e) {
2495
3376
  if (!(e instanceof QoreError)) return false;
@@ -2502,16 +3383,16 @@ async function stateKey(deps2, root) {
2502
3383
  return createHash2("sha256").update(trustKey(real, deps2.platform)).digest("hex").slice(0, 16);
2503
3384
  }
2504
3385
  async function saveLastStart(deps2, root, rec) {
2505
- await writeJsonAtomic(join13(deps2.config.dir, "dev"), `${await stateKey(deps2, root)}.json`, rec);
3386
+ await writeJsonAtomic(join16(deps2.config.dir, "dev"), `${await stateKey(deps2, root)}.json`, rec);
2506
3387
  }
2507
3388
  async function loadLastStart(deps2, root) {
2508
- const rec = await readJson(join13(deps2.config.dir, "dev", `${await stateKey(deps2, root)}.json`)).catch(() => null);
3389
+ const rec = await readJson(join16(deps2.config.dir, "dev", `${await stateKey(deps2, root)}.json`)).catch(() => null);
2509
3390
  return rec && rec.schemaVersion === 1 ? rec : null;
2510
3391
  }
2511
3392
 
2512
3393
  // src/core/dev/env.ts
2513
- function envPort(env, key) {
2514
- const raw = env[key];
3394
+ function envPort(env, key2) {
3395
+ const raw = env[key2];
2515
3396
  if (raw === void 0) return null;
2516
3397
  const v = raw.trim().replace(/^["']|["']$/g, "");
2517
3398
  if (!/^\d+$/.test(v)) return null;
@@ -2525,7 +3406,7 @@ async function devEnv(deps2, o) {
2525
3406
  if (o.mc && o.app) {
2526
3407
  try {
2527
3408
  const local = await fetchLocalEnv(deps2, o.mc, o.app);
2528
- const example = await readOrNull(join14(dirname6(target), ".env.example"));
3409
+ const example = await readOrNull(join17(dirname6(target), ".env.example"));
2529
3410
  const w = await syncEnvFile({
2530
3411
  target,
2531
3412
  example,
@@ -2579,7 +3460,7 @@ function notClonedReason(m, am) {
2579
3460
  }
2580
3461
 
2581
3462
  // src/core/dev/findings.ts
2582
- var plural = (n, one, many) => n === 1 ? one : many;
3463
+ var plural2 = (n, one2, many) => n === 1 ? one2 : many;
2583
3464
  function modulesMissing(o) {
2584
3465
  const actionFor = new Map(o.actions.map((a) => [a.module.slug, a]));
2585
3466
  const findings = [];
@@ -2612,8 +3493,8 @@ function modulesMissing(o) {
2612
3493
  findings.push({
2613
3494
  code: "dev.modules-missing",
2614
3495
  severity: "warn",
2615
- message: `${fixable.length} ${plural(fixable.length, "module of this app is", "modules of this app are")} not cloned: ${slugs.join(", ")}`,
2616
- why: deps2.length > 0 ? `${deps2.join("; ")}.` : `${plural(fixable.length, "It", "They")} will not run in this session.`,
3496
+ message: `${fixable.length} ${plural2(fixable.length, "module of this app is", "modules of this app are")} not cloned: ${slugs.join(", ")}`,
3497
+ why: deps2.length > 0 ? `${deps2.join("; ")}.` : `${plural2(fixable.length, "It", "They")} will not run in this session.`,
2617
3498
  modules: slugs,
2618
3499
  action: o.am ? { command: "qore sync", fix: "sync" } : { command: "qore sync" }
2619
3500
  });
@@ -2629,7 +3510,7 @@ function repoBehind(slug, s, o) {
2629
3510
  code: "dev.repo-behind",
2630
3511
  severity: "warn",
2631
3512
  module: slug,
2632
- message: `${slug} is ${s.behind} ${plural(s.behind, "commit", "commits")} behind ${s.upstreamRef ?? "its upstream"} (as of your last fetch).`,
3513
+ message: `${slug} is ${s.behind} ${plural2(s.behind, "commit", "commits")} behind ${s.upstreamRef ?? "its upstream"} (as of your last fetch).`,
2633
3514
  action: { command: o.workspace ? "qore sync --pull" : "git pull" }
2634
3515
  };
2635
3516
  }
@@ -2652,15 +3533,15 @@ function providerDown(o) {
2652
3533
  severity: "warn",
2653
3534
  module: o.slug,
2654
3535
  modules: o.providers,
2655
- 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(", ")}).`,
3536
+ 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(", ")} ${plural2(o.keys.length, "points", "point")} at ports nothing listens on (depends on: ${o.providers.join(", ")}).`,
2656
3537
  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.`,
2657
3538
  action: { command: o.fix }
2658
3539
  };
2659
3540
  }
2660
3541
 
2661
3542
  // src/core/dev/project.ts
2662
- import { access, readFile as readFile5 } from "fs/promises";
2663
- import { join as join15 } from "path";
3543
+ import { access, readFile as readFile7 } from "fs/promises";
3544
+ import { join as join18 } from "path";
2664
3545
  var LEGACY_DEV_SCRIPTS = ["node --env-file=.env --import ./dist/instrumentation.js dist/index.js"];
2665
3546
  var LOCKFILES = [["pnpm-lock.yaml", "pnpm"], ["package-lock.json", "npm"], ["yarn.lock", "yarn"]];
2666
3547
  var exists = (p) => access(p).then(() => true, () => false);
@@ -2668,7 +3549,7 @@ var unsupported = (message, hint, details) => new QoreError("QORE_DEV_UNSUPPORTE
2668
3549
  async function readPackageJson(root) {
2669
3550
  let raw;
2670
3551
  try {
2671
- raw = await readFile5(join15(root, "package.json"), "utf8");
3552
+ raw = await readFile7(join18(root, "package.json"), "utf8");
2672
3553
  } catch {
2673
3554
  throw unsupported("This repository has no package.json", "qore dev starts Node repositories through their package.json scripts.");
2674
3555
  }
@@ -2684,7 +3565,7 @@ async function detectPackageManager(root, pkg) {
2684
3565
  if (!m) throw unsupported(`packageManager "${pkg.packageManager}" is not pnpm, npm or yarn`, "qore dev supports pnpm, npm and yarn.");
2685
3566
  return { name: m[1], version: m[2].split("+")[0] };
2686
3567
  }
2687
- for (const [file, name] of LOCKFILES) if (await exists(join15(root, file))) return { name, version: null };
3568
+ for (const [file, name] of LOCKFILES) if (await exists(join18(root, file))) return { name, version: null };
2688
3569
  throw unsupported("No package manager declared and no lockfile found", 'Add "packageManager": "pnpm@<version>" to package.json.');
2689
3570
  }
2690
3571
  function planRecipe(o) {
@@ -2727,21 +3608,21 @@ function installArgs(pm) {
2727
3608
  }
2728
3609
 
2729
3610
  // src/core/dev/install.ts
2730
- import { stat as stat3 } from "fs/promises";
2731
- import { join as join16 } from "path";
3611
+ import { stat as stat5 } from "fs/promises";
3612
+ import { join as join19 } from "path";
2732
3613
  var MARKERS = {
2733
3614
  pnpm: [".modules.yaml"],
2734
3615
  npm: [".package-lock.json"],
2735
3616
  yarn: [".yarn-state.yml", ".yarn-integrity"]
2736
3617
  };
2737
3618
  var LOCK2 = { pnpm: "pnpm-lock.yaml", npm: "package-lock.json", yarn: "yarn.lock" };
2738
- var mtime = (p) => stat3(p).then((s) => s.mtimeMs, () => null);
3619
+ var mtime = (p) => stat5(p).then((s) => s.mtimeMs, () => null);
2739
3620
  async function needsInstall(root, pm) {
2740
- if (await mtime(join16(root, "node_modules")) === null) return true;
3621
+ if (await mtime(join19(root, "node_modules")) === null) return true;
2741
3622
  let marker = null;
2742
- for (const m of MARKERS[pm.name]) marker ??= await mtime(join16(root, "node_modules", m));
3623
+ for (const m of MARKERS[pm.name]) marker ??= await mtime(join19(root, "node_modules", m));
2743
3624
  if (marker === null) return true;
2744
- const lock = await mtime(join16(root, LOCK2[pm.name]));
3625
+ const lock = await mtime(join19(root, LOCK2[pm.name]));
2745
3626
  return lock !== null && lock > marker;
2746
3627
  }
2747
3628
 
@@ -2869,7 +3750,7 @@ function nodeVersionError(found, range, slug) {
2869
3750
  // src/core/dev/preflight.ts
2870
3751
  var pmMajor = (v) => v ? v.split(".")[0] : "latest";
2871
3752
  var GRACE_MS = 1e4;
2872
- var secs = (ms) => `${Math.round(ms / 1e3)} s`;
3753
+ var secs = (ms2) => `${Math.round(ms2 / 1e3)} s`;
2873
3754
  async function runTool(deps2, cmd, args, o) {
2874
3755
  if (o.signal.aborted) throw new CancelledError();
2875
3756
  const tail = [];
@@ -3006,12 +3887,12 @@ async function composeFailure(deps2, r, slug, env, signal, message, command, std
3006
3887
  details: { command, stderr, logs: logs.tail.slice(-20) }
3007
3888
  });
3008
3889
  }
3009
- async function startServices(deps2, r, slug, env, dbPort, waitMs, signal, onLine, report) {
3890
+ async function startServices(deps2, r, slug, env, dbPort2, waitMs, signal, onLine, report) {
3010
3891
  if (r.services.length === 0) {
3011
3892
  report({ module: slug, step: "services", status: "skip" });
3012
3893
  return;
3013
3894
  }
3014
- report({ module: slug, step: "services", status: "start", ...dbPort !== null ? { detail: `:${dbPort}` } : {} });
3895
+ report({ module: slug, step: "services", status: "start", ...dbPort2 !== null ? { detail: `:${dbPort2}` } : {} });
3015
3896
  const began = deps2.now().getTime();
3016
3897
  if (r.dockerServices) {
3017
3898
  const res = await runTool(deps2, "docker", ["info", "--format", "{{.ServerVersion}}"], { cwd: r.root, env, onLine, signal });
@@ -3026,14 +3907,14 @@ async function startServices(deps2, r, slug, env, dbPort, waitMs, signal, onLine
3026
3907
  const { cmd, args } = runArgs(r.pm, script);
3027
3908
  const res = await runTool(deps2, cmd, args, { cwd: r.root, env, onLine, signal });
3028
3909
  if (res.code !== 0) {
3029
- if (dbPort !== null && /port is already allocated|address already in use/i.test(res.tail.join("\n"))) {
3030
- throw await portInUseError(deps2, { slug, port: dbPort, key: "DB_PORT" });
3910
+ if (dbPort2 !== null && /port is already allocated|address already in use/i.test(res.tail.join("\n"))) {
3911
+ throw await portInUseError(deps2, { slug, port: dbPort2, key: "DB_PORT" });
3031
3912
  }
3032
3913
  throw await composeFailure(deps2, r, slug, env, signal, `${script} failed in ${slug}`, `${cmd} ${args.join(" ")}`, res.tail);
3033
3914
  }
3034
3915
  }
3035
- if (dbPort !== null) {
3036
- const ok2 = await deps2.ports.waitFor(dbPort, waitMs, signal);
3916
+ if (dbPort2 !== null) {
3917
+ const ok2 = await deps2.ports.waitFor(dbPort2, waitMs, signal);
3037
3918
  if (signal.aborted) throw new CancelledError();
3038
3919
  if (!ok2) {
3039
3920
  throw await composeFailure(
@@ -3042,14 +3923,14 @@ async function startServices(deps2, r, slug, env, dbPort, waitMs, signal, onLine
3042
3923
  slug,
3043
3924
  env,
3044
3925
  signal,
3045
- `The database for ${slug} did not accept connections on port ${dbPort} within ${waitMs / 1e3} s`,
3926
+ `The database for ${slug} did not accept connections on port ${dbPort2} within ${waitMs / 1e3} s`,
3046
3927
  void 0,
3047
3928
  []
3048
3929
  );
3049
3930
  }
3050
3931
  }
3051
3932
  const took = secs(deps2.now().getTime() - began);
3052
- report({ module: slug, step: "services", status: "done", detail: dbPort !== null ? `:${dbPort} (${took})` : `(${took})` });
3933
+ report({ module: slug, step: "services", status: "done", detail: dbPort2 !== null ? `:${dbPort2} (${took})` : `(${took})` });
3053
3934
  }
3054
3935
  async function checkPort(deps2, o) {
3055
3936
  if (o.port === null) return;
@@ -3057,13 +3938,13 @@ async function checkPort(deps2, o) {
3057
3938
  throw await portInUseError(deps2, { slug: o.slug, port: o.port, key: o.key, runs: o.runs });
3058
3939
  }
3059
3940
  function abortableDelay(signal) {
3060
- return (ms) => new Promise((resolve6) => {
3941
+ return (ms2) => new Promise((resolve7) => {
3061
3942
  const done = () => {
3062
3943
  clearTimeout(t);
3063
3944
  signal.removeEventListener("abort", done);
3064
- resolve6();
3945
+ resolve7();
3065
3946
  };
3066
- const t = setTimeout(done, ms);
3947
+ const t = setTimeout(done, ms2);
3067
3948
  signal.addEventListener("abort", done, { once: true });
3068
3949
  });
3069
3950
  }
@@ -3108,17 +3989,17 @@ async function leftoverOwnsPort(deps2, run4, port) {
3108
3989
  var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
3109
3990
  function connPorts(env) {
3110
3991
  const out = [];
3111
- for (const key of Object.keys(env).sort()) {
3112
- const m = /^CONN_(.+)_URL$/.exec(key);
3992
+ for (const key2 of Object.keys(env).sort()) {
3993
+ const m = /^CONN_(.+)_URL$/.exec(key2);
3113
3994
  if (!m) continue;
3114
3995
  let url;
3115
3996
  try {
3116
- url = new URL(env[key].trim().replace(/^["']|["']$/g, ""));
3997
+ url = new URL(env[key2].trim().replace(/^["']|["']$/g, ""));
3117
3998
  } catch {
3118
3999
  continue;
3119
4000
  }
3120
4001
  const port = Number(url.port);
3121
- if (port > 0 && LOCAL_HOSTS.has(url.hostname)) out.push({ key, alias: m[1], port });
4002
+ if (port > 0 && LOCAL_HOSTS.has(url.hostname)) out.push({ key: key2, alias: m[1], port });
3122
4003
  }
3123
4004
  return out;
3124
4005
  }
@@ -3139,259 +4020,34 @@ function hintContext(recipe, pkg, env) {
3139
4020
  return [r.cmd, ...r.args].join(" ");
3140
4021
  },
3141
4022
  ports
3142
- };
3143
- }
3144
-
3145
- // src/core/dev/runFile.ts
3146
- import { unlink as unlink2 } from "fs/promises";
3147
- import { join as join17 } from "path";
3148
- async function runFilePath(deps2, root) {
3149
- const dir2 = join17(deps2.config.dir, "run");
3150
- const name = `${await stateKey(deps2, root)}.json`;
3151
- return { dir: dir2, name, path: join17(dir2, name) };
3152
- }
3153
- async function writeRunFile(deps2, root, rec) {
3154
- const { dir: dir2, name } = await runFilePath(deps2, root);
3155
- await writeJsonAtomic(dir2, name, rec);
3156
- }
3157
- async function removeRunFile(deps2, root) {
3158
- const { path } = await runFilePath(deps2, root);
3159
- await unlink2(path).catch(() => {
3160
- });
3161
- }
3162
- async function leftoverRun(deps2, root, o = {}) {
3163
- const { path } = await runFilePath(deps2, root);
3164
- const rec = await readJson(path).catch(() => null);
3165
- if (!rec || rec.schemaVersion !== 1) return null;
3166
- if (deps2.launch.isAlive(rec.qorePid)) return rec;
3167
- if (o.prune !== false) await unlink2(path).catch(() => {
3168
- });
3169
- return null;
3170
- }
3171
-
3172
- // src/cli/project.ts
3173
- import { readdir as readdir2, stat as stat4 } from "fs/promises";
3174
- import { join as join18 } from "path";
3175
-
3176
- // src/core/projects/match.ts
3177
- function readings(ref) {
3178
- const s = ref.split("/").map((x) => x.trim());
3179
- if (s.some((x) => x === "")) return [];
3180
- if (s.length === 1) return [{ app: s[0] }];
3181
- if (s.length === 2) return [{ app: s[0], module: s[1] }, { tenant: s[0], app: s[1] }];
3182
- if (s.length === 3) return [{ tenant: s[0], app: s[1], module: s[2] }];
3183
- return [];
3184
- }
3185
- function hit(p, w) {
3186
- if (p.appSlug !== w.app || w.tenant !== void 0 && p.tenantSlug !== w.tenant) return null;
3187
- if (p.kind === "module") return w.module === p.moduleSlug ? { project: p } : null;
3188
- return w.module ? { project: p, module: w.module } : { project: p };
3189
- }
3190
- function matchProjects(projects, ref) {
3191
- const out = [];
3192
- for (const w of readings(ref)) {
3193
- for (const p of projects) {
3194
- const m = hit(p, w);
3195
- if (m) out.push(m);
3196
- }
3197
- }
3198
- return out;
3199
- }
3200
-
3201
- // src/cli/project.ts
3202
- async function loggedInAs(ctx) {
3203
- const cfg = await ctx.deps.config.read();
3204
- const portal = resolvePortal(ctx.opts().portal, cfg.defaultProfile);
3205
- const p = cfg.profiles[portal.apiOrigin];
3206
- if (!p || !await new TokenStore(ctx.deps.keychain, profileHash(p.apiOrigin, p.issuer)).getRefresh()) return null;
3207
- return p.email ?? p.sub ?? portal.apiOrigin;
3208
- }
3209
- var isDir = async (p) => (await stat4(p).catch(() => null))?.isDirectory() ?? false;
3210
- var label2 = (p) => `${p.tenantSlug} / ${p.appSlug}${p.kind === "module" ? ` / ${p.moduleSlug}` : ""}${p.source === "registry" ? ` (${p.path})` : ""}`;
3211
- var candidate = (p) => p.kind === "module" ? { kind: "module", id: p.moduleId, slug: p.moduleSlug, tenant: p.tenantSlug, name: p.path } : { kind: "app", slug: p.appSlug, tenant: p.tenantSlug, name: p.path };
3212
- var withCwd = (ctx, cwd) => ({ ...ctx, deps: { ...ctx.deps, cwd } });
3213
- async function inProject(ctx) {
3214
- if (await findWorkspace(ctx.deps.cwd, ctx.deps.home)) return true;
3215
- const repo = await findRepoRoot(ctx.deps.cwd);
3216
- return repo !== null && !isInside(repo, ctx.deps.home);
3217
- }
3218
- async function moduleDirs(wsPath) {
3219
- const out = [];
3220
- for (const d of await readdir2(wsPath, { withFileTypes: true }).catch(() => [])) {
3221
- if (d.isDirectory() && !d.name.startsWith(".") && await stat4(join18(wsPath, d.name, ".git")).catch(() => null)) out.push(d.name);
3222
- }
3223
- return out.sort();
3224
- }
3225
- async function pathFor(ctx, m, needModule) {
3226
- const p = m.project;
3227
- if (p.kind === "module") return p.path;
3228
- if (m.module) {
3229
- const dir2 = join18(p.path, m.module);
3230
- if (!await isDir(dir2)) {
3231
- throw new QoreError("QORE_PROJECT_NOT_FOUND", `${p.appSlug} has no checkout of module ${m.module} in ${p.path}`, {
3232
- hint: `Run \`qore sync\` in ${p.path} to clone the app's missing modules.`
3233
- });
3234
- }
3235
- return dir2;
3236
- }
3237
- if (!needModule) return p.path;
3238
- const dirs = await moduleDirs(p.path);
3239
- if (dirs.length === 1) return join18(p.path, dirs[0]);
3240
- if (dirs.length === 0) {
3241
- throw new QoreError("QORE_PROJECT_NOT_FOUND", `${p.appSlug} has no module checkouts in ${p.path}`, {
3242
- hint: `Run \`qore sync\` in ${p.path}.`
3243
- });
3244
- }
3245
- if (!ctx.io.interactive) {
3246
- throw new QoreError("QORE_CONTEXT_REQUIRED", `${p.appSlug} has several modules \u2014 which one?`, {
3247
- hint: `Pass ${p.appSlug}/<module>.`,
3248
- candidates: dirs.map((slug) => ({ kind: "module", slug, tenant: p.tenantSlug, name: join18(p.path, slug) }))
3249
- });
3250
- }
3251
- return join18(p.path, await ctx.io.select("Select a module", dirs.map((d) => ({ name: d, value: d }))));
3252
- }
3253
- async function liveMatches(matches) {
3254
- const live = [];
3255
- for (const m of matches) {
3256
- if (m.project.kind === "workspace" && m.module && !await isDir(join18(m.project.path, m.module))) continue;
3257
- live.push(m);
3258
- }
3259
- return live.length > 0 ? live : matches;
3260
- }
3261
- async function choose(ctx, matches, what, explicit) {
3262
- if (explicit && matches.length === 1) return matches[0];
3263
- if (!ctx.io.interactive) {
3264
- throw new QoreError("QORE_CONTEXT_REQUIRED", what, {
3265
- hint: "Pass a more specific reference: tenant/app or app/module.",
3266
- candidates: matches.map((m) => candidate(m.project))
3267
- });
3268
- }
3269
- const i = await ctx.io.select("Select a project", matches.map((m, idx) => ({
3270
- name: m.module ? `${label2(m.project)} / ${m.module}` : label2(m.project),
3271
- value: idx
3272
- })));
3273
- return matches[i];
3274
- }
3275
- async function projectCtx(ctx, o) {
3276
- const g = ctx.opts();
3277
- const cfg = await ctx.deps.config.read();
3278
- const portal = g.portal ? resolvePortal(g.portal, cfg.defaultProfile).apiOrigin : void 0;
3279
- if (o.ref) {
3280
- const matches = matchProjects(await knownProjects(ctx.deps, { portal }), o.ref);
3281
- if (matches.length === 0) {
3282
- const parts = o.ref.split("/");
3283
- const app = parts.length === 1 ? o.ref : parts.length === 3 ? parts[1] : "<app>";
3284
- throw new QoreError("QORE_PROJECT_NOT_FOUND", `No local project "${o.ref}"`, {
3285
- hint: cfg.projectsRoot ? `Run \`qore clone ${app}\`.` : `Run \`qore init\`, or \`qore clone ${app}\`.`
3286
- });
3287
- }
3288
- const m2 = await choose(ctx, await liveMatches(matches), `"${o.ref}" matches several projects`, true);
3289
- return relocate(ctx, await pathFor(ctx, m2, o.needModule));
3290
- }
3291
- if (o.module || o.app || await inProject(ctx)) return ctx;
3292
- const known = await knownProjects(ctx.deps, { portal });
3293
- if (known.length === 0) {
3294
- if (!cfg.projectsRoot) {
3295
- if (!await loggedInAs(ctx)) return ctx;
3296
- throw new QoreError("QORE_ROOT_NOT_SET", "Not inside a project, and qore knows no projects yet", {
3297
- hint: "Run `qore init` to choose a projects folder, or cd into a project."
3298
- });
3299
- }
3300
- throw new QoreError("QORE_PROJECT_NOT_FOUND", `Not inside a project, and ${cfg.projectsRoot} holds none yet`, {
3301
- hint: "Run `qore clone <app>`."
3302
- });
3303
- }
3304
- const m = await choose(ctx, known.map((project) => ({ project })), "Not inside a project \u2014 which one?", false);
3305
- return relocate(ctx, await pathFor(ctx, m, o.needModule));
3306
- }
3307
- function relocate(ctx, path) {
3308
- if (path === ctx.deps.cwd) return ctx;
3309
- if (!ctx.opts().json) ctx.io.err(`\u2192 in ${path}`);
3310
- return withCwd(ctx, path);
3311
- }
3312
- async function rememberContext(ctx, mc, app) {
3313
- const tenantSlug = mc.module.tenant.slug;
3314
- if (mc.workspace && mc.workspace.appId === app.id) {
3315
- await remember(ctx.deps, {
3316
- kind: "workspace",
3317
- path: mc.workspace.root,
3318
- portal: mc.workspace.portal,
3319
- tenantSlug,
3320
- appId: app.id,
3321
- appSlug: app.slug
3322
- });
3323
- return;
3324
- }
3325
- if (mc.repoRoot) {
3326
- await remember(ctx.deps, {
3327
- kind: "module",
3328
- path: mc.repoRoot,
3329
- portal: mc.portal.apiOrigin,
3330
- tenantSlug,
3331
- appId: app.id,
3332
- appSlug: app.slug,
3333
- moduleId: mc.module.id,
3334
- moduleSlug: mc.module.slug
3335
- });
3336
- }
3337
- }
3338
- async function rememberWorkspace(ctx, ws, app) {
3339
- if (ws.appId !== app.id) return;
3340
- await remember(ctx.deps, { kind: "workspace", path: ws.root, portal: ws.portal, tenantSlug: app.tenant.slug, appId: app.id, appSlug: app.slug });
3341
- }
3342
-
3343
- // src/cli/context.ts
3344
- var allOfKind = (c, kind) => !!c && c.length > 0 && c.every((x) => x.kind === kind && typeof x.id === "string");
3345
- async function moduleContextFor(ctx, flags, command) {
3346
- const g = ctx.opts();
3347
- const onRequest = g.verbose ? (l) => ctx.io.err(l) : void 0;
3348
- const base = { portal: g.portal, module: flags.module, command, onRequest };
3349
- try {
3350
- return await resolveModuleContext(ctx.deps, base);
3351
- } catch (e) {
3352
- if (!(e instanceof QoreError) || e.code !== "QORE_CONTEXT_REQUIRED" || !ctx.io.interactive) throw e;
3353
- if (allOfKind(e.candidates, "module")) {
3354
- const choose2 = await ctx.io.select(
3355
- "Select a module",
3356
- e.candidates.map((c) => ({ name: `${c.tenant ? `${c.tenant} / ` : ""}${c.slug} (${c.remote})`, value: c.id }))
3357
- );
3358
- return resolveModuleContext(ctx.deps, { ...base, choose: choose2 });
3359
- }
3360
- if (allOfKind(e.candidates, "tenant")) {
3361
- const tenant = await ctx.io.select("Select a tenant", e.candidates.map((c) => ({ name: c.name ?? c.slug, value: c.id })));
3362
- return resolveModuleContext(ctx.deps, { ...base, tenant });
3363
- }
3364
- throw e;
3365
- }
3366
- }
3367
- async function appFor(ctx, mc, app) {
3368
- try {
3369
- return pickApp2(mc, app);
3370
- } catch (e) {
3371
- if (!(e instanceof QoreError) || e.code !== "QORE_APP_AMBIGUOUS" || !ctx.io.interactive) throw e;
3372
- const id = await ctx.io.select("Select an app", mc.module.apps.map((a) => ({ name: a.name, value: a.id })));
3373
- return pickApp2(mc, id);
3374
- }
4023
+ };
3375
4024
  }
3376
- function sourceText(mc) {
3377
- if (mc.source.kind === "remote") return mc.source.remote;
3378
- if (mc.source.kind === "workspace") return WORKSPACE_FILE;
3379
- return mc.source.overrides ? `--module, overrides ${mc.source.overrides}` : "--module";
4025
+
4026
+ // src/core/dev/runFile.ts
4027
+ import { unlink as unlink2 } from "fs/promises";
4028
+ import { join as join20 } from "path";
4029
+ async function runFilePath(deps2, root) {
4030
+ const dir2 = join20(deps2.config.dir, "run");
4031
+ const name = `${await stateKey(deps2, root)}.json`;
4032
+ return { dir: dir2, name, path: join20(dir2, name) };
3380
4033
  }
3381
- function contextLine(mc, app) {
3382
- const parts = [mc.module.tenant.name, ...app ? [app.name] : [], mc.module.slug];
3383
- return `\u2192 ${parts.join(" / ")} (${sourceText(mc)})`;
4034
+ async function writeRunFile(deps2, root, rec) {
4035
+ const { dir: dir2, name } = await runFilePath(deps2, root);
4036
+ await writeJsonAtomic(dir2, name, rec);
3384
4037
  }
3385
- function contextJson(mc, app) {
3386
- const { id, slug, name, type, access: access2 } = mc.module;
3387
- return {
3388
- portal: mc.portal.builtin ?? mc.portal.apiOrigin,
3389
- tenant: mc.module.tenant,
3390
- app: app ?? null,
3391
- module: { id, slug, name, type, access: access2 },
3392
- source: mc.source.kind === "remote" ? mc.source.remote : mc.source.kind === "workspace" ? WORKSPACE_FILE : "--module",
3393
- ...mc.source.kind === "flag" && mc.source.overrides ? { overrides: mc.source.overrides } : {}
3394
- };
4038
+ async function removeRunFile(deps2, root) {
4039
+ const { path } = await runFilePath(deps2, root);
4040
+ await unlink2(path).catch(() => {
4041
+ });
4042
+ }
4043
+ async function leftoverRun(deps2, root, o = {}) {
4044
+ const { path } = await runFilePath(deps2, root);
4045
+ const rec = await readJson(path).catch(() => null);
4046
+ if (!rec || rec.schemaVersion !== 1) return null;
4047
+ if (deps2.launch.isAlive(rec.qorePid)) return rec;
4048
+ if (o.prune !== false) await unlink2(path).catch(() => {
4049
+ });
4050
+ return null;
3395
4051
  }
3396
4052
 
3397
4053
  // src/cli/findings.ts
@@ -3539,33 +4195,10 @@ async function inPreflight(signals, outer, fn) {
3539
4195
  }
3540
4196
  }
3541
4197
 
3542
- // src/cli/trustPrompt.ts
3543
- async function ensureTrusted(ctx, mc, app, o) {
3544
- const root = mc.repoRoot ?? ctx.deps.cwd;
3545
- if (!ctx.opts().json) ctx.io.err(contextLine(mc, app));
3546
- if (await isTrusted(ctx.deps, root, mc.module.id)) return;
3547
- if (o.yes) {
3548
- await trust(ctx.deps, root, mc.module.id);
3549
- return;
3550
- }
3551
- if (!ctx.io.interactive) {
3552
- throw new QoreError("QORE_CONTEXT_REQUIRED", `This repository is not trusted for ${mc.module.slug} yet`, {
3553
- hint: "Run interactively once, or pass --yes.",
3554
- details: { reason: "untrusted", repository: root }
3555
- });
3556
- }
3557
- if (mc.source.kind === "remote") {
3558
- const key = repoKey(mc.source.url);
3559
- ctx.io.err(` from remote ${mc.source.remote}: ${key ?? "(unparsable remote URL)"}`);
3560
- }
3561
- if (!await ctx.io.confirm("Pull local secrets for this module into this repository?")) throw new CancelledError();
3562
- await trust(ctx.deps, root, mc.module.id);
3563
- }
3564
-
3565
4198
  // src/cli/commands/devWorkspace.ts
3566
4199
  import { randomBytes as randomBytes5 } from "crypto";
3567
4200
  import { realpath as realpath7 } from "fs/promises";
3568
- import { join as join20, relative as relative6 } from "path";
4201
+ import { join as join22, relative as relative6 } from "path";
3569
4202
 
3570
4203
  // src/core/dev/order.ts
3571
4204
  function selectSkips(modules, sel) {
@@ -3701,8 +4334,8 @@ function exitCode(e) {
3701
4334
  }
3702
4335
  function supervise(o) {
3703
4336
  const now = o.now ?? Date.now;
3704
- const delay = o.delay ?? ((ms) => new Promise((r) => {
3705
- setTimeout(r, ms).unref();
4337
+ const delay = o.delay ?? ((ms2) => new Promise((r) => {
4338
+ setTimeout(r, ms2).unref();
3706
4339
  }));
3707
4340
  const abort = new AbortController();
3708
4341
  const emit = (e) => {
@@ -3722,15 +4355,15 @@ function supervise(o) {
3722
4355
  });
3723
4356
  const readiness = /* @__PURE__ */ new Map();
3724
4357
  for (const m of o.modules) {
3725
- let resolve6;
4358
+ let resolve7;
3726
4359
  let settled = false;
3727
4360
  const promise = new Promise((r) => {
3728
- resolve6 = r;
4361
+ resolve7 = r;
3729
4362
  });
3730
4363
  readiness.set(m.slug, { promise, settle: (r) => {
3731
4364
  if (settled) return false;
3732
4365
  settled = true;
3733
- resolve6(r);
4366
+ resolve7(r);
3734
4367
  return true;
3735
4368
  } });
3736
4369
  }
@@ -3822,7 +4455,7 @@ function supervise(o) {
3822
4455
 
3823
4456
  // src/core/workspace/stray.ts
3824
4457
  import { lstat as lstat2, readdir as readdir3 } from "fs/promises";
3825
- import { join as join19 } from "path";
4458
+ import { join as join21 } from "path";
3826
4459
  async function strayRepos(root, slugs) {
3827
4460
  let entries;
3828
4461
  try {
@@ -3834,7 +4467,7 @@ async function strayRepos(root, slugs) {
3834
4467
  for (const name of entries) {
3835
4468
  if (slugs.has(name)) continue;
3836
4469
  try {
3837
- await lstat2(join19(root, name, ".git"));
4470
+ await lstat2(join21(root, name, ".git"));
3838
4471
  names.push(name);
3839
4472
  } catch {
3840
4473
  }
@@ -3880,7 +4513,7 @@ function registerRun(program, ctx) {
3880
4513
  hint: "Drop --json; the child's own output passes through."
3881
4514
  });
3882
4515
  }
3883
- const c = await projectCtx(ctx, { ref: f.project, module: f.module, app: f.app, needModule: true });
4516
+ const c = await projectCtx(ctx, { ref: f.project, module: f.module, app: f.app, needModule: true, command: "run" });
3884
4517
  const { deps: deps2 } = c;
3885
4518
  const mc = await moduleContextFor(c, f, "run");
3886
4519
  const app = await appFor(c, mc, f.app);
@@ -3905,12 +4538,12 @@ function registerRun(program, ctx) {
3905
4538
 
3906
4539
  // src/cli/commands/devWorkspace.ts
3907
4540
  var INSTALL_PARALLELISM = 4;
3908
- var str = (v) => typeof v === "string" && v.length > 0;
4541
+ var str2 = (v) => typeof v === "string" && v.length > 0;
3909
4542
  var slugList = (raw) => raw?.split(",").map((s) => s.trim()).filter(Boolean);
3910
4543
  function usableLast(last, appId) {
3911
4544
  if (!last || last.kind !== "workspace") return false;
3912
4545
  const a = last.app;
3913
- return str(last.savedAt) && !!a && a.id === appId && str(a.slug) && str(a.name) && !!a.tenant && str(a.tenant.name) && Array.isArray(last.modules) && last.modules.every((m) => !!m && str(m.id) && str(m.slug) && str(m.type) && str(m.access) && Array.isArray(m.dependsOn));
4546
+ return str2(last.savedAt) && !!a && a.id === appId && str2(a.slug) && str2(a.name) && !!a.tenant && str2(a.tenant.name) && Array.isArray(last.modules) && last.modules.every((m) => !!m && str2(m.id) && str2(m.slug) && str2(m.type) && str2(m.access) && Array.isArray(m.dependsOn));
3914
4547
  }
3915
4548
  async function workspaceContext(ctx, ws) {
3916
4549
  const { deps: deps2 } = ctx;
@@ -3996,7 +4629,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
3996
4629
  continue;
3997
4630
  }
3998
4631
  try {
3999
- dirs[m.slug] = await deps2.git.inspect(join20(ws.root, m.slug));
4632
+ dirs[m.slug] = await deps2.git.inspect(join22(ws.root, m.slug));
4000
4633
  safe.push(m);
4001
4634
  } catch (e) {
4002
4635
  if (!(e instanceof QoreError) || e.code === "QORE_GIT_UNAVAILABLE") throw e;
@@ -4033,7 +4666,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
4033
4666
  const results = await applyActions(deps2, { portal: online.portal, app: am, actions, protocol: cloneProtocol(dirs), onResult: () => {
4034
4667
  } });
4035
4668
  renderResults(io2, results, false);
4036
- for (const r of results) if (r.status === "cloned") dirs[r.slug] = await deps2.git.inspect(join20(ws.root, r.slug));
4669
+ for (const r of results) if (r.status === "cloned") dirs[r.slug] = await deps2.git.inspect(join22(ws.root, r.slug));
4037
4670
  ({ present, plan } = computePlan());
4038
4671
  }
4039
4672
  for (const name of await strayRepos(ws.root, new Set(am.modules.map((m) => m.slug)))) renderFinding(ctx, strayClone(name, am.app.name));
@@ -4214,7 +4847,7 @@ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
4214
4847
  if (online && units.every((u) => !u.dev.offline)) {
4215
4848
  const envFile = (m) => {
4216
4849
  const u = units.find((x) => x.m.id === m.id);
4217
- return u ? relative6(rootReal, u.dev.envFile) : join20(m.slug, moduleDefaults(m.type).envFile);
4850
+ return u ? relative6(rootReal, u.dev.envFile) : join22(m.slug, moduleDefaults(m.type).envFile);
4218
4851
  };
4219
4852
  await saveLastStart(deps2, ws.root, {
4220
4853
  schemaVersion: 1,
@@ -4410,7 +5043,7 @@ function registerDev(program, ctx) {
4410
5043
  hint: "qore dev <project> | qore dev --project <project>"
4411
5044
  });
4412
5045
  }
4413
- const c = await projectCtx(ctx, { ref: project ?? f.project, module: f.module, app: f.app, needModule: false });
5046
+ const c = await projectCtx(ctx, { ref: project ?? f.project, module: f.module, app: f.app, needModule: false, command: "dev" });
4414
5047
  const ws = await workspaceRoot(c.deps);
4415
5048
  if (ws && f.module === void 0) return workspaceDev(c, ws, f, waitTimeout);
4416
5049
  return moduleDev(c, f, waitTimeout);
@@ -4425,7 +5058,7 @@ function parseWaitTimeout(raw) {
4425
5058
  }
4426
5059
  return n;
4427
5060
  }
4428
- var str2 = (v) => typeof v === "string" && v.length > 0;
5061
+ var str3 = (v) => typeof v === "string" && v.length > 0;
4429
5062
  var SHELL_SAFE = /^[A-Za-z0-9_./,:@%+=-]*$/;
4430
5063
  var shellQuote = (s) => SHELL_SAFE.test(s) ? s : `'${s.replace(/'/g, "'\\''")}'`;
4431
5064
  function cdAndDev(home, dir2) {
@@ -4453,7 +5086,7 @@ async function providerFindings(ctx, root, mc, app, env) {
4453
5086
  let fix = `qore clone ${app.slug}`;
4454
5087
  if (ws && ws.appId === app.id) fix = cdAndDev(deps2.home, ws.root);
4455
5088
  else if (providers.length === 1) {
4456
- const sibling = join21(dirname7(root), providers[0].slug);
5089
+ const sibling = join23(dirname7(root), providers[0].slug);
4457
5090
  if ((await deps2.git.inspect(sibling)).isRepo) fix = cdAndDev(deps2.home, sibling);
4458
5091
  }
4459
5092
  return providerDown({
@@ -4471,7 +5104,7 @@ async function providerFindings(ctx, root, mc, app, env) {
4471
5104
  function usableLast2(last) {
4472
5105
  if (!last || last.kind !== "module") return false;
4473
5106
  const m = last.module;
4474
- return str2(last.savedAt) && !!m && str2(m.id) && str2(m.slug) && str2(m.type) && !!m.tenant && str2(m.tenant.name) && !!last.app && str2(last.app.id) && str2(last.app.slug) && str2(last.app.name);
5107
+ return str3(last.savedAt) && !!m && str3(m.id) && str3(m.slug) && str3(m.type) && !!m.tenant && str3(m.tenant.name) && !!last.app && str3(last.app.id) && str3(last.app.slug) && str3(last.app.name);
4475
5108
  }
4476
5109
  async function devContext(ctx, f, root) {
4477
5110
  let mc;
@@ -4515,9 +5148,9 @@ async function moduleDev(ctx, f, waitTimeout) {
4515
5148
  childRunning.current = false;
4516
5149
  await removeRunFile(deps2, root);
4517
5150
  }
4518
- const ms = deps2.now().getTime() - startedAt.getTime();
5151
+ const ms2 = deps2.now().getTime() - startedAt.getTime();
4519
5152
  if (exit.code !== 0 && exit.signal === null) {
4520
- 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}.`);
5153
+ io2.err(ms2 < EARLY_EXIT_MS ? `\u2717 ${slug} exited after ${Math.round(ms2 / 1e3)} s with code ${exit.code ?? 1} \u2014 its output above says why.` : `\u2717 ${slug} exited with code ${exit.code ?? 1}.`);
4521
5154
  const hint = exitHint(tee.lines(), hintContext(recipe, pkg, devEnv2));
4522
5155
  if (hint) io2.err(` \u2192 ${hint.hint}`);
4523
5156
  }
@@ -4647,8 +5280,8 @@ function registerDebugTelemetry(program, ctx) {
4647
5280
  import pc3 from "picocolors";
4648
5281
 
4649
5282
  // src/core/doctor/checks.ts
4650
- import { realpath as realpath9, stat as stat5 } from "fs/promises";
4651
- import { basename as basename3, join as join22 } from "path";
5283
+ import { realpath as realpath9, stat as stat6 } from "fs/promises";
5284
+ import { basename as basename4, join as join24 } from "path";
4652
5285
 
4653
5286
  // src/core/diagnostics/ssh.ts
4654
5287
  var SSH_TEST_ARGS = ["-T", "-o", "BatchMode=yes", "-o", "ConnectTimeout=5", "git@github.com"];
@@ -4698,19 +5331,19 @@ var withError = (status2, label3, e) => ({ status: status2, label: label3, error
4698
5331
  var isCancel = (e, signal) => e instanceof CancelledError || signal.aborted;
4699
5332
  var portalLabel = (p) => p.builtin ?? p.apiOrigin;
4700
5333
  var ToolTimeout = class extends Error {
4701
- constructor(tool, ms) {
4702
- super(`${tool} did not answer within ${Math.round(ms / 1e3)} s`);
5334
+ constructor(tool, ms2) {
5335
+ super(`${tool} did not answer within ${Math.round(ms2 / 1e3)} s`);
4703
5336
  this.tool = tool;
4704
- this.ms = ms;
5337
+ this.ms = ms2;
4705
5338
  }
4706
5339
  tool;
4707
5340
  ms;
4708
5341
  };
4709
- async function timed(tool, signal, ms, fn) {
5342
+ async function timed(tool, signal, ms2, fn) {
4710
5343
  try {
4711
- return await fn(AbortSignal.any([signal, AbortSignal.timeout(ms)]));
5344
+ return await fn(AbortSignal.any([signal, AbortSignal.timeout(ms2)]));
4712
5345
  } catch (e) {
4713
- if (e instanceof CancelledError && !signal.aborted) throw new ToolTimeout(tool, ms);
5346
+ if (e instanceof CancelledError && !signal.aborted) throw new ToolTimeout(tool, ms2);
4714
5347
  throw e;
4715
5348
  }
4716
5349
  }
@@ -4741,8 +5374,8 @@ function checkOwnNode(deps2) {
4741
5374
  }
4742
5375
  return ok(`Node ${v} (\u226522 required)`);
4743
5376
  }
4744
- async function checkGit(deps2, signal, ms) {
4745
- const res = await timed("git", signal, ms, (s) => runTool(deps2, "git", ["--version"], { cwd: deps2.home, env: deps2.env, onLine: noop, signal: s }));
5377
+ async function checkGit(deps2, signal, ms2) {
5378
+ const res = await timed("git", signal, ms2, (s) => runTool(deps2, "git", ["--version"], { cwd: deps2.home, env: deps2.env, onLine: noop, signal: s }));
4746
5379
  if (res.spawnError === "ENOENT") {
4747
5380
  const e = new QoreError("QORE_GIT_UNAVAILABLE", "git is not installed or not on PATH", {
4748
5381
  hint: "Install git (https://git-scm.com/downloads) and open a new terminal."
@@ -4756,14 +5389,14 @@ async function checkGit(deps2, signal, ms) {
4756
5389
  const m = /git version (\d+)\.(\d+)/.exec(res.tail.join("\n"));
4757
5390
  return ok(m ? `git ${m[1]}.${m[2]}` : "git");
4758
5391
  }
4759
- async function checkGithubSsh(deps2, signal, ms) {
4760
- const res = await timed("ssh", signal, ms, (s) => runTool(deps2, "ssh", SSH_TEST_ARGS, { cwd: deps2.home, env: deps2.env, onLine: noop, signal: s }));
5392
+ async function checkGithubSsh(deps2, signal, ms2) {
5393
+ const res = await timed("ssh", signal, ms2, (s) => runTool(deps2, "ssh", SSH_TEST_ARGS, { cwd: deps2.home, env: deps2.env, onLine: noop, signal: s }));
4761
5394
  const r = classifySshTest(res);
4762
5395
  if (r.ok) return ok(`GitHub SSH ok (${r.user})`, { user: r.user });
4763
5396
  return withError("warn", `GitHub SSH: ${r.error.message} (https still works)`, r.error);
4764
5397
  }
4765
- async function checkDocker(deps2, signal, ms) {
4766
- return timed("docker", signal, ms, async (s) => {
5398
+ async function checkDocker(deps2, signal, ms2) {
5399
+ return timed("docker", signal, ms2, async (s) => {
4767
5400
  const o = { cwd: deps2.home, env: deps2.env, onLine: noop, signal: s };
4768
5401
  const info = await runTool(deps2, "docker", ["info", "--format", "{{.ServerVersion}}"], o);
4769
5402
  const infoErr = classifyDockerInfo(info, deps2.platform);
@@ -4791,9 +5424,9 @@ function jwtExp(token) {
4791
5424
  }
4792
5425
  function validDays(rt, now) {
4793
5426
  const exp = jwtExp(rt.rt);
4794
- const end = exp !== null ? exp * 1e3 : Date.parse(rt.obtainedAt) + IDLE_DAYS * DAY_MS;
4795
- if (Number.isNaN(end)) return null;
4796
- return Math.max(0, Math.floor((end - now.getTime()) / DAY_MS));
5427
+ const end2 = exp !== null ? exp * 1e3 : Date.parse(rt.obtainedAt) + IDLE_DAYS * DAY_MS;
5428
+ if (Number.isNaN(end2)) return null;
5429
+ return Math.max(0, Math.floor((end2 - now.getTime()) / DAY_MS));
4797
5430
  }
4798
5431
  async function checkSession(deps2, portal, onRequest, sessions, selected, signal) {
4799
5432
  try {
@@ -4849,7 +5482,7 @@ async function moduleBody(deps2, m, runs, signal) {
4849
5482
  if (text2 === null) return { status: "warn", label: "port unknown (run qore env pull)" };
4850
5483
  env = Object.fromEntries(parseEnv(text2));
4851
5484
  const port = envPort(env, "PORT");
4852
- if (port === null) return { status: "warn", label: `port unknown (no PORT in ${basename3(target)})` };
5485
+ if (port === null) return { status: "warn", label: `port unknown (no PORT in ${basename4(target)})` };
4853
5486
  if (!await deps2.ports.inUse(port)) return { status: "ok", label: `port ${port} free` };
4854
5487
  const e = await portInUseError(deps2, { slug: m.slug, port, key: "PORT", runs: await runs() });
4855
5488
  return { status: "fail", label: `port ${port} in use${ownerText(e)}`, error: e };
@@ -4868,13 +5501,13 @@ async function moduleBody(deps2, m, runs, signal) {
4868
5501
  flags: { install: true, services: true }
4869
5502
  });
4870
5503
  if (recipe.services.length === 0) return { status: "ok", label: "db none" };
4871
- const dbPort = env ? envPort(env, "DB_PORT") : null;
4872
- if (dbPort === null) return { status: "warn", label: "db port unknown (run qore env pull)" };
4873
- if (!await deps2.ports.inUse(dbPort)) return { status: "ok", label: `db:up (DB_PORT ${dbPort} free)` };
4874
- const pub = await deps2.ports.dockerPublisher(dbPort);
5504
+ const dbPort2 = env ? envPort(env, "DB_PORT") : null;
5505
+ if (dbPort2 === null) return { status: "warn", label: "db port unknown (run qore env pull)" };
5506
+ if (!await deps2.ports.inUse(dbPort2)) return { status: "ok", label: `db:up (DB_PORT ${dbPort2} free)` };
5507
+ const pub = await deps2.ports.dockerPublisher(dbPort2);
4875
5508
  if (pub?.workingDir && await sameDir(pub.workingDir, m.root)) return { status: "ok", label: "db running" };
4876
- const e = await portInUseError(deps2, { slug: m.slug, port: dbPort, key: "DB_PORT" });
4877
- return { status: "fail", label: `DB_PORT ${dbPort} in use${ownerText(e)}`, error: e };
5509
+ const e = await portInUseError(deps2, { slug: m.slug, port: dbPort2, key: "DB_PORT" });
5510
+ return { status: "fail", label: `DB_PORT ${dbPort2} in use${ownerText(e)}`, error: e };
4878
5511
  });
4879
5512
  const status2 = parts.reduce((w, p) => RANK[p.status] > RANK[w] ? p.status : w, "ok");
4880
5513
  const shown = parts.filter((p) => p.status !== "ok");
@@ -4895,16 +5528,16 @@ function runsAt(deps2, root) {
4895
5528
  return () => p ??= leftoverRun(deps2, root, { prune: false }).then((r) => r ? [r] : []);
4896
5529
  }
4897
5530
  var UNTRUSTED = { status: "warn", label: "not checked (repository not trusted)" };
4898
- async function checkRepoPm(deps2, m, recipe, signal, ms) {
5531
+ async function checkRepoPm(deps2, m, recipe, signal, ms2) {
4899
5532
  if (!await isTrusted(deps2, m.root, m.id)) return UNTRUSTED;
4900
- const found = await timed(recipe.pm.name, signal, ms, (s) => checkPackageManager(deps2, recipe, noop, s));
5533
+ const found = await timed(recipe.pm.name, signal, ms2, (s) => checkPackageManager(deps2, recipe, noop, s));
4901
5534
  const { name, version } = recipe.pm;
4902
5535
  if (found === null) return ok(`${name} (version unknown)`);
4903
5536
  return ok(version ? `${name} ${found} (matches)` : `${name} ${found}`, { found, ...version ? { pinned: version } : {} });
4904
5537
  }
4905
- async function checkRepoNode(deps2, m, recipe, pkg, signal, ms) {
5538
+ async function checkRepoNode(deps2, m, recipe, pkg, signal, ms2) {
4906
5539
  if (!await isTrusted(deps2, m.root, m.id)) return UNTRUSTED;
4907
- const found = await timed("node", signal, ms, (s) => checkNode(deps2, recipe, pkg, m.slug, s));
5540
+ const found = await timed("node", signal, ms2, (s) => checkNode(deps2, recipe, pkg, m.slug, s));
4908
5541
  return ok(`Node ${found ?? "?"} (engines ${pkg.engines.node})`);
4909
5542
  }
4910
5543
  async function loggedInPortals(deps2, profiles) {
@@ -4931,7 +5564,7 @@ async function resolveRepoModule(deps2, session, portal, meta, root) {
4931
5564
  async function checkProjectsRoot(cfg) {
4932
5565
  const root = cfg?.projectsRoot;
4933
5566
  if (!root) return ok("projects folder not set (optional \u2014 qore init)");
4934
- const s = await stat5(root).catch(() => null);
5567
+ const s = await stat6(root).catch(() => null);
4935
5568
  if (!s?.isDirectory()) return { status: "warn", label: `projects folder ${root} does not exist \u2014 qore init` };
4936
5569
  const e = await writableError(root);
4937
5570
  if (e) return { status: "fail", label: `projects folder ${root} is not writable (${e.code ?? e.message})` };
@@ -5034,7 +5667,7 @@ async function runDoctor(deps2, o = {}) {
5034
5667
  const repoRoot = wsRoot ? null : await findRepoRoot(deps2.cwd);
5035
5668
  if (wsRoot) {
5036
5669
  context.kind = "workspace";
5037
- context.label = basename3(wsRoot.root);
5670
+ context.label = basename4(wsRoot.root);
5038
5671
  if (!session) {
5039
5672
  await notChecked();
5040
5673
  return { results, context };
@@ -5054,7 +5687,7 @@ async function runDoctor(deps2, o = {}) {
5054
5687
  for (const m of am.modules) await workspaceModule(deps2, wsRoot, am, m, runs, signal, push, perModule);
5055
5688
  } else if (repoRoot) {
5056
5689
  context.kind = "module";
5057
- context.label = basename3(repoRoot);
5690
+ context.label = basename4(repoRoot);
5058
5691
  if (!session) {
5059
5692
  await notChecked();
5060
5693
  return { results, context };
@@ -5085,7 +5718,7 @@ async function workspaceModule(deps2, ws, am, m, runs, signal, push, perModule)
5085
5718
  await push(`module:${m.slug}`, "workspace", () => ({ status: "warn", label: "not a safe directory name \u2014 skipped" }), m.slug);
5086
5719
  return;
5087
5720
  }
5088
- const mod = { root: join22(ws.root, m.slug), slug: m.slug, type: m.type, id: m.id, access: m.access };
5721
+ const mod = { root: join24(ws.root, m.slug), slug: m.slug, type: m.type, id: m.id, access: m.access };
5089
5722
  let prepared = null;
5090
5723
  await push(`module:${m.slug}`, "workspace", async () => {
5091
5724
  const info = await deps2.git.inspect(mod.root);
@@ -5117,12 +5750,12 @@ function registerDoctor(program, ctx) {
5117
5750
  // src/cli/commands/env.ts
5118
5751
  import { randomBytes as randomBytes7 } from "crypto";
5119
5752
  import { chmod as chmod3, realpath as realpath10 } from "fs/promises";
5120
- import { dirname as dirname8, join as join23, relative as relative8 } from "path";
5753
+ import { dirname as dirname8, join as join25, relative as relative8 } from "path";
5121
5754
  var maskLen = (v) => v === void 0 ? "(unset)" : `\u2022\u2022\u2022\u2022 (${v.length} chars)`;
5122
5755
  function registerEnv(program, ctx) {
5123
5756
  const env = program.command("env").description("Local environment");
5124
5757
  env.command("pull").description("Write this module's local secrets into its env file").option("--module <slug|id>", "the module (default: from this repository's git remote)").option("--app <slug|id>", "the app, when the module is part of several").option("--project <ref>", "the project to pull for, from anywhere (see qore init)").option("--target <path>", "env file to write (inside the repository)").option("--use-portal", "take the portal's value for keys that differ locally").option("--yes", "trust this repository without asking").action(async (f) => {
5125
- const c = await projectCtx(ctx, { ref: f.project, module: f.module, app: f.app, needModule: true });
5758
+ const c = await projectCtx(ctx, { ref: f.project, module: f.module, app: f.app, needModule: true, command: "env pull" });
5126
5759
  const { deps: deps2, io: io2 } = c;
5127
5760
  const g = c.opts();
5128
5761
  const mc = await moduleContextFor(c, f, "env pull");
@@ -5136,7 +5769,7 @@ function registerEnv(program, ctx) {
5136
5769
  const target = f.target ? await resolveTarget(mc.repoRoot, f.target, deps2.cwd) : await resolveTarget(mc.repoRoot, cfg.envFile ?? defaults.envFile);
5137
5770
  await ensureTrusted(c, mc, app, { yes: f.yes });
5138
5771
  const existing = await readOrNull(target);
5139
- const example = existing === null ? await readOrNull(join23(dirname8(target), ".env.example")) : null;
5772
+ const example = existing === null ? await readOrNull(join25(dirname8(target), ".env.example")) : null;
5140
5773
  const local = await fetchLocalEnv(deps2, mc, app);
5141
5774
  const inputs = {
5142
5775
  existing,
@@ -5152,13 +5785,13 @@ function registerEnv(program, ctx) {
5152
5785
  const current = parseEnv(existing ?? example ?? "");
5153
5786
  const choices = {};
5154
5787
  let bulk;
5155
- for (const key of plan.conflicts) {
5788
+ for (const key2 of plan.conflicts) {
5156
5789
  if (bulk) {
5157
- choices[key] = bulk;
5790
+ choices[key2] = bulk;
5158
5791
  continue;
5159
5792
  }
5160
- const pick = await io2.select(
5161
- `${key} differs (local ${maskLen(current.get(key))}, portal ${maskLen(local.env[key])})`,
5793
+ const pick2 = await io2.select(
5794
+ `${key2} differs (local ${maskLen(current.get(key2))}, portal ${maskLen(local.env[key2])})`,
5162
5795
  [
5163
5796
  { name: "keep local", value: "keep" },
5164
5797
  { name: "use portal", value: "portal" },
@@ -5166,9 +5799,9 @@ function registerEnv(program, ctx) {
5166
5799
  { name: "keep local for all", value: "skip" }
5167
5800
  ]
5168
5801
  );
5169
- if (pick === "all") bulk = "use-portal";
5170
- if (pick === "skip") bulk = "keep";
5171
- choices[key] = pick === "portal" || pick === "all" ? "use-portal" : "keep";
5802
+ if (pick2 === "all") bulk = "use-portal";
5803
+ if (pick2 === "skip") bulk = "keep";
5804
+ choices[key2] = pick2 === "portal" || pick2 === "all" ? "use-portal" : "keep";
5172
5805
  }
5173
5806
  plan = planPull({ ...inputs, choices });
5174
5807
  }
@@ -5206,12 +5839,486 @@ function registerEnv(program, ctx) {
5206
5839
  });
5207
5840
  }
5208
5841
 
5842
+ // src/core/observability/compose.ts
5843
+ function pageWarnings(...pages) {
5844
+ return pages.some((p) => p.ingestPaused) ? ["ingest_paused"] : [];
5845
+ }
5846
+ function scope(q) {
5847
+ const s = { env: q.env };
5848
+ if (q.since !== void 0) s.since = q.since;
5849
+ if (q.start !== void 0) s.start = q.start;
5850
+ if (q.end !== void 0) s.end = q.end;
5851
+ return s;
5852
+ }
5853
+ async function errorDetail(c, id, q, withs) {
5854
+ const w = scope(q);
5855
+ const markdown = await c.issueMarkdown(id, w);
5856
+ const event = await c.issueEvent(id, w);
5857
+ const out = { issue: { markdown, event }, warnings: [] };
5858
+ if (withs.size === 0) return out;
5859
+ if (!event.traceId) {
5860
+ out.warnings.push("no_trace_id");
5861
+ return out;
5862
+ }
5863
+ if (withs.has("trace")) out.trace = await c.trace(event.traceId, w);
5864
+ if (withs.has("logs")) {
5865
+ const logs = await c.logs({ ...w, trace: event.traceId, limit: 100 });
5866
+ out.logs = logs.items;
5867
+ out.warnings.push(...pageWarnings(logs));
5868
+ }
5869
+ return out;
5870
+ }
5871
+ async function traceDetail(c, id, q, withs) {
5872
+ const w = scope(q);
5873
+ const out = { trace: await c.trace(id, w), warnings: [] };
5874
+ const pages = [];
5875
+ if (withs.has("logs")) {
5876
+ const p = await c.logs({ ...w, trace: id, limit: 100 });
5877
+ out.logs = p.items;
5878
+ pages.push(p);
5879
+ }
5880
+ if (withs.has("errors")) {
5881
+ const p = await c.issues({ ...w, trace: id, status: "all" });
5882
+ out.errors = p.items;
5883
+ pages.push(p);
5884
+ }
5885
+ out.warnings.push(...pageWarnings(...pages));
5886
+ return out;
5887
+ }
5888
+
5889
+ // src/core/observability/client.ts
5890
+ var MAX_RETRY_WAIT_S = 30;
5891
+ var NOT_PROVISIONED = "observability_not_provisioned";
5892
+ var wait = (ms2) => new Promise((r) => setTimeout(r, ms2));
5893
+ var ObsClient = class {
5894
+ constructor(api, moduleId, sleep = wait, slug = moduleId) {
5895
+ this.api = api;
5896
+ this.moduleId = moduleId;
5897
+ this.sleep = sleep;
5898
+ this.slug = slug;
5899
+ }
5900
+ api;
5901
+ moduleId;
5902
+ sleep;
5903
+ slug;
5904
+ path(resource, o, extra = {}) {
5905
+ const p = new URLSearchParams();
5906
+ const all = { env: o.env, since: o.since, start: o.start, end: o.end, q: o.q, limit: o.limit, ...extra };
5907
+ for (const [k, v] of Object.entries(all)) if (v !== void 0 && v !== "") p.set(k, String(v));
5908
+ return `/api/cli/v1/modules/${encodeURIComponent(this.moduleId)}/observability/${resource}?${p}`;
5909
+ }
5910
+ async call(fn) {
5911
+ try {
5912
+ return await this.retryOnce(fn);
5913
+ } catch (e) {
5914
+ if (e instanceof QoreError && e.code === "QORE_ACCESS_DENIED" && e.serverCode === "auth.scope.insufficient") {
5915
+ throw new QoreError("QORE_ACCESS_DENIED", "This login predates `qore errors/traces/logs`", {
5916
+ hint: "Run `qore login` again.",
5917
+ status: e.status,
5918
+ requestId: e.requestId,
5919
+ serverCode: e.serverCode
5920
+ });
5921
+ }
5922
+ if (e instanceof QoreError && e.serverCode === NOT_PROVISIONED) {
5923
+ throw new QoreError("QORE_RESOURCE_NOT_FOUND", `Module "${this.slug}" does not report to Sentry yet`, {
5924
+ hint: "Its Sentry project is not provisioned; pick another module with --module.",
5925
+ status: e.status,
5926
+ requestId: e.requestId,
5927
+ serverCode: e.serverCode,
5928
+ details: e.details
5929
+ });
5930
+ }
5931
+ throw e;
5932
+ }
5933
+ }
5934
+ async retryOnce(fn) {
5935
+ try {
5936
+ return await fn();
5937
+ } catch (e) {
5938
+ if (!(e instanceof QoreError) || e.code !== "QORE_RATE_LIMITED") throw e;
5939
+ const secs2 = typeof e.details?.retryAfter === "number" ? e.details.retryAfter : 1;
5940
+ await this.sleep(Math.min(secs2, MAX_RETRY_WAIT_S) * 1e3);
5941
+ return fn();
5942
+ }
5943
+ }
5944
+ issues(o) {
5945
+ return this.call(() => this.api.get(this.path("issues", o, { status: o.status, sort: o.sort, trace: o.trace })));
5946
+ }
5947
+ issueMarkdown(id, o) {
5948
+ return this.call(() => this.api.getText(this.path(`issues/${encodeURIComponent(id)}/markdown`, { env: o.env, since: o.since, start: o.start, end: o.end }, { event: "recommended" })));
5949
+ }
5950
+ issueEvent(id, o) {
5951
+ return this.call(() => this.api.get(this.path(`issues/${encodeURIComponent(id)}/events/recommended`, { env: o.env, since: o.since, start: o.start, end: o.end })));
5952
+ }
5953
+ logs(o) {
5954
+ return this.call(() => this.api.get(this.path("logs", o, { level: o.level, trace: o.trace })));
5955
+ }
5956
+ spans(o) {
5957
+ return this.call(() => this.api.get(this.path("spans", o, { sort: o.sort })));
5958
+ }
5959
+ trace(id, o) {
5960
+ return this.call(() => this.api.get(this.path(`traces/${encodeURIComponent(id)}`, { env: o.env, since: o.since, start: o.start, end: o.end })));
5961
+ }
5962
+ };
5963
+
5964
+ // src/core/observability/query.ts
5965
+ var ROOT_SPAN_FILTER = "is_transaction:true";
5966
+ var DURATION = /^\d{1,6}(ms|s|m)$/;
5967
+ var SINCE = /^(\d{1,4})([mhdw])$/;
5968
+ var SECONDS = { m: 60, h: 3600, d: 86400, w: 604800 };
5969
+ function parseDuration(s) {
5970
+ if (!DURATION.test(s)) throw new QoreError("QORE_CONTEXT_INVALID", `"${s}" is not a duration`, { hint: "Use e.g. 500ms, 1s or 2m." });
5971
+ return s;
5972
+ }
5973
+ function parseSince(s) {
5974
+ const m = SINCE.exec(s);
5975
+ if (!m || Number(m[1]) * SECONDS[m[2]] > 90 * 86400) {
5976
+ throw new QoreError("QORE_CONTEXT_INVALID", `"${s}" is not a time window`, { hint: "Use a count plus m, h, d or w, at most 90 days (e.g. 24h, 7d)." });
5977
+ }
5978
+ return s;
5979
+ }
5980
+ function joinQuery(...parts) {
5981
+ const q = parts.filter((p) => typeof p === "string" && p.length > 0).join(" ");
5982
+ return q || void 0;
5983
+ }
5984
+
5985
+ // src/cli/observability/context.ts
5986
+ var ALL = { kind: "module", slug: "all", name: "All modules" };
5987
+ var modCandidates = (mods, withAll) => [...mods.map((m) => ({ kind: "module", id: m.id, slug: m.slug, name: m.name })), ...withAll ? [ALL] : []];
5988
+ function withRepo(mods) {
5989
+ const r = mods.filter((m) => m.repoUrl !== null);
5990
+ if (r.length === 0) throw new QoreError("QORE_RESOURCE_NOT_FOUND", "No modules with a repository in this app");
5991
+ return r;
5992
+ }
5993
+ function windowOf(f) {
5994
+ if (Boolean(f.start) !== Boolean(f.end)) throw new QoreError("QORE_CONTEXT_INVALID", "--start and --end go together; pass both");
5995
+ if (f.start || f.end) return { start: f.start, end: f.end };
5996
+ return { since: parseSince(f.since ?? "24h") };
5997
+ }
5998
+ var windowText = (w) => w.since ? `last ${w.since}` : `${w.start ?? "\u2026"} \u2013 ${w.end ?? "now"}`;
5999
+ async function chooseModule(ctx, mods, flag, single) {
6000
+ if (flag === "all" && !single) return mods;
6001
+ if (flag && flag !== "all") {
6002
+ const m = mods.find((x) => x.slug === flag || x.id === flag);
6003
+ if (m) return [m];
6004
+ throw new QoreError("QORE_CONTEXT_REQUIRED", `No module "${flag}" in this app`, { candidates: modCandidates(mods, !single) });
6005
+ }
6006
+ const err = new QoreError(
6007
+ "QORE_CONTEXT_REQUIRED",
6008
+ single && flag === "all" ? "A detail view needs one module" : "Which module?",
6009
+ { candidates: modCandidates(mods, !single), hint: "Pass --module <slug>" + (single ? "." : " or --module all.") }
6010
+ );
6011
+ if (flag === "all") throw err;
6012
+ const choice = await pick(
6013
+ ctx,
6014
+ err,
6015
+ "Select a module",
6016
+ [...mods.map((m) => ({ name: m.slug, value: m.id })), ...single ? [] : [{ name: "All modules", value: "all" }]]
6017
+ );
6018
+ return choice === "all" ? mods : mods.filter((m) => m.id === choice);
6019
+ }
6020
+ async function chooseEnv(ctx, api, moduleId, appId, flag) {
6021
+ const st = await api.get(`/api/cli/v1/modules/${encodeURIComponent(moduleId)}/status?app_id=${encodeURIComponent(appId)}`);
6022
+ const names = st.environments.map((e) => e.name);
6023
+ if (names.length === 0) throw new QoreError("QORE_RESOURCE_NOT_FOUND", "No environments yet");
6024
+ const candidates = names.map((n) => ({ kind: "env", slug: n }));
6025
+ if (flag) {
6026
+ if (names.includes(flag)) return flag;
6027
+ throw new QoreError("QORE_CONTEXT_REQUIRED", `Unknown environment "${flag}"`, { candidates, hint: `Pass --env ${names.join("|")}.` });
6028
+ }
6029
+ const err = new QoreError("QORE_CONTEXT_REQUIRED", "Which environment?", { candidates, hint: `Pass --env ${names.join("|")}.` });
6030
+ return pick(ctx, err, "Select an environment", names.map((n) => ({ name: n, value: n })));
6031
+ }
6032
+ async function obsContext(ctx, flags, command, o) {
6033
+ const { deps: deps2, io: io2 } = ctx;
6034
+ const g = ctx.opts();
6035
+ const window = windowOf(flags);
6036
+ const onRequest = g.verbose ? (l) => io2.err(l) : void 0;
6037
+ const ws = flags.module === void 0 || flags.module === "all" ? await workspaceRoot(deps2) : null;
6038
+ let api, tenant, app, mods, base, apiOrigin;
6039
+ if (ws) {
6040
+ const portal = await flagPortal(deps2, g.portal, ws);
6041
+ apiOrigin = portal.apiOrigin;
6042
+ const session = await openSession(deps2, portal, command, onRequest);
6043
+ await ensureCapability(deps2, apiOrigin, "module.observability");
6044
+ await ensureCapability(deps2, apiOrigin, "app.modules");
6045
+ const am = await workspaceAppModules(session, portal, ws);
6046
+ api = session.api;
6047
+ ({ tenant } = am.app);
6048
+ app = { id: am.app.id, slug: am.app.slug, name: am.app.name };
6049
+ mods = await chooseModule(ctx, withRepo(am.modules), flags.module, o.single);
6050
+ base = { portal: portal.builtin ?? portal.apiOrigin, tenant, app, source: WORKSPACE_FILE };
6051
+ } else {
6052
+ const mc = await moduleContextFor(ctx, { module: flags.module === "all" ? void 0 : flags.module }, command);
6053
+ apiOrigin = mc.portal.apiOrigin;
6054
+ await ensureCapability(deps2, apiOrigin, "module.observability");
6055
+ app = await appFor(ctx, mc, flags.app);
6056
+ api = mc.session.api;
6057
+ tenant = mc.module.tenant;
6058
+ if (flags.module === "all") {
6059
+ if (o.single) throw new QoreError(
6060
+ "QORE_CONTEXT_REQUIRED",
6061
+ "A detail view needs one module",
6062
+ { candidates: [{ kind: "module", id: mc.module.id, slug: mc.module.slug, name: mc.module.name }] }
6063
+ );
6064
+ await ensureCapability(deps2, apiOrigin, "app.modules");
6065
+ const am = await api.get(`/api/cli/v1/apps/${encodeURIComponent(app.id)}/modules`);
6066
+ mods = withRepo(am.modules);
6067
+ } else {
6068
+ mods = [mc.module];
6069
+ }
6070
+ base = contextJson(mc, app);
6071
+ delete base.module;
6072
+ }
6073
+ await ensureCapability(deps2, apiOrigin, "module.status");
6074
+ const env = await chooseEnv(ctx, api, mods[0].id, app.id, flags.env);
6075
+ const fanOut2 = mods.length > 1 || flags.module === "all";
6076
+ const targets = mods.map((m) => ({ module: { id: m.id, slug: m.slug, name: m.name, type: m.type, access: m.access }, client: new ObsClient(api, m.id, void 0, m.slug) }));
6077
+ const where = fanOut2 ? "all modules" : mods[0].slug;
6078
+ return {
6079
+ targets,
6080
+ fanOut: fanOut2,
6081
+ env,
6082
+ window,
6083
+ header: `\u2192 ${tenant.name} / ${app.name} / ${where} \xB7 ${env} \xB7 ${windowText(window)}`,
6084
+ json: { ...base, ...fanOut2 ? { modules: targets.map((t) => t.module) } : { module: targets[0].module }, env, window }
6085
+ };
6086
+ }
6087
+
6088
+ // src/cli/format.ts
6089
+ function ago(iso, now) {
6090
+ if (!iso) return "\u2013";
6091
+ const s = Math.max(0, Math.round((now.getTime() - new Date(iso).getTime()) / 1e3));
6092
+ if (s < 60) return "just now";
6093
+ if (s < 3600) return `${Math.floor(s / 60)}m ago`;
6094
+ if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
6095
+ return `${Math.floor(s / 86400)}d ago`;
6096
+ }
6097
+ var pct = (f) => String(Number((f * 100).toFixed(2)));
6098
+ function uptime(h) {
6099
+ if (!h) return "\u2013";
6100
+ const kind = h.cause && h.kind !== "up" ? `${h.kind}: ${h.cause}` : h.kind;
6101
+ return h.uptime24h === null ? kind : `${kind} (${pct(h.uptime24h)}% 24h)`;
6102
+ }
6103
+ function table(rows) {
6104
+ const widths = rows[0].map((_, i) => Math.max(...rows.map((r) => (r[i] ?? "").length)));
6105
+ return rows.map((r) => " " + r.map((c, i) => i === r.length - 1 ? c : c.padEnd(widths[i])).join(" "));
6106
+ }
6107
+
6108
+ // src/cli/observability/render.ts
6109
+ var CONTROL = /[\x00-\x08\x0b-\x1f\x7f\x80-\x9f]/g;
6110
+ var clean = (s) => s.replace(CONTROL, "");
6111
+ function fence(content) {
6112
+ const lines = content.map(clean);
6113
+ const longest = Math.max(0, ...lines.flatMap((l) => (l.match(/`+/g) ?? []).map((r) => r.length)));
6114
+ const ticks = "`".repeat(Math.max(3, longest + 1));
6115
+ return [`${ticks}untrusted-data`, ...lines, ticks];
6116
+ }
6117
+ function ms(n) {
6118
+ return n < 1e3 ? `${Math.round(n)}ms` : `${(n / 1e3).toFixed(1)}s`;
6119
+ }
6120
+ var oneLine = (s) => clean((s ?? "").replace(/\s+/g, " ")).trim();
6121
+ function issuesTable(items, now, withModule) {
6122
+ const head = ["ID", "LEVEL", "EVENTS", "USERS", "LAST SEEN", "TITLE"];
6123
+ const rows = items.map((i) => [oneLine(i.shortId), oneLine(i.level), String(i.count), String(i.userCount), ago(i.lastSeen, now), oneLine(i.title)]);
6124
+ return table(withModule ? [["MODULE", ...head], ...rows.map((r, n) => [oneLine(items[n].module.slug), ...r])] : [head, ...rows]);
6125
+ }
6126
+ function spansTable(items, now, withModule) {
6127
+ const head = ["TRACE", "DURATION", "ROOT", "WHEN"];
6128
+ const rows = items.map((s) => [oneLine(s.traceId), ms(s.durationMs), oneLine(s.transaction ?? s.description ?? s.name), ago(s.timestamp, now)]);
6129
+ return table(withModule ? [["MODULE", ...head], ...rows.map((r, n) => [oneLine(items[n].module.slug), ...r])] : [head, ...rows]);
6130
+ }
6131
+ function logLines(items, withModule) {
6132
+ return items.map((l) => [oneLine(l.timestamp), oneLine(l.severity).toUpperCase(), ...withModule ? [oneLine(l.module.slug)] : [], oneLine(l.message)].join(" "));
6133
+ }
6134
+ function spanTree(t) {
6135
+ const all = [];
6136
+ const walk = (n) => {
6137
+ all.push(n);
6138
+ n.children.forEach(walk);
6139
+ };
6140
+ t.roots.forEach(walk);
6141
+ const slowest = all.reduce((a, n) => !a || n.end - n.start > a.end - a.start ? n : a, null);
6142
+ const out = [];
6143
+ const line2 = (n, depth) => {
6144
+ const label3 = [oneLine(n.op), oneLine(n.description ?? n.transaction)].filter(Boolean).join(" ");
6145
+ const errs = n.errors.map((e) => `\u2716 ${oneLine(e.shortId ?? e.eventId)}`).join(" ");
6146
+ out.push([
6147
+ `${" ".repeat(depth)}${label3}`,
6148
+ ms((n.end - n.start) * 1e3),
6149
+ `[${oneLine(n.module.slug)}]`,
6150
+ ...n === slowest ? ["\u25C0 slowest"] : [],
6151
+ ...errs ? [errs] : []
6152
+ ].join(" "));
6153
+ n.children.forEach((c) => line2(c, depth + 1));
6154
+ };
6155
+ t.roots.forEach((r) => line2(r, 0));
6156
+ return out;
6157
+ }
6158
+ var WARN = {
6159
+ ingest_paused: "! This tenant's Sentry quota is reached; new events are not being recorded.",
6160
+ no_trace_id: "! This error has no trace id, so there is no trace or trace logs to show.",
6161
+ sort_unavailable: "! Sorting by duration is not available here; showing the slowest of the most recent matches.",
6162
+ module_not_provisioned: "! Skipped, not reporting to Sentry yet:"
6163
+ };
6164
+ function warningLines(w, skipped = []) {
6165
+ return [...new Set(w)].map((k) => k === "module_not_provisioned" ? `${WARN[k]} ${skipped.join(", ")}` : WARN[k]);
6166
+ }
6167
+
6168
+ // src/cli/observability/flags.ts
6169
+ function addObsFlags(cmd) {
6170
+ return cmd.option("--env <name>", "the environment (required; prompted when interactive)").option("--module <slug|all>", "the module (default: this repository); all = every module of the app").option("--app <slug|id>", "the app, when the module is part of several").option("--since <window>", "time window, e.g. 1h, 24h, 7d (default 24h)").option("--start <iso>", "window start (use with --end, instead of --since)").option("--end <iso>", "window end (use with --start, instead of --since)").option("--query <search>", "Sentry search syntax, appended to the filter").option("--limit <n>", "rows per module, 1-100 (default 25)");
6171
+ }
6172
+ function limitOf(s) {
6173
+ if (s === void 0) return 25;
6174
+ const n = Number(s);
6175
+ if (!Number.isInteger(n) || n < 1 || n > 100) throw new QoreError("QORE_CONTEXT_INVALID", `--limit must be 1-100, not "${s}"`);
6176
+ return n;
6177
+ }
6178
+ function withList(s, allowed) {
6179
+ if (!s) return /* @__PURE__ */ new Set();
6180
+ const parts = s.split(",").map((p) => p.trim()).filter(Boolean);
6181
+ const bad = parts.filter((p) => !allowed.includes(p));
6182
+ if (bad.length) throw new QoreError("QORE_CONTEXT_INVALID", `--with accepts ${allowed.join(", ")}; not ${bad.join(", ")}`);
6183
+ return new Set(parts);
6184
+ }
6185
+ function one(flag, value, allowed) {
6186
+ if (!allowed.includes(value)) {
6187
+ throw new QoreError("QORE_CONTEXT_INVALID", `${flag} must be ${allowed.join("|")}, not "${value}"`);
6188
+ }
6189
+ return value;
6190
+ }
6191
+ async function fanOut(oc2, fn) {
6192
+ const out = { results: [], skipped: [] };
6193
+ let last;
6194
+ for (const target of oc2.targets) {
6195
+ try {
6196
+ out.results.push({ target, result: await fn(target) });
6197
+ } catch (e) {
6198
+ if (oc2.targets.length < 2 || !(e instanceof QoreError) || e.serverCode !== NOT_PROVISIONED) throw e;
6199
+ out.skipped.push({ id: target.module.id, slug: target.module.slug });
6200
+ last = e;
6201
+ }
6202
+ }
6203
+ if (out.results.length === 0 && last) {
6204
+ throw new QoreError("QORE_RESOURCE_NOT_FOUND", "No module of this app reports to Sentry yet", {
6205
+ serverCode: NOT_PROVISIONED,
6206
+ details: { modules: out.skipped.map((m) => m.slug) }
6207
+ });
6208
+ }
6209
+ return out;
6210
+ }
6211
+ var skipWarnings = (f) => f.skipped.length ? ["module_not_provisioned"] : [];
6212
+ var skipJson = (f) => f.skipped.length ? { unprovisionedModules: f.skipped } : {};
6213
+ function printWarnings(io2, w, json2, skipped = []) {
6214
+ if (!json2) for (const l of warningLines(w, skipped.map((m) => m.slug))) io2.err(l);
6215
+ }
6216
+
6217
+ // src/cli/commands/errors.ts
6218
+ var WITHS = ["trace", "logs"];
6219
+ var STATUSES = ["unresolved", "resolved", "all"];
6220
+ var SORTS = ["date", "freq", "new"];
6221
+ var MERGE = {
6222
+ date: (a, b) => b.lastSeen.localeCompare(a.lastSeen),
6223
+ freq: (a, b) => b.count - a.count,
6224
+ new: (a, b) => b.firstSeen.localeCompare(a.firstSeen)
6225
+ };
6226
+ function printDetail(ctx, oc2, d, showIssue) {
6227
+ const { io: io2 } = ctx;
6228
+ printWarnings(io2, d.warnings, false);
6229
+ if (showIssue) for (const l of fence(d.issue.markdown.split("\n"))) io2.out(l);
6230
+ if (d.trace) {
6231
+ io2.out("");
6232
+ io2.out("Trace");
6233
+ for (const l of fence(spanTree(d.trace))) io2.out(l);
6234
+ }
6235
+ if (d.logs) {
6236
+ io2.out("");
6237
+ io2.out(`Logs (${d.logs.length})`);
6238
+ for (const l of fence(logLines(d.logs, oc2.fanOut))) io2.out(l);
6239
+ }
6240
+ }
6241
+ async function interactiveDetail(ctx, oc2, c, id) {
6242
+ const first = await errorDetail(c, id, { env: oc2.env, ...oc2.window }, /* @__PURE__ */ new Set());
6243
+ printDetail(ctx, oc2, first, true);
6244
+ const traceId = first.issue.event.traceId;
6245
+ if (!traceId) return;
6246
+ const scope2 = { env: oc2.env, ...oc2.window };
6247
+ for (; ; ) {
6248
+ const next = await ctx.io.select("Next?", [
6249
+ { name: `Show trace ${oneLine(traceId).slice(0, 8)}`, value: "trace" },
6250
+ { name: "Logs of this trace", value: "logs" },
6251
+ { name: "Done", value: "done" }
6252
+ ]);
6253
+ if (next === "done") return;
6254
+ const extra = { issue: first.issue, warnings: [] };
6255
+ if (next === "trace") extra.trace = await c.trace(traceId, scope2);
6256
+ else {
6257
+ const p = await c.logs({ ...scope2, trace: traceId, limit: 100 });
6258
+ extra.logs = p.items;
6259
+ extra.warnings.push(...pageWarnings(p));
6260
+ }
6261
+ printDetail(ctx, oc2, extra, false);
6262
+ }
6263
+ }
6264
+ function registerErrors(program, ctx) {
6265
+ addObsFlags(program.command("errors [id]").description("Errors of a deployed environment, or one error with its stack trace (requires --env)")).option("--with <list>", `with an id: also show ${WITHS.join(",")}`).option("--status <status>", `${STATUSES.join("|")} (default unresolved)`).option("--sort <sort>", `${SORTS.join("|")} (default date)`).action(async (id, flags) => {
6266
+ const { io: io2, deps: deps2 } = ctx;
6267
+ const g = ctx.opts();
6268
+ const withs = withList(flags.with, WITHS);
6269
+ const status2 = one("--status", flags.status ?? "unresolved", STATUSES);
6270
+ const sort = one("--sort", flags.sort ?? "date", SORTS);
6271
+ const limit = limitOf(flags.limit);
6272
+ const oc2 = await obsContext(ctx, flags, "errors", { single: id !== void 0 });
6273
+ const q = { env: oc2.env, ...oc2.window, q: flags.query, limit };
6274
+ if (id) {
6275
+ const c2 = oc2.targets[0].client;
6276
+ if (g.json) {
6277
+ renderJson(io2, { context: oc2.json, ...await errorDetail(c2, id, q, withs) });
6278
+ return;
6279
+ }
6280
+ io2.err(oc2.header);
6281
+ if (io2.interactive && withs.size === 0) {
6282
+ await interactiveDetail(ctx, oc2, c2, id);
6283
+ return;
6284
+ }
6285
+ printDetail(ctx, oc2, await errorDetail(c2, id, q, withs), true);
6286
+ return;
6287
+ }
6288
+ const f = await fanOut(oc2, (t) => t.client.issues({ ...q, status: status2, sort }));
6289
+ const pages = f.results.map((r) => r.result);
6290
+ const items = pages.flatMap((p) => p.items);
6291
+ if (pages.length > 1) items.sort(MERGE[sort]);
6292
+ const warnings = [...pageWarnings(...pages), ...skipWarnings(f)];
6293
+ if (g.json) {
6294
+ renderJson(io2, { context: oc2.json, items, warnings, ...skipJson(f) });
6295
+ return;
6296
+ }
6297
+ io2.err(oc2.header);
6298
+ printWarnings(io2, warnings, false, f.skipped);
6299
+ if (items.length === 0) {
6300
+ io2.out(`No ${status2 === "all" ? "" : `${status2} `}errors in ${oc2.env} (${windowText(oc2.window)}).`);
6301
+ return;
6302
+ }
6303
+ for (const l of fence(issuesTable(items, deps2.now(), oc2.fanOut))) io2.out(l);
6304
+ if (!io2.interactive) return;
6305
+ const picked = await io2.select(
6306
+ "Which error to open?",
6307
+ [...items.map((i) => ({ name: `${oneLine(i.shortId)} ${oneLine(i.title)}`.slice(0, 100), value: i })), { name: "(none)", value: null }]
6308
+ );
6309
+ if (!picked) return;
6310
+ const c = oc2.targets.find((t) => t.module.id === picked.module.id)?.client;
6311
+ if (!c) throw new QoreError("QORE_RESOURCE_NOT_FOUND", `module ${picked.module.slug} is not among the queried modules`);
6312
+ await interactiveDetail(ctx, oc2, c, picked.shortId);
6313
+ });
6314
+ }
6315
+
5209
6316
  // src/cli/commands/init.ts
5210
6317
  import { realpath as realpath11 } from "fs/promises";
5211
6318
 
5212
6319
  // src/core/auth/oidc.ts
5213
6320
  import * as oc from "openid-client";
5214
- var LOGIN_SCOPE = "openid profile email offline_access qore:read qore:local-env";
6321
+ var LOGIN_SCOPE = "openid profile email offline_access qore:read qore:local-env qore:observability";
5215
6322
  function decodeSub(accessToken) {
5216
6323
  const part = accessToken.split(".")[1] ?? "";
5217
6324
  const claims = JSON.parse(Buffer.from(part, "base64url").toString("utf8"));
@@ -5262,9 +6369,9 @@ async function createOidc(opts) {
5262
6369
  throw mapOidcError(e, host);
5263
6370
  }
5264
6371
  return {
5265
- async startDevice(scope) {
6372
+ async startDevice(scope2) {
5266
6373
  try {
5267
- const r = await oc.initiateDeviceAuthorization(config, { scope });
6374
+ const r = await oc.initiateDeviceAuthorization(config, { scope: scope2 });
5268
6375
  return {
5269
6376
  userCode: r.user_code,
5270
6377
  verificationUri: r.verification_uri,
@@ -5284,9 +6391,9 @@ async function createOidc(opts) {
5284
6391
  throw mapOidcError(e, host);
5285
6392
  }
5286
6393
  },
5287
- async refresh(rt, scope) {
6394
+ async refresh(rt, scope2) {
5288
6395
  try {
5289
- const t = await oc.refreshTokenGrant(config, rt, scope ? { scope } : void 0);
6396
+ const t = await oc.refreshTokenGrant(config, rt, scope2 ? { scope: scope2 } : void 0);
5290
6397
  return toTokenSet(t, rt);
5291
6398
  } catch (e) {
5292
6399
  throw mapOidcError(e, host);
@@ -5538,6 +6645,34 @@ function registerInit(program, ctx) {
5538
6645
  });
5539
6646
  }
5540
6647
 
6648
+ // src/cli/commands/logs.ts
6649
+ var LEVELS = ["error", "warn", "info", "debug"];
6650
+ function registerLogs(program, ctx) {
6651
+ addObsFlags(program.command("logs").description("Logs of a deployed environment (requires --env)")).option("--level <level>", `exactly this severity: ${LEVELS.join("|")}`).option("--trace <id>", "only the logs of one trace").action(async (flags) => {
6652
+ const { io: io2 } = ctx;
6653
+ const g = ctx.opts();
6654
+ const limit = limitOf(flags.limit);
6655
+ const level = flags.level === void 0 ? void 0 : one("--level", flags.level, LEVELS);
6656
+ const oc2 = await obsContext(ctx, flags, "logs", { single: false });
6657
+ const q = { env: oc2.env, ...oc2.window, q: flags.query, limit, level, trace: flags.trace };
6658
+ const f = await fanOut(oc2, (t) => t.client.logs(q));
6659
+ const pages = f.results.map((r) => r.result);
6660
+ const items = pages.flatMap((p) => p.items).sort((a, b) => b.timestamp.localeCompare(a.timestamp));
6661
+ const warnings = [...pageWarnings(...pages), ...skipWarnings(f)];
6662
+ if (g.json) {
6663
+ renderJson(io2, { context: oc2.json, items, warnings, ...skipJson(f) });
6664
+ return;
6665
+ }
6666
+ io2.err(oc2.header);
6667
+ printWarnings(io2, warnings, false, f.skipped);
6668
+ if (items.length === 0) {
6669
+ io2.out(`No logs in ${oc2.env} (${windowText(oc2.window)}).`);
6670
+ return;
6671
+ }
6672
+ for (const l of fence(logLines(items, oc2.fanOut))) io2.out(l);
6673
+ });
6674
+ }
6675
+
5541
6676
  // src/cli/commands/logout.ts
5542
6677
  function registerLogout(program, ctx) {
5543
6678
  program.command("logout").description("End the CLI session").option("--all", "log out of every portal").action(async (cmd) => {
@@ -5570,26 +6705,6 @@ function registerLogout(program, ctx) {
5570
6705
  });
5571
6706
  }
5572
6707
 
5573
- // src/cli/format.ts
5574
- function ago(iso, now) {
5575
- if (!iso) return "\u2013";
5576
- const s = Math.max(0, Math.round((now.getTime() - new Date(iso).getTime()) / 1e3));
5577
- if (s < 60) return "just now";
5578
- if (s < 3600) return `${Math.floor(s / 60)}m ago`;
5579
- if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
5580
- return `${Math.floor(s / 86400)}d ago`;
5581
- }
5582
- var pct = (f) => String(Number((f * 100).toFixed(2)));
5583
- function uptime(h) {
5584
- if (!h) return "\u2013";
5585
- const kind = h.cause && h.kind !== "up" ? `${h.kind}: ${h.cause}` : h.kind;
5586
- return h.uptime24h === null ? kind : `${kind} (${pct(h.uptime24h)}% 24h)`;
5587
- }
5588
- function table(rows) {
5589
- const widths = rows[0].map((_, i) => Math.max(...rows.map((r) => (r[i] ?? "").length)));
5590
- return rows.map((r) => " " + r.map((c, i) => i === r.length - 1 ? c : c.padEnd(widths[i])).join(" "));
5591
- }
5592
-
5593
6708
  // src/cli/commands/status.ts
5594
6709
  var HEAD = ["ENV", "DEPLOY", "COMMIT", "DEPLOYED", "APP HEALTH"];
5595
6710
  var envRow = (e, now) => [e.name, e.paused ? "paused" : e.status, e.commit?.sha ?? "\u2013", ago(e.commit?.deployedAt ?? null, now), uptime(e.health)];
@@ -5631,7 +6746,7 @@ async function appStatus(ctx, ws, appFlag) {
5631
6746
  }
5632
6747
  function registerStatus(program, ctx) {
5633
6748
  program.command("status").description("Deployment and health per environment for this module (or, in a workspace root, the app)").option("--module <slug|id>", "the module (default: from this repository's git remote)").option("--app <slug|id>", "the app, when the module is part of several").option("--project <ref>", "the app or module, from anywhere (see qore init)").action(async (flags) => {
5634
- const c = await projectCtx(ctx, { ref: flags.project, module: flags.module, app: flags.app, needModule: false });
6749
+ const c = await projectCtx(ctx, { ref: flags.project, module: flags.module, app: flags.app, needModule: false, command: "status" });
5635
6750
  const { deps: deps2, io: io2 } = c;
5636
6751
  const g = c.opts();
5637
6752
  if (!flags.module) {
@@ -5664,7 +6779,7 @@ function registerStatus(program, ctx) {
5664
6779
  }
5665
6780
 
5666
6781
  // src/cli/commands/sync.ts
5667
- import { join as join24, relative as relative9 } from "path";
6782
+ import { join as join26, relative as relative9 } from "path";
5668
6783
  import { Option as Option2 } from "commander";
5669
6784
  function asFailure2(slug, e) {
5670
6785
  if (e instanceof QoreError && e.code !== "QORE_GIT_UNAVAILABLE") {
@@ -5696,7 +6811,7 @@ function registerSync(program, ctx) {
5696
6811
  continue;
5697
6812
  }
5698
6813
  try {
5699
- dirs[m.slug] = await deps2.git.inspect(join24(ws.root, m.slug));
6814
+ dirs[m.slug] = await deps2.git.inspect(join26(ws.root, m.slug));
5700
6815
  modulesForPlan.push(m);
5701
6816
  } catch (e) {
5702
6817
  earlyFailures.push(asFailure2(m.slug, e));
@@ -5717,6 +6832,109 @@ function registerSync(program, ctx) {
5717
6832
  });
5718
6833
  }
5719
6834
 
6835
+ // src/cli/commands/traces.ts
6836
+ var WITHS2 = ["logs", "errors"];
6837
+ async function spansOf(c, q, sort, warnings) {
6838
+ try {
6839
+ return await c.spans({ ...q, sort });
6840
+ } catch (e) {
6841
+ if (sort === "-duration" && e instanceof QoreError && e.serverCode === "observability.sort_unsupported") {
6842
+ warnings.push("sort_unavailable");
6843
+ return c.spans(q);
6844
+ }
6845
+ throw e;
6846
+ }
6847
+ }
6848
+ function registerTraces(program, ctx) {
6849
+ addObsFlags(program.command("traces [id]").description("Slow or recent requests, or one trace as a span tree (requires --env)")).option("--slower-than <duration>", "only requests slower than this, slowest first (e.g. 500ms, 1s)").option("--with <list>", `with a trace id: also show ${WITHS2.join(",")}`).action(async (id, flags) => {
6850
+ const { io: io2, deps: deps2 } = ctx;
6851
+ const g = ctx.opts();
6852
+ const withs = withList(flags.with, WITHS2);
6853
+ const slower = flags.slowerThan === void 0 ? void 0 : parseDuration(flags.slowerThan);
6854
+ const limit = limitOf(flags.limit);
6855
+ const oc2 = await obsContext(ctx, flags, "traces", { single: id !== void 0 });
6856
+ const q = { env: oc2.env, ...oc2.window, limit };
6857
+ const scope2 = { env: oc2.env, ...oc2.window };
6858
+ const printLogs = (items2) => {
6859
+ io2.out("");
6860
+ io2.out(`Logs (${items2.length})`);
6861
+ for (const l of fence(logLines(items2, oc2.fanOut))) io2.out(l);
6862
+ };
6863
+ const printErrors = (items2) => {
6864
+ io2.out("");
6865
+ io2.out(`Errors (${items2.length})`);
6866
+ for (const l of fence(issuesTable(items2, deps2.now(), oc2.fanOut))) io2.out(l);
6867
+ };
6868
+ const printTree = (trace2) => {
6869
+ for (const l of fence(spanTree(trace2))) io2.out(l);
6870
+ };
6871
+ const more = async (c2, traceId, w) => {
6872
+ const f2 = { ...scope2, trace: traceId };
6873
+ if (w === "logs") {
6874
+ const p = await c2.logs({ ...f2, limit: 100 });
6875
+ printWarnings(io2, pageWarnings(p), false);
6876
+ printLogs(p.items);
6877
+ } else {
6878
+ const p = await c2.issues({ ...f2, status: "all" });
6879
+ printWarnings(io2, pageWarnings(p), false);
6880
+ printErrors(p.items);
6881
+ }
6882
+ };
6883
+ if (id) {
6884
+ const c2 = oc2.targets[0].client;
6885
+ const b = await traceDetail(c2, id, q, withs);
6886
+ if (g.json) {
6887
+ renderJson(io2, { context: oc2.json, ...b });
6888
+ return;
6889
+ }
6890
+ io2.err(oc2.header);
6891
+ printWarnings(io2, b.warnings, false);
6892
+ printTree(b.trace);
6893
+ if (b.logs) printLogs(b.logs);
6894
+ if (b.errors) printErrors(b.errors);
6895
+ return;
6896
+ }
6897
+ const warnings = [];
6898
+ const listQ = { ...q, q: joinQuery(ROOT_SPAN_FILTER, slower && `span.duration:>${slower}`, flags.query) };
6899
+ const sort = slower ? "-duration" : "-timestamp";
6900
+ const f = await fanOut(oc2, (t) => spansOf(t.client, listQ, sort, warnings));
6901
+ const pages = f.results.map((r) => r.result);
6902
+ warnings.push(...pageWarnings(...pages), ...skipWarnings(f));
6903
+ const items = pages.flatMap((p) => p.items).sort((a, b) => slower ? b.durationMs - a.durationMs : b.timestamp.localeCompare(a.timestamp));
6904
+ const uniq = [...new Set(warnings)];
6905
+ if (g.json) {
6906
+ renderJson(io2, { context: oc2.json, items, warnings: uniq, ...skipJson(f) });
6907
+ return;
6908
+ }
6909
+ io2.err(oc2.header);
6910
+ printWarnings(io2, uniq, false, f.skipped);
6911
+ if (items.length === 0) {
6912
+ io2.out(`No ${slower ? `requests slower than ${slower}` : "traces"} in ${oc2.env} (${windowText(oc2.window)}).`);
6913
+ return;
6914
+ }
6915
+ for (const l of fence(spansTable(items, deps2.now(), oc2.fanOut))) io2.out(l);
6916
+ if (!io2.interactive) return;
6917
+ const picked = await io2.select(
6918
+ "Which trace to open?",
6919
+ [...items.map((s) => ({ name: `${oneLine(s.traceId).slice(0, 8)} ${oneLine(s.transaction ?? s.op)}`, value: s })), { name: "(none)", value: null }]
6920
+ );
6921
+ if (!picked) return;
6922
+ const c = oc2.targets.find((t) => t.module.id === picked.module.id)?.client;
6923
+ if (!c) throw new QoreError("QORE_RESOURCE_NOT_FOUND", `module ${picked.module.slug} is not among the queried modules`);
6924
+ const { trace } = await traceDetail(c, picked.traceId, q, /* @__PURE__ */ new Set());
6925
+ printTree(trace);
6926
+ for (; ; ) {
6927
+ const next = await io2.select("Next?", [
6928
+ { name: "Logs of this trace", value: "logs" },
6929
+ { name: "Errors in this trace", value: "errors" },
6930
+ { name: "Done", value: "done" }
6931
+ ]);
6932
+ if (next === "done") return;
6933
+ await more(c, picked.traceId, next);
6934
+ }
6935
+ });
6936
+ }
6937
+
5720
6938
  // src/cli/commands/version.ts
5721
6939
  function registerVersion(program, ctx) {
5722
6940
  program.command("version").description("Show the CLI and portal version").action(async () => {
@@ -5785,9 +7003,13 @@ var COMMANDS = [
5785
7003
  registerWhoami,
5786
7004
  registerTenant,
5787
7005
  registerStatus,
7006
+ registerLogs,
7007
+ registerTraces,
7008
+ registerErrors,
5788
7009
  registerEnv,
5789
7010
  registerRun,
5790
7011
  registerDev,
7012
+ registerDb,
5791
7013
  registerClone,
5792
7014
  registerSync,
5793
7015
  registerVersion,
@@ -5844,13 +7066,13 @@ function createTelemetry(deps2, o) {
5844
7066
  },
5845
7067
  async end(signal) {
5846
7068
  if (!sdk) return { drained: true };
5847
- const ms = crashed ? flushMs.crash : flushMs.clean;
7069
+ const ms2 = crashed ? flushMs.crash : flushMs.clean;
5848
7070
  const work = sdk.then(async (s) => {
5849
7071
  if (!s) return true;
5850
7072
  if (crashed) s.crash(crashed.e);
5851
- return s.end(ms);
7073
+ return s.end(ms2);
5852
7074
  }).catch(() => false);
5853
- return { drained: await withDeadline(work, ms, signal) };
7075
+ return { drained: await withDeadline(work, ms2, signal) };
5854
7076
  }
5855
7077
  };
5856
7078
  }
@@ -5871,16 +7093,16 @@ async function start(deps2, argv2, command, load, onScrub) {
5871
7093
  sdk.start();
5872
7094
  return sdk;
5873
7095
  }
5874
- function withDeadline(work, ms, signal) {
7096
+ function withDeadline(work, ms2, signal) {
5875
7097
  if (signal?.aborted) return Promise.resolve(false);
5876
- return new Promise((resolve6) => {
7098
+ return new Promise((resolve7) => {
5877
7099
  const done = (v) => {
5878
7100
  clearTimeout(timer);
5879
7101
  signal?.removeEventListener("abort", onAbort);
5880
- resolve6(v);
7102
+ resolve7(v);
5881
7103
  };
5882
7104
  const onAbort = () => done(false);
5883
- const timer = setTimeout(() => done(false), ms);
7105
+ const timer = setTimeout(() => done(false), ms2);
5884
7106
  signal?.addEventListener("abort", onAbort, { once: true });
5885
7107
  work.then(done, () => done(false));
5886
7108
  });
@@ -5928,8 +7150,8 @@ async function maybeNotifyUpdate(deps2, io2, o, timeoutMs = DEFAULT_TIMEOUT_MS)
5928
7150
  }
5929
7151
 
5930
7152
  // src/core/config.ts
5931
- import { readFile as readFile6 } from "fs/promises";
5932
- import { join as join25 } from "path";
7153
+ import { readFile as readFile8 } from "fs/promises";
7154
+ import { join as join27 } from "path";
5933
7155
  var empty = () => ({ schemaVersion: 1, profiles: {} });
5934
7156
  var ConfigStore = class {
5935
7157
  constructor(dir2) {
@@ -5937,12 +7159,12 @@ var ConfigStore = class {
5937
7159
  }
5938
7160
  dir;
5939
7161
  get file() {
5940
- return join25(this.dir, "config.json");
7162
+ return join27(this.dir, "config.json");
5941
7163
  }
5942
7164
  async read() {
5943
7165
  let raw;
5944
7166
  try {
5945
- raw = await readFile6(this.file, "utf8");
7167
+ raw = await readFile8(this.file, "utf8");
5946
7168
  } catch (e) {
5947
7169
  if (e.code === "ENOENT") return empty();
5948
7170
  throw e;
@@ -5968,9 +7190,9 @@ var ConfigStore = class {
5968
7190
 
5969
7191
  // src/platform/git.ts
5970
7192
  import { execFile, execFileSync } from "child_process";
5971
- import { mkdir as mkdir4, readdir as readdir4, stat as stat6 } from "fs/promises";
7193
+ import { mkdir as mkdir4, readdir as readdir4, stat as stat7 } from "fs/promises";
5972
7194
  import { homedir } from "os";
5973
- import { dirname as dirname9, join as join26 } from "path";
7195
+ import { dirname as dirname9, join as join28 } from "path";
5974
7196
  function parseRemotes(stdout) {
5975
7197
  const seen = /* @__PURE__ */ new Set();
5976
7198
  const out = [];
@@ -6008,8 +7230,8 @@ function parseStatus(stdout) {
6008
7230
  }
6009
7231
  var configuredSshCommand = /* @__PURE__ */ new Map();
6010
7232
  function hasConfiguredSshCommand(repoDir) {
6011
- const key = repoDir ?? "";
6012
- let v = configuredSshCommand.get(key);
7233
+ const key2 = repoDir ?? "";
7234
+ let v = configuredSshCommand.get(key2);
6013
7235
  if (v === void 0) {
6014
7236
  const args = repoDir ? ["-C", repoDir, "config", "--get", "core.sshCommand"] : ["config", "--get", "core.sshCommand"];
6015
7237
  try {
@@ -6023,7 +7245,7 @@ function hasConfiguredSshCommand(repoDir) {
6023
7245
  } catch {
6024
7246
  v = false;
6025
7247
  }
6026
- configuredSshCommand.set(key, v);
7248
+ configuredSshCommand.set(key2, v);
6027
7249
  }
6028
7250
  return v;
6029
7251
  }
@@ -6037,7 +7259,7 @@ function gitExecOptions(repoDir) {
6037
7259
  return { cwd: homedir(), env, windowsHide: true, maxBuffer: 8 * 1024 * 1024 };
6038
7260
  }
6039
7261
  function runGit(args, repoDir) {
6040
- return new Promise((resolve6, reject) => {
7262
+ return new Promise((resolve7, reject) => {
6041
7263
  execFile("git", args, gitExecOptions(repoDir), (err, stdout, stderr) => {
6042
7264
  if (err && err.code === "ENOENT") {
6043
7265
  reject(new QoreError("QORE_GIT_UNAVAILABLE", "git is not installed or not on PATH", {
@@ -6046,7 +7268,7 @@ function runGit(args, repoDir) {
6046
7268
  return;
6047
7269
  }
6048
7270
  const code = err ? typeof err.code === "number" ? err.code : 1 : 0;
6049
- resolve6({ code, stdout: String(stdout), stderr: String(stderr), err: err ?? void 0 });
7271
+ resolve7({ code, stdout: String(stdout), stderr: String(stderr), err: err ?? void 0 });
6050
7272
  });
6051
7273
  });
6052
7274
  }
@@ -6084,7 +7306,7 @@ async function status(repoRoot) {
6084
7306
  }
6085
7307
  async function statOrThrow(path) {
6086
7308
  try {
6087
- return await stat6(path);
7309
+ return await stat7(path);
6088
7310
  } catch (err) {
6089
7311
  if (err.code === "ENOENT") return void 0;
6090
7312
  throw new QoreError("QORE_INTERNAL", `could not stat ${path}`, { cause: err });
@@ -6101,7 +7323,7 @@ async function inspect(dir2) {
6101
7323
  throw new QoreError("QORE_INTERNAL", `could not read ${dir2}`, { cause: err });
6102
7324
  }
6103
7325
  const empty2 = entries.length === 0;
6104
- const isRepo = await statOrThrow(join26(dir2, ".git")) !== void 0;
7326
+ const isRepo = await statOrThrow(join28(dir2, ".git")) !== void 0;
6105
7327
  const dirRemotes = isRepo ? await remotes(dir2) : [];
6106
7328
  return { exists: true, empty: empty2, isRepo, remotes: dirRemotes };
6107
7329
  }
@@ -6163,13 +7385,13 @@ var OsKeychain = class {
6163
7385
 
6164
7386
  // src/platform/lock.ts
6165
7387
  import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
6166
- import { join as join27 } from "path";
7388
+ import { join as join29 } from "path";
6167
7389
  import lockfile from "proper-lockfile";
6168
7390
  function fileLock(dir2) {
6169
7391
  return async (name, fn) => {
6170
- const locks = join27(dir2, "locks");
7392
+ const locks = join29(dir2, "locks");
6171
7393
  await mkdir5(locks, { recursive: true, mode: 448 });
6172
- const target = join27(locks, `${name}.lock`);
7394
+ const target = join29(locks, `${name}.lock`);
6173
7395
  await writeFile5(target, "", { flag: "a" });
6174
7396
  let release;
6175
7397
  let compromised;
@@ -6213,7 +7435,7 @@ var STDERR_DRAIN_MS = 500;
6213
7435
  function makeSpawner(spawnFn, proc) {
6214
7436
  return {
6215
7437
  run(cmd, args, env, o) {
6216
- return new Promise((resolve6, reject) => {
7438
+ return new Promise((resolve7, reject) => {
6217
7439
  let child;
6218
7440
  try {
6219
7441
  child = spawnFn(cmd, args, {
@@ -6247,7 +7469,7 @@ function makeSpawner(spawnFn, proc) {
6247
7469
  cleanup();
6248
7470
  const stderr = child.stderr;
6249
7471
  if (!o?.onStderr || !stderr || stderr.readableEnded) {
6250
- resolve6({ code, signal });
7472
+ resolve7({ code, signal });
6251
7473
  return;
6252
7474
  }
6253
7475
  let settled = false;
@@ -6255,7 +7477,7 @@ function makeSpawner(spawnFn, proc) {
6255
7477
  if (settled) return;
6256
7478
  settled = true;
6257
7479
  clearTimeout(timer);
6258
- resolve6({ code, signal });
7480
+ resolve7({ code, signal });
6259
7481
  };
6260
7482
  const timer = setTimeout(finish, STDERR_DRAIN_MS);
6261
7483
  timer.unref?.();
@@ -6310,13 +7532,13 @@ var systemLauncher = {
6310
7532
  child.stdout?.on("data", (c) => out.push(c));
6311
7533
  child.stderr?.on("data", (c) => err.push(c));
6312
7534
  let closed = false;
6313
- const exited = new Promise((resolve6) => {
6314
- child.once("error", (e) => resolve6({ code: null, signal: null, spawnError: e.code ?? "error" }));
7535
+ const exited = new Promise((resolve7) => {
7536
+ child.once("error", (e) => resolve7({ code: null, signal: null, spawnError: e.code ?? "error" }));
6315
7537
  child.once("close", (code, signal) => {
6316
7538
  closed = true;
6317
7539
  out.flush();
6318
7540
  err.flush();
6319
- resolve6({ code, signal });
7541
+ resolve7({ code, signal });
6320
7542
  });
6321
7543
  });
6322
7544
  return {
@@ -6362,11 +7584,11 @@ var systemLauncher = {
6362
7584
  import { execFile as execFile2 } from "child_process";
6363
7585
  import { connect } from "net";
6364
7586
  function accepts(host, port) {
6365
- return new Promise((resolve6) => {
7587
+ return new Promise((resolve7) => {
6366
7588
  const s = connect({ host, port });
6367
7589
  const done = (v) => {
6368
7590
  s.destroy();
6369
- resolve6(v);
7591
+ resolve7(v);
6370
7592
  };
6371
7593
  s.setTimeout(500, () => done(false));
6372
7594
  s.once("connect", () => done(true));
@@ -6374,12 +7596,12 @@ function accepts(host, port) {
6374
7596
  });
6375
7597
  }
6376
7598
  function run2(cmd, args) {
6377
- return new Promise((resolve6) => {
7599
+ return new Promise((resolve7) => {
6378
7600
  execFile2(
6379
7601
  cmd,
6380
7602
  args,
6381
7603
  { timeout: 2e3, env: { ...process.env, LC_ALL: "C" }, windowsHide: true },
6382
- (e, stdout) => resolve6(e && !stdout ? null : String(stdout))
7604
+ (e, stdout) => resolve7(e && !stdout ? null : String(stdout))
6383
7605
  );
6384
7606
  });
6385
7607
  }
@@ -6442,6 +7664,26 @@ var systemPorts = {
6442
7664
  }
6443
7665
  };
6444
7666
 
7667
+ // src/platform/postgres.ts
7668
+ import pg from "pg";
7669
+ async function connectPostgres(url, o) {
7670
+ const client = new pg.Client({ connectionString: url, connectionTimeoutMillis: o.timeoutMs });
7671
+ client.on("error", () => {
7672
+ });
7673
+ try {
7674
+ await client.connect();
7675
+ await client.query("set default_transaction_read_only = on");
7676
+ } catch (e) {
7677
+ await client.end().catch(() => {
7678
+ });
7679
+ throw e;
7680
+ }
7681
+ return {
7682
+ query: async (sql, params) => ({ rows: (await client.query(sql, params)).rows }),
7683
+ close: () => client.end()
7684
+ };
7685
+ }
7686
+
6445
7687
  // src/buildInfo.ts
6446
7688
  var SENTRY_DSN = true ? "https://50b592efb53dbc1a31cb10d3945c6d52@o4512129906049024.ingest.de.sentry.io/4512174341750864" : "";
6447
7689
  var SENTRY_ENVIRONMENT = true ? "production" : "development";
@@ -6451,7 +7693,7 @@ import { execFile as execFile3 } from "child_process";
6451
7693
 
6452
7694
  // src/core/diagnostics/proxy.ts
6453
7695
  function parseScutilProxy(stdout) {
6454
- const value = (key) => stdout.match(new RegExp(`^\\s*${key}\\s*:\\s*(.+)$`, "m"))?.[1]?.trim();
7696
+ const value = (key2) => stdout.match(new RegExp(`^\\s*${key2}\\s*:\\s*(.+)$`, "m"))?.[1]?.trim();
6455
7697
  if (value("HTTPSEnable") === "1") {
6456
7698
  const host = value("HTTPSProxy");
6457
7699
  const port = value("HTTPSPort");
@@ -6472,12 +7714,12 @@ function parseRegProxy(stdout) {
6472
7714
 
6473
7715
  // src/platform/proxy.ts
6474
7716
  function run3(cmd, args) {
6475
- return new Promise((resolve6) => {
7717
+ return new Promise((resolve7) => {
6476
7718
  execFile3(
6477
7719
  cmd,
6478
7720
  args,
6479
7721
  { timeout: 2e3, env: { ...process.env, LC_ALL: "C" }, windowsHide: true },
6480
- (e, stdout) => resolve6(e && !stdout ? null : String(stdout))
7722
+ (e, stdout) => resolve7(e && !stdout ? null : String(stdout))
6481
7723
  );
6482
7724
  });
6483
7725
  }
@@ -6488,10 +7730,10 @@ async function detect(platform) {
6488
7730
  return out ? parseScutilProxy(out) : null;
6489
7731
  }
6490
7732
  if (platform === "win32") {
6491
- const key = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings";
7733
+ const key2 = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings";
6492
7734
  const [enable, server] = await Promise.all([
6493
- run3("reg", ["query", key, "/v", "ProxyEnable"]),
6494
- run3("reg", ["query", key, "/v", "ProxyServer"])
7735
+ run3("reg", ["query", key2, "/v", "ProxyEnable"]),
7736
+ run3("reg", ["query", key2, "/v", "ProxyServer"])
6495
7737
  ]);
6496
7738
  const combined = [enable, server].filter(Boolean).join("\n");
6497
7739
  return combined ? parseRegProxy(combined) : null;
@@ -6508,7 +7750,7 @@ function systemProxy(platform = process.platform) {
6508
7750
  }
6509
7751
 
6510
7752
  // src/version.ts
6511
- var VERSION = true ? "0.1.6" : "0.0.0-dev";
7753
+ var VERSION = true ? "0.1.8" : "0.0.0-dev";
6512
7754
 
6513
7755
  // src/cli.ts
6514
7756
  if (process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY) setGlobalDispatcher(new EnvHttpProxyAgent());
@@ -6556,7 +7798,9 @@ var deps = {
6556
7798
  },
6557
7799
  sentryDsn: SENTRY_DSN,
6558
7800
  sentryEnvironment: SENTRY_ENVIRONMENT,
6559
- systemProxy: () => systemProxy(process.platform)
7801
+ systemProxy: () => systemProxy(process.platform),
7802
+ viewerDir: fileURLToPath(new URL("../viewer/dist/", import.meta.url)),
7803
+ postgres: connectPostgres
6560
7804
  };
6561
7805
  var termination = preflightTermination(() => ac.abort(), deps.raise);
6562
7806
  var io = processIo(isInteractive({
@@ -6586,4 +7830,4 @@ if (!drained) {
6586
7830
  process.exit();
6587
7831
  }
6588
7832
  //# sourceMappingURL=cli.js.map
6589
- //# debugId=28c91dc8-dbb9-5c0a-91e3-f368931a4ed3
7833
+ //# debugId=c4bda056-b13b-52b5-831f-2afd3f6c6867