@quatronic/qore 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -52,7 +52,17 @@ var EXIT = {
52
52
  QORE_UNSUPPORTED_BY_SERVER: 7,
53
53
  QORE_UPGRADE_REQUIRED: 7,
54
54
  QORE_PORTAL_UNTRUSTED: 7,
55
- QORE_RATE_LIMITED: 8
55
+ QORE_RATE_LIMITED: 8,
56
+ QORE_PORT_IN_USE: 2,
57
+ QORE_DEV_UNSUPPORTED: 2,
58
+ QORE_INSTALL_FAILED: 1,
59
+ QORE_PACKAGE_MANAGER: 1,
60
+ QORE_DOCKER_MISSING: 1,
61
+ QORE_DOCKER_NOT_RUNNING: 1,
62
+ QORE_DOCKER_COMPOSE_MISSING: 1,
63
+ QORE_SERVICE_DATA_INCOMPATIBLE: 1,
64
+ QORE_NODE_VERSION: 1,
65
+ QORE_SERVICE_UNHEALTHY: 1
56
66
  };
57
67
  var QoreError = class extends Error {
58
68
  code;
@@ -100,7 +110,7 @@ function toJsonError(err) {
100
110
  function isInteractive(o) {
101
111
  return o.stdinTTY && o.stdoutTTY && !o.json && !o.noInput && !o.env.CI;
102
112
  }
103
- function processIo(interactive) {
113
+ function processIo(interactive, stderrTTY = Boolean(process.stderr.isTTY)) {
104
114
  return {
105
115
  out: (s) => {
106
116
  process.stdout.write(s + "\n");
@@ -109,6 +119,8 @@ function processIo(interactive) {
109
119
  process.stderr.write(s + "\n");
110
120
  },
111
121
  interactive,
122
+ stderrTTY,
123
+ stderrRaw: process.stderr,
112
124
  async select(message, choices) {
113
125
  try {
114
126
  return await select({ message, choices });
@@ -117,11 +129,11 @@ function processIo(interactive) {
117
129
  throw e;
118
130
  }
119
131
  },
120
- async confirm(message) {
132
+ async confirm(message, signal) {
121
133
  try {
122
- return await confirm({ message, default: false });
134
+ return await confirm({ message, default: false }, { signal });
123
135
  } catch (e) {
124
- if (e.name === "ExitPromptError") throw new CancelledError();
136
+ if (e.name === "ExitPromptError" || e.name === "AbortPromptError") throw new CancelledError();
125
137
  throw e;
126
138
  }
127
139
  },
@@ -176,7 +188,11 @@ function isAbortError(e) {
176
188
  function fetchError(e, host) {
177
189
  return isAbortError(e) ? new CancelledError() : networkError(e, host);
178
190
  }
179
- function networkError(e, host) {
191
+ function proxyHint(proxy, platform) {
192
+ const cmd = platform === "win32" ? `set HTTPS_PROXY=http://${proxy}` : `export HTTPS_PROXY=http://${proxy}`;
193
+ return `Your system uses the proxy ${proxy}, but qore reads only HTTPS_PROXY. Run: ${cmd}`;
194
+ }
195
+ function networkError(e, host, systemProxy2) {
180
196
  const code = causeCode(e) ?? "";
181
197
  let reason = "connect";
182
198
  let hint = "Check your network or VPN.";
@@ -188,10 +204,24 @@ function networkError(e, host) {
188
204
  hint = "A proxy is re-signing TLS. Point NODE_EXTRA_CA_CERTS at your organisation's CA certificate.";
189
205
  } else if (code.includes("TIMEOUT") || code === "ETIMEDOUT") {
190
206
  reason = "timeout";
207
+ } else if (systemProxy2) {
208
+ reason = "proxy";
209
+ hint = proxyHint(systemProxy2);
191
210
  } else if (!process.env.HTTPS_PROXY && !process.env.https_proxy) {
192
211
  hint = "Check your network. Behind a corporate proxy, set HTTPS_PROXY.";
193
212
  }
194
- return new QoreError("QORE_API_UNREACHABLE", `Could not reach ${host}`, { hint, details: { reason, host, errno: code || void 0 }, cause: e });
213
+ const details = { reason, host, errno: code || void 0 };
214
+ if (systemProxy2) details.proxy = systemProxy2;
215
+ return new QoreError("QORE_API_UNREACHABLE", `Could not reach ${host}`, { hint, details, cause: e });
216
+ }
217
+ function withSystemProxy(err, proxy, platform) {
218
+ return new QoreError(err.code, err.message, {
219
+ status: err.status,
220
+ requestId: err.requestId,
221
+ cause: err.cause,
222
+ details: { ...err.details, reason: "proxy", proxy },
223
+ hint: proxyHint(proxy, platform)
224
+ });
195
225
  }
196
226
  var BY_SERVER_CODE = {
197
227
  "app.access.denied": "QORE_ACCESS_DENIED",
@@ -263,6 +293,98 @@ function renderError(io2, err, json2, colorEnabled = pc.isColorSupported) {
263
293
  io2.err(c.red(`Error: ${e.message}`));
264
294
  if (e.hint) io2.err(e.hint);
265
295
  io2.err(c.dim(`Code: ${e.code}${e.requestId ? ` (request ${e.requestId})` : ""}`));
296
+ if (e.code === "QORE_SERVICE_UNHEALTHY" && Array.isArray(e.details?.logs)) {
297
+ for (const l of e.details.logs) io2.err(c.dim(` ${String(l)}`));
298
+ }
299
+ }
300
+ function renderDoctorJson(io2, report) {
301
+ const failing = report.results.filter((r) => r.status === "fail");
302
+ if (failing.length === 0) {
303
+ io2.out(JSON.stringify({ ok: true, data: report }));
304
+ return false;
305
+ }
306
+ const first = failing.find((r) => r.error);
307
+ const code = first?.error?.code ?? "QORE_INTERNAL";
308
+ const message = first?.error?.message ?? `${failing.length} of ${report.results.length} checks failed`;
309
+ const hint = first?.error?.hint;
310
+ const err = new QoreError(code, message, { hint, details: { results: report.results, context: report.context } });
311
+ io2.out(JSON.stringify({ ok: false, error: toJsonError(err) }));
312
+ return true;
313
+ }
314
+ var SYMBOL = { ok: "\u2713", warn: "!", fail: "\u2717" };
315
+ var TOOL_IDS = ["qore", "node", "git", "github-ssh", "docker"];
316
+ var ROW_WIDTH = 80;
317
+ var COL_B = 36;
318
+ function rawLine(r) {
319
+ return `${SYMBOL[r.status]} ${r.label}`;
320
+ }
321
+ function colorFor(c, status2) {
322
+ return status2 === "ok" ? c.green : status2 === "warn" ? c.yellow : c.red;
323
+ }
324
+ function coloredLine(c, r) {
325
+ return `${colorFor(c, r.status)(SYMBOL[r.status])} ${r.label}`;
326
+ }
327
+ function printCheck(io2, c, indent, r) {
328
+ io2.out(`${indent}${coloredLine(c, r)}`);
329
+ const hint = r.status === "fail" ? r.error?.hint : void 0;
330
+ if (typeof hint === "string") io2.out(c.dim(`${indent} ${hint}`));
331
+ }
332
+ function toolRows(io2, c, tools) {
333
+ for (let i = 0; i < tools.length; i += 2) {
334
+ const a = tools[i];
335
+ const b = tools[i + 1];
336
+ if (!b) {
337
+ printCheck(io2, c, "", a);
338
+ continue;
339
+ }
340
+ const rawA = rawLine(a);
341
+ const rawB = rawLine(b);
342
+ const colB = Math.max(COL_B, rawA.length + 2);
343
+ if (colB + rawB.length > ROW_WIDTH) {
344
+ printCheck(io2, c, "", a);
345
+ printCheck(io2, c, "", b);
346
+ continue;
347
+ }
348
+ const pad = " ".repeat(colB - rawA.length);
349
+ io2.out(`${coloredLine(c, a)}${pad}${coloredLine(c, b)}`);
350
+ if (a.status === "fail" && typeof a.error?.hint === "string") io2.out(c.dim(` ${a.error.hint}`));
351
+ if (b.status === "fail" && typeof b.error?.hint === "string") io2.out(c.dim(` ${b.error.hint}`));
352
+ }
353
+ }
354
+ function groupByModule(results) {
355
+ const order = [];
356
+ const groups = /* @__PURE__ */ new Map();
357
+ for (const r of results) {
358
+ const key = r.module ?? r.id;
359
+ if (!groups.has(key)) {
360
+ groups.set(key, []);
361
+ order.push(key);
362
+ }
363
+ groups.get(key).push(r);
364
+ }
365
+ const isSub = (r) => r.id.startsWith("pm:") || r.id.startsWith("node:");
366
+ return order.flatMap((key) => {
367
+ const g = groups.get(key);
368
+ return [...g.filter((r) => !isSub(r)), ...g.filter(isSub)];
369
+ });
370
+ }
371
+ function renderDoctorHuman(io2, report, colorEnabled = pc.isColorSupported) {
372
+ const c = createColors(colorEnabled);
373
+ const byId = new Map(report.results.map((r) => [r.id, r]));
374
+ const tools = TOOL_IDS.map((id) => byId.get(id)).filter((r) => r !== void 0);
375
+ toolRows(io2, c, tools);
376
+ for (const r of report.results.filter((r2) => r2.group === "session")) printCheck(io2, c, "", r);
377
+ const clock = byId.get("clock");
378
+ if (clock) printCheck(io2, c, "", clock);
379
+ const workspace = groupByModule(report.results.filter((r) => r.group === "workspace"));
380
+ if (workspace.length === 0) return;
381
+ if (report.context.kind !== "none") {
382
+ io2.out(`\u2192 ${report.context.kind === "workspace" ? "Workspace" : "Module"} ${report.context.label ?? ""}`);
383
+ }
384
+ for (const r of workspace) {
385
+ const indent = r.id.startsWith("pm:") || r.id.startsWith("node:") ? " " : " ";
386
+ printCheck(io2, c, indent, r);
387
+ }
266
388
  }
267
389
 
268
390
  // src/cli/program.ts
@@ -276,6 +398,12 @@ function jsonFromArgv(argv2) {
276
398
  function requestLog(ctx) {
277
399
  return ctx.opts().verbose ? (line2) => ctx.io.err(line2) : void 0;
278
400
  }
401
+ async function withProxyHint(deps2, e) {
402
+ if (!(e instanceof QoreError) || e.code !== "QORE_API_UNREACHABLE" || e.details?.reason !== "connect") return e;
403
+ if (deps2.env.HTTPS_PROXY || deps2.env.https_proxy || deps2.env.HTTP_PROXY || deps2.env.http_proxy) return e;
404
+ const proxy = await deps2.systemProxy();
405
+ return proxy ? withSystemProxy(e, proxy, deps2.platform) : e;
406
+ }
279
407
  function buildProgram(deps2, io2, signal, register, setExitCode) {
280
408
  const program = new Command("qore").description("Qore developer CLI").option("--portal <name|url>", "prod, staging, or a portal URL (default: the portal you last logged in to, else prod)").option("--json", "print exactly one JSON document on stdout").option("--no-input", "never prompt").option("--verbose", "log request lines (never secrets)").option("--no-color", "disable colours").exitOverride().configureOutput({ writeOut: (s) => io2.out(s.trimEnd()), writeErr: (s) => io2.err(s.trimEnd()) });
281
409
  const ctx = { deps: deps2, io: io2, signal, opts: () => program.opts(), setExitCode };
@@ -294,7 +422,7 @@ async function run(argv2, deps2, io2, signal, register) {
294
422
  await program.parseAsync(argv2, { from: "user" });
295
423
  return state.exitCode ?? 0;
296
424
  } catch (caught) {
297
- const e = signal.aborted && isAbortError(caught) ? new CancelledError() : caught;
425
+ const e = await withProxyHint(deps2, signal.aborted && isAbortError(caught) ? new CancelledError() : caught);
298
426
  if (e instanceof CommanderError) {
299
427
  if (e.exitCode === 0) return 0;
300
428
  if (json2) io2.out(JSON.stringify({ ok: false, error: { code: "QORE_CONTEXT_INVALID", message: e.message } }));
@@ -325,6 +453,9 @@ function compareVersions(a, b) {
325
453
  return 0;
326
454
  }
327
455
  async function fetchMeta(fetch3, apiOrigin) {
456
+ return (await fetchMetaWithDate(fetch3, apiOrigin)).meta;
457
+ }
458
+ async function fetchMetaWithDate(fetch3, apiOrigin) {
328
459
  let res;
329
460
  try {
330
461
  res = await fetch3(`${apiOrigin}/api/cli/meta`, { headers: { accept: "application/json" } });
@@ -339,7 +470,9 @@ async function fetchMeta(fetch3, apiOrigin) {
339
470
  });
340
471
  }
341
472
  if (!res.ok) throw await problemToError(res);
342
- return await res.json();
473
+ const header = res.headers.get("date");
474
+ const ms = header ? Date.parse(header) : NaN;
475
+ return { meta: await res.json(), date: Number.isNaN(ms) ? null : new Date(ms) };
343
476
  }
344
477
  function negotiate(meta, portal, cliVersion) {
345
478
  if (portal.pinnedIssuer && meta.auth.issuer !== portal.pinnedIssuer) {
@@ -369,9 +502,9 @@ function isMeta(doc) {
369
502
  return !!doc && typeof doc === "object" && Array.isArray(doc.capabilities);
370
503
  }
371
504
  async function loadMeta(deps2, apiOrigin, refresh) {
372
- const cached = (await deps2.config.read()).profiles[apiOrigin]?.meta;
373
- if (!refresh && cached && isMeta(cached.doc) && deps2.now().getTime() - new Date(cached.fetchedAt).getTime() < TTL_MS) {
374
- return { meta: cached.doc, cached: true };
505
+ const cached2 = (await deps2.config.read()).profiles[apiOrigin]?.meta;
506
+ if (!refresh && cached2 && isMeta(cached2.doc) && deps2.now().getTime() - new Date(cached2.fetchedAt).getTime() < TTL_MS) {
507
+ return { meta: cached2.doc, cached: true };
375
508
  }
376
509
  const meta = await fetchMeta(deps2.fetch, apiOrigin);
377
510
  await deps2.config.update((c) => {
@@ -440,6 +573,12 @@ function profileHash(apiOrigin, issuer) {
440
573
  }
441
574
 
442
575
  // src/core/api/client.ts
576
+ function clockSkewSeconds(dateHeader, now) {
577
+ if (!dateHeader) return null;
578
+ const serverMs = Date.parse(dateHeader);
579
+ if (Number.isNaN(serverMs)) return null;
580
+ return Math.round((serverMs - now.getTime()) / 1e3);
581
+ }
443
582
  var QoreApiClient = class {
444
583
  constructor(o) {
445
584
  this.o = o;
@@ -474,6 +613,15 @@ var QoreApiClient = class {
474
613
  if (res.status === 401) {
475
614
  res = await this.once(method, path, body, true);
476
615
  if (res.status === 401) {
616
+ const skew = clockSkewSeconds(res.headers.get("date"), (this.o.now ?? (() => /* @__PURE__ */ new Date()))());
617
+ if (skew !== null && Math.abs(skew) > 60) {
618
+ const minutes = Math.max(1, Math.round(Math.abs(skew) / 60));
619
+ throw new QoreError("QORE_CLOCK_SKEW", `Your clock is off by ${minutes} min; tokens look expired`, {
620
+ hint: "Turn on automatic date and time, then run the command again.",
621
+ status: 401,
622
+ details: { skewSeconds: skew }
623
+ });
624
+ }
477
625
  throw new QoreError("QORE_SESSION_EXPIRED", "The portal no longer accepts your session", {
478
626
  hint: "Run `qore login`.",
479
627
  status: 401,
@@ -597,7 +745,8 @@ async function openSession(deps2, portal, command, onRequest) {
597
745
  cliVersion: deps2.version,
598
746
  command,
599
747
  platform: `${deps2.platform} ${process.arch}; node ${process.versions.node}`,
600
- onRequest
748
+ onRequest,
749
+ now: deps2.now
601
750
  });
602
751
  return { profile, api, tokens, refresh };
603
752
  }
@@ -1124,10 +1273,10 @@ function unsafeSlugError(slug) {
1124
1273
  function exitFor(results) {
1125
1274
  const failures = results.filter((r) => r.status === "failed" || r.status === "conflict");
1126
1275
  if (failures.length === 0) return null;
1127
- const ok = results.some((r) => ["cloned", "present", "fetched", "pulled", "skipped"].includes(r.status));
1276
+ const ok2 = results.some((r) => ["cloned", "present", "fetched", "pulled", "skipped"].includes(r.status));
1128
1277
  const code = (r) => r.status === "conflict" ? "QORE_WORKSPACE_CONFLICT" : r.error?.code ?? "QORE_INTERNAL";
1129
1278
  const nonAccess = failures.find((r) => !ACCESS_CODES.has(code(r)));
1130
- if (ok && !nonAccess) return null;
1279
+ if (ok2 && !nonAccess) return null;
1131
1280
  const first = nonAccess ?? failures[0];
1132
1281
  const moduleCount = results.filter((r) => r.status !== "removed").length;
1133
1282
  return new QoreError(code(first), `${failures.length} of ${moduleCount} modules need attention`, {
@@ -1289,16 +1438,19 @@ function registerClone(program, ctx) {
1289
1438
  });
1290
1439
  }
1291
1440
 
1292
- // src/cli/commands/env.ts
1293
- import { randomBytes as randomBytes3 } from "crypto";
1294
- import { chmod as chmod2, readFile as readFile4, realpath as realpath3, rename as rename3, unlink, writeFile as writeFile3 } from "fs/promises";
1295
- import { basename as basename2, dirname as dirname4, join as join10, relative as relative5 } from "path";
1441
+ // src/cli/commands/dev.ts
1442
+ import { randomBytes as randomBytes5 } from "crypto";
1443
+ import { realpath as realpath5 } from "fs/promises";
1444
+ import { constants as constants3 } from "os";
1445
+ import { relative as relative6 } from "path";
1446
+ import { StringDecoder } from "string_decoder";
1296
1447
 
1297
1448
  // src/core/context/repoConfig.ts
1298
1449
  import { readFile as readFile3 } from "fs/promises";
1299
1450
  import { join as join7 } from "path";
1300
1451
  var FORBIDDEN = ["portal", "api", "apiOrigin", "issuer", "tenant", "tenantId", "app", "appId", "module", "moduleId"];
1301
1452
  var KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
1453
+ var SCRIPT = /^[A-Za-z0-9:_.-]+$/;
1302
1454
  var invalid2 = (msg, details) => new QoreError("QORE_CONTEXT_INVALID", `.qore.json: ${msg}`, { hint: "Fix .qore.json in the repository root; see the $schema it names.", details });
1303
1455
  function moduleDefaults(type) {
1304
1456
  return type === "frontend" ? { envFile: ".env.local", localSecrets: ["SESSION_COOKIE_SECRET"] } : { envFile: ".env", localSecrets: [] };
@@ -1336,12 +1488,30 @@ async function readRepoConfig(root) {
1336
1488
  if (new Set(doc.localSecrets).size !== doc.localSecrets.length) throw invalid2("localSecrets must not contain duplicates");
1337
1489
  out.localSecrets = doc.localSecrets;
1338
1490
  }
1491
+ if (doc.dev !== void 0) {
1492
+ const d = doc.dev;
1493
+ if (!d || typeof d !== "object" || Array.isArray(d)) throw invalid2("dev must be an object");
1494
+ const dev = {};
1495
+ if (d.install !== void 0) {
1496
+ if (typeof d.install !== "boolean") throw invalid2("dev.install must be true or false");
1497
+ dev.install = d.install;
1498
+ }
1499
+ if (d.services !== void 0) {
1500
+ if (!Array.isArray(d.services) || !d.services.every((s) => typeof s === "string" && SCRIPT.test(s))) {
1501
+ throw invalid2("dev.services must be a list of script names");
1502
+ }
1503
+ if (new Set(d.services).size !== d.services.length) throw invalid2("dev.services must not contain duplicates");
1504
+ dev.services = d.services;
1505
+ }
1506
+ if (d.script !== void 0) {
1507
+ if (typeof d.script !== "string" || !SCRIPT.test(d.script)) throw invalid2("dev.script must be a script name");
1508
+ dev.script = d.script;
1509
+ }
1510
+ out.dev = dev;
1511
+ }
1339
1512
  return out;
1340
1513
  }
1341
1514
 
1342
- // src/core/context/resolver.ts
1343
- import { isAbsolute, join as join9, relative as relative3 } from "path";
1344
-
1345
1515
  // src/core/context/repoRoot.ts
1346
1516
  import { stat } from "fs/promises";
1347
1517
  import { dirname as dirname2, join as join8, resolve as resolve3 } from "path";
@@ -1360,6 +1530,7 @@ async function findRepoRoot(start) {
1360
1530
  }
1361
1531
 
1362
1532
  // src/core/context/resolver.ts
1533
+ import { isAbsolute, join as join9, relative as relative3 } from "path";
1363
1534
  var UUID3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1364
1535
  var HINT_FLAG = "cd into a module repository, or pass --module <slug|id>.";
1365
1536
  var RESOLVE = "/api/cli/v1/repos/resolve";
@@ -1503,8 +1674,8 @@ function profileOrder(cfg, cachedOrigins, portalArg, wsPortal) {
1503
1674
  if (portalArg) return [resolvePortal(portalArg, cfg.defaultProfile)];
1504
1675
  const rank = (o) => o === BUILTIN_PORTALS.prod.apiOrigin ? 0 : o === BUILTIN_PORTALS.staging.apiOrigin ? 1 : 2;
1505
1676
  const rest = Object.keys(cfg.profiles).sort((a, b) => rank(a) - rank(b) || a.localeCompare(b));
1506
- const cached = cachedOrigins.filter((c) => cfg.profiles[c]);
1507
- const origins = [...wsPortal ? [wsPortal.apiOrigin] : [], ...cached, resolvePortal(void 0, cfg.defaultProfile).apiOrigin, ...rest];
1677
+ const cached2 = cachedOrigins.filter((c) => cfg.profiles[c]);
1678
+ const origins = [...wsPortal ? [wsPortal.apiOrigin] : [], ...cached2, resolvePortal(void 0, cfg.defaultProfile).apiOrigin, ...rest];
1508
1679
  return [...new Set(origins)].filter((c) => cfg.profiles[c]).map((c) => resolvePortal(c, void 0));
1509
1680
  }
1510
1681
  function withDetails(e, extra, hint) {
@@ -1518,13 +1689,13 @@ function withDetails(e, extra, hint) {
1518
1689
  cause: e
1519
1690
  });
1520
1691
  }
1521
- async function onProfile(deps2, o, cache, portal, keyed, keys, repoRoot, workspace, unsupported) {
1692
+ async function onProfile(deps2, o, cache, portal, keyed, keys, repoRoot, workspace, unsupported2) {
1522
1693
  const session = await openSession(deps2, portal, o.command, o.onRequest);
1523
1694
  try {
1524
1695
  await ensureCapability(deps2, portal.apiOrigin, "repo.resolve");
1525
1696
  } catch (e) {
1526
1697
  if (!(e instanceof QoreError) || e.code !== "QORE_UNSUPPORTED_BY_SERVER") throw e;
1527
- unsupported(e);
1698
+ unsupported2(e);
1528
1699
  return null;
1529
1700
  }
1530
1701
  const hits = [];
@@ -1598,14 +1769,14 @@ async function resolveModuleContext(deps2, o) {
1598
1769
  const answered = [];
1599
1770
  const skipped = [];
1600
1771
  const skipErrors = [];
1601
- let unsupported;
1772
+ let unsupported2;
1602
1773
  for (const portal of profileOrder(cfg, await cache.originsFor(keys), o.portal, wsPortal)) {
1603
1774
  tried.push(label(portal));
1604
1775
  let capabilityMissing = false;
1605
1776
  try {
1606
1777
  const mc = await onProfile(deps2, o, cache, portal, keyed, keys, root, ws, (e) => {
1607
1778
  capabilityMissing = true;
1608
- unsupported ??= e;
1779
+ unsupported2 ??= e;
1609
1780
  skipped.push({ profile: label(portal), code: e.code, message: e.message });
1610
1781
  });
1611
1782
  if (mc) return mc;
@@ -1619,7 +1790,7 @@ async function resolveModuleContext(deps2, o) {
1619
1790
  const details = { triedProfiles: tried };
1620
1791
  if (skipped.length > 0) details.skippedProfiles = skipped;
1621
1792
  if (answered.length === 0) {
1622
- const cause = skipErrors[0] ?? unsupported;
1793
+ const cause = skipErrors[0] ?? unsupported2;
1623
1794
  if (cause) throw withDetails(cause, details);
1624
1795
  }
1625
1796
  const unchecked = skipped.length ? ` ${skipped.map((s) => s.profile).join(", ")} could not be checked (${skipped.map((s) => s.code).join(", ")}).` : "";
@@ -1648,6 +1819,45 @@ function pickApp(mc, app) {
1648
1819
  throw new QoreError("QORE_APP_AMBIGUOUS", `${mc.module.slug} is part of several apps`, { hint: "Pass --app <slug>.", candidates: appCandidates(apps) });
1649
1820
  }
1650
1821
 
1822
+ // src/core/diagnostics/node.ts
1823
+ var KEEP_LINES = 50;
1824
+ function earlyExitHint(tail, o) {
1825
+ const text = tail.join("\n");
1826
+ const missing = text.match(/Cannot find (?:module|package) '([^']+)'/);
1827
+ if (missing) {
1828
+ return { reason: "missing-module", hint: `A dependency is missing (${missing[1]}). Run \`${o.installCommand}\`, then qore dev again.` };
1829
+ }
1830
+ if (/ZodError:/.test(text)) {
1831
+ const keys = [...text.matchAll(/"path":\s*\[\s*"([^"]+)"/g)].map((m) => m[1]);
1832
+ if (keys.length > 0) {
1833
+ return {
1834
+ reason: "invalid-env",
1835
+ keys,
1836
+ 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.`
1837
+ };
1838
+ }
1839
+ }
1840
+ const addrInUse = text.match(/listen EADDRINUSE: address already in use (?:\S*:)?(\d+)/);
1841
+ if (addrInUse) {
1842
+ const port = Number(addrInUse[1]);
1843
+ return {
1844
+ reason: "port-in-use",
1845
+ port,
1846
+ hint: `Port ${port} was taken while the server started. Run qore dev again; if it repeats, lsof -iTCP:${port} -sTCP:LISTEN names the holder.`
1847
+ };
1848
+ }
1849
+ if (/ENOSPC: System limit for number of file watchers reached/.test(text)) {
1850
+ return {
1851
+ reason: "watcher-limit",
1852
+ 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"
1853
+ };
1854
+ }
1855
+ return null;
1856
+ }
1857
+
1858
+ // src/core/dev/env.ts
1859
+ import { dirname as dirname5, join as join12 } from "path";
1860
+
1651
1861
  // src/core/env/fetch.ts
1652
1862
  async function fetchLocalEnv(deps2, mc, app) {
1653
1863
  await ensureCapability(deps2, mc.portal.apiOrigin, "module.local-env");
@@ -1812,6 +2022,579 @@ function planPull(o) {
1812
2022
  };
1813
2023
  }
1814
2024
 
2025
+ // src/core/env/write.ts
2026
+ import { chmod as chmod2, readFile as readFile4, rename as rename3, unlink, writeFile as writeFile3 } from "fs/promises";
2027
+ import { randomBytes as randomBytes3 } from "crypto";
2028
+ import { basename as basename2, dirname as dirname4, join as join10 } from "path";
2029
+ async function readOrNull(p) {
2030
+ try {
2031
+ return await readFile4(p, "utf8");
2032
+ } catch (e) {
2033
+ if (e.code === "ENOENT") return null;
2034
+ throw e;
2035
+ }
2036
+ }
2037
+ async function writeSecurely(target, text) {
2038
+ const base = basename2(target);
2039
+ const tmpName = base.endsWith(".local") ? `${base.slice(0, -".local".length)}.qore-${randomBytes3(6).toString("hex")}.local` : `${base}.qore-${randomBytes3(6).toString("hex")}.tmp`;
2040
+ const tmp = join10(dirname4(target), tmpName);
2041
+ try {
2042
+ await writeFile3(tmp, text, { mode: 384, flag: "wx" });
2043
+ await chmod2(tmp, 384);
2044
+ await rename3(tmp, target);
2045
+ } finally {
2046
+ await unlink(tmp).catch(() => {
2047
+ });
2048
+ }
2049
+ }
2050
+ async function syncEnvFile(o) {
2051
+ const existing = await readOrNull(o.target);
2052
+ const plan = planPull({
2053
+ existing,
2054
+ example: existing === null ? o.example : null,
2055
+ fetched: o.fetched,
2056
+ managed: o.managed,
2057
+ localSecrets: o.localSecrets,
2058
+ defaultChoice: "keep",
2059
+ random: o.random
2060
+ });
2061
+ if (existing !== null && plan.text === existing) await chmod2(o.target, 384);
2062
+ else await writeSecurely(o.target, plan.text);
2063
+ return { created: plan.created, kept: plan.kept };
2064
+ }
2065
+
2066
+ // src/core/dev/lastStart.ts
2067
+ import { createHash as createHash2 } from "crypto";
2068
+ import { realpath as realpath3 } from "fs/promises";
2069
+ import { join as join11 } from "path";
2070
+ var NETWORK_REASONS = /* @__PURE__ */ new Set(["connect", "dns", "tls", "timeout"]);
2071
+ function isOffline(e) {
2072
+ if (!(e instanceof QoreError)) return false;
2073
+ if (e.code === "QORE_API_UNREACHABLE" || e.code === "QORE_API_UNAVAILABLE") return true;
2074
+ if (e.code !== "QORE_AUTH_UNAVAILABLE") return false;
2075
+ return (e.status ?? 0) >= 500 || NETWORK_REASONS.has(String(e.details?.reason));
2076
+ }
2077
+ async function stateKey(deps2, root) {
2078
+ const real = await realpath3(root).catch(() => root);
2079
+ return createHash2("sha256").update(trustKey(real, deps2.platform)).digest("hex").slice(0, 16);
2080
+ }
2081
+ async function saveLastStart(deps2, root, rec) {
2082
+ await writeJsonAtomic(join11(deps2.config.dir, "dev"), `${await stateKey(deps2, root)}.json`, rec);
2083
+ }
2084
+ async function loadLastStart(deps2, root) {
2085
+ const rec = await readJson(join11(deps2.config.dir, "dev", `${await stateKey(deps2, root)}.json`)).catch(() => null);
2086
+ return rec && rec.schemaVersion === 1 ? rec : null;
2087
+ }
2088
+
2089
+ // src/core/dev/env.ts
2090
+ function envPort(env, key) {
2091
+ const raw = env[key];
2092
+ if (raw === void 0) return null;
2093
+ const v = raw.trim().replace(/^["']|["']$/g, "");
2094
+ if (!/^\d+$/.test(v)) return null;
2095
+ const n = Number(v);
2096
+ return n >= 1024 && n <= 65535 ? n : null;
2097
+ }
2098
+ async function devEnv(deps2, o) {
2099
+ const defaults = moduleDefaults(o.type);
2100
+ const target = await resolveTarget(o.root, o.cfg.envFile ?? defaults.envFile);
2101
+ let cause = null;
2102
+ if (o.mc && o.app) {
2103
+ try {
2104
+ const local = await fetchLocalEnv(deps2, o.mc, o.app);
2105
+ const example = await readOrNull(join12(dirname5(target), ".env.example"));
2106
+ const w = await syncEnvFile({
2107
+ target,
2108
+ example,
2109
+ fetched: local.env,
2110
+ managed: local.managedKeys,
2111
+ localSecrets: o.cfg.localSecrets ?? defaults.localSecrets,
2112
+ random: o.random
2113
+ });
2114
+ return {
2115
+ env: local.env,
2116
+ offline: false,
2117
+ envFile: target,
2118
+ port: envPort(local.env, "PORT"),
2119
+ dbPort: envPort(local.env, "DB_PORT"),
2120
+ ...w
2121
+ };
2122
+ } catch (e) {
2123
+ if (!isOffline(e)) throw e;
2124
+ cause = e;
2125
+ }
2126
+ }
2127
+ const text = await readOrNull(target);
2128
+ if (text === null || !await isTrusted(deps2, o.root, o.moduleId)) {
2129
+ throw cause ?? new QoreError("QORE_API_UNREACHABLE", "The portal is unreachable", {
2130
+ hint: "The portal is unreachable and there is no env file to start from yet."
2131
+ });
2132
+ }
2133
+ const env = Object.fromEntries(parseEnv(text));
2134
+ return { env, offline: true, envFile: target, port: envPort(env, "PORT"), dbPort: envPort(env, "DB_PORT"), created: false, kept: [] };
2135
+ }
2136
+
2137
+ // src/core/dev/project.ts
2138
+ import { access, readFile as readFile5 } from "fs/promises";
2139
+ import { join as join13 } from "path";
2140
+ var LEGACY_DEV_SCRIPTS = ["node --env-file=.env --import ./dist/instrumentation.js dist/index.js"];
2141
+ var LOCKFILES = [["pnpm-lock.yaml", "pnpm"], ["package-lock.json", "npm"], ["yarn.lock", "yarn"]];
2142
+ var exists = (p) => access(p).then(() => true, () => false);
2143
+ var unsupported = (message, hint, details) => new QoreError("QORE_DEV_UNSUPPORTED", message, { hint, details });
2144
+ async function readPackageJson(root) {
2145
+ let raw;
2146
+ try {
2147
+ raw = await readFile5(join13(root, "package.json"), "utf8");
2148
+ } catch {
2149
+ throw unsupported("This repository has no package.json", "qore dev starts Node repositories through their package.json scripts.");
2150
+ }
2151
+ try {
2152
+ return JSON.parse(raw);
2153
+ } catch {
2154
+ throw unsupported("package.json is not valid JSON", "Fix package.json, then run qore dev again.");
2155
+ }
2156
+ }
2157
+ async function detectPackageManager(root, pkg) {
2158
+ if (typeof pkg.packageManager === "string") {
2159
+ const m = /^(pnpm|npm|yarn)@(.+)$/.exec(pkg.packageManager.trim());
2160
+ if (!m) throw unsupported(`packageManager "${pkg.packageManager}" is not pnpm, npm or yarn`, "qore dev supports pnpm, npm and yarn.");
2161
+ return { name: m[1], version: m[2].split("+")[0] };
2162
+ }
2163
+ for (const [file, name] of LOCKFILES) if (await exists(join13(root, file))) return { name, version: null };
2164
+ throw unsupported("No package manager declared and no lockfile found", 'Add "packageManager": "pnpm@<version>" to package.json.');
2165
+ }
2166
+ function planRecipe(o) {
2167
+ const scripts = o.pkg.scripts ?? {};
2168
+ const script = o.cfg.dev?.script ?? "dev";
2169
+ const found = scripts[script];
2170
+ if (found === void 0) {
2171
+ throw unsupported(`${o.slug} has no "${script}" script in package.json`, 'Add a "dev" script, or set dev.script in .qore.json.', { script });
2172
+ }
2173
+ if (LEGACY_DEV_SCRIPTS.includes(found.trim())) {
2174
+ throw unsupported(
2175
+ `${o.slug}'s dev script is the old backend one, which needs a build and a .env first`,
2176
+ 'Update it to the current template: "dev": "node scripts/dev.mjs" plus scripts/dev.mjs.',
2177
+ { script, found }
2178
+ );
2179
+ }
2180
+ const configured = o.cfg.dev?.services;
2181
+ for (const s of configured ?? []) {
2182
+ if (scripts[s] === void 0) {
2183
+ throw new QoreError("QORE_CONTEXT_INVALID", `.qore.json: dev.services names "${s}", which is not a script in package.json`, {
2184
+ hint: "Fix dev.services in .qore.json."
2185
+ });
2186
+ }
2187
+ }
2188
+ const services = !o.flags.services ? [] : configured ?? (scripts["db:up"] !== void 0 ? ["db:up"] : []);
2189
+ return {
2190
+ root: o.root,
2191
+ pm: o.pm,
2192
+ script,
2193
+ services,
2194
+ install: (o.cfg.dev?.install ?? true) && o.flags.install,
2195
+ dockerServices: services.some((s) => /\bdocker\b/.test(scripts[s] ?? ""))
2196
+ };
2197
+ }
2198
+ function runArgs(pm, script) {
2199
+ return { cmd: pm.name, args: ["run", script] };
2200
+ }
2201
+ function installArgs(pm) {
2202
+ return { cmd: pm.name, args: ["install"] };
2203
+ }
2204
+
2205
+ // src/core/dev/install.ts
2206
+ import { stat as stat2 } from "fs/promises";
2207
+ import { join as join14 } from "path";
2208
+ var MARKERS = {
2209
+ pnpm: [".modules.yaml"],
2210
+ npm: [".package-lock.json"],
2211
+ yarn: [".yarn-state.yml", ".yarn-integrity"]
2212
+ };
2213
+ var LOCK = { pnpm: "pnpm-lock.yaml", npm: "package-lock.json", yarn: "yarn.lock" };
2214
+ var mtime = (p) => stat2(p).then((s) => s.mtimeMs, () => null);
2215
+ async function needsInstall(root, pm) {
2216
+ if (await mtime(join14(root, "node_modules")) === null) return true;
2217
+ let marker = null;
2218
+ for (const m of MARKERS[pm.name]) marker ??= await mtime(join14(root, "node_modules", m));
2219
+ if (marker === null) return true;
2220
+ const lock = await mtime(join14(root, LOCK[pm.name]));
2221
+ return lock !== null && lock > marker;
2222
+ }
2223
+
2224
+ // src/core/diagnostics/docker.ts
2225
+ function dockerInstallUrl(platform) {
2226
+ if (platform === "darwin") return "https://docs.docker.com/desktop/setup/install/mac-install/";
2227
+ if (platform === "win32") return "https://docs.docker.com/desktop/setup/install/windows-install/";
2228
+ return "https://docs.docker.com/engine/install/";
2229
+ }
2230
+ function classifyDockerInfo(r, platform) {
2231
+ if (r.spawnError === "ENOENT") {
2232
+ return new QoreError("QORE_DOCKER_MISSING", "Docker is not installed", {
2233
+ hint: `Install Docker: ${dockerInstallUrl(platform)} \u2014 or run qore dev --no-services.`
2234
+ });
2235
+ }
2236
+ if (r.code === 0) return null;
2237
+ const hint = "Start Docker Desktop, then run qore dev again \u2014 or qore dev --no-services.";
2238
+ if (/Cannot connect to the Docker daemon|Is the docker daemon running|error during connect/i.test(r.tail.join("\n"))) {
2239
+ return new QoreError("QORE_DOCKER_NOT_RUNNING", "Docker is not running", { hint, details: { reason: "daemon" } });
2240
+ }
2241
+ return new QoreError("QORE_DOCKER_NOT_RUNNING", "Docker is not running", { hint, details: { stderr: r.tail } });
2242
+ }
2243
+ function classifyComposeVersion(r) {
2244
+ if (r.code === 0) return null;
2245
+ const hint = "Update Docker Desktop (or install the compose plugin). The legacy docker-compose v1 is not supported.";
2246
+ if (/unknown command: docker compose|'compose' is not a docker command/.test(r.tail.join("\n"))) {
2247
+ return new QoreError("QORE_DOCKER_COMPOSE_MISSING", "Docker Compose v2 is missing", { hint });
2248
+ }
2249
+ return new QoreError("QORE_DOCKER_COMPOSE_MISSING", "Docker Compose v2 is missing", { hint, details: { stderr: r.tail } });
2250
+ }
2251
+
2252
+ // src/core/diagnostics/postgres.ts
2253
+ var INCOMPATIBLE = /database files are incompatible with server|Counter to that, there appears to be PostgreSQL data in|in 18\+, these Docker images are configured to store database data/;
2254
+ function classifyComposeLogs(logs, o) {
2255
+ const text = logs.join("\n");
2256
+ if (!INCOMPATIBLE.test(text)) return null;
2257
+ const details = { reason: "pg-data-version" };
2258
+ const m = text.match(/initialized by PostgreSQL version (\d+).*this version (\d+)/);
2259
+ if (m) {
2260
+ details.found = m[1];
2261
+ details.expected = m[2];
2262
+ }
2263
+ return new QoreError("QORE_SERVICE_DATA_INCOMPATIBLE", `The local database volume of ${o.slug} was created by another Postgres version`, {
2264
+ hint: "pnpm db:reset recreates it \u2014 this deletes all local data in it. Qore never runs it for you.",
2265
+ details
2266
+ });
2267
+ }
2268
+
2269
+ // src/core/diagnostics/ports.ts
2270
+ function parseDockerPs(stdout) {
2271
+ const line2 = stdout.split(/\r?\n/).find((l) => l.trim());
2272
+ if (!line2) return null;
2273
+ const [id, name, project, workingDir] = line2.split(" ");
2274
+ if (!id || !name) return null;
2275
+ return { id, name, project: project || null, workingDir: workingDir || null };
2276
+ }
2277
+ function parseLsofCwd(stdout) {
2278
+ const m = stdout.match(/^fcwd\nn(.+)$/m);
2279
+ return m ? m[1] : null;
2280
+ }
2281
+ function parseEtime(s) {
2282
+ const m = s.trim().match(/^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/);
2283
+ if (!m) return null;
2284
+ const days = m[1] ? Number(m[1]) : 0;
2285
+ const hours = m[2] ? Number(m[2]) : 0;
2286
+ const minutes = Number(m[3]);
2287
+ const seconds = Number(m[4]);
2288
+ return days * 86400 + hours * 3600 + minutes * 60 + seconds;
2289
+ }
2290
+ function humanAge(seconds) {
2291
+ if (seconds < 60) return `${seconds} s`;
2292
+ if (seconds < 3600) return `${Math.round(seconds / 60)} min`;
2293
+ if (seconds < 86400) return `${Math.round(seconds / 3600)} h`;
2294
+ return `${Math.round(seconds / 86400)} d`;
2295
+ }
2296
+
2297
+ // src/core/diagnostics/pm.ts
2298
+ function pmVersionMismatch(pm, printed) {
2299
+ if (pm.version === null) return null;
2300
+ const lines = printed.split("\n");
2301
+ let found = null;
2302
+ for (let i = lines.length - 1; i >= 0; i--) {
2303
+ const line2 = lines[i].trim();
2304
+ if (/^\d+\.\d+\.\d+/.test(line2)) {
2305
+ found = line2;
2306
+ break;
2307
+ }
2308
+ }
2309
+ if (found === null) return null;
2310
+ if (found.split(".")[0] === pm.version.split(".")[0]) return null;
2311
+ return new QoreError("QORE_PACKAGE_MANAGER", `${pm.name} ${found} does not match the pinned ${pm.name}@${pm.version}`, {
2312
+ hint: `Install the pinned major: npm i -g ${pm.name}@${pm.version.split(".")[0]}. (pnpm \u2265 9.7 switches to the pinned version by itself.)`,
2313
+ details: { found, pinned: pm.version }
2314
+ });
2315
+ }
2316
+ function classifyInstall(tail, command, root) {
2317
+ const joined = tail.join("\n");
2318
+ if (/ERR_PNPM_OUTDATED_LOCKFILE/.test(joined)) {
2319
+ return new QoreError("QORE_INSTALL_FAILED", "The lockfile does not match package.json", {
2320
+ hint: `Someone committed one without the other. Run \`${command} --no-frozen-lockfile\` in ${root} and commit the lockfile.`,
2321
+ details: { reason: "outdated-lockfile", command, stderr: tail }
2322
+ });
2323
+ }
2324
+ return new QoreError("QORE_INSTALL_FAILED", `${command} failed`, {
2325
+ hint: `Run \`${command}\` in ${root} to see the full output.`,
2326
+ details: { command, stderr: tail }
2327
+ });
2328
+ }
2329
+
2330
+ // src/core/diagnostics/versions.ts
2331
+ import semver from "semver";
2332
+ function nodeSatisfies(found, range) {
2333
+ if (semver.validRange(range) === null) return null;
2334
+ return semver.satisfies(found, range);
2335
+ }
2336
+ function nodeVersionError(found, range, slug) {
2337
+ const min = semver.minVersion(range);
2338
+ const major = min ? min.major : range;
2339
+ return new QoreError("QORE_NODE_VERSION", `${slug} needs Node ${range}, found ${found}`, {
2340
+ hint: `nvm install ${major} / fnm use ${major}, then run qore dev again.`,
2341
+ details: { found, required: range }
2342
+ });
2343
+ }
2344
+
2345
+ // src/core/dev/preflight.ts
2346
+ var pmMajor = (v) => v ? v.split(".")[0] : "latest";
2347
+ var GRACE_MS = 1e4;
2348
+ async function runTool(deps2, cmd, args, o) {
2349
+ if (o.signal.aborted) throw new CancelledError();
2350
+ const tail = [];
2351
+ const c = deps2.launch.launch(cmd, args, { cwd: o.cwd, env: { ...o.env, LC_ALL: "C" }, onLine: (l) => {
2352
+ tail.push(l.line);
2353
+ if (tail.length > 50) tail.shift();
2354
+ o.onLine(l);
2355
+ } });
2356
+ let kill;
2357
+ const onAbort = () => {
2358
+ c.stop("SIGTERM");
2359
+ kill = setTimeout(() => c.stop("SIGKILL"), GRACE_MS);
2360
+ };
2361
+ o.signal.addEventListener("abort", onAbort, { once: true });
2362
+ let exit;
2363
+ try {
2364
+ exit = await c.exited;
2365
+ } finally {
2366
+ o.signal.removeEventListener("abort", onAbort);
2367
+ clearTimeout(kill);
2368
+ }
2369
+ if (o.signal.aborted) throw new CancelledError();
2370
+ return { code: exit.code, spawnError: exit.spawnError, tail };
2371
+ }
2372
+ async function checkPackageManager(deps2, r, onLine, signal) {
2373
+ const res = await runTool(deps2, r.pm.name, ["--version"], { cwd: r.root, env: deps2.env, onLine, signal });
2374
+ const hint = `Install it: npm i -g ${r.pm.name}@${pmMajor(r.pm.version)}`;
2375
+ if (res.spawnError === "ENOENT") {
2376
+ throw new QoreError("QORE_PACKAGE_MANAGER", `${r.pm.name} is not installed`, { hint, details: { packageManager: r.pm } });
2377
+ }
2378
+ if (res.code !== 0) {
2379
+ throw new QoreError("QORE_PACKAGE_MANAGER", `${r.pm.name} is not installed`, { hint, details: { packageManager: r.pm, stderr: res.tail } });
2380
+ }
2381
+ const mismatch = pmVersionMismatch(r.pm, res.tail.join("\n"));
2382
+ if (mismatch) throw mismatch;
2383
+ return res.tail.map((l) => l.trim()).findLast((l) => /^\d+\.\d+\.\d+/.test(l)) ?? null;
2384
+ }
2385
+ async function checkNode(deps2, r, pkg, slug, signal) {
2386
+ const range = pkg.engines?.node;
2387
+ if (!range) return null;
2388
+ const res = await runTool(deps2, "node", ["--version"], { cwd: r.root, env: deps2.env, onLine: () => {
2389
+ }, signal });
2390
+ if (res.spawnError === "ENOENT") {
2391
+ throw new QoreError("QORE_NODE_VERSION", "node is not on PATH", {
2392
+ hint: "Install Node (https://nodejs.org) or run nvm use, then reinstall qore.",
2393
+ details: { required: range }
2394
+ });
2395
+ }
2396
+ if (res.spawnError || res.code !== 0) {
2397
+ throw new QoreError("QORE_NODE_VERSION", "node --version failed", { details: { required: range, stderr: res.tail } });
2398
+ }
2399
+ const found = (res.tail.at(-1) ?? "").trim().replace(/^v/, "");
2400
+ if (nodeSatisfies(found, range) === false) throw nodeVersionError(found, range, slug);
2401
+ return found;
2402
+ }
2403
+ async function installIfNeeded(deps2, r, slug, onLine, report, signal) {
2404
+ report({ module: slug, step: "install", status: "start" });
2405
+ if (!r.install || !await needsInstall(r.root, r.pm)) {
2406
+ report({ module: slug, step: "install", status: "skip" });
2407
+ return;
2408
+ }
2409
+ const { cmd, args } = installArgs(r.pm);
2410
+ const res = await runTool(deps2, cmd, args, { cwd: r.root, env: deps2.env, onLine, signal });
2411
+ if (res.code !== 0) {
2412
+ const command = [cmd, ...args].join(" ");
2413
+ const err = classifyInstall(res.tail, command, r.root);
2414
+ if (err.details?.reason === "outdated-lockfile") throw err;
2415
+ throw new QoreError("QORE_INSTALL_FAILED", `${command} failed in ${slug}`, { hint: err.hint, details: err.details });
2416
+ }
2417
+ report({ module: slug, step: "install", status: "done" });
2418
+ }
2419
+ async function portInUseError(deps2, o) {
2420
+ const owner = await deps2.ports.owner(o.port);
2421
+ const qoreRun = (o.runs ?? []).find((r) => r.children.some((c) => c.port === o.port) || owner !== null && (r.qorePid === owner.pid || r.children.some((c) => c.pid === owner.pid)));
2422
+ if (qoreRun) {
2423
+ return new QoreError("QORE_PORT_IN_USE", `A previous qore dev is still running (pid ${qoreRun.qorePid})`, {
2424
+ hint: `Stop it (Ctrl+C in its terminal, or kill ${qoreRun.qorePid}), then run qore dev again.`,
2425
+ details: { port: o.port, module: o.slug, key: o.key, owner: "qore", pid: qoreRun.qorePid }
2426
+ });
2427
+ }
2428
+ if (owner) {
2429
+ if (/^(com\.docker\.backend|com\.docker\.vpnkit|docker-proxy|vpnkit|dockerd)$/.test(owner.command)) {
2430
+ const pub = await deps2.ports.dockerPublisher(o.port);
2431
+ if (pub) {
2432
+ return new QoreError("QORE_PORT_IN_USE", `Port ${o.port} is held by the Docker container ${pub.name} (needed by ${o.slug})`, {
2433
+ hint: `It belongs to compose project ${pub.project} in ${pub.workingDir} \u2014 typically another module's or tenant's database. Stop it there with docker compose down, or docker stop ${pub.name}.`,
2434
+ details: { port: o.port, module: o.slug, key: o.key, owner: "docker", ...pub }
2435
+ });
2436
+ }
2437
+ }
2438
+ const detail2 = await deps2.ports.ownerDetail(owner.pid);
2439
+ const extras = [];
2440
+ if (detail2.ageSeconds !== null) extras.push(`started ${humanAge(detail2.ageSeconds)} ago`);
2441
+ if (detail2.cwd !== null) {
2442
+ const cwd = deps2.home && detail2.cwd.startsWith(deps2.home) ? `~${detail2.cwd.slice(deps2.home.length)}` : detail2.cwd;
2443
+ extras.push(`in ${cwd}`);
2444
+ }
2445
+ const suffix = extras.length ? `, ${extras.join(" ")}` : "";
2446
+ return new QoreError("QORE_PORT_IN_USE", `Port ${o.port} is already in use (needed by ${o.slug})`, {
2447
+ hint: `Held by: ${owner.command} (pid ${owner.pid})${suffix}. Stop that process. Qore never moves a port: other modules and the local login rely on ${o.port}.`,
2448
+ details: {
2449
+ port: o.port,
2450
+ module: o.slug,
2451
+ key: o.key,
2452
+ owner: "process",
2453
+ pid: owner.pid,
2454
+ command: owner.command,
2455
+ ...detail2.cwd !== null ? { cwd: detail2.cwd } : {},
2456
+ ...detail2.ageSeconds !== null ? { ageSeconds: detail2.ageSeconds } : {}
2457
+ }
2458
+ });
2459
+ }
2460
+ const findHint = deps2.platform === "win32" ? `netstat -ano | findstr :${o.port}` : `lsof -iTCP:${o.port} -sTCP:LISTEN`;
2461
+ return new QoreError("QORE_PORT_IN_USE", `Port ${o.port} is already in use (needed by ${o.slug})`, {
2462
+ hint: `Find it with: ${findHint}`,
2463
+ details: { port: o.port, module: o.slug, key: o.key }
2464
+ });
2465
+ }
2466
+ async function composeFailure(deps2, r, slug, env, signal, message, command, stderr) {
2467
+ if (!r.dockerServices) {
2468
+ return new QoreError("QORE_SERVICE_UNHEALTHY", message, { details: { command, stderr } });
2469
+ }
2470
+ const logs = await runTool(deps2, "docker", ["compose", "logs", "--no-color", "--tail", "50"], { cwd: r.root, env, onLine: () => {
2471
+ }, signal });
2472
+ const dataErr = classifyComposeLogs(logs.tail, { slug, script: "db:up" });
2473
+ if (dataErr) return dataErr;
2474
+ return new QoreError("QORE_SERVICE_UNHEALTHY", message, {
2475
+ hint: `The last lines of docker compose logs are in details.logs; run \`docker compose logs\` in ${r.root} for all of them.`,
2476
+ details: { command, stderr, logs: logs.tail.slice(-20) }
2477
+ });
2478
+ }
2479
+ async function startServices(deps2, r, slug, env, dbPort, waitMs, signal, onLine, report) {
2480
+ report({ module: slug, step: "services", status: "start" });
2481
+ if (r.dockerServices) {
2482
+ const res = await runTool(deps2, "docker", ["info", "--format", "{{.ServerVersion}}"], { cwd: r.root, env, onLine, signal });
2483
+ const dockerErr = classifyDockerInfo(res, deps2.platform);
2484
+ if (dockerErr) throw dockerErr;
2485
+ const compose = await runTool(deps2, "docker", ["compose", "version"], { cwd: r.root, env, onLine: () => {
2486
+ }, signal });
2487
+ const composeErr = classifyComposeVersion(compose);
2488
+ if (composeErr) throw composeErr;
2489
+ }
2490
+ for (const script of r.services) {
2491
+ const { cmd, args } = runArgs(r.pm, script);
2492
+ const res = await runTool(deps2, cmd, args, { cwd: r.root, env, onLine, signal });
2493
+ if (res.code !== 0) {
2494
+ if (dbPort !== null && /port is already allocated|address already in use/i.test(res.tail.join("\n"))) {
2495
+ throw await portInUseError(deps2, { slug, port: dbPort, key: "DB_PORT" });
2496
+ }
2497
+ throw await composeFailure(deps2, r, slug, env, signal, `${script} failed in ${slug}`, `${cmd} ${args.join(" ")}`, res.tail);
2498
+ }
2499
+ }
2500
+ if (dbPort !== null && r.services.length > 0) {
2501
+ const ok2 = await deps2.ports.waitFor(dbPort, waitMs, signal);
2502
+ if (signal.aborted) throw new CancelledError();
2503
+ if (!ok2) {
2504
+ throw await composeFailure(
2505
+ deps2,
2506
+ r,
2507
+ slug,
2508
+ env,
2509
+ signal,
2510
+ `The database for ${slug} did not accept connections on port ${dbPort} within ${waitMs / 1e3} s`,
2511
+ void 0,
2512
+ []
2513
+ );
2514
+ }
2515
+ }
2516
+ report({ module: slug, step: "services", status: "done" });
2517
+ }
2518
+ async function checkPort(deps2, o) {
2519
+ if (o.port === null) return;
2520
+ if (!await deps2.ports.inUse(o.port)) return;
2521
+ throw await portInUseError(deps2, { slug: o.slug, port: o.port, key: o.key, runs: o.runs });
2522
+ }
2523
+ function abortableDelay(signal) {
2524
+ return (ms) => new Promise((resolve5) => {
2525
+ const done = () => {
2526
+ clearTimeout(t);
2527
+ signal.removeEventListener("abort", done);
2528
+ resolve5();
2529
+ };
2530
+ const t = setTimeout(done, ms);
2531
+ signal.addEventListener("abort", done, { once: true });
2532
+ });
2533
+ }
2534
+ async function waitForDocker(deps2, o) {
2535
+ const delay = o.delay ?? abortableDelay(o.signal);
2536
+ const now = o.now ?? Date.now;
2537
+ const deadline = now() + o.timeoutMs;
2538
+ for (; ; ) {
2539
+ await delay(Math.min(o.pollMs, Math.max(0, deadline - now())));
2540
+ if (o.signal.aborted) throw new CancelledError();
2541
+ const remaining = deadline - now();
2542
+ if (remaining <= 0) return false;
2543
+ const poll = AbortSignal.any([o.signal, AbortSignal.timeout(remaining)]);
2544
+ let res;
2545
+ try {
2546
+ res = await runTool(deps2, "docker", ["info", "--format", "{{.ServerVersion}}"], { cwd: o.cwd, env: o.env ?? deps2.env, onLine: () => {
2547
+ }, signal: poll });
2548
+ } catch (e) {
2549
+ if (e instanceof CancelledError && !o.signal.aborted) return false;
2550
+ throw e;
2551
+ }
2552
+ if (classifyDockerInfo(res, deps2.platform) === null) return true;
2553
+ }
2554
+ }
2555
+ var MAX_PARENT_HOPS = 10;
2556
+ var AGE_SLACK_S = 5;
2557
+ async function leftoverOwnsPort(deps2, run4, port) {
2558
+ if (deps2.platform === "win32" || !deps2.launch.signal(run4.qorePid, 0)) return false;
2559
+ const { ageSeconds } = await deps2.ports.ownerDetail(run4.qorePid);
2560
+ const recordAge = (deps2.now().getTime() - Date.parse(run4.startedAt)) / 1e3;
2561
+ if (ageSeconds === null || !Number.isFinite(recordAge) || ageSeconds < recordAge - AGE_SLACK_S) return false;
2562
+ const ours = /* @__PURE__ */ new Set([run4.qorePid, ...run4.children.map((c) => c.pid).filter((p) => p !== null)]);
2563
+ let pid = (await deps2.ports.owner(port))?.pid ?? null;
2564
+ for (let hop = 0; pid !== null && pid > 1 && hop <= MAX_PARENT_HOPS; hop++) {
2565
+ if (ours.has(pid)) return true;
2566
+ pid = await deps2.ports.parentPid(pid);
2567
+ }
2568
+ return false;
2569
+ }
2570
+
2571
+ // src/core/dev/runFile.ts
2572
+ import { unlink as unlink2 } from "fs/promises";
2573
+ import { join as join15 } from "path";
2574
+ async function runFilePath(deps2, root) {
2575
+ const dir2 = join15(deps2.config.dir, "run");
2576
+ const name = `${await stateKey(deps2, root)}.json`;
2577
+ return { dir: dir2, name, path: join15(dir2, name) };
2578
+ }
2579
+ async function writeRunFile(deps2, root, rec) {
2580
+ const { dir: dir2, name } = await runFilePath(deps2, root);
2581
+ await writeJsonAtomic(dir2, name, rec);
2582
+ }
2583
+ async function removeRunFile(deps2, root) {
2584
+ const { path } = await runFilePath(deps2, root);
2585
+ await unlink2(path).catch(() => {
2586
+ });
2587
+ }
2588
+ async function leftoverRun(deps2, root, o = {}) {
2589
+ const { path } = await runFilePath(deps2, root);
2590
+ const rec = await readJson(path).catch(() => null);
2591
+ if (!rec || rec.schemaVersion !== 1) return null;
2592
+ if (deps2.launch.isAlive(rec.qorePid)) return rec;
2593
+ if (o.prune !== false) await unlink2(path).catch(() => {
2594
+ });
2595
+ return null;
2596
+ }
2597
+
1815
2598
  // src/cli/context.ts
1816
2599
  var allOfKind = (c, kind) => !!c && c.length > 0 && c.every((x) => x.kind === kind && typeof x.id === "string");
1817
2600
  async function moduleContextFor(ctx, flags, command) {
@@ -1855,17 +2638,106 @@ function contextLine(mc, app) {
1855
2638
  return `\u2192 ${parts.join(" / ")} (${sourceText(mc)})`;
1856
2639
  }
1857
2640
  function contextJson(mc, app) {
1858
- const { id, slug, name, type, access } = mc.module;
2641
+ const { id, slug, name, type, access: access2 } = mc.module;
1859
2642
  return {
1860
2643
  portal: mc.portal.builtin ?? mc.portal.apiOrigin,
1861
2644
  tenant: mc.module.tenant,
1862
2645
  app: app ?? null,
1863
- module: { id, slug, name, type, access },
2646
+ module: { id, slug, name, type, access: access2 },
1864
2647
  source: mc.source.kind === "remote" ? mc.source.remote : mc.source.kind === "workspace" ? WORKSPACE_FILE : "--module",
1865
2648
  ...mc.source.kind === "flag" && mc.source.overrides ? { overrides: mc.source.overrides } : {}
1866
2649
  };
1867
2650
  }
1868
2651
 
2652
+ // src/cli/devRecover.ts
2653
+ var DOCKER_WAIT = { timeoutMs: 12e4, pollMs: 2e3 };
2654
+ var STOP_WAIT_MS = 1e4;
2655
+ function canRecover(ctx) {
2656
+ const g = ctx.opts();
2657
+ return ctx.io.interactive && !g.json && g.input !== false;
2658
+ }
2659
+ var daemonDown = (e) => e instanceof QoreError && e.code === "QORE_DOCKER_NOT_RUNNING" && e.details?.reason === "daemon";
2660
+ function dockerRecovery(ctx, signal) {
2661
+ let answer;
2662
+ return async (at, fn) => {
2663
+ try {
2664
+ await fn();
2665
+ } catch (e) {
2666
+ if (!daemonDown(e) || !canRecover(ctx)) throw e;
2667
+ answer ??= (async () => {
2668
+ ctx.io.err("Waiting for Docker\u2026 (Ctrl+C to stop)");
2669
+ const ok2 = await waitForDocker(ctx.deps, { ...at, signal, ...DOCKER_WAIT });
2670
+ if (ok2) ctx.io.err("\u2713 Docker is running");
2671
+ return ok2;
2672
+ })();
2673
+ if (!await answer) throw e;
2674
+ await fn();
2675
+ }
2676
+ };
2677
+ }
2678
+ async function stopLeftover(ctx, e, runs, signal) {
2679
+ const { deps: deps2, io: io2 } = ctx;
2680
+ const port = e.details?.port;
2681
+ const run4 = runs.find((r) => r.qorePid === e.details?.pid);
2682
+ if (!run4 || typeof port !== "number" || !await leftoverOwnsPort(deps2, run4, port)) return false;
2683
+ if (!await io2.confirm(`Stop the previous qore dev (pid ${run4.qorePid})?`, signal)) return false;
2684
+ if (!deps2.launch.signal(run4.qorePid, "SIGTERM")) {
2685
+ io2.err(`! pid ${run4.qorePid} could not be signalled`);
2686
+ return false;
2687
+ }
2688
+ if (await deps2.ports.waitFree(port, STOP_WAIT_MS, signal)) return true;
2689
+ if (!signal.aborted) io2.err(`! pid ${run4.qorePid} did not exit within ${STOP_WAIT_MS / 1e3} s`);
2690
+ return false;
2691
+ }
2692
+ async function checkPortOrStopLeftover(ctx, o, signal) {
2693
+ try {
2694
+ await checkPort(ctx.deps, o);
2695
+ } catch (e) {
2696
+ if (!(e instanceof QoreError) || e.code !== "QORE_PORT_IN_USE" || e.details?.owner !== "qore" || !canRecover(ctx)) throw e;
2697
+ const freed = await stopLeftover(ctx, e, o.runs, signal);
2698
+ if (signal.aborted) throw new CancelledError();
2699
+ if (!freed) throw e;
2700
+ await checkPort(ctx.deps, o);
2701
+ }
2702
+ }
2703
+
2704
+ // src/cli/termination.ts
2705
+ var preflightHook = {};
2706
+ function preflightTermination(abort, raise) {
2707
+ let by;
2708
+ preflightHook.onSignal = (sig) => {
2709
+ by ??= sig;
2710
+ abort();
2711
+ };
2712
+ return { reRaise: async (flush) => {
2713
+ if (!by) return;
2714
+ await flush();
2715
+ raise(by);
2716
+ } };
2717
+ }
2718
+ async function inPreflight(signals, outer, fn) {
2719
+ const ac2 = new AbortController();
2720
+ const onOuter = () => ac2.abort();
2721
+ outer.addEventListener("abort", onOuter);
2722
+ if (outer.aborted) ac2.abort();
2723
+ const handlers = ["SIGTERM", "SIGHUP"].map((sig) => [sig, () => {
2724
+ preflightHook.onSignal?.(sig);
2725
+ ac2.abort();
2726
+ }]);
2727
+ let armed = false;
2728
+ const arm = () => {
2729
+ if (armed) return;
2730
+ armed = true;
2731
+ for (const [sig, h] of handlers) signals.on(sig, h);
2732
+ };
2733
+ try {
2734
+ return await fn({ signal: ac2.signal, arm });
2735
+ } finally {
2736
+ if (armed) for (const [sig, h] of handlers) signals.off(sig, h);
2737
+ outer.removeEventListener("abort", onOuter);
2738
+ }
2739
+ }
2740
+
1869
2741
  // src/cli/trustPrompt.ts
1870
2742
  async function ensureTrusted(ctx, mc, app, o) {
1871
2743
  const root = mc.repoRoot ?? ctx.deps.cwd;
@@ -1889,37 +2761,1401 @@ async function ensureTrusted(ctx, mc, app, o) {
1889
2761
  await trust(ctx.deps, root, mc.module.id);
1890
2762
  }
1891
2763
 
1892
- // src/cli/commands/env.ts
1893
- var maskLen = (v) => v === void 0 ? "(unset)" : `\u2022\u2022\u2022\u2022 (${v.length} chars)`;
1894
- async function readOrNull(p) {
1895
- try {
1896
- return await readFile4(p, "utf8");
1897
- } catch (e) {
1898
- if (e.code === "ENOENT") return null;
1899
- throw e;
2764
+ // src/cli/commands/devWorkspace.ts
2765
+ import { randomBytes as randomBytes4 } from "crypto";
2766
+ import { realpath as realpath4 } from "fs/promises";
2767
+ import { join as join16, relative as relative5 } from "path";
2768
+
2769
+ // src/core/dev/order.ts
2770
+ function selectSkips(modules, sel) {
2771
+ if (sel.only && sel.skip) {
2772
+ throw new QoreError("QORE_CONTEXT_INVALID", "--only and --skip cannot be used together", {
2773
+ hint: "Pass either --only or --skip, not both."
2774
+ });
2775
+ }
2776
+ const slugs = modules.map((m) => m.slug).sort();
2777
+ const known = new Set(slugs);
2778
+ for (const s of [...sel.only ?? [], ...sel.skip ?? []]) {
2779
+ if (!known.has(s)) {
2780
+ throw new QoreError("QORE_CONTEXT_INVALID", `Unknown module: ${s}`, {
2781
+ hint: `Valid slugs: ${slugs.join(", ")}`
2782
+ });
2783
+ }
2784
+ }
2785
+ const onlySet = sel.only ? new Set(sel.only) : null;
2786
+ const skipSet = sel.skip ? new Set(sel.skip) : null;
2787
+ const reasons = /* @__PURE__ */ new Map();
2788
+ for (const m of modules) {
2789
+ if (onlySet && !onlySet.has(m.slug) || skipSet && skipSet.has(m.slug)) {
2790
+ reasons.set(m.slug, "excluded");
2791
+ } else if (!m.present) {
2792
+ reasons.set(m.slug, "not-cloned");
2793
+ } else if (!m.canWrite) {
2794
+ reasons.set(m.slug, "no-write");
2795
+ }
2796
+ }
2797
+ return reasons;
2798
+ }
2799
+ function tarjanSccs(starting, byId, startingIds) {
2800
+ let index = 0;
2801
+ const indices = /* @__PURE__ */ new Map();
2802
+ const lowlink = /* @__PURE__ */ new Map();
2803
+ const onStack = /* @__PURE__ */ new Map();
2804
+ const stack = [];
2805
+ const sccs = [];
2806
+ function strongconnect(id) {
2807
+ indices.set(id, index);
2808
+ lowlink.set(id, index);
2809
+ index++;
2810
+ stack.push(id);
2811
+ onStack.set(id, true);
2812
+ const m = byId.get(id);
2813
+ for (const dep of m.dependsOn) {
2814
+ if (!startingIds.has(dep)) continue;
2815
+ if (!indices.has(dep)) {
2816
+ strongconnect(dep);
2817
+ lowlink.set(id, Math.min(lowlink.get(id), lowlink.get(dep)));
2818
+ } else if (onStack.get(dep)) {
2819
+ lowlink.set(id, Math.min(lowlink.get(id), indices.get(dep)));
2820
+ }
2821
+ }
2822
+ if (lowlink.get(id) === indices.get(id)) {
2823
+ const comp = [];
2824
+ let w;
2825
+ do {
2826
+ w = stack.pop();
2827
+ onStack.set(w, false);
2828
+ comp.push(w);
2829
+ } while (w !== id);
2830
+ sccs.push(comp);
2831
+ }
2832
+ }
2833
+ for (const m of starting) {
2834
+ if (!indices.has(m.id)) strongconnect(m.id);
2835
+ }
2836
+ return sccs;
2837
+ }
2838
+ function planDev(modules, sel) {
2839
+ const reasons = selectSkips(modules, sel);
2840
+ const byId = new Map(modules.map((m) => [m.id, m]));
2841
+ const starting = modules.filter((m) => !reasons.has(m.slug));
2842
+ const startingIds = new Set(starting.map((m) => m.id));
2843
+ const sccs = tarjanSccs(starting, byId, startingIds);
2844
+ const sccOf = /* @__PURE__ */ new Map();
2845
+ const sccSlugs = sccs.map((comp) => comp.map((id) => byId.get(id).slug).sort());
2846
+ sccs.forEach((comp, i) => comp.forEach((id) => sccOf.set(id, i)));
2847
+ const cycles = sccSlugs.filter((slugs, i) => slugs.length > 1 || sccs[i].some((id) => byId.get(id).dependsOn.includes(id))).map((slugs) => slugs);
2848
+ const providerSccsOf = /* @__PURE__ */ new Map();
2849
+ sccs.forEach((comp, i) => {
2850
+ const set = /* @__PURE__ */ new Set();
2851
+ for (const id of comp) {
2852
+ for (const dep of byId.get(id).dependsOn) {
2853
+ if (!startingIds.has(dep)) continue;
2854
+ const j = sccOf.get(dep);
2855
+ if (j !== i) set.add(j);
2856
+ }
2857
+ }
2858
+ providerSccsOf.set(i, set);
2859
+ });
2860
+ const minSlug = sccSlugs.map((slugs) => slugs[0]);
2861
+ const visited = new Array(sccs.length).fill(false);
2862
+ const orderedSccIndices = [];
2863
+ function visit(i) {
2864
+ if (visited[i]) return;
2865
+ visited[i] = true;
2866
+ const providers = [...providerSccsOf.get(i)].sort((a, b) => minSlug[a].localeCompare(minSlug[b]));
2867
+ for (const p of providers) visit(p);
2868
+ orderedSccIndices.push(i);
2869
+ }
2870
+ const order = sccs.map((_, i) => i).sort((a, b) => minSlug[a].localeCompare(minSlug[b]));
2871
+ for (const i of order) visit(i);
2872
+ const start = [];
2873
+ for (const i of orderedSccIndices) start.push(...sccSlugs[i]);
2874
+ const waitFor = {};
2875
+ for (const m of starting) {
2876
+ const mySccId = sccOf.get(m.id);
2877
+ const providers = m.dependsOn.filter((d) => startingIds.has(d) && sccOf.get(d) !== mySccId).map((d) => byId.get(d).slug);
2878
+ waitFor[m.slug] = [...new Set(providers)].sort();
2879
+ }
2880
+ const skipped = [...reasons.entries()].map(([slug, reason]) => ({ slug, reason })).sort((a, b) => a.slug.localeCompare(b.slug));
2881
+ const missingProviders = {};
2882
+ for (const m of starting) {
2883
+ const missing = m.dependsOn.map((d) => byId.get(d)).filter((dep) => dep !== void 0 && reasons.has(dep.slug)).map((dep) => dep.slug);
2884
+ if (missing.length > 0) {
2885
+ missingProviders[m.slug] = [...new Set(missing)].sort();
2886
+ }
1900
2887
  }
2888
+ return { start, waitFor, cycles, skipped, missingProviders };
1901
2889
  }
1902
- async function writeSecurely(target, text) {
1903
- const base = basename2(target);
1904
- const tmpName = base.endsWith(".local") ? `${base.slice(0, -".local".length)}.qore-${randomBytes3(6).toString("hex")}.local` : `${base}.qore-${randomBytes3(6).toString("hex")}.tmp`;
1905
- const tmp = join10(dirname4(target), tmpName);
1906
- try {
1907
- await writeFile3(tmp, text, { mode: 384, flag: "wx" });
1908
- await chmod2(tmp, 384);
1909
- await rename3(tmp, target);
1910
- } finally {
1911
- await unlink(tmp).catch(() => {
2890
+
2891
+ // src/core/dev/supervisor.ts
2892
+ import { constants } from "os";
2893
+ var EARLY_MS = 1e4;
2894
+ var GRACE_MS2 = 1e4;
2895
+ function exitCode(e) {
2896
+ if (e.spawnError) return 1;
2897
+ if (e.code !== null) return e.code;
2898
+ const n = e.signal ? constants.signals[e.signal] : void 0;
2899
+ return n === void 0 ? 1 : 128 + n;
2900
+ }
2901
+ function supervise(o) {
2902
+ const now = o.now ?? Date.now;
2903
+ const delay = o.delay ?? ((ms) => new Promise((r) => {
2904
+ setTimeout(r, ms).unref();
2905
+ }));
2906
+ const abort = new AbortController();
2907
+ const emit = (e) => {
2908
+ try {
2909
+ o.onEvent(e);
2910
+ } catch {
2911
+ }
2912
+ };
2913
+ const running = [];
2914
+ const tails = /* @__PURE__ */ new Map();
2915
+ let unlaunched = o.modules.length;
2916
+ let stopCode = null;
2917
+ let firstFailure = null;
2918
+ let resolveDone;
2919
+ const done = new Promise((r) => {
2920
+ resolveDone = r;
2921
+ });
2922
+ const readiness = /* @__PURE__ */ new Map();
2923
+ for (const m of o.modules) {
2924
+ let resolve5;
2925
+ let settled = false;
2926
+ const promise = new Promise((r) => {
2927
+ resolve5 = r;
1912
2928
  });
2929
+ readiness.set(m.slug, { promise, settle: (r) => {
2930
+ if (settled) return false;
2931
+ settled = true;
2932
+ resolve5(r);
2933
+ return true;
2934
+ } });
1913
2935
  }
1914
- }
1915
- function registerEnv(program, ctx) {
1916
- const env = program.command("env").description("Local environment");
1917
- 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("--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) => {
1918
- const { deps: deps2, io: io2 } = ctx;
1919
- const g = ctx.opts();
1920
- const mc = await moduleContextFor(ctx, f, "env pull");
1921
- if (!mc.repoRoot) {
1922
- throw new QoreError("QORE_CONTEXT_REQUIRED", "env pull writes into a module repository", { hint: "cd into the module's repository." });
2936
+ const live = () => running.filter((c) => c.alive);
2937
+ const checkDone = () => {
2938
+ if (live().length > 0) return;
2939
+ if (stopCode !== null) resolveDone(stopCode);
2940
+ else if (unlaunched === 0) resolveDone(firstFailure ?? 0);
2941
+ };
2942
+ const beginStop = (reason, code) => {
2943
+ if (stopCode !== null) return;
2944
+ stopCode = code;
2945
+ abort.abort();
2946
+ for (const r of readiness.values()) r.settle("timeout");
2947
+ emit({ type: "stopping", reason });
2948
+ const targets = live().reverse();
2949
+ for (const c of targets) c.launched.stop("SIGTERM");
2950
+ if (targets.length > 0) {
2951
+ void delay(GRACE_MS2).then(() => {
2952
+ for (const c of live().reverse()) c.launched.stop("SIGKILL");
2953
+ });
2954
+ }
2955
+ checkDone();
2956
+ };
2957
+ const launch = (m) => {
2958
+ unlaunched--;
2959
+ const ready = readiness.get(m.slug);
2960
+ const startedAt = now();
2961
+ const launched = o.launcher.launch(m.cmd, m.args, {
2962
+ cwd: m.cwd,
2963
+ env: m.env,
2964
+ onLine: (l) => {
2965
+ const tail = tails.get(m.slug) ?? [];
2966
+ tail.push(l.line);
2967
+ if (tail.length > KEEP_LINES) tail.shift();
2968
+ tails.set(m.slug, tail);
2969
+ emit({ type: "line", module: m.slug, stream: l.stream, line: l.line });
2970
+ }
2971
+ });
2972
+ const child = { mod: m, launched, alive: true };
2973
+ running.push(child);
2974
+ emit({ type: "start", module: m.slug, pid: launched.pid });
2975
+ if (m.port === null) ready.settle("ready");
2976
+ else {
2977
+ const port = m.port;
2978
+ void o.ports.waitFor(port, o.waitMs, abort.signal).then((ok2) => {
2979
+ if (ready.settle(ok2 ? "ready" : "timeout") && ok2) emit({ type: "ready", module: m.slug, port });
2980
+ });
2981
+ }
2982
+ void launched.exited.then((e) => {
2983
+ child.alive = false;
2984
+ ready.settle("exited");
2985
+ const code = exitCode(e);
2986
+ const early = !!e.spawnError || now() - startedAt < EARLY_MS;
2987
+ const rawCode = e.spawnError ? null : e.code;
2988
+ const rawSignal = e.spawnError ? null : e.signal;
2989
+ const hint = early && rawSignal === null && rawCode !== null && rawCode !== 0 ? earlyExitHint(tails.get(m.slug) ?? [], { installCommand: m.installCmd }) ?? void 0 : void 0;
2990
+ emit({ type: "exit", module: m.slug, code: rawCode, signal: rawSignal, early, hint });
2991
+ if (code !== 0 && firstFailure === null) firstFailure = code;
2992
+ if (o.killOthers) beginStop("kill-others", code);
2993
+ checkDone();
2994
+ });
2995
+ };
2996
+ const start = async (m) => {
2997
+ const providers = m.waitFor.filter((p) => readiness.has(p));
2998
+ const results = await Promise.all(providers.map((p) => readiness.get(p).promise));
2999
+ if (stopCode !== null) return;
3000
+ providers.forEach((p, i) => {
3001
+ const why = results[i];
3002
+ if (why !== "ready") emit({ type: "provider-missing", module: m.slug, provider: p, why });
3003
+ });
3004
+ launch(m);
3005
+ };
3006
+ for (const m of o.modules) void start(m);
3007
+ checkDone();
3008
+ return {
3009
+ done,
3010
+ stop(reason) {
3011
+ if (reason === "interrupt") beginStop("interrupt", 130);
3012
+ else beginStop("signal", 128 + constants.signals[reason]);
3013
+ },
3014
+ escalate() {
3015
+ beginStop("interrupt", 130);
3016
+ for (const c of live().reverse()) c.launched.stop("SIGKILL");
3017
+ },
3018
+ children: () => live().map((c) => ({ module: c.mod.slug, pid: c.launched.pid, port: c.mod.port }))
3019
+ };
3020
+ }
3021
+
3022
+ // src/core/workspace/access.ts
3023
+ function devTeamsOf(am, m) {
3024
+ const own = m.devTeamIds?.length ? am.devTeams.filter((t) => m.devTeamIds.includes(t.id)) : [];
3025
+ return own.length ? own : am.devTeams;
3026
+ }
3027
+ function askMaintainer(am, m) {
3028
+ const teams = devTeamsOf(am, m);
3029
+ const names = teams.map((t) => t.name);
3030
+ const maintainers = [...new Set(teams.flatMap((t) => t.maintainers))];
3031
+ const who = names.length ? names.join(", ") : "the app's dev team";
3032
+ return { teams: names, maintainers, text: `ask a maintainer of ${who}${maintainers.length ? ` (${maintainers.join(", ")})` : ""}` };
3033
+ }
3034
+ function notClonedReason(m, am) {
3035
+ if (am && !m.githubLinked) return `GitHub account not linked (${am.accountUrl})`;
3036
+ if (am && m.repoAccess === "none") {
3037
+ const names = devTeamsOf(am, m).map((t) => t.name);
3038
+ return names.length ? `no repository access (dev team ${names.join(", ")})` : "no repository access (ask your tenant owner)";
3039
+ }
3040
+ return "run qore sync";
3041
+ }
3042
+
3043
+ // src/cli/devOutput.ts
3044
+ import { createColors as createColors2 } from "picocolors";
3045
+ function renderProblems(io2, problems, color) {
3046
+ problems.forEach((p, i) => {
3047
+ if (i > 0) io2.err("");
3048
+ renderError(io2, p, false, color);
3049
+ });
3050
+ }
3051
+ function combineProblems(problems) {
3052
+ if (problems.length === 0) throw new Error("combineProblems needs at least one problem");
3053
+ if (problems.length === 1) return problems[0];
3054
+ const first = problems[0];
3055
+ return new QoreError(first.code, `${problems.length} problems before starting`, {
3056
+ details: { problems: problems.map((p) => toJsonError(p)) }
3057
+ });
3058
+ }
3059
+ function prefixer(slugs, color) {
3060
+ const c = createColors2(color);
3061
+ const palette = [c.cyan, c.magenta, c.yellow, c.green, c.blue, c.red];
3062
+ const sorted = [...new Set(slugs)].sort();
3063
+ const width = Math.max(0, ...sorted.map((s) => s.length));
3064
+ return (slug, line2) => {
3065
+ const i = sorted.indexOf(slug);
3066
+ const paint = i === -1 ? (s) => s : palette[i % palette.length];
3067
+ return `${paint(slug.padEnd(width))} \u2502 ${line2}`;
3068
+ };
3069
+ }
3070
+
3071
+ // src/cli/commands/run.ts
3072
+ import { constants as constants2 } from "os";
3073
+ var childRunning = { current: false };
3074
+ function registerRun(program, ctx) {
3075
+ program.command("run").description("Run a command with this module's local secrets in its environment").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("--yes", "trust this repository without asking").argument("<command>", "the command to run (after --)").argument("[args...]", "its arguments, passed verbatim").action(async (cmd, args, f) => {
3076
+ const { deps: deps2 } = ctx;
3077
+ if (ctx.opts().json) {
3078
+ throw new QoreError("QORE_CONTEXT_INVALID", "run has no --json output", {
3079
+ hint: "Drop --json; the child's own output passes through."
3080
+ });
3081
+ }
3082
+ const mc = await moduleContextFor(ctx, f, "run");
3083
+ const app = await appFor(ctx, mc, f.app);
3084
+ await ensureTrusted(ctx, mc, app, { yes: f.yes });
3085
+ const local = await fetchLocalEnv(deps2, mc, app);
3086
+ childRunning.current = true;
3087
+ let exit;
3088
+ try {
3089
+ exit = await deps2.spawn.run(cmd, args, { ...deps2.env, ...local.env });
3090
+ } finally {
3091
+ childRunning.current = false;
3092
+ }
3093
+ if (exit.signal) {
3094
+ deps2.raise(exit.signal);
3095
+ ctx.setExitCode(128 + (constants2.signals[exit.signal] ?? 0));
3096
+ return;
3097
+ }
3098
+ ctx.setExitCode(exit.code ?? 1);
3099
+ });
3100
+ }
3101
+
3102
+ // src/cli/commands/devWorkspace.ts
3103
+ var INSTALL_PARALLELISM = 4;
3104
+ var str = (v) => typeof v === "string" && v.length > 0;
3105
+ var slugList = (raw) => raw?.split(",").map((s) => s.trim()).filter(Boolean);
3106
+ function usableLast(last, appId) {
3107
+ if (!last || last.kind !== "workspace") return false;
3108
+ const a = last.app;
3109
+ 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));
3110
+ }
3111
+ async function workspaceContext(ctx, ws) {
3112
+ const { deps: deps2 } = ctx;
3113
+ try {
3114
+ const portal = await flagPortal(deps2, ctx.opts().portal, ws);
3115
+ const session = await openSession(deps2, portal, "dev", requestLog(ctx));
3116
+ await ensureCapability(deps2, portal.apiOrigin, "app.modules");
3117
+ await ensureCapability(deps2, portal.apiOrigin, "module.local-env");
3118
+ return { am: await workspaceAppModules(session, portal, ws), online: { portal, session }, offlineCause: null };
3119
+ } catch (e) {
3120
+ if (!isOffline(e)) throw e;
3121
+ const last = await loadLastStart(deps2, ws.root);
3122
+ if (!usableLast(last, ws.appId)) throw e;
3123
+ const modules = last.modules.map((m) => ({
3124
+ id: m.id,
3125
+ slug: m.slug,
3126
+ name: m.name,
3127
+ type: m.type,
3128
+ access: m.access,
3129
+ repoUrl: m.repoUrl,
3130
+ repoAccess: "none",
3131
+ githubLinked: false,
3132
+ dependsOn: m.dependsOn
3133
+ }));
3134
+ if (!ctx.opts().json) ctx.io.err(`\u2192 ${last.app.tenant.name} / ${last.app.name} (offline \u2014 last started ${last.savedAt})`);
3135
+ return { am: { app: last.app, accountUrl: "", devTeams: [], modules }, online: null, offlineCause: e };
3136
+ }
3137
+ }
3138
+ function deadAliases(env, ports) {
3139
+ return Object.keys(env).filter((k) => {
3140
+ if (!/^CONN_(.+)_URL$/.test(k)) return false;
3141
+ let port;
3142
+ try {
3143
+ port = Number(new URL(env[k].trim().replace(/^["']|["']$/g, "")).port);
3144
+ } catch {
3145
+ return false;
3146
+ }
3147
+ return port > 0 && !ports.has(port);
3148
+ }).sort();
3149
+ }
3150
+ async function pool(items, n, fn) {
3151
+ let next = 0;
3152
+ const results = await Promise.allSettled(Array.from({ length: Math.min(n, items.length) }, async () => {
3153
+ while (next < items.length) await fn(items[next++]);
3154
+ }));
3155
+ const failed = results.find((r) => r.status === "rejected");
3156
+ if (failed) throw failed.reason;
3157
+ }
3158
+ async function workspaceDev(ctx, ws, f, waitTimeout) {
3159
+ const ready = await inPreflight(ctx.deps.signals, ctx.signal, (p) => prepareWorkspace(ctx, ws, f, waitTimeout, p));
3160
+ await superviseAll(ctx, ws, ready.units, ready.waitFor, ready.o);
3161
+ }
3162
+ async function prepareWorkspace(ctx, ws, f, waitTimeout, p) {
3163
+ const { deps: deps2, io: io2 } = ctx;
3164
+ const g = ctx.opts();
3165
+ const json2 = Boolean(g.json);
3166
+ if (f.app !== void 0) {
3167
+ throw new QoreError("QORE_CONTEXT_INVALID", "--app is for a module repository", {
3168
+ hint: "Drop --app; a workspace root starts the modules of its own app."
3169
+ });
3170
+ }
3171
+ const note = (human, event) => json2 ? io2.out(JSON.stringify(event)) : io2.err(human);
3172
+ const warn = (message, extra = {}) => note(`! ${message}`, { type: "warning", ...extra, message });
3173
+ const { am, online, offlineCause } = await workspaceContext(ctx, ws);
3174
+ if (online && !json2) io2.err(`\u2192 ${am.app.tenant.name} / ${am.app.name} (qore.workspace.json)`);
3175
+ if (!online) warn("The portal is unreachable \u2014 starting every module from its env file as last written; values may be stale.");
3176
+ const color = !json2 && g.color !== false && !deps2.env.NO_COLOR;
3177
+ const problems = [];
3178
+ const attempt = async (slug, fn) => {
3179
+ try {
3180
+ await fn();
3181
+ return true;
3182
+ } catch (e) {
3183
+ if (!(e instanceof QoreError)) throw e;
3184
+ problems.push(withDetails(e, { module: slug }));
3185
+ return false;
3186
+ }
3187
+ };
3188
+ const fail = async (last) => {
3189
+ const all = last ? [...problems, last] : problems;
3190
+ const enriched = await Promise.all(all.map((p2) => withProxyHint(deps2, p2)));
3191
+ if (!json2 && enriched.length > 1) {
3192
+ renderProblems(io2, enriched, color);
3193
+ io2.err("");
3194
+ }
3195
+ throw combineProblems(enriched);
3196
+ };
3197
+ const dirs = {};
3198
+ const safe = [];
3199
+ for (const m of am.modules) {
3200
+ if (!isSafeSlug(m.slug)) {
3201
+ warn(`${JSON.stringify(m.slug)} is not a safe directory name \u2014 skipped`, { module: m.slug });
3202
+ continue;
3203
+ }
3204
+ try {
3205
+ dirs[m.slug] = await deps2.git.inspect(join16(ws.root, m.slug));
3206
+ safe.push(m);
3207
+ } catch (e) {
3208
+ if (!(e instanceof QoreError) || e.code === "QORE_GIT_UNAVAILABLE") throw e;
3209
+ problems.push(withDetails(e, { module: m.slug }));
3210
+ }
3211
+ }
3212
+ const present = new Map(planWorkspace(ws.root, safe, dirs, []).actions.filter((a) => a.kind === "present").map((a) => [a.module.slug, a.dir]));
3213
+ const bySlug = new Map(safe.map((m) => [m.slug, m]));
3214
+ const plan = planDev(safe.map((m) => ({
3215
+ id: m.id,
3216
+ slug: m.slug,
3217
+ present: present.has(m.slug),
3218
+ canWrite: m.access === "write" || m.access === "maintain",
3219
+ dependsOn: m.dependsOn
3220
+ })), { only: slugList(f.only), skip: slugList(f.skip) });
3221
+ for (const s of plan.skipped) {
3222
+ const m = bySlug.get(s.slug);
3223
+ if (s.reason === "no-write") {
3224
+ const ask = askMaintainer(am, m);
3225
+ note(
3226
+ `- ${s.slug} no write access \u2014 local secrets need write; ${ask.text}`,
3227
+ { type: "skip", module: s.slug, reason: s.reason, devTeams: ask.teams, maintainers: ask.maintainers }
3228
+ );
3229
+ } else {
3230
+ note(`- ${s.slug} ${s.reason === "not-cloned" ? `not cloned \u2014 ${notClonedReason(m, online ? am : void 0)}` : "excluded"}`, { type: "skip", module: s.slug, reason: s.reason });
3231
+ }
3232
+ }
3233
+ for (const c of plan.cycles) warn(`${c.join(", ")} depend on each other \u2014 they start together, without waiting`, { modules: c });
3234
+ if (plan.start.length === 0) {
3235
+ await fail(new QoreError("QORE_CONTEXT_REQUIRED", "No module of this workspace can be started", {
3236
+ hint: "Clone the modules with qore sync, or change --only / --skip.",
3237
+ details: { skipped: plan.skipped }
3238
+ }));
3239
+ }
3240
+ const units = plan.start.map((slug) => ({ m: bySlug.get(slug), dir: present.get(slug) }));
3241
+ if (online) {
3242
+ const untrusted = [];
3243
+ for (const u of units) if (!await isTrusted(deps2, u.dir, u.m.id)) untrusted.push(u);
3244
+ if (untrusted.length > 0) {
3245
+ const names = untrusted.map((u) => u.m.slug).join(", ");
3246
+ if (!f.yes) {
3247
+ if (!io2.interactive) {
3248
+ await fail(new QoreError("QORE_CONTEXT_REQUIRED", `These repositories are not trusted yet: ${names}`, {
3249
+ hint: "Run interactively once, or pass --yes.",
3250
+ details: { reason: "untrusted", modules: untrusted.map((u) => u.m.slug) }
3251
+ }));
3252
+ }
3253
+ if (!await io2.confirm(`Pull local secrets for ${names} into their repositories?`)) throw new CancelledError();
3254
+ }
3255
+ for (const u of untrusted) await trust(deps2, u.dir, u.m.id);
3256
+ }
3257
+ }
3258
+ p.arm();
3259
+ const prefix = prefixer(plan.start, color);
3260
+ const onLine = (slug) => (l) => {
3261
+ if (json2) io2.out(JSON.stringify({ type: "line", module: slug, stream: l.stream, line: l.line }));
3262
+ else if (l.stream === "stdout") io2.out(prefix(slug, l.line));
3263
+ else io2.err(prefix(slug, l.line));
3264
+ };
3265
+ const report = () => {
3266
+ };
3267
+ for (const u of units) {
3268
+ await attempt(u.m.slug, async () => {
3269
+ u.cfg = await readRepoConfig(u.dir);
3270
+ u.pkg = await readPackageJson(u.dir);
3271
+ u.recipe = planRecipe({
3272
+ root: u.dir,
3273
+ pkg: u.pkg,
3274
+ pm: await detectPackageManager(u.dir, u.pkg),
3275
+ cfg: u.cfg,
3276
+ slug: u.m.slug,
3277
+ flags: { install: f.install, services: f.services }
3278
+ });
3279
+ });
3280
+ }
3281
+ for (const u of units) {
3282
+ u.trusted = !offlineCause || await isTrusted(deps2, u.dir, u.m.id);
3283
+ if (!u.trusted) problems.push(withDetails(offlineCause, { module: u.m.slug }));
3284
+ }
3285
+ const withRecipe = units.filter((u) => u.recipe && u.trusted);
3286
+ for (const name of [...new Set(withRecipe.map((u) => u.recipe.pm.name))]) {
3287
+ const users = withRecipe.filter((u) => u.recipe.pm.name === name);
3288
+ const pmOk = await attempt(users[0].m.slug, () => checkPackageManager(deps2, users[0].recipe, () => {
3289
+ }, p.signal));
3290
+ for (const u of users) u.pmOk = pmOk;
3291
+ }
3292
+ for (const u of withRecipe) {
3293
+ const nodeOk = await attempt(u.m.slug, () => checkNode(deps2, u.recipe, u.pkg, u.m.slug, p.signal));
3294
+ u.pmOk = u.pmOk && nodeOk;
3295
+ }
3296
+ await pool(units.filter((u) => u.pmOk && u.trusted), INSTALL_PARALLELISM, async (u) => {
3297
+ u.installed = await attempt(u.m.slug, () => installIfNeeded(deps2, u.recipe, u.m.slug, onLine(u.m.slug), report, p.signal));
3298
+ });
3299
+ const runs = [await leftoverRun(deps2, ws.root)].filter((r) => r !== null);
3300
+ const appRef = { id: am.app.id, slug: am.app.slug, name: am.app.name };
3301
+ const mcFor = (u) => ({
3302
+ portal: online.portal,
3303
+ session: online.session,
3304
+ source: { kind: "workspace" },
3305
+ repoRoot: u.dir,
3306
+ workspace: ws,
3307
+ module: { id: u.m.id, slug: u.m.slug, name: u.m.name, type: u.m.type, access: u.m.access, tenant: am.app.tenant, apps: [appRef] }
3308
+ });
3309
+ const rootReal = await realpath4(ws.root);
3310
+ const withDocker = dockerRecovery(ctx, p.signal);
3311
+ for (const u of units.filter((x) => x.recipe && x.trusted)) {
3312
+ const slug = u.m.slug;
3313
+ await attempt(slug, async () => {
3314
+ try {
3315
+ u.dev = await devEnv(deps2, {
3316
+ mc: online ? mcFor(u) : null,
3317
+ app: online ? appRef : null,
3318
+ root: u.dir,
3319
+ type: u.m.type,
3320
+ moduleId: u.m.id,
3321
+ cfg: u.cfg,
3322
+ random: () => randomBytes4(32).toString("hex")
3323
+ });
3324
+ } catch (e) {
3325
+ if (offlineCause && isOffline(e)) throw offlineCause;
3326
+ throw e;
3327
+ }
3328
+ });
3329
+ const dev = u.dev;
3330
+ if (!dev) continue;
3331
+ if (online && dev.offline) {
3332
+ warn(`${slug}: the portal is unreachable \u2014 starting from ${relative5(rootReal, dev.envFile)} as last written; values may be stale.`, { module: slug });
3333
+ }
3334
+ if (dev.kept.length) {
3335
+ warn(
3336
+ `${slug}: ${dev.kept.length} key(s) differ locally and were kept: ${dev.kept.join(", ")} \u2014 qore env pull --use-portal takes the portal's values`,
3337
+ { module: slug, keys: dev.kept }
3338
+ );
3339
+ }
3340
+ if (u.installed) {
3341
+ const env = { ...deps2.env, ...dev.env };
3342
+ await attempt(slug, () => withDocker({ cwd: u.dir, env }, () => startServices(deps2, u.recipe, slug, env, dev.dbPort, waitTimeout * 1e3, p.signal, onLine(slug), report)));
3343
+ }
3344
+ await attempt(slug, () => checkPortOrStopLeftover(ctx, { slug, port: dev.port, key: "PORT", runs }, p.signal));
3345
+ }
3346
+ if (p.signal.aborted) throw new CancelledError();
3347
+ const byPort = /* @__PURE__ */ new Map();
3348
+ for (const u of units) if (u.dev?.port != null) byPort.set(u.dev.port, [...byPort.get(u.dev.port) ?? [], u.m.slug]);
3349
+ for (const [port, slugs] of byPort) {
3350
+ if (slugs.length < 2) continue;
3351
+ problems.push(new QoreError("QORE_PORT_IN_USE", `${slugs.slice(0, -1).join(", ")} and ${slugs.at(-1)} both have port ${port}`, {
3352
+ hint: "Ports are unique per organisation; a module from another organisation can share one. Run one of them (--only / --skip).",
3353
+ details: { port, module: slugs[0], modules: slugs }
3354
+ }));
3355
+ }
3356
+ if (problems.length > 0) await fail();
3357
+ const listening = new Set(units.map((u) => u.dev.port).filter((p2) => p2 !== null));
3358
+ for (const [consumer, providers] of Object.entries(plan.missingProviders)) {
3359
+ const env = units.find((u) => u.m.slug === consumer).dev.env;
3360
+ const aliases = providers.length === 1 ? deadAliases(env, listening) : [];
3361
+ for (const provider of providers) {
3362
+ const what = aliases.length ? `${aliases.join(", ")} ${aliases.length === 1 ? "points" : "point"}` : "its CONN_*_URL points";
3363
+ warn(
3364
+ `${consumer}: ${provider} will not run \u2014 ${what} at a port nothing listens on. Qore does not fall back to staging.`,
3365
+ { module: consumer, provider, aliases }
3366
+ );
3367
+ }
3368
+ }
3369
+ if (online && units.every((u) => !u.dev.offline)) {
3370
+ const envFile = (m) => {
3371
+ const u = units.find((x) => x.m.id === m.id);
3372
+ return u ? relative5(rootReal, u.dev.envFile) : join16(m.slug, moduleDefaults(m.type).envFile);
3373
+ };
3374
+ await saveLastStart(deps2, ws.root, {
3375
+ schemaVersion: 1,
3376
+ kind: "workspace",
3377
+ portal: online.portal.apiOrigin,
3378
+ savedAt: deps2.now().toISOString(),
3379
+ app: { ...appRef, tenant: { id: am.app.tenant.id, slug: am.app.tenant.slug, name: am.app.tenant.name } },
3380
+ modules: safe.map((m) => ({
3381
+ id: m.id,
3382
+ slug: m.slug,
3383
+ name: m.name,
3384
+ type: m.type,
3385
+ access: m.access,
3386
+ repoUrl: m.repoUrl,
3387
+ dependsOn: m.dependsOn,
3388
+ envFile: envFile(m)
3389
+ }))
3390
+ });
3391
+ }
3392
+ if (p.signal.aborted) throw new CancelledError();
3393
+ return { units, waitFor: plan.waitFor, o: { waitTimeout, killOthers: Boolean(f.killOthers), json: json2, color, prefix } };
3394
+ }
3395
+ async function superviseAll(ctx, ws, units, waitFor, o) {
3396
+ const { deps: deps2, io: io2 } = ctx;
3397
+ const startedAt = deps2.now().toISOString();
3398
+ const began = /* @__PURE__ */ new Map();
3399
+ const ref = {};
3400
+ let stopping = false;
3401
+ let runWrite = Promise.resolve();
3402
+ const writeRun = () => {
3403
+ runWrite = runWrite.then(() => writeRunFile(deps2, ws.root, {
3404
+ schemaVersion: 1,
3405
+ qorePid: deps2.pid,
3406
+ root: ws.root,
3407
+ startedAt,
3408
+ children: ref.sup?.children() ?? []
3409
+ })).catch(() => {
3410
+ });
3411
+ };
3412
+ const render = (e) => {
3413
+ switch (e.type) {
3414
+ case "line":
3415
+ return e.stream === "stdout" ? io2.out(o.prefix(e.module, e.line)) : io2.err(o.prefix(e.module, e.line));
3416
+ case "start":
3417
+ return io2.err(`\u25B6 ${e.module}${e.pid === null ? "" : ` (pid ${e.pid})`}`);
3418
+ case "ready":
3419
+ return io2.err(`\u2713 ${e.module} is listening on ${e.port}`);
3420
+ case "provider-missing":
3421
+ return io2.err(`! ${e.provider} did not come up within ${o.waitTimeout} s (or exited); starting ${e.module} anyway \u2014 see its output above`);
3422
+ case "stopping":
3423
+ return io2.err("\u25A0 stopping");
3424
+ case "exit": {
3425
+ if (stopping) return io2.err(`\u25A0 ${e.module} stopped`);
3426
+ if (e.code === null && e.signal === null) return io2.err(`\u2717 ${e.module} could not be started`);
3427
+ const after = e.early ? ` after ${Math.round((deps2.now().getTime() - (began.get(e.module) ?? deps2.now().getTime())) / 1e3)} s` : "";
3428
+ const how = e.signal ? `was ended by ${e.signal}` : `exited with code ${e.code}`;
3429
+ io2.err(`${e.code === 0 ? "\u25A0" : "\u2717"} ${e.module} ${how}${after}`);
3430
+ if (e.hint) io2.err(` \u2192 ${e.hint.hint}`);
3431
+ return;
3432
+ }
3433
+ }
3434
+ };
3435
+ const onEvent = (e) => {
3436
+ if (e.type === "start") {
3437
+ began.set(e.module, deps2.now().getTime());
3438
+ writeRun();
3439
+ }
3440
+ if (e.type === "stopping") stopping = true;
3441
+ if (o.json) io2.out(JSON.stringify(e));
3442
+ else render(e);
3443
+ };
3444
+ const s = ref.sup = supervise({
3445
+ modules: units.map((u) => {
3446
+ const { cmd, args } = runArgs(u.recipe.pm, u.recipe.script);
3447
+ const { cmd: installCmd, args: installArgv } = installArgs(u.recipe.pm);
3448
+ return {
3449
+ slug: u.m.slug,
3450
+ cmd,
3451
+ args,
3452
+ cwd: u.dir,
3453
+ port: u.dev.port,
3454
+ waitFor: waitFor[u.m.slug] ?? [],
3455
+ installCmd: [installCmd, ...installArgv].join(" "),
3456
+ env: { ...deps2.env, ...u.dev.env, ...o.color ? { FORCE_COLOR: "1" } : {} }
3457
+ };
3458
+ }),
3459
+ launcher: deps2.launch,
3460
+ ports: deps2.ports,
3461
+ waitMs: o.waitTimeout * 1e3,
3462
+ killOthers: o.killOthers,
3463
+ onEvent
3464
+ });
3465
+ const onTerm = () => s.stop("SIGTERM");
3466
+ const onHup = () => s.stop("SIGHUP");
3467
+ const onAbort = () => s.stop("interrupt");
3468
+ deps2.signals.on("SIGTERM", onTerm);
3469
+ deps2.signals.on("SIGHUP", onHup);
3470
+ ctx.signal.addEventListener("abort", onAbort);
3471
+ childRunning.current = true;
3472
+ childRunning.onInterrupt = () => s.escalate();
3473
+ try {
3474
+ if (ctx.signal.aborted) onAbort();
3475
+ writeRun();
3476
+ ctx.setExitCode(await s.done);
3477
+ } finally {
3478
+ deps2.signals.off("SIGTERM", onTerm);
3479
+ deps2.signals.off("SIGHUP", onHup);
3480
+ ctx.signal.removeEventListener("abort", onAbort);
3481
+ childRunning.current = false;
3482
+ childRunning.onInterrupt = void 0;
3483
+ await runWrite;
3484
+ await removeRunFile(deps2, ws.root);
3485
+ }
3486
+ }
3487
+
3488
+ // src/cli/commands/dev.ts
3489
+ var EARLY_EXIT_MS = 1e4;
3490
+ function makeStderrTee(sink) {
3491
+ const decoder = new StringDecoder("utf8");
3492
+ let buf = "";
3493
+ let forwarding = true;
3494
+ const tail = [];
3495
+ const record = (line2) => {
3496
+ tail.push(line2);
3497
+ if (tail.length > KEEP_LINES) tail.shift();
3498
+ };
3499
+ const onError = () => {
3500
+ forwarding = false;
3501
+ };
3502
+ sink.on("error", onError);
3503
+ return {
3504
+ push: (chunk) => {
3505
+ if (forwarding) {
3506
+ try {
3507
+ sink.write(chunk);
3508
+ } catch {
3509
+ forwarding = false;
3510
+ }
3511
+ }
3512
+ buf += decoder.write(chunk);
3513
+ let i;
3514
+ while ((i = buf.indexOf("\n")) >= 0) {
3515
+ record(buf.slice(0, i).replace(/\r$/, ""));
3516
+ buf = buf.slice(i + 1);
3517
+ }
3518
+ },
3519
+ lines: () => {
3520
+ const rest = (buf + decoder.end()).replace(/\r$/, "");
3521
+ return rest ? [...tail, rest].slice(-KEEP_LINES) : tail.slice();
3522
+ },
3523
+ dispose: () => {
3524
+ sink.removeListener("error", onError);
3525
+ }
3526
+ };
3527
+ }
3528
+ function registerDev(program, ctx) {
3529
+ program.command("dev").description("Start this module's dev server \u2014 or, in a workspace root, every module of 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("--only <slugs>", "workspace: start only these modules (comma-separated)").option("--skip <slugs>", "workspace: start all but these modules (comma-separated)").option("--no-install", "do not run the package manager's install").option("--no-services", "do not start local services (db:up)").option("--kill-others", "workspace: the first module to exit stops the rest").option("--wait-timeout <s>", "workspace: seconds to wait for a provider's port", "60").option("--yes", "trust the repositories without asking").action(async (f) => {
3530
+ const waitTimeout = parseWaitTimeout(f.waitTimeout);
3531
+ const ws = await workspaceRoot(ctx.deps);
3532
+ if (ws && f.module === void 0) return workspaceDev(ctx, ws, f, waitTimeout);
3533
+ return moduleDev(ctx, f, waitTimeout);
3534
+ });
3535
+ }
3536
+ function parseWaitTimeout(raw) {
3537
+ const n = /^\d+$/.test(raw.trim()) ? Number(raw.trim()) : NaN;
3538
+ if (!Number.isInteger(n) || n < 1 || n > 600) {
3539
+ throw new QoreError("QORE_CONTEXT_INVALID", `--wait-timeout must be a whole number of seconds from 1 to 600, not "${raw}"`, {
3540
+ hint: "Pass e.g. --wait-timeout 60."
3541
+ });
3542
+ }
3543
+ return n;
3544
+ }
3545
+ var str2 = (v) => typeof v === "string" && v.length > 0;
3546
+ function usableLast2(last) {
3547
+ if (!last || last.kind !== "module") return false;
3548
+ const m = last.module;
3549
+ 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);
3550
+ }
3551
+ async function devContext(ctx, f, root) {
3552
+ let mc;
3553
+ try {
3554
+ mc = await moduleContextFor(ctx, f, "dev");
3555
+ } catch (e) {
3556
+ if (!isOffline(e) || f.module) throw e;
3557
+ const last = await loadLastStart(ctx.deps, root);
3558
+ if (!usableLast2(last)) throw e;
3559
+ if (f.app && f.app !== last.app.id && f.app !== last.app.slug) throw e;
3560
+ if (!await isTrusted(ctx.deps, root, last.module.id)) throw e;
3561
+ ctx.io.err(`\u2192 ${last.module.tenant.name} / ${last.app.name} / ${last.module.slug} (offline \u2014 last started ${last.savedAt})`);
3562
+ return { online: null, module: last.module, offlineCause: e };
3563
+ }
3564
+ const app = await appFor(ctx, mc, f.app);
3565
+ await ensureTrusted(ctx, mc, app, { yes: f.yes });
3566
+ return { online: { mc, app }, module: mc.module, offlineCause: null };
3567
+ }
3568
+ async function moduleDev(ctx, f, waitTimeout) {
3569
+ const { deps: deps2, io: io2 } = ctx;
3570
+ const { root, slug, recipe, env, port } = await inPreflight(deps2.signals, ctx.signal, (p) => prepareModule(ctx, f, waitTimeout, p));
3571
+ const { cmd, args } = runArgs(recipe.pm, recipe.script);
3572
+ const startedAt = deps2.now();
3573
+ await writeRunFile(deps2, root, {
3574
+ schemaVersion: 1,
3575
+ qorePid: deps2.pid,
3576
+ root,
3577
+ startedAt: startedAt.toISOString(),
3578
+ children: [{ module: slug, pid: null, port }]
3579
+ });
3580
+ childRunning.current = true;
3581
+ const tee = makeStderrTee(io2.stderrRaw);
3582
+ const spawnEnv = ctx.opts().color !== false && io2.stderrTTY && env.FORCE_COLOR === void 0 && env.NO_COLOR === void 0 ? { ...env, FORCE_COLOR: "1" } : env;
3583
+ let exit;
3584
+ try {
3585
+ exit = await deps2.spawn.run(cmd, args, spawnEnv, { cwd: root, onStderr: tee.push });
3586
+ } finally {
3587
+ tee.dispose();
3588
+ childRunning.current = false;
3589
+ await removeRunFile(deps2, root);
3590
+ }
3591
+ const ms = deps2.now().getTime() - startedAt.getTime();
3592
+ if (exit.code !== 0 && exit.signal === null && ms < EARLY_EXIT_MS) {
3593
+ io2.err(`\u2717 ${slug} exited after ${Math.round(ms / 1e3)} s with code ${exit.code ?? 1} \u2014 its output above says why.`);
3594
+ const { cmd: installCmd, args: installArgv } = installArgs(recipe.pm);
3595
+ const hint = earlyExitHint(tee.lines(), { installCommand: [installCmd, ...installArgv].join(" ") });
3596
+ if (hint) io2.err(` \u2192 ${hint.hint}`);
3597
+ }
3598
+ if (exit.signal) {
3599
+ deps2.raise(exit.signal);
3600
+ ctx.setExitCode(128 + (constants3.signals[exit.signal] ?? 0));
3601
+ return;
3602
+ }
3603
+ ctx.setExitCode(exit.code ?? 1);
3604
+ }
3605
+ async function prepareModule(ctx, f, waitTimeout, p) {
3606
+ const { deps: deps2, io: io2 } = ctx;
3607
+ if (ctx.opts().json) {
3608
+ throw new QoreError("QORE_CONTEXT_INVALID", "dev in a module repository has no --json; the dev server's output passes through", {
3609
+ hint: "Drop --json."
3610
+ });
3611
+ }
3612
+ for (const [flag, set] of [["--only", f.only !== void 0], ["--skip", f.skip !== void 0], ["--kill-others", Boolean(f.killOthers)]]) {
3613
+ if (set) {
3614
+ throw new QoreError("QORE_CONTEXT_INVALID", `${flag} is for a workspace root`, {
3615
+ hint: `Drop ${flag}, or run qore dev in the workspace root (where qore.workspace.json is).`
3616
+ });
3617
+ }
3618
+ }
3619
+ const root = await findRepoRoot(deps2.cwd);
3620
+ if (!root) {
3621
+ throw new QoreError("QORE_CONTEXT_REQUIRED", "Not inside a module repository", {
3622
+ hint: "cd into a module repository, or run qore dev in a workspace root."
3623
+ });
3624
+ }
3625
+ const c = await devContext(ctx, f, root);
3626
+ p.arm();
3627
+ const slug = c.module.slug;
3628
+ const cfg = await readRepoConfig(root);
3629
+ const pkg = await readPackageJson(root);
3630
+ const recipe = planRecipe({
3631
+ root,
3632
+ pkg,
3633
+ pm: await detectPackageManager(root, pkg),
3634
+ cfg,
3635
+ slug,
3636
+ flags: { install: f.install, services: f.services }
3637
+ });
3638
+ const runs = [await leftoverRun(deps2, root)].filter((r) => r !== null);
3639
+ const onLine = (l) => l.stream === "stdout" ? io2.out(l.line) : io2.err(l.line);
3640
+ const report = () => {
3641
+ };
3642
+ await checkPackageManager(deps2, recipe, () => {
3643
+ }, p.signal);
3644
+ await checkNode(deps2, recipe, pkg, slug, p.signal);
3645
+ await installIfNeeded(deps2, recipe, slug, onLine, report, p.signal);
3646
+ let dev;
3647
+ try {
3648
+ dev = await devEnv(deps2, {
3649
+ mc: c.online?.mc ?? null,
3650
+ app: c.online?.app ?? null,
3651
+ root,
3652
+ type: c.module.type,
3653
+ moduleId: c.module.id,
3654
+ cfg,
3655
+ random: () => randomBytes5(32).toString("hex")
3656
+ });
3657
+ } catch (e) {
3658
+ if (c.offlineCause && isOffline(e)) throw c.offlineCause;
3659
+ throw e;
3660
+ }
3661
+ const envFile = relative6(await realpath5(root), dev.envFile);
3662
+ if (dev.offline) io2.err(`! The portal is unreachable \u2014 starting from ${envFile} as last written; values may be stale.`);
3663
+ if (dev.kept.length) {
3664
+ io2.err(`! ${dev.kept.length} key(s) differ locally and were kept: ${dev.kept.join(", ")} \u2014 qore env pull --use-portal takes the portal's values`);
3665
+ }
3666
+ const env = { ...deps2.env, ...dev.env };
3667
+ await dockerRecovery(ctx, p.signal)({ cwd: root, env }, () => startServices(deps2, recipe, slug, env, dev.dbPort, waitTimeout * 1e3, p.signal, onLine, report));
3668
+ await checkPortOrStopLeftover(ctx, { slug, port: dev.port, key: "PORT", runs }, p.signal);
3669
+ if (c.online && !dev.offline) {
3670
+ const { mc, app } = c.online;
3671
+ const { id, name, type, access: access2, tenant, apps } = mc.module;
3672
+ await saveLastStart(deps2, root, {
3673
+ schemaVersion: 1,
3674
+ kind: "module",
3675
+ portal: mc.portal.apiOrigin,
3676
+ savedAt: deps2.now().toISOString(),
3677
+ module: {
3678
+ id,
3679
+ slug,
3680
+ name,
3681
+ type,
3682
+ access: access2,
3683
+ tenant: { id: tenant.id, slug: tenant.slug, name: tenant.name },
3684
+ apps: apps.map((a) => ({ id: a.id, slug: a.slug, name: a.name }))
3685
+ },
3686
+ app: { id: app.id, slug: app.slug, name: app.name },
3687
+ envFile
3688
+ });
3689
+ }
3690
+ if (p.signal.aborted) throw new CancelledError();
3691
+ return { root, slug, recipe, env, port: dev.port };
3692
+ }
3693
+
3694
+ // src/cli/commands/doctor.ts
3695
+ import pc3 from "picocolors";
3696
+
3697
+ // src/core/doctor/checks.ts
3698
+ import { realpath as realpath6 } from "fs/promises";
3699
+ import { basename as basename3, join as join17 } from "path";
3700
+
3701
+ // src/core/diagnostics/ssh.ts
3702
+ var SSH_TEST_ARGS = ["-T", "-o", "BatchMode=yes", "-o", "ConnectTimeout=5", "git@github.com"];
3703
+ var COMMAND = "ssh -T git@github.com";
3704
+ function classifySshTest(r) {
3705
+ const text = r.tail.join("\n");
3706
+ const hi = /Hi ([^!\s]+)! You've successfully authenticated/.exec(text);
3707
+ if (hi) return { ok: true, user: hi[1] };
3708
+ const details = { protocol: "ssh" };
3709
+ if (r.spawnError) {
3710
+ return { ok: false, error: new QoreError("QORE_GIT_AUTH", "ssh is not installed or not on PATH", {
3711
+ hint: "Clone over https instead (the default for qore clone and qore sync).",
3712
+ details: { ...details, reason: "ssh-missing" }
3713
+ }) };
3714
+ }
3715
+ if (/Permission denied \(publickey\)/.test(text)) {
3716
+ return { ok: false, error: new QoreError("QORE_GIT_AUTH", "GitHub refused your SSH key", {
3717
+ hint: "Add your key to GitHub (https://github.com/settings/keys) and your ssh agent, or keep cloning over https.",
3718
+ details
3719
+ }) };
3720
+ }
3721
+ if (/Host key verification failed/.test(text)) {
3722
+ return { ok: false, error: new QoreError("QORE_GIT_AUTH", "ssh could not verify github.com's host key", {
3723
+ hint: `Run \`${COMMAND}\` once to accept its host key (or to see why it changed).`,
3724
+ details: { ...details, reason: "host-key" }
3725
+ }) };
3726
+ }
3727
+ const last = r.tail.map((l) => l.trim()).filter(Boolean).slice(-3);
3728
+ return { ok: false, error: new QoreError("QORE_GIT_AUTH", "GitHub SSH did not answer as expected", {
3729
+ hint: [...last, `\`${COMMAND}\``].join("\n"),
3730
+ details: { ...details, stderr: r.tail }
3731
+ }) };
3732
+ }
3733
+
3734
+ // src/core/doctor/checks.ts
3735
+ var NODE_RANGE = ">=22";
3736
+ var SKEW_S = 60;
3737
+ var TOOL_TIMEOUT_MS = 1e4;
3738
+ var IDLE_DAYS = 30;
3739
+ var DAY_MS = 864e5;
3740
+ var UPGRADE = "npm i -g @quatronic/qore";
3741
+ var RANK = { ok: 0, warn: 1, fail: 2 };
3742
+ var noop = () => {
3743
+ };
3744
+ var ok = (label2, details) => ({ status: "ok", label: label2, ...details ? { details } : {} });
3745
+ var withError = (status2, label2, e) => ({ status: status2, label: label2, error: toJsonError(e) });
3746
+ var isCancel = (e, signal) => e instanceof CancelledError || signal.aborted;
3747
+ var portalLabel = (p) => p.builtin ?? p.apiOrigin;
3748
+ var ToolTimeout = class extends Error {
3749
+ constructor(tool, ms) {
3750
+ super(`${tool} did not answer within ${Math.round(ms / 1e3)} s`);
3751
+ this.tool = tool;
3752
+ this.ms = ms;
3753
+ }
3754
+ tool;
3755
+ ms;
3756
+ };
3757
+ async function timed(tool, signal, ms, fn) {
3758
+ try {
3759
+ return await fn(AbortSignal.any([signal, AbortSignal.timeout(ms)]));
3760
+ } catch (e) {
3761
+ if (e instanceof CancelledError && !signal.aborted) throw new ToolTimeout(tool, ms);
3762
+ throw e;
3763
+ }
3764
+ }
3765
+ function errorLabel(e) {
3766
+ return e instanceof Error ? e.message : String(e);
3767
+ }
3768
+ function checkQore(deps2, meta) {
3769
+ const v = deps2.version;
3770
+ if (!meta.ok) return withError("warn", `qore ${v} \u2014 could not check for updates`, meta.error);
3771
+ const { minCliVersion: min, latestCliVersion: latest } = meta.meta;
3772
+ if (!v.endsWith("-dev") && compareVersions(v, min) < 0) {
3773
+ const e = new QoreError("QORE_UPGRADE_REQUIRED", `qore ${v} is too old for this portal (needs ${min})`, { hint: `Run: ${UPGRADE}` });
3774
+ return withError("fail", `qore ${v} is too old for this portal (needs ${min}) \u2014 ${UPGRADE}`, e);
3775
+ }
3776
+ if (!v.endsWith("-dev") && compareVersions(v, latest) < 0) {
3777
+ return { status: "warn", label: `qore ${v} \u2014 ${latest} is available (${UPGRADE})`, details: { latest } };
3778
+ }
3779
+ return ok(`qore ${v} (latest)`);
3780
+ }
3781
+ function checkOwnNode(deps2) {
3782
+ const v = deps2.nodeVersion;
3783
+ if (nodeSatisfies(v, NODE_RANGE) === false) {
3784
+ const e = new QoreError("QORE_NODE_VERSION", `qore needs Node ${NODE_RANGE}, found ${v}`, {
3785
+ hint: "nvm install 22 / fnm use 22, then reinstall qore.",
3786
+ details: { found: v, required: NODE_RANGE }
3787
+ });
3788
+ return withError("fail", e.message, e);
3789
+ }
3790
+ return ok(`Node ${v} (\u226522 required)`);
3791
+ }
3792
+ async function checkGit(deps2, signal, ms) {
3793
+ const res = await timed("git", signal, ms, (s) => runTool(deps2, "git", ["--version"], { cwd: deps2.home, env: deps2.env, onLine: noop, signal: s }));
3794
+ if (res.spawnError === "ENOENT") {
3795
+ const e = new QoreError("QORE_GIT_UNAVAILABLE", "git is not installed or not on PATH", {
3796
+ hint: "Install git (https://git-scm.com/downloads) and open a new terminal."
3797
+ });
3798
+ return withError("fail", e.message, e);
3799
+ }
3800
+ if (res.code !== 0) {
3801
+ const e = new QoreError("QORE_GIT_UNAVAILABLE", "git --version failed", { details: { stderr: res.tail } });
3802
+ return withError("fail", e.message, e);
3803
+ }
3804
+ const m = /git version (\d+)\.(\d+)/.exec(res.tail.join("\n"));
3805
+ return ok(m ? `git ${m[1]}.${m[2]}` : "git");
3806
+ }
3807
+ async function checkGithubSsh(deps2, signal, ms) {
3808
+ const res = await timed("ssh", signal, ms, (s) => runTool(deps2, "ssh", SSH_TEST_ARGS, { cwd: deps2.home, env: deps2.env, onLine: noop, signal: s }));
3809
+ const r = classifySshTest(res);
3810
+ if (r.ok) return ok(`GitHub SSH ok (${r.user})`, { user: r.user });
3811
+ return withError("warn", `GitHub SSH: ${r.error.message} (https still works)`, r.error);
3812
+ }
3813
+ async function checkDocker(deps2, signal, ms) {
3814
+ return timed("docker", signal, ms, async (s) => {
3815
+ const o = { cwd: deps2.home, env: deps2.env, onLine: noop, signal: s };
3816
+ const info = await runTool(deps2, "docker", ["info", "--format", "{{.ServerVersion}}"], o);
3817
+ const infoErr = classifyDockerInfo(info, deps2.platform);
3818
+ if (infoErr) return withError("warn", `${infoErr.message} (only needed for db:up)`, infoErr);
3819
+ const compose = await runTool(deps2, "docker", ["compose", "version"], o);
3820
+ return dockerBody(info.tail, compose);
3821
+ });
3822
+ }
3823
+ function dockerBody(infoTail, compose) {
3824
+ const composeErr = classifyComposeVersion(compose);
3825
+ if (composeErr) return withError("fail", composeErr.message, composeErr);
3826
+ const version = infoTail.map((l) => l.trim()).findLast(Boolean) ?? "";
3827
+ const major = /v?(\d+)\.\d+/.exec(compose.tail.join("\n"))?.[1];
3828
+ return ok(`Docker ${version} running \xB7 compose ${major ? `v${major}` : "ok"}`);
3829
+ }
3830
+ function jwtExp(token) {
3831
+ const payload = token.split(".")[1];
3832
+ if (!payload) return null;
3833
+ try {
3834
+ const exp = JSON.parse(Buffer.from(payload, "base64url").toString("utf8")).exp;
3835
+ return typeof exp === "number" && exp > 0 ? exp : null;
3836
+ } catch {
3837
+ return null;
3838
+ }
3839
+ }
3840
+ function validDays(rt, now) {
3841
+ const exp = jwtExp(rt.rt);
3842
+ const end = exp !== null ? exp * 1e3 : Date.parse(rt.obtainedAt) + IDLE_DAYS * DAY_MS;
3843
+ if (Number.isNaN(end)) return null;
3844
+ return Math.max(0, Math.floor((end - now.getTime()) / DAY_MS));
3845
+ }
3846
+ async function checkSession(deps2, portal, onRequest, sessions, selected, signal) {
3847
+ try {
3848
+ const session = await openSession(deps2, portal, "doctor", onRequest);
3849
+ const me = await session.api.get("/api/cli/v1/me");
3850
+ sessions.set(portal.apiOrigin, session);
3851
+ const rt = await session.tokens.getRefresh();
3852
+ const days = rt ? validDays(rt, deps2.now()) : null;
3853
+ const valid = days === null ? "" : ` \xB7 session valid ${days} ${days === 1 ? "day" : "days"}`;
3854
+ return ok(`Logged in to ${portalLabel(portal)} as ${me.email}${valid}`, { email: me.email, ...days !== null ? { validDays: days } : {} });
3855
+ } catch (e) {
3856
+ if (isCancel(e, signal) || selected) throw e;
3857
+ return withError("warn", `${portalLabel(portal)}: ${errorLabel(e)}`, e);
3858
+ }
3859
+ }
3860
+ function checkClock(meta) {
3861
+ if (!meta.ok) return withError("warn", "Clock \u2014 could not check (the portal did not answer)", meta.error);
3862
+ if (!meta.date) return { status: "warn", label: "Clock \u2014 could not check (no Date header)" };
3863
+ const skew = Math.round((meta.date.getTime() - meta.fetchedAt.getTime()) / 1e3);
3864
+ if (Math.abs(skew) > SKEW_S) {
3865
+ const minutes = Math.max(1, Math.round(Math.abs(skew) / 60));
3866
+ const e = new QoreError("QORE_CLOCK_SKEW", `Your clock is off by ${minutes} min`, {
3867
+ hint: "Turn on automatic date and time.",
3868
+ details: { skewSeconds: skew }
3869
+ });
3870
+ return withError("fail", e.message, e);
3871
+ }
3872
+ return ok("Clock in sync", { skewSeconds: skew });
3873
+ }
3874
+ function ownerText(e) {
3875
+ const d = e.details ?? {};
3876
+ if (d.owner === "process") return ` by ${String(d.command)} (pid ${String(d.pid)})`;
3877
+ if (d.owner === "docker") return ` by the Docker container ${String(d.name)}`;
3878
+ if (d.owner === "qore") return ` by a previous qore dev (pid ${String(d.pid)})`;
3879
+ return "";
3880
+ }
3881
+ var sameDir = async (a, b) => await realpath6(a).catch(() => a) === await realpath6(b).catch(() => b);
3882
+ async function moduleBody(deps2, m, runs, signal) {
3883
+ const parts = [];
3884
+ const part = async (fn) => {
3885
+ try {
3886
+ parts.push(await fn());
3887
+ } catch (e) {
3888
+ if (isCancel(e, signal)) throw e;
3889
+ parts.push({ status: "fail", label: errorLabel(e), error: e });
3890
+ }
3891
+ };
3892
+ const cfg = await readRepoConfig(m.root);
3893
+ let env = null;
3894
+ await part(async () => {
3895
+ const target = await resolveTarget(m.root, cfg.envFile ?? moduleDefaults(m.type).envFile);
3896
+ const text = await readOrNull(target);
3897
+ if (text === null) return { status: "warn", label: "port unknown (run qore env pull)" };
3898
+ env = Object.fromEntries(parseEnv(text));
3899
+ const port = envPort(env, "PORT");
3900
+ if (port === null) return { status: "warn", label: `port unknown (no PORT in ${basename3(target)})` };
3901
+ if (!await deps2.ports.inUse(port)) return { status: "ok", label: `port ${port} free` };
3902
+ const e = await portInUseError(deps2, { slug: m.slug, port, key: "PORT", runs: await runs() });
3903
+ return { status: "fail", label: `port ${port} in use${ownerText(e)}`, error: e };
3904
+ });
3905
+ await part(() => m.access === "write" || m.access === "maintain" ? { status: "ok", label: "write" } : { status: "warn", label: m.access === "read" ? "read only \u2014 qore dev needs write" : `${m.access} access \u2014 qore dev needs write` });
3906
+ let recipe;
3907
+ let pkg;
3908
+ await part(async () => {
3909
+ pkg = await readPackageJson(m.root);
3910
+ recipe = planRecipe({
3911
+ root: m.root,
3912
+ pkg,
3913
+ pm: await detectPackageManager(m.root, pkg),
3914
+ cfg,
3915
+ slug: m.slug,
3916
+ flags: { install: true, services: true }
3917
+ });
3918
+ if (recipe.services.length === 0) return { status: "ok", label: "db none" };
3919
+ const dbPort = env ? envPort(env, "DB_PORT") : null;
3920
+ if (dbPort === null) return { status: "warn", label: "db port unknown (run qore env pull)" };
3921
+ if (!await deps2.ports.inUse(dbPort)) return { status: "ok", label: `db:up (DB_PORT ${dbPort} free)` };
3922
+ const pub = await deps2.ports.dockerPublisher(dbPort);
3923
+ if (pub?.workingDir && await sameDir(pub.workingDir, m.root)) return { status: "ok", label: "db running" };
3924
+ const e = await portInUseError(deps2, { slug: m.slug, port: dbPort, key: "DB_PORT" });
3925
+ return { status: "fail", label: `DB_PORT ${dbPort} in use${ownerText(e)}`, error: e };
3926
+ });
3927
+ const status2 = parts.reduce((w, p) => RANK[p.status] > RANK[w] ? p.status : w, "ok");
3928
+ const shown = parts.filter((p) => p.status !== "ok");
3929
+ const first = parts.find((p) => p.status === "fail" && p.error) ?? parts.find((p) => p.error);
3930
+ return {
3931
+ recipe,
3932
+ pkg,
3933
+ body: {
3934
+ status: status2,
3935
+ label: (shown.length ? shown : parts).map((p) => p.label).join(" \xB7 "),
3936
+ ...first ? { error: toJsonError(first.error) } : {},
3937
+ details: { parts: parts.map((p) => ({ status: p.status, label: p.label, ...p.error ? { error: toJsonError(p.error) } : {} })) }
3938
+ }
3939
+ };
3940
+ }
3941
+ function runsAt(deps2, root) {
3942
+ let p;
3943
+ return () => p ??= leftoverRun(deps2, root, { prune: false }).then((r) => r ? [r] : []);
3944
+ }
3945
+ var UNTRUSTED = { status: "warn", label: "not checked (repository not trusted)" };
3946
+ async function checkRepoPm(deps2, m, recipe, signal, ms) {
3947
+ if (!await isTrusted(deps2, m.root, m.id)) return UNTRUSTED;
3948
+ const found = await timed(recipe.pm.name, signal, ms, (s) => checkPackageManager(deps2, recipe, noop, s));
3949
+ const { name, version } = recipe.pm;
3950
+ if (found === null) return ok(`${name} (version unknown)`);
3951
+ return ok(version ? `${name} ${found} (matches)` : `${name} ${found}`, { found, ...version ? { pinned: version } : {} });
3952
+ }
3953
+ async function checkRepoNode(deps2, m, recipe, pkg, signal, ms) {
3954
+ if (!await isTrusted(deps2, m.root, m.id)) return UNTRUSTED;
3955
+ const found = await timed("node", signal, ms, (s) => checkNode(deps2, recipe, pkg, m.slug, s));
3956
+ return ok(`Node ${found ?? "?"} (engines ${pkg.engines.node})`);
3957
+ }
3958
+ async function loggedInPortals(deps2, profiles) {
3959
+ const rank = (o) => o === BUILTIN_PORTALS.prod.apiOrigin ? 0 : o === BUILTIN_PORTALS.staging.apiOrigin ? 1 : 2;
3960
+ const out = [];
3961
+ for (const origin of Object.keys(profiles).sort((a, b) => rank(a) - rank(b) || a.localeCompare(b))) {
3962
+ const p = profiles[origin];
3963
+ if (await new TokenStore(deps2.keychain, profileHash(p.apiOrigin, p.issuer)).getRefresh()) out.push(resolvePortal(origin, void 0));
3964
+ }
3965
+ return out;
3966
+ }
3967
+ async function resolveRepoModule(deps2, session, portal, meta, root) {
3968
+ if (meta.ok) requireCapability(meta.meta, "repo.resolve", portal.apiOrigin);
3969
+ const keys = [...new Set((await deps2.git.remotes(root)).map((r) => repoKey(r.url)).filter((k) => k !== null))];
3970
+ if (keys.length === 0) return { status: "warn", label: "this repository has no remote Qore can match" };
3971
+ const { matches } = await session.api.post("/api/cli/v1/repos/resolve", { repoKeys: keys });
3972
+ const ids = [...new Set(matches.map((x) => x.module.id))];
3973
+ if (ids.length === 0) return { status: "warn", label: `this repository matches no module on ${portalLabel(portal)}` };
3974
+ if (ids.length > 1) {
3975
+ return { status: "warn", label: `this repository matches several modules (${[...new Set(matches.map((x) => x.module.slug))].join(", ")})` };
3976
+ }
3977
+ return session.api.get(`/api/cli/v1/modules/${encodeURIComponent(ids[0])}`);
3978
+ }
3979
+ async function runDoctor(deps2, o = {}) {
3980
+ const signal = o.signal ?? new AbortController().signal;
3981
+ const toolMs = o.toolTimeoutMs ?? TOOL_TIMEOUT_MS;
3982
+ const results = [];
3983
+ const context = { kind: "none" };
3984
+ const push = async (id, group, fn, module) => {
3985
+ let body;
3986
+ try {
3987
+ body = await fn();
3988
+ } catch (e) {
3989
+ if (e instanceof ToolTimeout) body = { status: "warn", label: e.message, details: { timeoutMs: e.ms } };
3990
+ else if (isCancel(e, signal)) throw e;
3991
+ else body = withError("fail", errorLabel(e), e);
3992
+ }
3993
+ const r = { id, group, ...module ? { module } : {}, ...body };
3994
+ results.push(r);
3995
+ return r;
3996
+ };
3997
+ let cfg;
3998
+ try {
3999
+ cfg = await deps2.config.read();
4000
+ } catch (e) {
4001
+ if (isCancel(e, signal)) throw e;
4002
+ await push("config", "tools", () => withError("fail", errorLabel(e), e));
4003
+ cfg = { schemaVersion: 1, profiles: {} };
4004
+ }
4005
+ let ws = null;
4006
+ let wsError = null;
4007
+ try {
4008
+ ws = await findWorkspace(deps2.cwd, deps2.home);
4009
+ } catch (e) {
4010
+ wsError = e;
4011
+ }
4012
+ const portal = o.portal ? resolvePortal(o.portal, cfg.defaultProfile) : ws ? resolvePortal(ws.portal, void 0) : resolvePortal(void 0, cfg.defaultProfile);
4013
+ let meta;
4014
+ try {
4015
+ meta = { ok: true, ...await fetchMetaWithDate(deps2.fetch, portal.apiOrigin), fetchedAt: deps2.now() };
4016
+ } catch (e) {
4017
+ if (isCancel(e, signal)) throw e;
4018
+ meta = { ok: false, error: e };
4019
+ }
4020
+ await push("qore", "tools", () => checkQore(deps2, meta));
4021
+ await push("node", "tools", () => checkOwnNode(deps2));
4022
+ await push("git", "tools", () => checkGit(deps2, signal, toolMs));
4023
+ await push("github-ssh", "tools", () => checkGithubSsh(deps2, signal, toolMs));
4024
+ await push("docker", "tools", () => checkDocker(deps2, signal, toolMs));
4025
+ const sessions = /* @__PURE__ */ new Map();
4026
+ let loggedIn = [];
4027
+ try {
4028
+ loggedIn = await loggedInPortals(deps2, cfg.profiles);
4029
+ if (loggedIn.length === 0) await push("session", "session", () => ({ status: "warn", label: "not logged in \u2014 qore login" }));
4030
+ } catch (e) {
4031
+ if (isCancel(e, signal)) throw e;
4032
+ await push("session", "session", () => {
4033
+ throw e;
4034
+ });
4035
+ }
4036
+ for (const p of loggedIn) {
4037
+ await push(
4038
+ `session:${portalLabel(p)}`,
4039
+ "session",
4040
+ () => checkSession(deps2, p, o.onRequest, sessions, p.apiOrigin === portal.apiOrigin, signal)
4041
+ );
4042
+ }
4043
+ await push("clock", "tools", () => checkClock(meta));
4044
+ const session = sessions.get(portal.apiOrigin);
4045
+ const notChecked = () => push(
4046
+ "modules",
4047
+ "workspace",
4048
+ () => ({ status: "warn", label: `modules not checked \u2014 no working session for ${portalLabel(portal)}` })
4049
+ );
4050
+ const perModule = async (m, prepared) => {
4051
+ if (!prepared?.recipe) return;
4052
+ const { recipe, pkg } = prepared;
4053
+ await push(`pm:${m.slug}`, "workspace", () => checkRepoPm(deps2, m, recipe, signal, toolMs), m.slug);
4054
+ if (pkg?.engines?.node) await push(`node:${m.slug}`, "workspace", () => checkRepoNode(deps2, m, recipe, pkg, signal, toolMs), m.slug);
4055
+ };
4056
+ if (wsError) {
4057
+ await push("context", "workspace", () => {
4058
+ throw wsError;
4059
+ });
4060
+ return { results, context };
4061
+ }
4062
+ const wsRoot = ws ? await workspaceRoot(deps2).catch(() => null) : null;
4063
+ const repoRoot = wsRoot ? null : await findRepoRoot(deps2.cwd);
4064
+ if (wsRoot) {
4065
+ context.kind = "workspace";
4066
+ context.label = basename3(wsRoot.root);
4067
+ if (!session) {
4068
+ await notChecked();
4069
+ return { results, context };
4070
+ }
4071
+ let am;
4072
+ try {
4073
+ if (meta.ok) requireCapability(meta.meta, "app.modules", portal.apiOrigin);
4074
+ am = await workspaceAppModules(session, portal, wsRoot);
4075
+ } catch (e) {
4076
+ await push("modules", "workspace", () => {
4077
+ throw e;
4078
+ });
4079
+ return { results, context };
4080
+ }
4081
+ context.label = `${am.app.slug} (${am.app.tenant.name} / ${am.app.name})`;
4082
+ const runs = runsAt(deps2, wsRoot.root);
4083
+ for (const m of am.modules) await workspaceModule(deps2, wsRoot, am, m, runs, signal, push, perModule);
4084
+ } else if (repoRoot) {
4085
+ context.kind = "module";
4086
+ context.label = basename3(repoRoot);
4087
+ if (!session) {
4088
+ await notChecked();
4089
+ return { results, context };
4090
+ }
4091
+ let d = null;
4092
+ try {
4093
+ const resolved = await resolveRepoModule(deps2, session, portal, meta, repoRoot);
4094
+ if ("status" in resolved) await push("module", "workspace", () => resolved);
4095
+ else d = resolved;
4096
+ } catch (e) {
4097
+ if (isCancel(e, signal)) throw e;
4098
+ await push("module", "workspace", () => {
4099
+ throw e;
4100
+ });
4101
+ }
4102
+ if (!d) return { results, context };
4103
+ context.label = `${d.tenant.name} / ${d.slug}`;
4104
+ const m = { root: repoRoot, slug: d.slug, type: d.type, id: d.id, access: d.access };
4105
+ const runs = runsAt(deps2, repoRoot);
4106
+ let prepared = null;
4107
+ await push(`module:${m.slug}`, "workspace", async () => (prepared = await moduleBody(deps2, m, runs, signal)).body, m.slug);
4108
+ await perModule(m, prepared);
4109
+ }
4110
+ return { results, context };
4111
+ }
4112
+ async function workspaceModule(deps2, ws, am, m, runs, signal, push, perModule) {
4113
+ if (!isSafeSlug(m.slug)) {
4114
+ await push(`module:${m.slug}`, "workspace", () => ({ status: "warn", label: "not a safe directory name \u2014 skipped" }), m.slug);
4115
+ return;
4116
+ }
4117
+ const mod = { root: join17(ws.root, m.slug), slug: m.slug, type: m.type, id: m.id, access: m.access };
4118
+ let prepared = null;
4119
+ await push(`module:${m.slug}`, "workspace", async () => {
4120
+ const info = await deps2.git.inspect(mod.root);
4121
+ const action = planWorkspace(ws.root, [m], { [m.slug]: info }, []).actions[0];
4122
+ if (action.kind === "clone") return { status: "warn", label: `not cloned \u2014 ${notClonedReason(m, am)}` };
4123
+ if (action.kind !== "present") return { status: "warn", label: action.reason ?? action.kind };
4124
+ prepared = await moduleBody(deps2, mod, runs, signal);
4125
+ return prepared.body;
4126
+ }, m.slug);
4127
+ await perModule(mod, prepared);
4128
+ }
4129
+
4130
+ // src/cli/commands/doctor.ts
4131
+ function registerDoctor(program, ctx) {
4132
+ program.command("doctor").description("Check qore's own health: tools, session, and this workspace or module").action(async () => {
4133
+ const { deps: deps2, io: io2, signal } = ctx;
4134
+ const g = ctx.opts();
4135
+ const report = await runDoctor(deps2, { portal: g.portal, onRequest: requestLog(ctx), signal });
4136
+ const failed = report.results.some((r) => r.status === "fail");
4137
+ if (g.json) {
4138
+ renderDoctorJson(io2, report);
4139
+ } else {
4140
+ renderDoctorHuman(io2, report, pc3.isColorSupported && g.color !== false);
4141
+ }
4142
+ ctx.setExitCode(failed ? 1 : 0);
4143
+ });
4144
+ }
4145
+
4146
+ // src/cli/commands/env.ts
4147
+ import { randomBytes as randomBytes6 } from "crypto";
4148
+ import { chmod as chmod3, realpath as realpath7 } from "fs/promises";
4149
+ import { dirname as dirname6, join as join18, relative as relative7 } from "path";
4150
+ var maskLen = (v) => v === void 0 ? "(unset)" : `\u2022\u2022\u2022\u2022 (${v.length} chars)`;
4151
+ function registerEnv(program, ctx) {
4152
+ const env = program.command("env").description("Local environment");
4153
+ 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("--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) => {
4154
+ const { deps: deps2, io: io2 } = ctx;
4155
+ const g = ctx.opts();
4156
+ const mc = await moduleContextFor(ctx, f, "env pull");
4157
+ if (!mc.repoRoot) {
4158
+ throw new QoreError("QORE_CONTEXT_REQUIRED", "env pull writes into a module repository", { hint: "cd into the module's repository." });
1923
4159
  }
1924
4160
  const app = await appFor(ctx, mc, f.app);
1925
4161
  const cfg = await readRepoConfig(mc.repoRoot);
@@ -1927,7 +4163,7 @@ function registerEnv(program, ctx) {
1927
4163
  const target = f.target ? await resolveTarget(mc.repoRoot, f.target, deps2.cwd) : await resolveTarget(mc.repoRoot, cfg.envFile ?? defaults.envFile);
1928
4164
  await ensureTrusted(ctx, mc, app, { yes: f.yes });
1929
4165
  const existing = await readOrNull(target);
1930
- const example = existing === null ? await readOrNull(join10(dirname4(target), ".env.example")) : null;
4166
+ const example = existing === null ? await readOrNull(join18(dirname6(target), ".env.example")) : null;
1931
4167
  const local = await fetchLocalEnv(deps2, mc, app);
1932
4168
  const inputs = {
1933
4169
  existing,
@@ -1936,7 +4172,7 @@ function registerEnv(program, ctx) {
1936
4172
  managed: local.managedKeys,
1937
4173
  localSecrets: cfg.localSecrets ?? defaults.localSecrets,
1938
4174
  defaultChoice: f.usePortal ? "use-portal" : "keep",
1939
- random: () => randomBytes3(32).toString("hex")
4175
+ random: () => randomBytes6(32).toString("hex")
1940
4176
  };
1941
4177
  let plan = planPull(inputs);
1942
4178
  if (plan.conflicts.length && io2.interactive && !f.usePortal) {
@@ -1964,11 +4200,11 @@ function registerEnv(program, ctx) {
1964
4200
  plan = planPull({ ...inputs, choices });
1965
4201
  }
1966
4202
  if (existing !== null && plan.text === existing) {
1967
- await chmod2(target, 384);
4203
+ await chmod3(target, 384);
1968
4204
  } else {
1969
4205
  await writeSecurely(target, plan.text);
1970
4206
  }
1971
- const rel = relative5(await realpath3(mc.repoRoot), target);
4207
+ const rel = relative7(await realpath7(mc.repoRoot), target);
1972
4208
  if (g.json) {
1973
4209
  const names = {
1974
4210
  created: plan.created,
@@ -2029,8 +4265,9 @@ function mapOidcError(e, host) {
2029
4265
  if (err === "access_denied") return new QoreError("QORE_LOGIN_DENIED", "The login was denied in the browser");
2030
4266
  if (err === "expired_token") return new QoreError("QORE_LOGIN_EXPIRED", "The login code expired", { hint: "Run `qore login` again." });
2031
4267
  if (err === "invalid_grant") return new QoreError("QORE_SESSION_EXPIRED", "Your session has ended", { hint: "Run `qore login`." });
2032
- const status2 = e.status;
2033
- if (status2 && status2 >= 500) return new QoreError("QORE_AUTH_UNAVAILABLE", "Keycloak is unavailable", { cause: e });
4268
+ const res = e.code === "OAUTH_RESPONSE_IS_NOT_CONFORM" ? e.cause : void 0;
4269
+ const status2 = e.status ?? (res instanceof Response ? res.status : void 0);
4270
+ if (status2 && status2 >= 500) return new QoreError("QORE_AUTH_UNAVAILABLE", "Keycloak is unavailable", { status: status2, cause: e });
2034
4271
  if (e instanceof TypeError) return unreachable(e, host);
2035
4272
  const cause = e?.cause;
2036
4273
  if (cause instanceof TypeError) return unreachable(cause, host);
@@ -2234,37 +4471,6 @@ function registerLogout(program, ctx) {
2234
4471
  });
2235
4472
  }
2236
4473
 
2237
- // src/cli/commands/run.ts
2238
- import { constants } from "os";
2239
- var childRunning = { current: false };
2240
- function registerRun(program, ctx) {
2241
- program.command("run").description("Run a command with this module's local secrets in its environment").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("--yes", "trust this repository without asking").argument("<command>", "the command to run (after --)").argument("[args...]", "its arguments, passed verbatim").action(async (cmd, args, f) => {
2242
- const { deps: deps2 } = ctx;
2243
- if (ctx.opts().json) {
2244
- throw new QoreError("QORE_CONTEXT_INVALID", "run has no --json output", {
2245
- hint: "Drop --json; the child's own output passes through."
2246
- });
2247
- }
2248
- const mc = await moduleContextFor(ctx, f, "run");
2249
- const app = await appFor(ctx, mc, f.app);
2250
- await ensureTrusted(ctx, mc, app, { yes: f.yes });
2251
- const local = await fetchLocalEnv(deps2, mc, app);
2252
- childRunning.current = true;
2253
- let exit;
2254
- try {
2255
- exit = await deps2.spawn.run(cmd, args, { ...deps2.env, ...local.env });
2256
- } finally {
2257
- childRunning.current = false;
2258
- }
2259
- if (exit.signal) {
2260
- deps2.raise(exit.signal);
2261
- ctx.setExitCode(128 + (constants.signals[exit.signal] ?? 0));
2262
- return;
2263
- }
2264
- ctx.setExitCode(exit.code ?? 1);
2265
- });
2266
- }
2267
-
2268
4474
  // src/cli/format.ts
2269
4475
  function ago(iso, now) {
2270
4476
  if (!iso) return "\u2013";
@@ -2357,7 +4563,7 @@ function registerStatus(program, ctx) {
2357
4563
 
2358
4564
  // src/cli/commands/sync.ts
2359
4565
  import { lstat as lstat2, readdir } from "fs/promises";
2360
- import { join as join11, relative as relative6 } from "path";
4566
+ import { join as join19, relative as relative8 } from "path";
2361
4567
  import { Option as Option2 } from "commander";
2362
4568
  function asFailure2(slug, e) {
2363
4569
  if (e instanceof QoreError && e.code !== "QORE_GIT_UNAVAILABLE") {
@@ -2376,7 +4582,7 @@ async function strayRepos(root, slugs) {
2376
4582
  for (const name of entries) {
2377
4583
  if (slugs.has(name)) continue;
2378
4584
  try {
2379
- await lstat2(join11(root, name, ".git"));
4585
+ await lstat2(join19(root, name, ".git"));
2380
4586
  names.push(name);
2381
4587
  } catch {
2382
4588
  }
@@ -2407,7 +4613,7 @@ function registerSync(program, ctx) {
2407
4613
  continue;
2408
4614
  }
2409
4615
  try {
2410
- dirs[m.slug] = await deps2.git.inspect(join11(ws.root, m.slug));
4616
+ dirs[m.slug] = await deps2.git.inspect(join19(ws.root, m.slug));
2411
4617
  modulesForPlan.push(m);
2412
4618
  } catch (e) {
2413
4619
  earlyFailures.push(asFailure2(m.slug, e));
@@ -2419,7 +4625,7 @@ function registerSync(program, ctx) {
2419
4625
  } });
2420
4626
  await syncPresent(deps2, applied, actions, Boolean(flags.pull), { app, protocol: flags.protocol });
2421
4627
  for (const r of applied) {
2422
- if (r.status === "cloned" && r.dir) r.dir = relative6(deps2.cwd, r.dir) || ".";
4628
+ if (r.status === "cloned" && r.dir) r.dir = relative8(deps2.cwd, r.dir) || ".";
2423
4629
  }
2424
4630
  const removedResults = removed.map((slug) => ({ slug, status: "removed", reason: "not a module of this app \u2014 left in place" }));
2425
4631
  const results = [...earlyFailures, ...applied, ...removedResults];
@@ -2543,20 +4749,22 @@ var COMMANDS = [
2543
4749
  registerStatus,
2544
4750
  registerEnv,
2545
4751
  registerRun,
4752
+ registerDev,
2546
4753
  registerClone,
2547
4754
  registerSync,
2548
- registerVersion
4755
+ registerVersion,
4756
+ registerDoctor
2549
4757
  ];
2550
4758
 
2551
4759
  // src/cli/updateCheck.ts
2552
4760
  var REGISTRY_URL = "https://registry.npmjs.org/@quatronic%2Fqore/latest";
2553
- var DAY_MS = 24 * 60 * 60 * 1e3;
4761
+ var DAY_MS2 = 24 * 60 * 60 * 1e3;
2554
4762
  var DEFAULT_TIMEOUT_MS = 1500;
2555
4763
  async function maybeNotifyUpdate(deps2, io2, o, timeoutMs = DEFAULT_TIMEOUT_MS) {
2556
4764
  if (o.json || deps2.env.CI || deps2.env.QORE_NO_UPDATE_CHECK === "1" || !io2.interactive) return;
2557
4765
  const cfg = await deps2.config.read();
2558
4766
  const last = cfg.updateCheck?.lastCheckedAt;
2559
- if (last && deps2.now().getTime() - new Date(last).getTime() < DAY_MS) return;
4767
+ if (last && deps2.now().getTime() - new Date(last).getTime() < DAY_MS2) return;
2560
4768
  const controller = new AbortController();
2561
4769
  const timer = setTimeout(() => controller.abort(), timeoutMs);
2562
4770
  let latest;
@@ -2580,8 +4788,8 @@ async function maybeNotifyUpdate(deps2, io2, o, timeoutMs = DEFAULT_TIMEOUT_MS)
2580
4788
  }
2581
4789
 
2582
4790
  // src/core/config.ts
2583
- import { readFile as readFile5 } from "fs/promises";
2584
- import { join as join12 } from "path";
4791
+ import { readFile as readFile6 } from "fs/promises";
4792
+ import { join as join20 } from "path";
2585
4793
  var empty = () => ({ schemaVersion: 1, profiles: {} });
2586
4794
  var ConfigStore = class {
2587
4795
  constructor(dir2) {
@@ -2589,12 +4797,12 @@ var ConfigStore = class {
2589
4797
  }
2590
4798
  dir;
2591
4799
  get file() {
2592
- return join12(this.dir, "config.json");
4800
+ return join20(this.dir, "config.json");
2593
4801
  }
2594
4802
  async read() {
2595
4803
  let raw;
2596
4804
  try {
2597
- raw = await readFile5(this.file, "utf8");
4805
+ raw = await readFile6(this.file, "utf8");
2598
4806
  } catch (e) {
2599
4807
  if (e.code === "ENOENT") return empty();
2600
4808
  throw e;
@@ -2620,9 +4828,9 @@ var ConfigStore = class {
2620
4828
 
2621
4829
  // src/platform/git.ts
2622
4830
  import { execFile, execFileSync } from "child_process";
2623
- import { mkdir as mkdir3, readdir as readdir2, stat as stat2 } from "fs/promises";
4831
+ import { mkdir as mkdir3, readdir as readdir2, stat as stat3 } from "fs/promises";
2624
4832
  import { homedir } from "os";
2625
- import { dirname as dirname5, join as join13 } from "path";
4833
+ import { dirname as dirname7, join as join21 } from "path";
2626
4834
  function parseRemotes(stdout) {
2627
4835
  const seen = /* @__PURE__ */ new Set();
2628
4836
  const out = [];
@@ -2692,7 +4900,7 @@ async function remotes(repoRoot) {
2692
4900
  return parseRemotes(r.stdout);
2693
4901
  }
2694
4902
  async function clone(url, dir2) {
2695
- await mkdir3(dirname5(dir2), { recursive: true });
4903
+ await mkdir3(dirname7(dir2), { recursive: true });
2696
4904
  const r = await runGit(["clone", "--", url, dir2]);
2697
4905
  return toResult(r, "git clone");
2698
4906
  }
@@ -2727,7 +4935,7 @@ async function status(repoRoot) {
2727
4935
  }
2728
4936
  async function statOrThrow(path) {
2729
4937
  try {
2730
- return await stat2(path);
4938
+ return await stat3(path);
2731
4939
  } catch (err) {
2732
4940
  if (err.code === "ENOENT") return void 0;
2733
4941
  throw new QoreError("QORE_INTERNAL", `could not stat ${path}`, { cause: err });
@@ -2744,7 +4952,7 @@ async function inspect(dir2) {
2744
4952
  throw new QoreError("QORE_INTERNAL", `could not read ${dir2}`, { cause: err });
2745
4953
  }
2746
4954
  const empty2 = entries.length === 0;
2747
- const isRepo = await statOrThrow(join13(dir2, ".git")) !== void 0;
4955
+ const isRepo = await statOrThrow(join21(dir2, ".git")) !== void 0;
2748
4956
  const dirRemotes = isRepo ? await remotes(dir2) : [];
2749
4957
  return { exists: true, empty: empty2, isRepo, remotes: dirRemotes };
2750
4958
  }
@@ -2806,13 +5014,13 @@ var OsKeychain = class {
2806
5014
 
2807
5015
  // src/platform/lock.ts
2808
5016
  import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
2809
- import { join as join14 } from "path";
5017
+ import { join as join22 } from "path";
2810
5018
  import lockfile from "proper-lockfile";
2811
5019
  function fileLock(dir2) {
2812
5020
  return async (name, fn) => {
2813
- const locks = join14(dir2, "locks");
5021
+ const locks = join22(dir2, "locks");
2814
5022
  await mkdir4(locks, { recursive: true, mode: 448 });
2815
- const target = join14(locks, `${name}.lock`);
5023
+ const target = join22(locks, `${name}.lock`);
2816
5024
  await writeFile4(target, "", { flag: "a" });
2817
5025
  let release;
2818
5026
  let compromised;
@@ -2852,17 +5060,25 @@ function signalsFor(platform) {
2852
5060
  return platform === "win32" ? { ignore: ["SIGINT", "SIGBREAK"], forward: [] } : { ignore: ["SIGINT", "SIGQUIT"], forward: ["SIGTERM", "SIGHUP"] };
2853
5061
  }
2854
5062
  var { ignore: IGNORE, forward: FORWARD } = signalsFor(process.platform);
5063
+ var STDERR_DRAIN_MS = 500;
2855
5064
  function makeSpawner(spawnFn, proc) {
2856
5065
  return {
2857
- run(cmd, args, env) {
5066
+ run(cmd, args, env, o) {
2858
5067
  return new Promise((resolve5, reject) => {
2859
5068
  let child;
2860
5069
  try {
2861
- child = spawnFn(cmd, args, { stdio: "inherit", shell: false, env, windowsHide: true });
5070
+ child = spawnFn(cmd, args, {
5071
+ stdio: o?.onStderr ? ["inherit", "inherit", "pipe"] : "inherit",
5072
+ shell: false,
5073
+ env,
5074
+ windowsHide: true,
5075
+ cwd: o?.cwd
5076
+ });
2862
5077
  } catch (e) {
2863
5078
  reject(new QoreError("QORE_INTERNAL", `Could not start ${cmd}: ${e.code ?? "error"}`));
2864
5079
  return;
2865
5080
  }
5081
+ if (o?.onStderr) child.stderr?.on("data", o.onStderr);
2866
5082
  const ignore = () => {
2867
5083
  };
2868
5084
  const forwarders = FORWARD.map((sig) => [sig, () => {
@@ -2880,7 +5096,22 @@ function makeSpawner(spawnFn, proc) {
2880
5096
  });
2881
5097
  child.once("exit", (code, signal) => {
2882
5098
  cleanup();
2883
- resolve5({ code, signal });
5099
+ const stderr = child.stderr;
5100
+ if (!o?.onStderr || !stderr || stderr.readableEnded) {
5101
+ resolve5({ code, signal });
5102
+ return;
5103
+ }
5104
+ let settled = false;
5105
+ const finish = () => {
5106
+ if (settled) return;
5107
+ settled = true;
5108
+ clearTimeout(timer);
5109
+ resolve5({ code, signal });
5110
+ };
5111
+ const timer = setTimeout(finish, STDERR_DRAIN_MS);
5112
+ timer.unref?.();
5113
+ stderr.once("end", finish);
5114
+ stderr.once("close", finish);
2884
5115
  });
2885
5116
  });
2886
5117
  }
@@ -2888,8 +5119,239 @@ function makeSpawner(spawnFn, proc) {
2888
5119
  }
2889
5120
  var crossSpawner = makeSpawner(spawn, process);
2890
5121
 
5122
+ // src/platform/launch.ts
5123
+ import spawn2 from "cross-spawn";
5124
+ function lineSplitter(stream, onLine) {
5125
+ let buf = "";
5126
+ return {
5127
+ push(chunk) {
5128
+ buf += chunk;
5129
+ let i;
5130
+ while ((i = buf.indexOf("\n")) >= 0) {
5131
+ onLine({ stream, line: buf.slice(0, i).replace(/\r$/, "") });
5132
+ buf = buf.slice(i + 1);
5133
+ }
5134
+ },
5135
+ flush() {
5136
+ if (buf) {
5137
+ onLine({ stream, line: buf.replace(/\r$/, "") });
5138
+ buf = "";
5139
+ }
5140
+ }
5141
+ };
5142
+ }
5143
+ var systemLauncher = {
5144
+ launch(cmd, args, o) {
5145
+ const posix2 = process.platform !== "win32";
5146
+ let child;
5147
+ try {
5148
+ child = spawn2(cmd, args, { cwd: o.cwd, env: o.env, stdio: ["ignore", "pipe", "pipe"], shell: false, detached: posix2, windowsHide: true });
5149
+ } catch (e) {
5150
+ return { pid: null, exited: Promise.resolve({ code: null, signal: null, spawnError: e.code ?? "error" }), stop() {
5151
+ } };
5152
+ }
5153
+ const out = lineSplitter("stdout", o.onLine);
5154
+ const err = lineSplitter("stderr", o.onLine);
5155
+ child.stdout?.setEncoding("utf8");
5156
+ child.stderr?.setEncoding("utf8");
5157
+ child.stdout?.on("data", (c) => out.push(c));
5158
+ child.stderr?.on("data", (c) => err.push(c));
5159
+ let closed = false;
5160
+ const exited = new Promise((resolve5) => {
5161
+ child.once("error", (e) => resolve5({ code: null, signal: null, spawnError: e.code ?? "error" }));
5162
+ child.once("close", (code, signal) => {
5163
+ closed = true;
5164
+ out.flush();
5165
+ err.flush();
5166
+ resolve5({ code, signal });
5167
+ });
5168
+ });
5169
+ return {
5170
+ pid: child.pid ?? null,
5171
+ exited,
5172
+ stop(signal) {
5173
+ if (child.pid === void 0 || closed) return;
5174
+ if (posix2) {
5175
+ try {
5176
+ process.kill(-child.pid, signal);
5177
+ } catch {
5178
+ }
5179
+ } else {
5180
+ spawn2(
5181
+ "taskkill",
5182
+ ["/pid", String(child.pid), "/T", ...signal === "SIGKILL" ? ["/F"] : []],
5183
+ { stdio: "ignore", windowsHide: true }
5184
+ ).on("error", () => {
5185
+ });
5186
+ }
5187
+ }
5188
+ };
5189
+ },
5190
+ isAlive(pid) {
5191
+ try {
5192
+ process.kill(pid, 0);
5193
+ return true;
5194
+ } catch (e) {
5195
+ return e.code === "EPERM";
5196
+ }
5197
+ },
5198
+ signal(pid, sig) {
5199
+ try {
5200
+ process.kill(pid, sig);
5201
+ return true;
5202
+ } catch {
5203
+ return false;
5204
+ }
5205
+ }
5206
+ };
5207
+
5208
+ // src/platform/ports.ts
5209
+ import { execFile as execFile2 } from "child_process";
5210
+ import { connect } from "net";
5211
+ function accepts(host, port) {
5212
+ return new Promise((resolve5) => {
5213
+ const s = connect({ host, port });
5214
+ const done = (v) => {
5215
+ s.destroy();
5216
+ resolve5(v);
5217
+ };
5218
+ s.setTimeout(500, () => done(false));
5219
+ s.once("connect", () => done(true));
5220
+ s.once("error", () => done(false));
5221
+ });
5222
+ }
5223
+ function run2(cmd, args) {
5224
+ return new Promise((resolve5) => {
5225
+ execFile2(
5226
+ cmd,
5227
+ args,
5228
+ { timeout: 2e3, env: { ...process.env, LC_ALL: "C" }, windowsHide: true },
5229
+ (e, stdout) => resolve5(e && !stdout ? null : String(stdout))
5230
+ );
5231
+ });
5232
+ }
5233
+ var systemPorts = {
5234
+ async inUse(port) {
5235
+ return await accepts("127.0.0.1", port) || await accepts("::1", port);
5236
+ },
5237
+ async waitFor(port, timeoutMs, signal) {
5238
+ const until = Date.now() + timeoutMs;
5239
+ while (!signal.aborted) {
5240
+ if (await this.inUse(port)) return true;
5241
+ if (Date.now() >= until) return false;
5242
+ await new Promise((r) => setTimeout(r, 250));
5243
+ }
5244
+ return false;
5245
+ },
5246
+ async waitFree(port, timeoutMs, signal) {
5247
+ const until = Date.now() + timeoutMs;
5248
+ while (!signal.aborted) {
5249
+ if (!await this.inUse(port)) return true;
5250
+ if (Date.now() >= until) return false;
5251
+ await new Promise((r) => setTimeout(r, 250));
5252
+ }
5253
+ return false;
5254
+ },
5255
+ async owner(port) {
5256
+ if (process.platform === "win32") {
5257
+ const out2 = await run2("netstat", ["-ano", "-p", "TCP"]);
5258
+ const line2 = out2?.split(/\r?\n/).find((l) => /LISTENING/.test(l) && new RegExp(`[:.]${port}\\s`).test(l));
5259
+ const pid2 = line2 ? Number(line2.trim().split(/\s+/).pop()) : NaN;
5260
+ if (!Number.isInteger(pid2) || pid2 <= 0) return null;
5261
+ const task = await run2("tasklist", ["/FI", `PID eq ${pid2}`, "/FO", "CSV", "/NH"]);
5262
+ return { pid: pid2, command: task?.split(",")[0]?.replace(/"/g, "").trim() || "unknown" };
5263
+ }
5264
+ const out = await run2("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-Fpc"]);
5265
+ const pid = Number(out?.match(/^p(\d+)$/m)?.[1]);
5266
+ if (!Number.isInteger(pid) || pid <= 0) return null;
5267
+ return { pid, command: out?.match(/^c(.+)$/m)?.[1] ?? "unknown" };
5268
+ },
5269
+ async ownerDetail(pid) {
5270
+ if (process.platform === "win32") return { cwd: null, ageSeconds: null };
5271
+ const lsof = await run2("lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"]);
5272
+ const ps = await run2("ps", ["-o", "etime=", "-p", String(pid)]);
5273
+ return { cwd: lsof ? parseLsofCwd(lsof) : null, ageSeconds: ps ? parseEtime(ps.trim()) : null };
5274
+ },
5275
+ async dockerPublisher(port) {
5276
+ const out = await run2("docker", [
5277
+ "ps",
5278
+ "--filter",
5279
+ `publish=${port}`,
5280
+ "--format",
5281
+ '{{.ID}} {{.Names}} {{.Label "com.docker.compose.project"}} {{.Label "com.docker.compose.project.working_dir"}}'
5282
+ ]);
5283
+ return out ? parseDockerPs(out) : null;
5284
+ },
5285
+ async parentPid(pid) {
5286
+ if (process.platform === "win32") return null;
5287
+ const n = Number((await run2("ps", ["-o", "ppid=", "-p", String(pid)]))?.trim());
5288
+ return Number.isInteger(n) && n > 0 ? n : null;
5289
+ }
5290
+ };
5291
+
5292
+ // src/platform/proxy.ts
5293
+ import { execFile as execFile3 } from "child_process";
5294
+
5295
+ // src/core/diagnostics/proxy.ts
5296
+ function parseScutilProxy(stdout) {
5297
+ const value = (key) => stdout.match(new RegExp(`^\\s*${key}\\s*:\\s*(.+)$`, "m"))?.[1]?.trim();
5298
+ if (value("HTTPSEnable") === "1") {
5299
+ const host = value("HTTPSProxy");
5300
+ const port = value("HTTPSPort");
5301
+ if (host && port) return `${host}:${port}`;
5302
+ }
5303
+ if (value("HTTPEnable") === "1") {
5304
+ const host = value("HTTPProxy");
5305
+ const port = value("HTTPPort");
5306
+ if (host && port) return `${host}:${port}`;
5307
+ }
5308
+ return null;
5309
+ }
5310
+ function parseRegProxy(stdout) {
5311
+ const enabled = /ProxyEnable\s+REG_DWORD\s+(0x[0-9a-fA-F]+)/.exec(stdout)?.[1];
5312
+ if (enabled?.toLowerCase() !== "0x1") return null;
5313
+ return /ProxyServer\s+REG_SZ\s+(\S+)/.exec(stdout)?.[1] ?? null;
5314
+ }
5315
+
5316
+ // src/platform/proxy.ts
5317
+ function run3(cmd, args) {
5318
+ return new Promise((resolve5) => {
5319
+ execFile3(
5320
+ cmd,
5321
+ args,
5322
+ { timeout: 2e3, env: { ...process.env, LC_ALL: "C" }, windowsHide: true },
5323
+ (e, stdout) => resolve5(e && !stdout ? null : String(stdout))
5324
+ );
5325
+ });
5326
+ }
5327
+ async function detect(platform) {
5328
+ try {
5329
+ if (platform === "darwin") {
5330
+ const out = await run3("scutil", ["--proxy"]);
5331
+ return out ? parseScutilProxy(out) : null;
5332
+ }
5333
+ if (platform === "win32") {
5334
+ const key = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings";
5335
+ const [enable, server] = await Promise.all([
5336
+ run3("reg", ["query", key, "/v", "ProxyEnable"]),
5337
+ run3("reg", ["query", key, "/v", "ProxyServer"])
5338
+ ]);
5339
+ const combined = [enable, server].filter(Boolean).join("\n");
5340
+ return combined ? parseRegProxy(combined) : null;
5341
+ }
5342
+ return null;
5343
+ } catch {
5344
+ return null;
5345
+ }
5346
+ }
5347
+ var cached;
5348
+ function systemProxy(platform = process.platform) {
5349
+ if (!cached) cached = detect(platform).catch(() => null);
5350
+ return cached;
5351
+ }
5352
+
2891
5353
  // src/version.ts
2892
- var VERSION = true ? "0.1.2" : "0.0.0-dev";
5354
+ var VERSION = true ? "0.1.3" : "0.0.0-dev";
2893
5355
 
2894
5356
  // src/cli.ts
2895
5357
  if (process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY) setGlobalDispatcher(new EnvHttpProxyAgent());
@@ -2900,7 +5362,10 @@ var json = preSeparatorArgv.includes("--json");
2900
5362
  var ac = new AbortController();
2901
5363
  process.on("SIGINT", () => {
2902
5364
  if (ac.signal.aborted) {
2903
- if (childRunning.current) return;
5365
+ if (childRunning.current) {
5366
+ childRunning.onInterrupt?.();
5367
+ return;
5368
+ }
2904
5369
  process.exit(130);
2905
5370
  }
2906
5371
  ac.abort();
@@ -2916,15 +5381,22 @@ var deps = {
2916
5381
  env: process.env,
2917
5382
  platform: process.platform,
2918
5383
  version: VERSION,
5384
+ nodeVersion: process.versions.node,
2919
5385
  cwd: process.cwd(),
2920
5386
  home: homedir2(),
2921
5387
  git: systemGit,
2922
5388
  spawn: crossSpawner,
5389
+ launch: systemLauncher,
5390
+ ports: systemPorts,
5391
+ signals: process,
2923
5392
  raise: (signal) => {
2924
5393
  process.removeAllListeners(signal);
2925
5394
  process.kill(process.pid, signal);
2926
- }
5395
+ },
5396
+ pid: process.pid,
5397
+ systemProxy: () => systemProxy(process.platform)
2927
5398
  };
5399
+ var termination = preflightTermination(() => ac.abort(), deps.raise);
2928
5400
  var io = processIo(isInteractive({
2929
5401
  stdinTTY: Boolean(process.stdin.isTTY),
2930
5402
  stdoutTTY: Boolean(process.stdout.isTTY),
@@ -2933,6 +5405,11 @@ var io = processIo(isInteractive({
2933
5405
  env: process.env
2934
5406
  }));
2935
5407
  process.exitCode = await run(argv, deps, io, ac.signal, COMMANDS);
5408
+ await termination.reRaise(async () => {
5409
+ await Promise.all([process.stdout, process.stderr].map((s) => new Promise((r) => {
5410
+ s.write("", () => r());
5411
+ })));
5412
+ });
2936
5413
  if (!ac.signal.aborted) {
2937
5414
  try {
2938
5415
  await maybeNotifyUpdate(deps, io, { json });